← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~trb143/openlp/refactor26 into lp:openlp/android2

 

Tim Bentley has proposed merging lp:~trb143/openlp/refactor26 into lp:openlp/android2.

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~trb143/openlp/refactor26/+merge/291039

More moving code around.
Utils directory is removed but more tidy ups are needed.
Changed enough code to warrant a merge.

lp:~trb143/openlp/refactor26 (revision 2670)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/1406/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1324/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1263/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Windows_Functional_Tests/1084/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Windows_Interface_Tests/675/
[SUCCESS] https://ci.openlp.io/job/Branch-05a-Code_Analysis/742/
[SUCCESS] https://ci.openlp.io/job/Branch-05b-Test_Coverage/610/

-- 
The attached diff has been truncated due to its size.
Your team OpenLP Core is requested to review the proposed merge of lp:~trb143/openlp/refactor26 into lp:openlp/android2.
=== added file '.bzrignore'
--- .bzrignore	1970-01-01 00:00:00 +0000
+++ .bzrignore	2016-04-05 20:22:40 +0000
@@ -0,0 +1,47 @@
+*.pyc
+*.*~
+\#*\#
+*.eric4project
+*.eric5project
+*.ropeproject
+*.e4*
+.eric4project
+.komodotools
+*.komodoproject
+list
+openlp.org 2.0.e4*
+documentation/build/html
+documentation/build/doctrees
+*.log*
+dist
+OpenLP.egg-info
+build
+resources/innosetup/Output
+_eric4project
+.pylint.d
+*.qm
+openlp/core/resources.py.old
+*.qm
+resources/windows/warnOpenLP.txt
+openlp.cfg
+.idea
+openlp.pro
+.kdev4
+tests.kdev4
+*.nja
+*.orig
+__pycache__
+*.dll
+.directory
+*.kate-swp
+# Git files
+.git
+.gitignore
+# Rejected diff's
+*.rej
+*.~\?~
+.coverage
+cover
+*.kdev4
+coverage
+tags

=== renamed file '.bzrignore' => '.bzrignore.moved'
=== added file '.coveragerc'
--- .coveragerc	1970-01-01 00:00:00 +0000
+++ .coveragerc	2016-04-05 20:22:40 +0000
@@ -0,0 +1,5 @@
+[run]
+source = openlp
+
+[html]
+directory = coverage

=== added file 'LICENSE'
--- LICENSE	1970-01-01 00:00:00 +0000
+++ LICENSE	2016-04-05 20:22:40 +0000
@@ -0,0 +1,339 @@
+            GNU GENERAL PUBLIC LICENSE
+               Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+            GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+             END OF TERMS AND CONDITIONS
+
+        How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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; either version 2 of the License, or
+    (at your option) any later version.
+
+    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.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.

=== added file 'MANIFEST.in'
--- MANIFEST.in	1970-01-01 00:00:00 +0000
+++ MANIFEST.in	2016-04-05 20:22:40 +0000
@@ -0,0 +1,16 @@
+recursive-include openlp *.py
+recursive-include openlp *.sqlite
+recursive-include openlp *.csv
+recursive-include openlp *.html
+recursive-include openlp *.js
+recursive-include openlp *.css
+recursive-include openlp *.png
+recursive-include openlp *.ps
+recursive-include openlp *.json
+recursive-include documentation *
+recursive-include resources *
+recursive-include scripts *
+include copyright.txt
+include LICENSE
+include README.txt
+include openlp/.version

=== added file 'README.txt'
--- README.txt	1970-01-01 00:00:00 +0000
+++ README.txt	2016-04-05 20:22:40 +0000
@@ -0,0 +1,15 @@
+OpenLP
+======
+
+You're probably reading this because you've just downloaded the source code for
+OpenLP. If you are looking for the installer file, please go to the download
+page on the web site::
+
+    http://openlp.org/download
+
+If you're looking for how to contribute to OpenLP, then please look at the
+OpenLP wiki::
+
+    http://wiki.openlp.org/
+
+Thanks for downloading OpenLP!

=== added file 'copyright.txt'
--- copyright.txt	1970-01-01 00:00:00 +0000
+++ copyright.txt	2016-04-05 20:22:40 +0000
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################

=== added directory 'documentation'
=== added file 'documentation/manual.txt'
--- documentation/manual.txt	1970-01-01 00:00:00 +0000
+++ documentation/manual.txt	2016-04-05 20:22:40 +0000
@@ -0,0 +1,7 @@
+OpenLP Manual
+=============
+
+If you're reading this file, you're probably looking for the OpenLP manual. The
+manual is hosted online at http://manual.openlp.org/. If you want to help with
+the manual, contact the OpenLP team via IRC in the #openlp.org channel on the
+Freenode network.

=== added file 'documentation/openlp.1'
--- documentation/openlp.1	1970-01-01 00:00:00 +0000
+++ documentation/openlp.1	2016-04-05 20:22:40 +0000
@@ -0,0 +1,47 @@
+.\" DO NOT MODIFY THIS FILE!  It was generated by help2man 1.40.9.
+.TH OPENLP "1" "May 2012" "OpenLP 1.9.9" "User Commands"
+.SH NAME
+OpenLP \- Church worship presentation software
+.SH SYNOPSIS
+.B openlp
+[\fIoptions\fR] [\fIqt-options\fR]
+.SH OPTIONS
+.TP
+\fB\-\-version\fR
+show program's version number and exit
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-e\fR, \fB\-\-no\-error\-form\fR
+Disable the error notification form.
+.TP
+\fB\-l\fR LEVEL, \fB\-\-log\-level\fR=\fILEVEL\fR
+Set logging to LEVEL level. Valid values are "debug",
+"info", "warning".
+.TP
+\fB\-p\fR, \fB\-\-portable\fR
+Specify if this should be run as a portable app, off a
+USB flash drive (not implemented).
+.TP
+\fB\-d\fR, \fB\-\-dev\-version\fR
+Ignore the version file and pull the version directly
+from Bazaar
+.TP
+\fB\-s\fR STYLE, \fB\-\-style\fR=\fISTYLE\fR
+Set the Qt5 style (passed directly to Qt5).
+.TP
+\fB\-\-testing\fR
+Run by testing framework
+.SH "SEE ALSO"
+The full documentation for
+.B OpenLP
+is maintained as a Texinfo manual.  If the
+.B info
+and
+.B OpenLP
+programs are properly installed at your site, the command
+.IP
+.B info OpenLP
+.PP
+should give you access to the complete manual.

=== added directory 'openlp'
=== added file 'openlp.py'
--- openlp.py	1970-01-01 00:00:00 +0000
+++ openlp.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+
+import sys
+import multiprocessing
+
+from openlp.core.common import is_win, is_macosx
+from openlp.core import main
+
+
+if __name__ == '__main__':
+    """
+    Instantiate and run the application.
+    """
+    # Add support for using multiprocessing from frozen Windows executable (built using PyInstaller),
+    # see https://docs.python.org/3/library/multiprocessing.html#multiprocessing.freeze_support
+    if is_win():
+        multiprocessing.freeze_support()
+    # Mac OS X passes arguments like '-psn_XXXX' to the application. This argument is actually a process serial number.
+    # However, this causes a conflict with other OpenLP arguments. Since we do not use this argument we can delete it
+    # to avoid any potential conflicts.
+    if is_macosx():
+        sys.argv = [x for x in sys.argv if not x.startswith('-psn')]
+    main()

=== added file 'openlp/.version'
--- openlp/.version	1970-01-01 00:00:00 +0000
+++ openlp/.version	2016-04-05 20:22:40 +0000
@@ -0,0 +1,1 @@
+2.4

=== added file 'openlp/__init__.py'
--- openlp/__init__.py	1970-01-01 00:00:00 +0000
+++ openlp/__init__.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`openlp` module contains all the project produced OpenLP functionality
+"""
+
+from openlp import core, plugins
+
+__all__ = ['core', 'plugins']

=== added directory 'openlp/core'
=== added file 'openlp/core/__init__.py'
--- openlp/core/__init__.py	1970-01-01 00:00:00 +0000
+++ openlp/core/__init__.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,391 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+
+"""
+The :mod:`core` module provides all core application functions
+
+All the core functions of the OpenLP application including the GUI, settings,
+logging and a plugin framework are contained within the openlp.core module.
+"""
+
+import argparse
+import logging
+import os
+import shutil
+import sys
+import time
+from traceback import format_exception
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManager, Settings, UiStrings, \
+    check_directory_exists, is_macosx, is_win, translate
+from openlp.core.common.versionchecker import VersionThread, get_application_version
+from openlp.core.lib import ScreenList
+from openlp.core.resources import qInitResources
+from openlp.core.ui import SplashScreen
+from openlp.core.ui.exceptionform import ExceptionForm
+from openlp.core.ui.firsttimeform import FirstTimeForm
+from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm
+from openlp.core.ui.mainwindow import MainWindow
+
+__all__ = ['OpenLP', 'main']
+
+
+log = logging.getLogger()
+
+WIN_REPAIR_STYLESHEET = """
+QMainWindow::separator
+{
+  border: none;
+}
+
+QDockWidget::title
+{
+  border: 1px solid palette(dark);
+  padding-left: 5px;
+  padding-top: 2px;
+  margin: 1px 0;
+}
+
+QToolBar
+{
+  border: none;
+  margin: 0;
+  padding: 0;
+}
+"""
+
+
+class OpenLP(OpenLPMixin, QtWidgets.QApplication):
+    """
+    The core application class. This class inherits from Qt's QApplication
+    class in order to provide the core of the application.
+    """
+
+    args = []
+
+    def exec(self):
+        """
+        Override exec method to allow the shared memory to be released on exit
+        """
+        self.is_event_loop_active = True
+        result = QtWidgets.QApplication.exec()
+        self.shared_memory.detach()
+        return result
+
+    def run(self, args):
+        """
+        Run the OpenLP application.
+
+        :param args: Some Args
+        """
+        self.is_event_loop_active = False
+        # On Windows, the args passed into the constructor are ignored. Not very handy, so set the ones we want to use.
+        # On Linux and FreeBSD, in order to set the WM_CLASS property for X11, we pass "OpenLP" in as a command line
+        # argument. This interferes with files being passed in as command line arguments, so we remove it from the list.
+        if 'OpenLP' in args:
+            args.remove('OpenLP')
+        self.args.extend(args)
+        # Decide how many screens we have and their size
+        screens = ScreenList.create(self.desktop())
+        # First time checks in settings
+        has_run_wizard = Settings().value('core/has run wizard')
+        if not has_run_wizard:
+            ftw = FirstTimeForm()
+            ftw.initialize(screens)
+            if ftw.exec() == QtWidgets.QDialog.Accepted:
+                Settings().setValue('core/has run wizard', True)
+            elif ftw.was_cancelled:
+                QtCore.QCoreApplication.exit()
+                sys.exit()
+        # Correct stylesheet bugs
+        application_stylesheet = ''
+        if not Settings().value('advanced/alternate rows'):
+            base_color = self.palette().color(QtGui.QPalette.Active, QtGui.QPalette.Base)
+            alternate_rows_repair_stylesheet = \
+                'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n'
+            application_stylesheet += alternate_rows_repair_stylesheet
+        if is_win():
+            application_stylesheet += WIN_REPAIR_STYLESHEET
+        if application_stylesheet:
+            self.setStyleSheet(application_stylesheet)
+        show_splash = Settings().value('core/show splash')
+        if show_splash:
+            self.splash = SplashScreen()
+            self.splash.show()
+        # make sure Qt really display the splash screen
+        self.processEvents()
+        # Check if OpenLP has been upgrade and if a backup of data should be created
+        self.backup_on_upgrade(has_run_wizard)
+        # start the main app window
+        self.main_window = MainWindow()
+        Registry().execute('bootstrap_initialise')
+        Registry().execute('bootstrap_post_set_up')
+        Registry().initialise = False
+        self.main_window.show()
+        if show_splash:
+            # now kill the splashscreen
+            self.splash.finish(self.main_window)
+            log.debug('Splashscreen closed')
+        # make sure Qt really display the splash screen
+        self.processEvents()
+        self.main_window.repaint()
+        self.processEvents()
+        if not has_run_wizard:
+            self.main_window.first_time()
+        # update_check = Settings().value('core/update check')
+        # if update_check:
+        #     version = VersionThread(self.main_window)
+        #     version.start()
+        self.main_window.is_display_blank()
+        self.main_window.app_startup()
+        return self.exec()
+
+    def is_already_running(self):
+        """
+        Look to see if OpenLP is already running and ask if a 2nd instance is to be started.
+        """
+        self.shared_memory = QtCore.QSharedMemory('OpenLP')
+        if self.shared_memory.attach():
+            status = QtWidgets.QMessageBox.critical(None, UiStrings().Error, UiStrings().OpenLPStart,
+                                                    QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
+                                                                                          QtWidgets.QMessageBox.No))
+            if status == QtWidgets.QMessageBox.No:
+                return True
+            return False
+        else:
+            self.shared_memory.create(1)
+            return False
+
+    def hook_exception(self, exc_type, value, traceback):
+        """
+        Add an exception hook so that any uncaught exceptions are displayed in this window rather than somewhere where
+        users cannot see it and cannot report when we encounter these problems.
+
+        :param exc_type: The class of exception.
+        :param value: The actual exception object.
+        :param traceback: A traceback object with the details of where the exception occurred.
+        """
+        # We can't log.exception here because the last exception no longer exists, we're actually busy handling it.
+        log.critical(''.join(format_exception(exc_type, value, traceback)))
+        if not hasattr(self, 'exception_form'):
+            self.exception_form = ExceptionForm()
+        self.exception_form.exception_text_edit.setPlainText(''.join(format_exception(exc_type, value, traceback)))
+        self.set_normal_cursor()
+        self.exception_form.exec()
+
+    def backup_on_upgrade(self, has_run_wizard):
+        """
+        Check if OpenLP has been upgraded, and ask if a backup of data should be made
+
+        :param has_run_wizard: OpenLP has been run before
+        """
+        data_version = Settings().value('core/application version')
+        openlp_version = get_application_version()['version']
+        # New installation, no need to create backup
+        if not has_run_wizard:
+            Settings().setValue('core/application version', openlp_version)
+        # If data_version is different from the current version ask if we should backup the data folder
+        elif data_version != openlp_version:
+            if QtWidgets.QMessageBox.question(None, translate('OpenLP', 'Backup'),
+                                              translate('OpenLP', 'OpenLP has been upgraded, do you want to create '
+                                                                  'a backup of OpenLPs data folder?'),
+                                              QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
+                                              QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes:
+                # Create copy of data folder
+                data_folder_path = AppLocation.get_data_path()
+                timestamp = time.strftime("%Y%m%d-%H%M%S")
+                data_folder_backup_path = data_folder_path + '-' + timestamp
+                try:
+                    shutil.copytree(data_folder_path, data_folder_backup_path)
+                except OSError:
+                    QtWidgets.QMessageBox.warning(None, translate('OpenLP', 'Backup'),
+                                                  translate('OpenLP', 'Backup of the data folder failed!'))
+                    return
+                QtWidgets.QMessageBox.information(None, translate('OpenLP', 'Backup'),
+                                                  translate('OpenLP',
+                                                            'A backup of the data folder has been created at %s')
+                                                  % data_folder_backup_path)
+            # Update the version in the settings
+            Settings().setValue('core/application version', openlp_version)
+
+    def process_events(self):
+        """
+        Wrapper to make ProcessEvents visible and named correctly
+        """
+        self.processEvents()
+
+    def set_busy_cursor(self):
+        """
+        Sets the Busy Cursor for the Application
+        """
+        self.setOverrideCursor(QtCore.Qt.BusyCursor)
+        self.processEvents()
+
+    def set_normal_cursor(self):
+        """
+        Sets the Normal Cursor for the Application
+        """
+        self.restoreOverrideCursor()
+        self.processEvents()
+
+    def event(self, event):
+        """
+        Enables platform specific event handling i.e. direct file opening on OS X
+
+        :param event: The event
+        """
+        if event.type() == QtCore.QEvent.FileOpen:
+            file_name = event.file()
+            log.debug('Got open file event for %s!', file_name)
+            self.args.insert(0, file_name)
+            return True
+        # Mac OS X should restore app window when user clicked on the OpenLP icon
+        # in the Dock bar. However, OpenLP consists of multiple windows and this
+        # does not work. This workaround fixes that.
+        # The main OpenLP window is restored when it was previously minimized.
+        elif event.type() == QtCore.QEvent.ApplicationActivate:
+            if is_macosx() and hasattr(self, 'main_window'):
+                if self.main_window.isMinimized():
+                    # Copied from QWidget.setWindowState() docs on how to restore and activate a minimized window
+                    # while preserving its maximized and/or full-screen state.
+                    self.main_window.setWindowState(self.main_window.windowState() & ~QtCore.Qt.WindowMinimized |
+                                                    QtCore.Qt.WindowActive)
+                    return True
+        return QtWidgets.QApplication.event(self, event)
+
+
+def parse_options(args):
+    """
+    Parse the command line arguments
+
+    :param args: list of command line arguments
+    :return: a tuple of parsed options of type optparse.Value and a list of remaining argsZ
+    """
+    # Set up command line options.
+    parser = argparse.ArgumentParser(prog='openlp.py')
+    parser.add_argument('-e', '--no-error-form', dest='no_error_form', action='store_true',
+                        help='Disable the error notification form.')
+    parser.add_argument('-l', '--log-level', dest='loglevel', default='warning', metavar='LEVEL',
+                        help='Set logging to LEVEL level. Valid values are "debug", "info", "warning".')
+    parser.add_argument('-p', '--portable', dest='portable', action='store_true',
+                        help='Specify if this should be run as a portable app, '
+                             'off a USB flash drive (not implemented).')
+    parser.add_argument('-d', '--dev-version', dest='dev_version', action='store_true',
+                        help='Ignore the version file and pull the version directly from Bazaar')
+    parser.add_argument('-s', '--style', dest='style', help='Set the Qt5 style (passed directly to Qt5).')
+    parser.add_argument('rargs', nargs='?', default=[])
+    # Parse command line options and deal with them. Use args supplied pragmatically if possible.
+    return parser.parse_args(args) if args else parser.parse_args()
+
+
+def set_up_logging(log_path):
+    """
+    Setup our logging using log_path
+
+    :param log_path: the path
+    """
+    check_directory_exists(log_path, True)
+    filename = os.path.join(log_path, 'openlp.log')
+    logfile = logging.FileHandler(filename, 'w', encoding="UTF-8")
+    logfile.setFormatter(logging.Formatter('%(asctime)s %(name)-55s %(levelname)-8s %(message)s'))
+    log.addHandler(logfile)
+    if log.isEnabledFor(logging.DEBUG):
+        print('Logging to: %s' % filename)
+
+
+def main(args=None):
+    """
+    The main function which parses command line options and then runs
+
+    :param args: Some args
+    """
+    args = parse_options(args)
+    qt_args = []
+    if args and args.loglevel.lower() in ['d', 'debug']:
+        log.setLevel(logging.DEBUG)
+    elif args and args.loglevel.lower() in ['w', 'warning']:
+        log.setLevel(logging.WARNING)
+    else:
+        log.setLevel(logging.INFO)
+    if args and args.style:
+        qt_args.extend(['-style', args.style])
+    # Throw the rest of the arguments at Qt, just in case.
+    qt_args.extend(args.rargs)
+    # Bug #1018855: Set the WM_CLASS property in X11
+    if not is_win() and not is_macosx():
+        qt_args.append('OpenLP')
+    # Initialise the resources
+    qInitResources()
+    # Now create and actually run the application.
+    application = OpenLP(qt_args)
+    application.setOrganizationName('OpenLP')
+    application.setOrganizationDomain('openlp.org')
+    application.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
+    if args and args.portable:
+        application.setApplicationName('OpenLPPortable')
+        Settings.setDefaultFormat(Settings.IniFormat)
+        # Get location OpenLPPortable.ini
+        application_path = AppLocation.get_directory(AppLocation.AppDir)
+        set_up_logging(os.path.abspath(os.path.join(application_path, '..', '..', 'Other')))
+        log.info('Running portable')
+        portable_settings_file = os.path.abspath(os.path.join(application_path, '..', '..', 'Data', 'OpenLP.ini'))
+        # Make this our settings file
+        log.info('INI file: %s', portable_settings_file)
+        Settings.set_filename(portable_settings_file)
+        portable_settings = Settings()
+        # Set our data path
+        data_path = os.path.abspath(os.path.join(application_path, '..', '..', 'Data',))
+        log.info('Data path: %s', data_path)
+        # Point to our data path
+        portable_settings.setValue('advanced/data path', data_path)
+        portable_settings.setValue('advanced/is portable', True)
+        portable_settings.sync()
+    else:
+        application.setApplicationName('OpenLP')
+        set_up_logging(AppLocation.get_directory(AppLocation.CacheDir))
+    Registry.create()
+    Registry().register('application', application)
+    application.setApplicationVersion(get_application_version()['version'])
+    # Instance check
+    if application.is_already_running():
+        sys.exit()
+    # Remove/convert obsolete settings.
+    Settings().remove_obsolete_settings()
+    # First time checks in settings
+    if not Settings().value('core/has run wizard'):
+        if not FirstTimeLanguageForm().exec():
+            # if cancel then stop processing
+            sys.exit()
+    # i18n Set Language
+    language = LanguageManager.get_language()
+    application_translator, default_translator = LanguageManager.get_translator(language)
+    if not application_translator.isEmpty():
+        application.installTranslator(application_translator)
+    if not default_translator.isEmpty():
+        application.installTranslator(default_translator)
+    else:
+        log.debug('Could not find default_translator.')
+    if args and not args.no_error_form:
+        sys.excepthook = application.hook_exception
+    sys.exit(application.run(qt_args))

=== added directory 'openlp/core/common'
=== added file 'openlp/core/common/__init__.py'
--- openlp/core/common/__init__.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/__init__.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,373 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`common` module contains most of the components and libraries that make
+OpenLP work.
+"""
+import hashlib
+import logging
+import os
+import re
+import sys
+import traceback
+from ipaddress import IPv4Address, IPv6Address, AddressValueError
+from shutil import which
+
+from PyQt5 import QtCore, QtGui
+from PyQt5.QtCore import QCryptographicHash as QHash
+
+log = logging.getLogger(__name__ + '.__init__')
+
+
+FIRST_CAMEL_REGEX = re.compile('(.)([A-Z][a-z]+)')
+SECOND_CAMEL_REGEX = re.compile('([a-z0-9])([A-Z])')
+CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]', re.UNICODE)
+INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]', re.UNICODE)
+IMAGES_FILTER = None
+
+
+def trace_error_handler(logger):
+    """
+    Log the calling path of an exception
+
+    :param logger: logger to use so traceback is logged to correct class
+    """
+    log_string = "OpenLP Error trace"
+    for tb in traceback.extract_stack():
+        log_string = '%s\n   File %s at line %d \n\t called %s' % (log_string, tb[0], tb[1], tb[3])
+    logger.error(log_string)
+
+
+def check_directory_exists(directory, do_not_log=False):
+    """
+    Check a theme directory exists and if not create it
+
+    :param directory: The directory to make sure exists
+    :param do_not_log: To not log anything. This is need for the start up, when the log isn't ready.
+    """
+    if not do_not_log:
+        log.debug('check_directory_exists %s' % directory)
+    try:
+        if not os.path.exists(directory):
+            os.makedirs(directory)
+    except IOError as e:
+        if not do_not_log:
+            log.exception('failed to check if directory exists or create directory')
+
+
+def get_frozen_path(frozen_option, non_frozen_option):
+    """
+    Return a path based on the system status.
+
+    :param frozen_option:
+    :param non_frozen_option:
+    """
+    if hasattr(sys, 'frozen') and sys.frozen == 1:
+        return frozen_option
+    return non_frozen_option
+
+
+class ThemeLevel(object):
+    """
+    Provides an enumeration for the level a theme applies to
+    """
+    Global = 1
+    Service = 2
+    Song = 3
+
+
+def translate(context, text, comment=None, qt_translate=QtCore.QCoreApplication.translate):
+    """
+    A special shortcut method to wrap around the Qt5 translation functions. This abstracts the translation procedure so
+    that we can change it if at a later date if necessary, without having to redo the whole of OpenLP.
+
+    :param context: The translation context, used to give each string a context or a namespace.
+    :param text: The text to put into the translation tables for translation.
+    :param comment: An identifying string for when the same text is used in different roles within the same context.
+    :param qt_translate:
+    """
+    return qt_translate(context, text, comment)
+
+
+class SlideLimits(object):
+    """
+    Provides an enumeration for behaviour of OpenLP at the end limits of each service item when pressing the up/down
+    arrow keys
+    """
+    End = 1
+    Wrap = 2
+    Next = 3
+
+
+def de_hump(name):
+    """
+    Change any Camel Case string to python string
+    """
+    sub_name = FIRST_CAMEL_REGEX.sub(r'\1_\2', name)
+    return SECOND_CAMEL_REGEX.sub(r'\1_\2', sub_name).lower()
+
+
+def is_win():
+    """
+    Returns true if running on a system with a nt kernel e.g. Windows, Wine
+
+    :return: True if system is running a nt kernel false otherwise
+    """
+    return os.name.startswith('nt')
+
+
+def is_macosx():
+    """
+    Returns true if running on a system with a darwin kernel e.g. Mac OS X
+
+    :return: True if system is running a darwin kernel false otherwise
+    """
+    return sys.platform.startswith('darwin')
+
+
+def is_linux():
+    """
+    Returns true if running on a system with a linux kernel e.g. Ubuntu, Debian, etc
+
+    :return: True if system is running a linux kernel false otherwise
+    """
+    return sys.platform.startswith('linux')
+
+
+def verify_ipv4(addr):
+    """
+    Validate an IPv4 address
+
+    :param addr: Address to validate
+    :returns: bool
+    """
+    try:
+        valid = IPv4Address(addr)
+        return True
+    except AddressValueError:
+        return False
+
+
+def verify_ipv6(addr):
+    """
+    Validate an IPv6 address
+
+    :param addr: Address to validate
+    :returns: bool
+    """
+    try:
+        valid = IPv6Address(addr)
+        return True
+    except AddressValueError:
+        return False
+
+
+def verify_ip_address(addr):
+    """
+    Validate an IP address as either IPv4 or IPv6
+
+    :param addr: Address to validate
+    :returns: bool
+    """
+    return True if verify_ipv4(addr) else verify_ipv6(addr)
+
+
+def md5_hash(salt, data=None):
+    """
+    Returns the hashed output of md5sum on salt,data
+    using Python3 hashlib
+
+    :param salt: Initial salt
+    :param data: OPTIONAL Data to hash
+    :returns: str
+    """
+    log.debug('md5_hash(salt="%s")' % salt)
+    hash_obj = hashlib.new('md5')
+    hash_obj.update(salt)
+    if data:
+        hash_obj.update(data)
+    hash_value = hash_obj.hexdigest()
+    log.debug('md5_hash() returning "%s"' % hash_value)
+    return hash_value
+
+
+def qmd5_hash(salt, data=None):
+    """
+    Returns the hashed output of MD5Sum on salt, data
+    using PyQt5.QCryptographicHash.
+
+    :param salt: Initial salt
+    :param data: OPTIONAL Data to hash
+    :returns: str
+    """
+    log.debug('qmd5_hash(salt="%s"' % salt)
+    hash_obj = QHash(QHash.Md5)
+    hash_obj.addData(salt)
+    hash_obj.addData(data)
+    hash_value = hash_obj.result().toHex()
+    log.debug('qmd5_hash() returning "%s"' % hash_value)
+    return hash_value.data()
+
+
+def clean_button_text(button_text):
+    """
+    Clean the & and other characters out of button text
+
+    :param button_text: The text to clean
+    """
+    return button_text.replace('&', '').replace('< ', '').replace(' >', '')
+
+
+from .openlpmixin import OpenLPMixin
+from .registry import Registry
+from .registrymixin import RegistryMixin
+from .registryproperties import RegistryProperties
+from .uistrings import UiStrings
+from .settings import Settings
+from .applocation import AppLocation
+from .actions import ActionList
+from .languagemanager import LanguageManager
+
+
+def add_actions(target, actions):
+    """
+    Adds multiple actions to a menu or toolbar in one command.
+
+    :param target: The menu or toolbar to add actions to
+    :param actions: The actions to be added. An action consisting of the keyword ``None``
+        will result in a separator being inserted into the target.
+    """
+    for action in actions:
+        if action is None:
+            target.addSeparator()
+        else:
+            target.addAction(action)
+
+
+def get_uno_command(connection_type='pipe'):
+    """
+    Returns the UNO command to launch an libreoffice.org instance.
+    """
+    for command in ['libreoffice', 'soffice']:
+        if which(command):
+            break
+    else:
+        raise FileNotFoundError('Command not found')
+
+    OPTIONS = '--nologo --norestore --minimized --nodefault --nofirststartwizard'
+    if connection_type == 'pipe':
+        CONNECTION = '"--accept=pipe,name=openlp_pipe;urp;"'
+    else:
+        CONNECTION = '"--accept=socket,host=localhost,port=2002;urp;"'
+    return '%s %s %s' % (command, OPTIONS, CONNECTION)
+
+
+def get_uno_instance(resolver, connection_type='pipe'):
+    """
+    Returns a running libreoffice.org instance.
+
+    :param resolver: The UNO resolver to use to find a running instance.
+    """
+    log.debug('get UNO Desktop Openoffice - resolve')
+    if connection_type == 'pipe':
+        return resolver.resolve('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext')
+    else:
+        return resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext')
+
+
+def get_filesystem_encoding():
+    """
+    Returns the name of the encoding used to convert Unicode filenames into system file names.
+    """
+    encoding = sys.getfilesystemencoding()
+    if encoding is None:
+        encoding = sys.getdefaultencoding()
+    return encoding
+
+
+def split_filename(path):
+    """
+    Return a list of the parts in a given path.
+    """
+    path = os.path.abspath(path)
+    if not os.path.isfile(path):
+        return path, ''
+    else:
+        return os.path.split(path)
+
+
+def delete_file(file_path_name):
+    """
+    Deletes a file from the system.
+
+    :param file_path_name: The file, including path, to delete.
+    """
+    if not file_path_name:
+        return False
+    try:
+        if os.path.exists(file_path_name):
+            os.remove(file_path_name)
+        return True
+    except (IOError, OSError):
+        log.exception("Unable to delete file %s" % file_path_name)
+        return False
+
+
+def get_images_filter():
+    """
+    Returns a filter string for a file dialog containing all the supported image formats.
+    """
+    global IMAGES_FILTER
+    if not IMAGES_FILTER:
+        log.debug('Generating images filter.')
+        formats = list(map(bytes.decode, list(map(bytes, QtGui.QImageReader.supportedImageFormats()))))
+        visible_formats = '(*.%s)' % '; *.'.join(formats)
+        actual_formats = '(*.%s)' % ' *.'.join(formats)
+        IMAGES_FILTER = '%s %s %s' % (translate('OpenLP', 'Image Files'), visible_formats, actual_formats)
+    return IMAGES_FILTER
+
+
+def is_not_image_file(file_name):
+    """
+    Validate that the file is not an image file.
+
+    :param file_name: File name to be checked.
+    """
+    if not file_name:
+        return True
+    else:
+        formats = [bytes(fmt).decode().lower() for fmt in QtGui.QImageReader.supportedImageFormats()]
+        file_part, file_extension = os.path.splitext(str(file_name))
+        if file_extension[1:].lower() in formats and os.path.exists(file_name):
+            return False
+        return True
+
+
+def clean_filename(filename):
+    """
+    Removes invalid characters from the given ``filename``.
+
+    :param filename:  The "dirty" file name to clean.
+    """
+    if not isinstance(filename, str):
+        filename = str(filename, 'utf-8')
+    return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename))

=== added file 'openlp/core/common/actions.py'
--- openlp/core/common/actions.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/actions.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,388 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`~openlp.core.utils.actions` module provides action list classes used
+by the shortcuts system.
+"""
+import logging
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import Settings
+
+
+log = logging.getLogger(__name__)
+
+
+class ActionCategory(object):
+    """
+    The :class:`~openlp.core.utils.ActionCategory` class encapsulates a category for the
+    :class:`~openlp.core.utils.CategoryList` class.
+    """
+    def __init__(self, name, weight=0):
+        """
+        Constructor
+        """
+        self.name = name
+        self.weight = weight
+        self.actions = CategoryActionList()
+
+
+class CategoryActionList(object):
+    """
+    The :class:`~openlp.core.utils.CategoryActionList` class provides a sorted list of actions within a category.
+    """
+    def __init__(self):
+        """
+        Constructor
+        """
+        self.index = 0
+        self.actions = []
+
+    def __contains__(self, key):
+        """
+        Implement the __contains__() method to make this class a dictionary type
+        """
+        for weight, action in self.actions:
+            if action == key:
+                return True
+        return False
+
+    def __len__(self):
+        """
+        Implement the __len__() method to make this class a dictionary type
+        """
+        return len(self.actions)
+
+    def __iter__(self):
+        """
+        Implement the __getitem__() method to make this class iterable
+        """
+        return self
+
+    def __next__(self):
+        """
+        Python 3 "next" method.
+        """
+        if self.index >= len(self.actions):
+            self.index = 0
+            raise StopIteration
+        else:
+            self.index += 1
+            return self.actions[self.index - 1][1]
+
+    def append(self, action):
+        """
+        Append an action
+        """
+        weight = 0
+        if self.actions:
+            weight = self.actions[-1][0] + 1
+        self.add(action, weight)
+
+    def add(self, action, weight=0):
+        """
+        Add an action.
+        """
+        self.actions.append((weight, action))
+        self.actions.sort(key=lambda act: act[0])
+
+    def remove(self, action):
+        """
+        Remove an action
+        """
+        for item in self.actions:
+            if item[1] == action:
+                self.actions.remove(item)
+                return
+        raise ValueError('Action "%s" does not exist.' % action)
+
+
+class CategoryList(object):
+    """
+    The :class:`~openlp.core.utils.CategoryList` class encapsulates a category list for the
+    :class:`~openlp.core.utils.ActionList` class and provides an iterator interface for walking through the list of
+    actions in this category.
+    """
+
+    def __init__(self):
+        """
+        Constructor
+        """
+        self.index = 0
+        self.categories = []
+
+    def __getitem__(self, key):
+        """
+        Implement the __getitem__() method to make this class like a dictionary
+        """
+        for category in self.categories:
+            if category.name == key:
+                return category
+        raise KeyError('Category "%s" does not exist.' % key)
+
+    def __len__(self):
+        """
+        Implement the __len__() method to make this class like a dictionary
+        """
+        return len(self.categories)
+
+    def __iter__(self):
+        """
+        Implement the __iter__() method to make this class like a dictionary
+        """
+        return self
+
+    def __next__(self):
+        """
+        Python 3 "next" method for iterator.
+        """
+        if self.index >= len(self.categories):
+            self.index = 0
+            raise StopIteration
+        else:
+            self.index += 1
+            return self.categories[self.index - 1]
+
+    def __contains__(self, key):
+        """
+        Implement the __contains__() method to make this class like a dictionary
+        """
+        for category in self.categories:
+            if category.name == key:
+                return True
+        return False
+
+    def append(self, name, actions=None):
+        """
+        Append a category
+        """
+        weight = 0
+        if self.categories:
+            weight = self.categories[-1].weight + 1
+        self.add(name, weight, actions)
+
+    def add(self, name, weight=0, actions=None):
+        """
+        Add a category
+        """
+        category = ActionCategory(name, weight)
+        if actions:
+            for action in actions:
+                if isinstance(action, tuple):
+                    category.actions.add(action[0], action[1])
+                else:
+                    category.actions.append(action)
+        self.categories.append(category)
+        self.categories.sort(key=lambda cat: cat.weight)
+
+    def remove(self, name):
+        """
+        Remove a category
+        """
+        for category in self.categories:
+            if category.name == name:
+                self.categories.remove(category)
+                return
+        raise ValueError('Category "%s" does not exist.' % name)
+
+
+class ActionList(object):
+    """
+    The :class:`~openlp.core.utils.ActionList` class contains a list of menu actions and categories associated with
+    those actions. Each category also has a weight by which it is sorted when iterating through the list of actions or
+    categories.
+    """
+    instance = None
+    shortcut_map = {}
+
+    def __init__(self):
+        """
+        Constructor
+        """
+        self.categories = CategoryList()
+
+    @staticmethod
+    def get_instance():
+        """
+        Get the instance of this class.
+        """
+        if ActionList.instance is None:
+            ActionList.instance = ActionList()
+        return ActionList.instance
+
+    def add_action(self, action, category=None, weight=None):
+        """
+        Add an action to the list of actions.
+
+        **Note**: The action's objectName must be set when you want to add it!
+
+        :param action: The action to add (QAction). **Note**, the action must not have an empty ``objectName``.
+        :param category: The category this action belongs to. The category has to be a python string. . **Note**,
+            if the category is ``None``, the category and its actions are being hidden in the shortcut dialog. However,
+            if they are added, it is possible to avoid assigning shortcuts twice, which is important.
+        :param weight: The weight specifies how important a category is. However, this only has an impact on the order
+            the categories are displayed.
+        """
+        if category not in self.categories:
+            self.categories.append(category)
+        settings = Settings()
+        settings.beginGroup('shortcuts')
+        # Get the default shortcut from the config.
+        action.default_shortcuts = settings.get_default_value(action.objectName())
+        if weight is None:
+            self.categories[category].actions.append(action)
+        else:
+            self.categories[category].actions.add(action, weight)
+        # Load the shortcut from the config.
+        shortcuts = settings.value(action.objectName())
+        settings.endGroup()
+        if not shortcuts:
+            action.setShortcuts([])
+            return
+        # We have to do this to ensure that the loaded shortcut list e. g. STRG+O (German) is converted to CTRL+O,
+        # which is only done when we convert the strings in this way (QKeySequencet -> uncode).
+        shortcuts = list(map(QtGui.QKeySequence.toString, list(map(QtGui.QKeySequence, shortcuts))))
+        # Check the alternate shortcut first, to avoid problems when the alternate shortcut becomes the primary shortcut
+        #  after removing the (initial) primary shortcut due to conflicts.
+        if len(shortcuts) == 2:
+            existing_actions = ActionList.shortcut_map.get(shortcuts[1], [])
+            # Check for conflicts with other actions considering the shortcut context.
+            if self._is_shortcut_available(existing_actions, action):
+                actions = ActionList.shortcut_map.get(shortcuts[1], [])
+                actions.append(action)
+                ActionList.shortcut_map[shortcuts[1]] = actions
+            else:
+                log.warning('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' %
+                            (shortcuts[1], action.objectName()))
+                shortcuts.remove(shortcuts[1])
+        # Check the primary shortcut.
+        existing_actions = ActionList.shortcut_map.get(shortcuts[0], [])
+        # Check for conflicts with other actions considering the shortcut context.
+        if self._is_shortcut_available(existing_actions, action):
+            actions = ActionList.shortcut_map.get(shortcuts[0], [])
+            actions.append(action)
+            ActionList.shortcut_map[shortcuts[0]] = actions
+        else:
+            log.warning('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' %
+                        (shortcuts[0], action.objectName()))
+            shortcuts.remove(shortcuts[0])
+        action.setShortcuts([QtGui.QKeySequence(shortcut) for shortcut in shortcuts])
+
+    def remove_action(self, action, category=None):
+        """
+        This removes an action from its category. Empty categories are automatically removed.
+
+        :param action:  The ``QAction`` object to be removed.
+        :param category: The name (unicode string) of the category, which contains the action. Defaults to None.
+        """
+        if category not in self.categories:
+            return
+        self.categories[category].actions.remove(action)
+        # Remove empty categories.
+        if not self.categories[category].actions:
+            self.categories.remove(category)
+        shortcuts = list(map(QtGui.QKeySequence.toString, action.shortcuts()))
+        for shortcut in shortcuts:
+            # Remove action from the list of actions which are using this shortcut.
+            ActionList.shortcut_map[shortcut].remove(action)
+            # Remove empty entries.
+            if not ActionList.shortcut_map[shortcut]:
+                del ActionList.shortcut_map[shortcut]
+
+    def add_category(self, name, weight):
+        """
+        Add an empty category to the list of categories. This is only convenient for categories with a given weight.
+
+        :param name: The category's name.
+        :param weight: The category's weight (int).
+        """
+        if name in self.categories:
+            # Only change the weight and resort the categories again.
+            for category in self.categories:
+                if category.name == name:
+                    category.weight = weight
+            self.categories.categories.sort(key=lambda cat: cat.weight)
+            return
+        self.categories.add(name, weight)
+
+    def update_shortcut_map(self, action, old_shortcuts):
+        """
+        Remove the action for the given ``old_shortcuts`` from the ``shortcut_map`` to ensure its up-to-dateness.
+        **Note**: The new action's shortcuts **must** be assigned to the given ``action`` **before** calling this
+        method.
+
+        :param action: The action whose shortcuts are supposed to be updated in the ``shortcut_map``.
+        :param old_shortcuts: A list of unicode key sequences.
+        """
+        for old_shortcut in old_shortcuts:
+            # Remove action from the list of actions which are using this shortcut.
+            ActionList.shortcut_map[old_shortcut].remove(action)
+            # Remove empty entries.
+            if not ActionList.shortcut_map[old_shortcut]:
+                del ActionList.shortcut_map[old_shortcut]
+        new_shortcuts = list(map(QtGui.QKeySequence.toString, action.shortcuts()))
+        # Add the new shortcuts to the map.
+        for new_shortcut in new_shortcuts:
+            existing_actions = ActionList.shortcut_map.get(new_shortcut, [])
+            existing_actions.append(action)
+            ActionList.shortcut_map[new_shortcut] = existing_actions
+
+    def _is_shortcut_available(self, existing_actions, action):
+        """
+        Checks if the given ``action`` may use its assigned shortcut(s) or not. Returns ``True`` or ``False.
+
+        :param existing_actions: A list of actions which already use a particular shortcut.
+        :param action: The action which wants to use a particular shortcut.
+        """
+        global_context = action.shortcutContext() in [QtCore.Qt.WindowShortcut, QtCore.Qt.ApplicationShortcut]
+        affected_actions = []
+        if global_context:
+            affected_actions = [a for a in self.get_all_child_objects(action.parent()) if isinstance(a,
+                                                                                                     QtWidgets.QAction)]
+        for existing_action in existing_actions:
+            if action is existing_action:
+                continue
+            if existing_action in affected_actions:
+                return False
+            if existing_action.shortcutContext() in [QtCore.Qt.WindowShortcut, QtCore.Qt.ApplicationShortcut]:
+                return False
+            elif action in self.get_all_child_objects(existing_action.parent()):
+                return False
+        return True
+
+    def get_all_child_objects(self, qobject):
+        """
+        Goes recursively through the children of ``qobject`` and returns a list of all child objects.
+        """
+        children = qobject.children()
+        # Append the children's children.
+        children.extend(list(map(self.get_all_child_objects, children)))
+        return children
+
+
+class CategoryOrder(object):
+    """
+    An enumeration class for category weights.
+    """
+    standard_menu = -20
+    standard_toolbar = -10

=== added file 'openlp/core/common/applocation.py'
--- openlp/core/common/applocation.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/applocation.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`openlp.core.common.applocation` module provides an utility for OpenLP receiving the data path etc.
+"""
+import logging
+import os
+import sys
+
+from openlp.core.common import Settings, is_win, is_macosx
+
+
+if not is_win() and not is_macosx():
+    try:
+        from xdg import BaseDirectory
+        XDG_BASE_AVAILABLE = True
+    except ImportError:
+        XDG_BASE_AVAILABLE = False
+
+import openlp
+from openlp.core.common import check_directory_exists, get_frozen_path
+
+
+log = logging.getLogger(__name__)
+
+
+class AppLocation(object):
+    """
+    The :class:`AppLocation` class is a static class which retrieves a directory based on the directory type.
+    """
+    AppDir = 1
+    DataDir = 2
+    PluginsDir = 3
+    VersionDir = 4
+    CacheDir = 5
+    LanguageDir = 6
+
+    # Base path where data/config/cache dir is located
+    BaseDir = None
+
+    @staticmethod
+    def get_directory(dir_type=AppDir):
+        """
+        Return the appropriate directory according to the directory type.
+
+        :param dir_type: The directory type you want, for instance the data directory. Default *AppLocation.AppDir*
+        """
+        if dir_type == AppLocation.AppDir:
+            return get_frozen_path(os.path.abspath(os.path.dirname(sys.argv[0])), os.path.dirname(openlp.__file__))
+        elif dir_type == AppLocation.PluginsDir:
+            app_path = os.path.abspath(os.path.dirname(sys.argv[0]))
+            return get_frozen_path(os.path.join(app_path, 'plugins'),
+                                   os.path.join(os.path.dirname(openlp.__file__), 'plugins'))
+        elif dir_type == AppLocation.VersionDir:
+            return get_frozen_path(os.path.abspath(os.path.dirname(sys.argv[0])), os.path.dirname(openlp.__file__))
+        elif dir_type == AppLocation.LanguageDir:
+            app_path = get_frozen_path(os.path.abspath(os.path.dirname(sys.argv[0])), _get_os_dir_path(dir_type))
+            return os.path.join(app_path, 'i18n')
+        elif dir_type == AppLocation.DataDir and AppLocation.BaseDir:
+            return os.path.join(AppLocation.BaseDir, 'data')
+        else:
+            return _get_os_dir_path(dir_type)
+
+    @staticmethod
+    def get_data_path():
+        """
+        Return the path OpenLP stores all its data under.
+        """
+        # Check if we have a different data location.
+        if Settings().contains('advanced/data path'):
+            path = Settings().value('advanced/data path')
+        else:
+            path = AppLocation.get_directory(AppLocation.DataDir)
+            check_directory_exists(path)
+        return os.path.normpath(path)
+
+    @staticmethod
+    def get_files(section=None, extension=None):
+        """
+        Get a list of files from the data files path.
+
+        :param section: Defaults to *None*. The section of code getting the files - used to load from a section's
+            data subdirectory.
+        :param extension:
+            Defaults to *None*. The extension to search for. For example::
+
+                '.png'
+        """
+        path = AppLocation.get_data_path()
+        if section:
+            path = os.path.join(path, section)
+        try:
+            files = os.listdir(path)
+        except OSError:
+            return []
+        if extension:
+            return [filename for filename in files if extension == os.path.splitext(filename)[1]]
+        else:
+            # no filtering required
+            return files
+
+    @staticmethod
+    def get_section_data_path(section):
+        """
+        Return the path a particular module stores its data under.
+        """
+        data_path = AppLocation.get_data_path()
+        path = os.path.join(data_path, section)
+        check_directory_exists(path)
+        return path
+
+
+def _get_os_dir_path(dir_type):
+    """
+    Return a path based on which OS and environment we are running in.
+    """
+    # If running from source, return the language directory from the source directory
+    if dir_type == AppLocation.LanguageDir:
+        directory = os.path.abspath(os.path.join(os.path.dirname(openlp.__file__), '..', 'resources'))
+        if os.path.exists(directory):
+            return directory
+    if is_win():
+        if dir_type == AppLocation.DataDir:
+            return os.path.join(str(os.getenv('APPDATA')), 'openlp', 'data')
+        elif dir_type == AppLocation.LanguageDir:
+            return os.path.dirname(openlp.__file__)
+        return os.path.join(str(os.getenv('APPDATA')), 'openlp')
+    elif is_macosx():
+        if dir_type == AppLocation.DataDir:
+            return os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp', 'Data')
+        elif dir_type == AppLocation.LanguageDir:
+            return os.path.dirname(openlp.__file__)
+        return os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp')
+    else:
+        if dir_type == AppLocation.LanguageDir:
+            for prefix in ['/usr/local', '/usr']:
+                directory = os.path.join(prefix, 'share', 'openlp')
+                if os.path.exists(directory):
+                    return directory
+            return os.path.join('/usr', 'share', 'openlp')
+        if XDG_BASE_AVAILABLE:
+            if dir_type == AppLocation.DataDir:
+                return os.path.join(str(BaseDirectory.xdg_data_home), 'openlp')
+            elif dir_type == AppLocation.CacheDir:
+                return os.path.join(str(BaseDirectory.xdg_cache_home), 'openlp')
+        if dir_type == AppLocation.DataDir:
+            return os.path.join(str(os.getenv('HOME')), '.openlp', 'data')
+        return os.path.join(str(os.getenv('HOME')), '.openlp')

=== added file 'openlp/core/common/db.py'
--- openlp/core/common/db.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/db.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`db` module provides helper functions for database related methods.
+"""
+import sqlalchemy
+import logging
+
+from copy import deepcopy
+
+log = logging.getLogger(__name__)
+
+
+def drop_column(op, tablename, columnname):
+    drop_columns(op, tablename, [columnname])
+
+
+def drop_columns(op, tablename, columns):
+    """
+    Column dropping functionality for SQLite, as there is no DROP COLUMN support in SQLite
+
+    From https://github.com/klugjohannes/alembic-sqlite
+    """
+
+    # get the db engine and reflect database tables
+    engine = op.get_bind()
+    meta = sqlalchemy.MetaData(bind=engine)
+    meta.reflect()
+
+    # create a select statement from the old table
+    old_table = meta.tables[tablename]
+    select = sqlalchemy.sql.select([c for c in old_table.c if c.name not in columns])
+
+    # get remaining columns without table attribute attached
+    remaining_columns = [deepcopy(c) for c in old_table.columns if c.name not in columns]
+    for column in remaining_columns:
+        column.table = None
+
+    # create a temporary new table
+    new_tablename = '{0}_new'.format(tablename)
+    op.create_table(new_tablename, *remaining_columns)
+    meta.reflect()
+    new_table = meta.tables[new_tablename]
+
+    # copy data from old table
+    insert = sqlalchemy.sql.insert(new_table).from_select([c.name for c in remaining_columns], select)
+    engine.execute(insert)
+
+    # drop the old table and rename the new table to take the old tables
+    # position
+    op.drop_table(tablename)
+    op.rename_table(new_tablename, tablename)

=== added file 'openlp/core/common/languagemanager.py'
--- openlp/core/common/languagemanager.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/languagemanager.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,206 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`languagemanager` module provides all the translation settings and language file loading for OpenLP.
+"""
+import locale
+import logging
+import re
+
+from PyQt5 import QtCore, QtWidgets
+
+
+from openlp.core.common import AppLocation, Settings, translate, is_win, is_macosx
+
+log = logging.getLogger(__name__)
+
+ICU_COLLATOR = None
+DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE)
+
+
+class LanguageManager(object):
+    """
+    Helper for Language selection
+    """
+    __qm_list__ = {}
+    auto_language = False
+
+    @staticmethod
+    def get_translator(language):
+        """
+        Set up a translator to use in this instance of OpenLP
+
+        :param language: The language to load into the translator
+        """
+        if LanguageManager.auto_language:
+            language = QtCore.QLocale.system().name()
+        lang_path = AppLocation.get_directory(AppLocation.LanguageDir)
+        app_translator = QtCore.QTranslator()
+        app_translator.load(language, lang_path)
+        # A translator for buttons and other default strings provided by Qt.
+        if not is_win() and not is_macosx():
+            lang_path = QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath)
+        default_translator = QtCore.QTranslator()
+        default_translator.load('qt_%s' % language, lang_path)
+        return app_translator, default_translator
+
+    @staticmethod
+    def find_qm_files():
+        """
+        Find all available language files in this OpenLP install
+        """
+        log.debug('Translation files: %s', AppLocation.get_directory(AppLocation.LanguageDir))
+        trans_dir = QtCore.QDir(AppLocation.get_directory(AppLocation.LanguageDir))
+        file_names = trans_dir.entryList(['*.qm'], QtCore.QDir.Files, QtCore.QDir.Name)
+        # Remove qm files from the list which start with "qt_".
+        file_names = [file_ for file_ in file_names if not file_.startswith('qt_')]
+        return list(map(trans_dir.filePath, file_names))
+
+    @staticmethod
+    def language_name(qm_file):
+        """
+        Load the language name from a language file
+
+        :param qm_file: The file to obtain the name from
+        """
+        translator = QtCore.QTranslator()
+        translator.load(qm_file)
+        return translator.translate('OpenLP.MainWindow', 'English', 'Please add the name of your language here')
+
+    @staticmethod
+    def get_language():
+        """
+        Retrieve a saved language to use from settings
+        """
+        language = Settings().value('core/language')
+        language = str(language)
+        log.info('Language file: \'%s\' Loaded from conf file' % language)
+        if re.match(r'[[].*[]]', language):
+            LanguageManager.auto_language = True
+            language = re.sub(r'[\[\]]', '', language)
+        return language
+
+    @staticmethod
+    def set_language(action, message=True):
+        """
+        Set the language to translate OpenLP into
+
+        :param action:  The language menu option
+        :param message:  Display the message option
+        """
+        language = 'en'
+        if action:
+            action_name = str(action.objectName())
+            if action_name == 'autoLanguageItem':
+                LanguageManager.auto_language = True
+            else:
+                LanguageManager.auto_language = False
+                qm_list = LanguageManager.get_qm_list()
+                language = str(qm_list[action_name])
+        if LanguageManager.auto_language:
+            language = '[%s]' % language
+        Settings().setValue('core/language', language)
+        log.info('Language file: \'%s\' written to conf file' % language)
+        if message:
+            QtWidgets.QMessageBox.information(None,
+                                              translate('OpenLP.LanguageManager', 'Language'),
+                                              translate('OpenLP.LanguageManager',
+                                                        'Please restart OpenLP to use your new language setting.'))
+
+    @staticmethod
+    def init_qm_list():
+        """
+        Initialise the list of available translations
+        """
+        LanguageManager.__qm_list__ = {}
+        qm_files = LanguageManager.find_qm_files()
+        for counter, qmf in enumerate(qm_files):
+            reg_ex = QtCore.QRegExp("^.*i18n/(.*).qm")
+            if reg_ex.exactMatch(qmf):
+                name = '%s' % reg_ex.cap(1)
+                LanguageManager.__qm_list__['%#2i %s' % (counter + 1, LanguageManager.language_name(qmf))] = name
+
+    @staticmethod
+    def get_qm_list():
+        """
+        Return the list of available translations
+        """
+        if not LanguageManager.__qm_list__:
+            LanguageManager.init_qm_list()
+        return LanguageManager.__qm_list__
+
+
+def format_time(text, local_time):
+    """
+    Workaround for Python built-in time formatting function time.strftime().
+
+    time.strftime() accepts only ascii characters. This function accepts
+    unicode string and passes individual % placeholders to time.strftime().
+    This ensures only ascii characters are passed to time.strftime().
+
+    :param text:  The text to be processed.
+    :param local_time: The time to be used to add to the string.  This is a time object
+    """
+
+    def match_formatting(match):
+        """
+        Format the match
+        """
+        return local_time.strftime(match.group())
+
+    return re.sub('\%[a-zA-Z]', match_formatting, text)
+
+
+def get_locale_key(string):
+    """
+    Creates a key for case insensitive, locale aware string sorting.
+
+    :param string: The corresponding string.
+    """
+    string = string.lower()
+    # ICU is the prefered way to handle locale sort key, we fallback to locale.strxfrm which will work in most cases.
+    global ICU_COLLATOR
+    try:
+        if ICU_COLLATOR is None:
+            import icu
+            language = LanguageManager.get_language()
+            icu_locale = icu.Locale(language)
+            ICU_COLLATOR = icu.Collator.createInstance(icu_locale)
+        return ICU_COLLATOR.getSortKey(string)
+    except:
+        return locale.strxfrm(string).encode()
+
+
+def get_natural_key(string):
+    """
+    Generate a key for locale aware natural string sorting.
+
+    :param string: string to be sorted by
+    Returns a list of string compare keys and integers.
+    """
+    key = DIGITS_OR_NONDIGITS.findall(string)
+    key = [int(part) if part.isdigit() else get_locale_key(part) for part in key]
+    # Python 3 does not support comparison of different types anymore. So make sure, that we do not compare str
+    # and int.
+    if string and string[0].isdigit():
+        return [b''] + key
+    return key

=== added file 'openlp/core/common/openlpmixin.py'
--- openlp/core/common/openlpmixin.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/openlpmixin.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Provide Error Handling and login Services
+"""
+import logging
+import inspect
+
+from openlp.core.common import trace_error_handler
+
+DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed',
+                       'preview_size_changed', 'resizeEvent']
+
+
+class OpenLPMixin(object):
+    """
+    Base Calling object for OpenLP classes.
+    """
+    def __init__(self, *args, **kwargs):
+        super(OpenLPMixin, self).__init__(*args, **kwargs)
+        self.logger = logging.getLogger("%s.%s" % (self.__module__, self.__class__.__name__))
+        if self.logger.getEffectiveLevel() == logging.DEBUG:
+            for name, m in inspect.getmembers(self, inspect.ismethod):
+                if name not in DO_NOT_TRACE_EVENTS:
+                    if not name.startswith("_") and not name.startswith("log"):
+                        setattr(self, name, self.logging_wrapper(m, self))
+
+    def logging_wrapper(self, func, parent):
+        """
+        Code to added debug wrapper to work on called functions within a decorated class.
+        """
+        def wrapped(*args, **kwargs):
+            parent.logger.debug("Entering %s" % func.__name__)
+            try:
+                return func(*args, **kwargs)
+            except Exception as e:
+                if parent.logger.getEffectiveLevel() <= logging.ERROR:
+                    parent.logger.error('Exception in %s : %s' % (func.__name__, e))
+                raise e
+        return wrapped
+
+    def log_debug(self, message):
+        """
+        Common log debug handler
+        """
+        self.logger.debug(message)
+
+    def log_info(self, message):
+        """
+        Common log info handler
+        """
+        self.logger.info(message)
+
+    def log_error(self, message):
+        """
+        Common log error handler which prints the calling path
+        """
+        trace_error_handler(self.logger)
+        self.logger.error(message)
+
+    def log_exception(self, message):
+        """
+        Common log exception handler which prints the calling path
+        """
+        trace_error_handler(self.logger)
+        self.logger.exception(message)

=== added file 'openlp/core/common/registry.py'
--- openlp/core/common/registry.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/registry.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Provide Registry Services
+"""
+import logging
+import sys
+
+from openlp.core.common import trace_error_handler
+
+log = logging.getLogger(__name__)
+
+
+class Registry(object):
+    """
+    This is the Component Registry.  It is a singleton object and is used to provide a look up service for common
+    objects.
+    """
+    log.info('Registry loaded')
+    __instance__ = None
+
+    def __new__(cls):
+        """
+        Re-implement the __new__ method to make sure we create a true singleton.
+        """
+        if not cls.__instance__:
+            cls.__instance__ = object.__new__(cls)
+        return cls.__instance__
+
+    @classmethod
+    def create(cls):
+        """
+        The constructor for the component registry providing a single registry of objects.
+        """
+        log.info('Registry Initialising')
+        registry = cls()
+        registry.service_list = {}
+        registry.functions_list = {}
+        # Allow the tests to remove Registry entries but not the live system
+        registry.running_under_test = 'nose' in sys.argv[0]
+        registry.initialising = True
+        return registry
+
+    def get(self, key):
+        """
+        Extracts the registry value from the list based on the key passed in
+
+        :param key: The service to be retrieved.
+        """
+        if key in self.service_list:
+            return self.service_list[key]
+        else:
+            if not self.initialising:
+                trace_error_handler(log)
+                log.error('Service %s not found in list' % key)
+                raise KeyError('Service %s not found in list' % key)
+
+    def register(self, key, reference):
+        """
+        Registers a component against a key.
+
+        :param key: The service to be created this is usually a major class like "renderer" or "main_window" .
+        :param reference: The service address to be saved.
+        """
+        if key in self.service_list:
+            trace_error_handler(log)
+            log.error('Duplicate service exception %s' % key)
+            raise KeyError('Duplicate service exception %s' % key)
+        else:
+            self.service_list[key] = reference
+
+    def remove(self, key):
+        """
+        Removes the registry value from the list based on the key passed in (Only valid and active for testing
+        framework).
+
+        :param key: The service to be deleted.
+        """
+        if key in self.service_list:
+            del self.service_list[key]
+
+    def register_function(self, event, function):
+        """
+        Register an event and associated function to be called
+
+        :param event:  The function description like "live_display_hide" where a number of places in the code
+            will/may need to respond to a single action and the caller does not need to understand or know about the
+            recipients.
+        :param function: The function to be called when the event happens.
+        """
+        if event in self.functions_list:
+            self.functions_list[event].append(function)
+        else:
+            self.functions_list[event] = [function]
+
+    def remove_function(self, event, function):
+        """
+        Remove an event and associated handler
+
+        :param event: The function description..
+        :param function: The function to be called when the event happens.
+        """
+        if event in self.functions_list:
+            self.functions_list[event].remove(function)
+
+    def execute(self, event, *args, **kwargs):
+        """
+        Execute all the handlers associated with the event and return an array of results.
+
+        :param event: The function to be processed
+        :param args:  Parameters to be passed to the function.
+        :param kwargs: Parameters to be passed to the function.
+        """
+        results = []
+        if event in self.functions_list:
+            for function in self.functions_list[event]:
+                try:
+                    result = function(*args, **kwargs)
+                    if result:
+                        results.append(result)
+                except TypeError:
+                    # Who has called me can help in debugging
+                    trace_error_handler(log)
+                    log.exception('Exception for function %s', function)
+        else:
+            trace_error_handler(log)
+            log.error("Event %s called but not registered" % event)
+        return results

=== added file 'openlp/core/common/registrymixin.py'
--- openlp/core/common/registrymixin.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/registrymixin.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Provide Registry Services
+"""
+from openlp.core.common import Registry, de_hump
+
+
+class RegistryMixin(object):
+    """
+    This adds registry components to classes to use at run time.
+    """
+    def __init__(self, parent):
+        """
+        Register the class and bootstrap hooks.
+        """
+        try:
+            super(RegistryMixin, self).__init__(parent)
+        except TypeError:
+            super(RegistryMixin, self).__init__()
+        Registry().register(de_hump(self.__class__.__name__), self)
+        Registry().register_function('bootstrap_initialise', self.bootstrap_initialise)
+        Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up)
+
+    def bootstrap_initialise(self):
+        """
+        Dummy method to be overridden
+        """
+        pass
+
+    def bootstrap_post_set_up(self):
+        """
+        Dummy method to be overridden
+        """
+        pass

=== added file 'openlp/core/common/registryproperties.py'
--- openlp/core/common/registryproperties.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/registryproperties.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Provide Registry values for adding to classes
+"""
+from openlp.core.common import Registry, is_win
+
+
+class RegistryProperties(object):
+    """
+    This adds registry components to classes to use at run time.
+    """
+
+    @property
+    def application(self):
+        """
+        Adds the openlp to the class dynamically.
+        Windows needs to access the application in a dynamic manner.
+        """
+        if is_win():
+            return Registry().get('application')
+        else:
+            if not hasattr(self, '_application') or not self._application:
+                self._application = Registry().get('application')
+            return self._application
+
+    @property
+    def plugin_manager(self):
+        """
+        Adds the plugin manager to the class dynamically
+        """
+        if not hasattr(self, '_plugin_manager') or not self._plugin_manager:
+            self._plugin_manager = Registry().get('plugin_manager')
+        return self._plugin_manager
+
+    @property
+    def image_manager(self):
+        """
+        Adds the image manager to the class dynamically
+        """
+        if not hasattr(self, '_image_manager') or not self._image_manager:
+            self._image_manager = Registry().get('image_manager')
+        return self._image_manager
+
+    @property
+    def media_controller(self):
+        """
+        Adds the media controller to the class dynamically
+        """
+        if not hasattr(self, '_media_controller') or not self._media_controller:
+            self._media_controller = Registry().get('media_controller')
+        return self._media_controller
+
+    @property
+    def service_manager(self):
+        """
+        Adds the service manager to the class dynamically
+        """
+        if not hasattr(self, '_service_manager') or not self._service_manager:
+            self._service_manager = Registry().get('service_manager')
+        return self._service_manager
+
+    @property
+    def preview_controller(self):
+        """
+        Adds the preview controller to the class dynamically
+        """
+        if not hasattr(self, '_preview_controller') or not self._preview_controller:
+            self._preview_controller = Registry().get('preview_controller')
+        return self._preview_controller
+
+    @property
+    def live_controller(self):
+        """
+        Adds the live controller to the class dynamically
+        """
+        if not hasattr(self, '_live_controller') or not self._live_controller:
+            self._live_controller = Registry().get('live_controller')
+        return self._live_controller
+
+    @property
+    def main_window(self):
+        """
+        Adds the main window to the class dynamically
+        """
+        if not hasattr(self, '_main_window') or not self._main_window:
+            self._main_window = Registry().get('main_window')
+        return self._main_window
+
+    @property
+    def renderer(self):
+        """
+        Adds the Renderer to the class dynamically
+        """
+        if not hasattr(self, '_renderer') or not self._renderer:
+            self._renderer = Registry().get('renderer')
+        return self._renderer
+
+    @property
+    def theme_manager(self):
+        """
+        Adds the theme manager to the class dynamically
+        """
+        if not hasattr(self, '_theme_manager') or not self._theme_manager:
+            self._theme_manager = Registry().get('theme_manager')
+        return self._theme_manager
+
+    @property
+    def settings_form(self):
+        """
+        Adds the settings form to the class dynamically
+        """
+        if not hasattr(self, '_settings_form') or not self._settings_form:
+            self._settings_form = Registry().get('settings_form')
+        return self._settings_form
+
+    @property
+    def alerts_manager(self):
+        """
+        Adds the alerts manager to the class dynamically
+        """
+        if not hasattr(self, '_alerts_manager') or not self._alerts_manager:
+            self._alerts_manager = Registry().get('alerts_manager')
+        return self._alerts_manager
+
+    @property
+    def projector_manager(self):
+        """
+        Adds the projector manager to the class dynamically
+        """
+        if not hasattr(self, '_projector_manager') or not self._projector_manager:
+            self._projector_manager = Registry().get('projector_manager')
+        return self._projector_manager

=== added file 'openlp/core/common/settings.py'
--- openlp/core/common/settings.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/settings.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,496 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+This class contains the core default settings.
+"""
+import datetime
+import logging
+import os
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import ThemeLevel, SlideLimits, UiStrings, is_win, is_linux
+
+
+log = logging.getLogger(__name__)
+
+
+# Fix for bug #1014422.
+X11_BYPASS_DEFAULT = True
+if is_linux():
+    # Default to False on Gnome.
+    X11_BYPASS_DEFAULT = bool(not os.environ.get('GNOME_DESKTOP_SESSION_ID'))
+    # Default to False on Xfce.
+    if os.environ.get('DESKTOP_SESSION') == 'xfce':
+        X11_BYPASS_DEFAULT = False
+
+
+def recent_files_conv(value):
+    """
+    If the value is not a list convert it to a list
+    :param value: Value to convert
+    :return: value as a List
+    """
+    if isinstance(value, list):
+        return value
+    elif isinstance(value, str):
+        return [value]
+    elif isinstance(value, bytes):
+        return [value.decode()]
+    return []
+
+
+def media_players_conv(string):
+    """
+    If phonon is in the setting string replace it with system
+    :param string: String to convert
+    :return: Converted string
+    """
+    values = string.split(',')
+    for index, value in enumerate(values):
+        if value == 'phonon':
+            values[index] = 'system'
+    string = ','.join(values)
+    return string
+
+
+class Settings(QtCore.QSettings):
+    """
+    Class to wrap QSettings.
+
+    * Exposes all the methods of QSettings.
+    * Adds functionality for OpenLP Portable. If the ``defaultFormat`` is set to
+      ``IniFormat``, and the path to the Ini file is set using ``set_filename``,
+      then the Settings constructor (without any arguments) will create a Settings
+      object for accessing settings stored in that Ini file.
+
+    ``__default_settings__``
+        This dict contains all core settings with their default values.
+
+    ``__obsolete_settings__``
+        Each entry is structured in the following way::
+
+            ('general/enable slide loop', 'advanced/slide limits', [(SlideLimits.Wrap, True), (SlideLimits.End, False)])
+
+        The first entry is the *old key*; it will be removed.
+
+        The second entry is the *new key*; we will add it to the config. If this is just an empty string, we just remove
+        the old key. The last entry is a list containing two-pair tuples. If the list is empty, no conversion is made.
+        If the first value is callable i.e. a function, the function will be called with the old setting's value.
+        Otherwise each pair describes how to convert the old setting's value::
+
+            (SlideLimits.Wrap, True)
+
+        This means, that if the value of ``general/enable slide loop`` is equal (``==``) ``True`` then we set
+        ``advanced/slide limits`` to ``SlideLimits.Wrap``. **NOTE**, this means that the rules have to cover all cases!
+        So, if the type of the old value is bool, then there must be two rules.
+    """
+    __default_settings__ = {
+        'advanced/add page break': False,
+        'advanced/alternate rows': not is_win(),
+        'advanced/current media plugin': -1,
+        'advanced/data path': '',
+        'advanced/default color': '#ffffff',
+        'advanced/default image': ':/graphics/openlp-splash-screen.png',
+        # 7 stands for now, 0 to 6 is Monday to Sunday.
+        'advanced/default service day': 7,
+        'advanced/default service enabled': True,
+        'advanced/default service hour': 11,
+        'advanced/default service minute': 0,
+        'advanced/default service name': UiStrings().DefaultServiceName,
+        'advanced/display size': 0,
+        'advanced/double click live': False,
+        'advanced/enable exit confirmation': True,
+        'advanced/expand service item': False,
+        'advanced/slide max height': 0,
+        'advanced/hide mouse': True,
+        'advanced/is portable': False,
+        'advanced/max recent files': 20,
+        'advanced/print file meta data': False,
+        'advanced/print notes': False,
+        'advanced/print slide text': False,
+        'advanced/recent file count': 4,
+        'advanced/save current plugin': False,
+        'advanced/slide limits': SlideLimits.End,
+        'advanced/single click preview': False,
+        'advanced/single click service preview': False,
+        'advanced/x11 bypass wm': X11_BYPASS_DEFAULT,
+        'advanced/search as type': True,
+        'crashreport/last directory': '',
+        'formattingTags/html_tags': '',
+        'core/audio repeat list': False,
+        'core/auto open': False,
+        'core/auto preview': False,
+        'core/audio start paused': True,
+        'core/auto unblank': False,
+        'core/blank warning': False,
+        'core/ccli number': '',
+        'core/has run wizard': False,
+        'core/language': '[en]',
+        'core/last version test': '',
+        'core/loop delay': 5,
+        'core/recent files': [],
+        'core/save prompt': False,
+        'core/screen blank': False,
+        'core/show splash': True,
+        'core/songselect password': '',
+        'core/songselect username': '',
+        'core/update check': True,
+        'core/view mode': 'default',
+        # The other display settings (display position and dimensions) are defined in the ScreenList class due to a
+        # circular dependency.
+        'core/display on monitor': True,
+        'core/override position': False,
+        'core/application version': '0.0',
+        'images/background color': '#000000',
+        'media/players': 'system,webkit',
+        'media/override player': QtCore.Qt.Unchecked,
+        'players/background color': '#000000',
+        'servicemanager/last directory': '',
+        'servicemanager/last file': '',
+        'servicemanager/service theme': '',
+        'SettingsImport/file_date_created': datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
+        'SettingsImport/Make_Changes': 'At_Own_RISK',
+        'SettingsImport/type': 'OpenLP_settings_export',
+        'SettingsImport/version': '',
+        'themes/global theme': '',
+        'themes/last directory': '',
+        'themes/last directory export': '',
+        'themes/last directory import': '',
+        'themes/theme level': ThemeLevel.Song,
+        'themes/wrap footer': False,
+        'user interface/live panel': True,
+        'user interface/live splitter geometry': QtCore.QByteArray(),
+        'user interface/lock panel': False,
+        'user interface/main window geometry': QtCore.QByteArray(),
+        'user interface/main window position': QtCore.QPoint(0, 0),
+        'user interface/main window splitter geometry': QtCore.QByteArray(),
+        'user interface/main window state': QtCore.QByteArray(),
+        'user interface/preview panel': True,
+        'user interface/preview splitter geometry': QtCore.QByteArray(),
+        'projector/db type': 'sqlite',
+        'projector/db username': '',
+        'projector/db password': '',
+        'projector/db hostname': '',
+        'projector/db database': '',
+        'projector/enable': True,
+        'projector/connect on start': False,
+        'projector/last directory import': '',
+        'projector/last directory export': '',
+        'projector/poll time': 20,  # PJLink  timeout is 30 seconds
+        'projector/socket timeout': 5,  # 5 second socket timeout
+        'projector/source dialog type': 0  # Source select dialog box type
+    }
+    __file_path__ = ''
+    __obsolete_settings__ = [
+        # Changed during 2.2.x development.
+        # ('advanced/stylesheet fix', '', []),
+        # ('general/recent files', 'core/recent files', [(recent_files_conv, None)]),
+        ('songs/search as type', 'advanced/search as type', []),
+        ('media/players', 'media/players_temp', [(media_players_conv, None)]),  # Convert phonon to system
+        ('media/players_temp', 'media/players', [])  # Move temp setting from above to correct setting
+    ]
+
+    @staticmethod
+    def extend_default_settings(default_values):
+        """
+        Static method to merge the given ``default_values`` with the ``Settings.__default_settings__``.
+
+        :param default_values: A dict with setting keys and their default values.
+        """
+        Settings.__default_settings__.update(default_values)
+
+    @staticmethod
+    def set_filename(ini_file):
+        """
+        Sets the complete path to an Ini file to be used by Settings objects.
+
+        Does not affect existing Settings objects.
+        """
+        Settings.__file_path__ = ini_file
+
+    @staticmethod
+    def set_up_default_values():
+        """
+        This static method is called on start up. It is used to perform any operation on the __default_settings__ dict.
+        """
+        # Make sure the string is translated (when building the dict the string is not translated because the translate
+        # function was not set up as this stage).
+        Settings.__default_settings__['advanced/default service name'] = UiStrings().DefaultServiceName
+
+    def __init__(self, *args):
+        """
+        Constructor which checks if this should be a native settings object, or an INI file.
+        """
+        if not args and Settings.__file_path__ and Settings.defaultFormat() == Settings.IniFormat:
+            QtCore.QSettings.__init__(self, Settings.__file_path__, Settings.IniFormat)
+        else:
+            QtCore.QSettings.__init__(self, *args)
+        # Add shortcuts here so QKeySequence has a QApplication instance to use.
+        Settings.__default_settings__.update({
+            'shortcuts/aboutItem': [QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_F1)],
+            'shortcuts/addToService': [],
+            'shortcuts/audioPauseItem': [],
+            'shortcuts/displayTagItem': [],
+            'shortcuts/blankScreen': [QtGui.QKeySequence(QtCore.Qt.Key_Period)],
+            'shortcuts/collapse': [QtGui.QKeySequence(QtCore.Qt.Key_Minus)],
+            'shortcuts/desktopScreen': [QtGui.QKeySequence(QtCore.Qt.Key_D)],
+            'shortcuts/delete': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)],
+            'shortcuts/down': [QtGui.QKeySequence(QtCore.Qt.Key_Down)],
+            'shortcuts/editSong': [],
+            'shortcuts/escapeItem': [QtGui.QKeySequence(QtCore.Qt.Key_Escape)],
+            'shortcuts/expand': [QtGui.QKeySequence(QtCore.Qt.Key_Plus)],
+            'shortcuts/exportThemeItem': [],
+            'shortcuts/fileNewItem': [QtGui.QKeySequence(QtGui.QKeySequence.New)],
+            'shortcuts/fileSaveAsItem': [QtGui.QKeySequence(QtGui.QKeySequence.SaveAs)],
+            'shortcuts/fileExitItem': [QtGui.QKeySequence(QtGui.QKeySequence.Quit)],
+            'shortcuts/fileSaveItem': [QtGui.QKeySequence(QtGui.QKeySequence.Save)],
+            'shortcuts/fileOpenItem': [QtGui.QKeySequence(QtGui.QKeySequence.Open)],
+            'shortcuts/goLive': [],
+            'shortcuts/importThemeItem': [],
+            'shortcuts/importBibleItem': [],
+            'shortcuts/listViewBiblesDeleteItem': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)],
+            'shortcuts/listViewBiblesPreviewItem': [QtGui.QKeySequence(QtCore.Qt.Key_Return),
+                                                    QtGui.QKeySequence(QtCore.Qt.Key_Enter)],
+            'shortcuts/listViewBiblesLiveItem': [QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Return),
+                                                 QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Enter)],
+            'shortcuts/listViewBiblesServiceItem': [QtGui.QKeySequence(QtCore.Qt.Key_Plus),
+                                                    QtGui.QKeySequence(QtCore.Qt.Key_Equal)],
+            'shortcuts/listViewCustomDeleteItem': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)],
+            'shortcuts/listViewCustomPreviewItem': [QtGui.QKeySequence(QtCore.Qt.Key_Return),
+                                                    QtGui.QKeySequence(QtCore.Qt.Key_Enter)],
+            'shortcuts/listViewCustomLiveItem': [QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Return),
+                                                 QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Enter)],
+            'shortcuts/listViewCustomServiceItem': [QtGui.QKeySequence(QtCore.Qt.Key_Plus),
+                                                    QtGui.QKeySequence(QtCore.Qt.Key_Equal)],
+            'shortcuts/listViewImagesDeleteItem': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)],
+            'shortcuts/listViewImagesPreviewItem': [QtGui.QKeySequence(QtCore.Qt.Key_Return),
+                                                    QtGui.QKeySequence(QtCore.Qt.Key_Enter)],
+            'shortcuts/listViewImagesLiveItem': [QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Return),
+                                                 QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Enter)],
+            'shortcuts/listViewImagesServiceItem': [QtGui.QKeySequence(QtCore.Qt.Key_Plus),
+                                                    QtGui.QKeySequence(QtCore.Qt.Key_Equal)],
+            'shortcuts/listViewMediaDeleteItem': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)],
+            'shortcuts/listViewMediaPreviewItem': [QtGui.QKeySequence(QtCore.Qt.Key_Return),
+                                                   QtGui.QKeySequence(QtCore.Qt.Key_Enter)],
+            'shortcuts/listViewMediaLiveItem': [QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Return),
+                                                QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Enter)],
+            'shortcuts/listViewMediaServiceItem': [QtGui.QKeySequence(QtCore.Qt.Key_Plus),
+                                                   QtGui.QKeySequence(QtCore.Qt.Key_Equal)],
+            'shortcuts/listViewPresentationsDeleteItem': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)],
+            'shortcuts/listViewPresentationsPreviewItem': [QtGui.QKeySequence(QtCore.Qt.Key_Return),
+                                                           QtGui.QKeySequence(QtCore.Qt.Key_Enter)],
+            'shortcuts/listViewPresentationsLiveItem': [QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Return),
+                                                        QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Enter)],
+            'shortcuts/listViewPresentationsServiceItem': [QtGui.QKeySequence(QtCore.Qt.Key_Plus),
+                                                           QtGui.QKeySequence(QtCore.Qt.Key_Equal)],
+            'shortcuts/listViewSongsDeleteItem': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)],
+            'shortcuts/listViewSongsPreviewItem': [QtGui.QKeySequence(QtCore.Qt.Key_Return),
+                                                   QtGui.QKeySequence(QtCore.Qt.Key_Enter)],
+            'shortcuts/listViewSongsLiveItem': [QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Return),
+                                                QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Enter)],
+            'shortcuts/listViewSongsServiceItem': [QtGui.QKeySequence(QtCore.Qt.Key_Plus),
+                                                   QtGui.QKeySequence(QtCore.Qt.Key_Equal)],
+            'shortcuts/lockPanel': [],
+            'shortcuts/modeDefaultItem': [],
+            'shortcuts/modeLiveItem': [],
+            'shortcuts/make_live': [QtGui.QKeySequence(QtCore.Qt.Key_Return), QtGui.QKeySequence(QtCore.Qt.Key_Enter)],
+            'shortcuts/moveUp': [QtGui.QKeySequence(QtCore.Qt.Key_PageUp)],
+            'shortcuts/moveTop': [QtGui.QKeySequence(QtCore.Qt.Key_Home)],
+            'shortcuts/modeSetupItem': [],
+            'shortcuts/moveBottom': [QtGui.QKeySequence(QtCore.Qt.Key_End)],
+            'shortcuts/moveDown': [QtGui.QKeySequence(QtCore.Qt.Key_PageDown)],
+            'shortcuts/nextTrackItem': [],
+            'shortcuts/nextItem_live': [QtGui.QKeySequence(QtCore.Qt.Key_Down),
+                                        QtGui.QKeySequence(QtCore.Qt.Key_PageDown)],
+            'shortcuts/nextItem_preview': [QtGui.QKeySequence(QtCore.Qt.Key_Down),
+                                           QtGui.QKeySequence(QtCore.Qt.Key_PageDown)],
+            'shortcuts/nextService': [QtGui.QKeySequence(QtCore.Qt.Key_Right)],
+            'shortcuts/newService': [],
+            'shortcuts/offlineHelpItem': [QtGui.QKeySequence(QtGui.QKeySequence.HelpContents)],
+            'shortcuts/onlineHelpItem': [QtGui.QKeySequence(QtGui.QKeySequence.HelpContents)],
+            'shortcuts/openService': [],
+            'shortcuts/saveService': [],
+            'shortcuts/previousItem_live': [QtGui.QKeySequence(QtCore.Qt.Key_Up),
+                                            QtGui.QKeySequence(QtCore.Qt.Key_PageUp)],
+            'shortcuts/playbackPause': [],
+            'shortcuts/playbackPlay': [],
+            'shortcuts/playbackStop': [],
+            'shortcuts/playSlidesLoop': [],
+            'shortcuts/playSlidesOnce': [],
+            'shortcuts/previousService': [QtGui.QKeySequence(QtCore.Qt.Key_Left)],
+            'shortcuts/previousItem_preview': [QtGui.QKeySequence(QtCore.Qt.Key_Up),
+                                               QtGui.QKeySequence(QtCore.Qt.Key_PageUp)],
+            'shortcuts/printServiceItem': [QtGui.QKeySequence(QtGui.QKeySequence.Print)],
+            'shortcuts/songExportItem': [],
+            'shortcuts/songUsageStatus': [QtGui.QKeySequence(QtCore.Qt.Key_F4)],
+            'shortcuts/searchShortcut': [QtGui.QKeySequence(QtGui.QKeySequence.Find)],
+            'shortcuts/settingsShortcutsItem': [],
+            'shortcuts/settingsImportItem': [],
+            'shortcuts/settingsPluginListItem': [QtGui.QKeySequence(QtCore.Qt.ALT + QtCore.Qt.Key_F7)],
+            'shortcuts/songUsageDelete': [],
+            'shortcuts/settingsConfigureItem': [QtGui.QKeySequence(QtGui.QKeySequence.Preferences)],
+            'shortcuts/shortcutAction_B': [QtGui.QKeySequence(QtCore.Qt.Key_B)],
+            'shortcuts/shortcutAction_C': [QtGui.QKeySequence(QtCore.Qt.Key_C)],
+            'shortcuts/shortcutAction_E': [QtGui.QKeySequence(QtCore.Qt.Key_E)],
+            'shortcuts/shortcutAction_I': [QtGui.QKeySequence(QtCore.Qt.Key_I)],
+            'shortcuts/shortcutAction_O': [QtGui.QKeySequence(QtCore.Qt.Key_O)],
+            'shortcuts/shortcutAction_P': [QtGui.QKeySequence(QtCore.Qt.Key_P)],
+            'shortcuts/shortcutAction_V': [QtGui.QKeySequence(QtCore.Qt.Key_V)],
+            'shortcuts/shortcutAction_0': [QtGui.QKeySequence(QtCore.Qt.Key_0)],
+            'shortcuts/shortcutAction_1': [QtGui.QKeySequence(QtCore.Qt.Key_1)],
+            'shortcuts/shortcutAction_2': [QtGui.QKeySequence(QtCore.Qt.Key_2)],
+            'shortcuts/shortcutAction_3': [QtGui.QKeySequence(QtCore.Qt.Key_3)],
+            'shortcuts/shortcutAction_4': [QtGui.QKeySequence(QtCore.Qt.Key_4)],
+            'shortcuts/shortcutAction_5': [QtGui.QKeySequence(QtCore.Qt.Key_5)],
+            'shortcuts/shortcutAction_6': [QtGui.QKeySequence(QtCore.Qt.Key_6)],
+            'shortcuts/shortcutAction_7': [QtGui.QKeySequence(QtCore.Qt.Key_7)],
+            'shortcuts/shortcutAction_8': [QtGui.QKeySequence(QtCore.Qt.Key_8)],
+            'shortcuts/shortcutAction_9': [QtGui.QKeySequence(QtCore.Qt.Key_9)],
+            'shortcuts/settingsExportItem': [],
+            'shortcuts/songUsageReport': [],
+            'shortcuts/songImportItem': [],
+            'shortcuts/themeScreen': [QtGui.QKeySequence(QtCore.Qt.Key_T)],
+            'shortcuts/toolsReindexItem': [],
+            'shortcuts/toolsFindDuplicates': [],
+            'shortcuts/toolsAlertItem': [QtGui.QKeySequence(QtCore.Qt.Key_F7)],
+            'shortcuts/toolsFirstTimeWizard': [],
+            'shortcuts/toolsOpenDataFolder': [],
+            'shortcuts/toolsAddToolItem': [],
+            'shortcuts/updateThemeImages': [],
+            'shortcuts/up': [QtGui.QKeySequence(QtCore.Qt.Key_Up)],
+            'shortcuts/viewProjectorManagerItem': [QtGui.QKeySequence(QtCore.Qt.Key_F6)],
+            'shortcuts/viewThemeManagerItem': [QtGui.QKeySequence(QtCore.Qt.Key_F10)],
+            'shortcuts/viewMediaManagerItem': [QtGui.QKeySequence(QtCore.Qt.Key_F8)],
+            'shortcuts/viewPreviewPanel': [QtGui.QKeySequence(QtCore.Qt.Key_F11)],
+            'shortcuts/viewLivePanel': [QtGui.QKeySequence(QtCore.Qt.Key_F12)],
+            'shortcuts/viewServiceManagerItem': [QtGui.QKeySequence(QtCore.Qt.Key_F9)],
+            'shortcuts/webSiteItem': []
+        })
+
+    def get_default_value(self, key):
+        """
+        Get the default value of the given key
+        """
+        if self.group():
+            key = self.group() + '/' + key
+        return Settings.__default_settings__[key]
+
+    def remove_obsolete_settings(self):
+        """
+        This method is only called to clean up the config. It removes old settings and it renames settings. See
+        ``__obsolete_settings__`` for more details.
+        """
+        for old_key, new_key, rules in Settings.__obsolete_settings__:
+            # Once removed we don't have to do this again.
+            if self.contains(old_key):
+                if new_key:
+                    # Get the value of the old_key.
+                    old_value = super(Settings, self).value(old_key)
+                    # When we want to convert the value, we have to figure out the default value (because we cannot get
+                    # the default value from the central settings dict.
+                    if rules:
+                        default_value = rules[0][1]
+                        old_value = self._convert_value(old_value, default_value)
+                    # Iterate over our rules and check what the old_value should be "converted" to.
+                    for new, old in rules:
+                        # If the value matches with the condition (rule), then use the provided value. This is used to
+                        # convert values. E. g. an old value 1 results in True, and 0 in False.
+                        if callable(new):
+                            old_value = new(old_value)
+                        elif old == old_value:
+                            old_value = new
+                            break
+                    self.setValue(new_key, old_value)
+                self.remove(old_key)
+
+    def value(self, key):
+        """
+        Returns the value for the given ``key``. The returned ``value`` is of the same type as the default value in the
+        *Settings.__default_settings__* dict.
+
+        :param key: The key to return the value from.
+        """
+        # if group() is not empty the group has not been specified together with the key.
+        if self.group():
+            default_value = Settings.__default_settings__[self.group() + '/' + key]
+        else:
+            default_value = Settings.__default_settings__[key]
+        setting = super(Settings, self).value(key, default_value)
+        return self._convert_value(setting, default_value)
+
+    def _convert_value(self, setting, default_value):
+        """
+        This converts the given ``setting`` to the type of the given ``default_value``.
+
+        :param setting: The setting to convert. This could be ``true`` for example.Settings()
+        :param default_value: Indication the type the setting should be converted to. For example ``True``
+        (type is boolean), meaning that we convert the string ``true`` to a python boolean.
+
+        **Note**, this method only converts a few types and might need to be extended if a certain type is missing!
+        """
+        # Handle 'None' type (empty value) properly.
+        if setting is None:
+            # An empty string saved to the settings results in a None type being returned.
+            # Convert it to empty unicode string.
+            if isinstance(default_value, str):
+                return ''
+            # An empty list saved to the settings results in a None type being returned.
+            else:
+                return []
+        # Convert the setting to the correct type.
+        if isinstance(default_value, bool):
+            if isinstance(setting, bool):
+                return setting
+            # Sometimes setting is string instead of a boolean.
+            return setting == 'true'
+        if isinstance(default_value, int):
+            return int(setting)
+        return setting
+
+    def get_files_from_config(self, plugin):
+        """
+        This removes the settings needed for old way we saved files (e. g. the image paths for the image plugin). A list
+        of file paths are returned.
+
+         **Note**: Only a list of paths is returned; this does not convert anything!
+
+         :param plugin: The Plugin object.The caller has to convert/save the list himself; o
+        """
+        files_list = []
+        # We need QSettings instead of Settings here to bypass our central settings dict.
+        # Do NOT do this anywhere else!
+        settings = QtCore.QSettings(self.fileName(), Settings.IniFormat)
+        settings.beginGroup(plugin.settings_section)
+        if settings.contains('%s count' % plugin.name):
+            # Get the count.
+            list_count = int(settings.value('%s count' % plugin.name, 0))
+            if list_count:
+                for counter in range(list_count):
+                    # The keys were named e. g.: "image 0"
+                    item = settings.value('%s %d' % (plugin.name, counter), '')
+                    if item:
+                        files_list.append(item)
+                    settings.remove('%s %d' % (plugin.name, counter))
+            settings.remove('%s count' % plugin.name)
+        settings.endGroup()
+        return files_list

=== added file 'openlp/core/common/uistrings.py'
--- openlp/core/common/uistrings.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/uistrings.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`uistrings` module provides standard strings for OpenLP.
+"""
+import logging
+
+from openlp.core.common import translate
+
+
+log = logging.getLogger(__name__)
+
+
+class UiStrings(object):
+    """
+    Provide standard strings for objects to use.
+    """
+    __instance__ = None
+
+    def __new__(cls):
+        """
+        Override the default object creation method to return a single instance.
+        """
+        if not cls.__instance__:
+            cls.__instance__ = object.__new__(cls)
+        return cls.__instance__
+
+    def __init__(self):
+        """
+        These strings should need a good reason to be retranslated elsewhere.
+        Should some/more/less of these have an &amp; attached?
+        """
+        self.About = translate('OpenLP.Ui', 'About')
+        self.Add = translate('OpenLP.Ui', '&Add')
+        self.AddGroup = translate('OpenLP.Ui', 'Add group')
+        self.Advanced = translate('OpenLP.Ui', 'Advanced')
+        self.AllFiles = translate('OpenLP.Ui', 'All Files')
+        self.Automatic = translate('OpenLP.Ui', 'Automatic')
+        self.BackgroundColor = translate('OpenLP.Ui', 'Background Color')
+        self.Bottom = translate('OpenLP.Ui', 'Bottom')
+        self.Browse = translate('OpenLP.Ui', 'Browse...')
+        self.Cancel = translate('OpenLP.Ui', 'Cancel')
+        self.CCLINumberLabel = translate('OpenLP.Ui', 'CCLI number:')
+        self.CCLISongNumberLabel = translate('OpenLP.Ui', 'CCLI song number:')
+        self.CreateService = translate('OpenLP.Ui', 'Create a new service.')
+        self.ConfirmDelete = translate('OpenLP.Ui', 'Confirm Delete')
+        self.Continuous = translate('OpenLP.Ui', 'Continuous')
+        self.Default = translate('OpenLP.Ui', 'Default')
+        self.DefaultColor = translate('OpenLP.Ui', 'Default Color:')
+        self.DefaultServiceName = translate('OpenLP.Ui', 'Service %Y-%m-%d %H-%M',
+                                            'This may not contain any of the following characters: /\\?*|<>\[\]":+\n'
+                                            'See http://docs.python.org/library/datetime'
+                                            '.html#strftime-strptime-behavior for more information.')
+        self.Delete = translate('OpenLP.Ui', '&Delete')
+        self.DisplayStyle = translate('OpenLP.Ui', 'Display style:')
+        self.Duplicate = translate('OpenLP.Ui', 'Duplicate Error')
+        self.Edit = translate('OpenLP.Ui', '&Edit')
+        self.EmptyField = translate('OpenLP.Ui', 'Empty Field')
+        self.Error = translate('OpenLP.Ui', 'Error')
+        self.Export = translate('OpenLP.Ui', 'Export')
+        self.File = translate('OpenLP.Ui', 'File')
+        self.FileNotFound = translate('OpenLP.Ui', 'File Not Found')
+        self.FileNotFoundMessage = translate('OpenLP.Ui', 'File %s not found.\nPlease try selecting it individually.')
+        self.FontSizePtUnit = translate('OpenLP.Ui', 'pt', 'Abbreviated font pointsize unit')
+        self.Help = translate('OpenLP.Ui', 'Help')
+        self.Hours = translate('OpenLP.Ui', 'h', 'The abbreviated unit for hours')
+        self.IFdSs = translate('OpenLP.Ui', 'Invalid Folder Selected', 'Singular')
+        self.IFSs = translate('OpenLP.Ui', 'Invalid File Selected', 'Singular')
+        self.IFSp = translate('OpenLP.Ui', 'Invalid Files Selected', 'Plural')
+        self.Image = translate('OpenLP.Ui', 'Image')
+        self.Import = translate('OpenLP.Ui', 'Import')
+        self.LayoutStyle = translate('OpenLP.Ui', 'Layout style:')
+        self.Live = translate('OpenLP.Ui', 'Live')
+        self.LiveBGError = translate('OpenLP.Ui', 'Live Background Error')
+        self.LiveToolbar = translate('OpenLP.Ui', 'Live Toolbar')
+        self.Load = translate('OpenLP.Ui', 'Load')
+        self.Manufacturer = translate('OpenLP.Ui', 'Manufacturer', 'Singular')
+        self.Manufacturers = translate('OpenLP.Ui', 'Manufacturers', 'Plural')
+        self.Model = translate('OpenLP.Ui', 'Model', 'Singular')
+        self.Models = translate('OpenLP.Ui', 'Models', 'Plural')
+        self.Minutes = translate('OpenLP.Ui', 'm', 'The abbreviated unit for minutes')
+        self.Middle = translate('OpenLP.Ui', 'Middle')
+        self.New = translate('OpenLP.Ui', 'New')
+        self.NewService = translate('OpenLP.Ui', 'New Service')
+        self.NewTheme = translate('OpenLP.Ui', 'New Theme')
+        self.NextTrack = translate('OpenLP.Ui', 'Next Track')
+        self.NFdSs = translate('OpenLP.Ui', 'No Folder Selected', 'Singular')
+        self.NFSs = translate('OpenLP.Ui', 'No File Selected', 'Singular')
+        self.NFSp = translate('OpenLP.Ui', 'No Files Selected', 'Plural')
+        self.NISs = translate('OpenLP.Ui', 'No Item Selected', 'Singular')
+        self.NISp = translate('OpenLP.Ui', 'No Items Selected', 'Plural')
+        self.OLP = translate('OpenLP.Ui', 'OpenLP')
+        self.OLPV2 = "%s %s" % (self.OLP, "2")
+        self.OLPV2x = "%s %s" % (self.OLP, "2.4")
+        self.OpenLPStart = translate('OpenLP.Ui', 'OpenLP is already running. Do you wish to continue?')
+        self.OpenService = translate('OpenLP.Ui', 'Open service.')
+        self.PlaySlidesInLoop = translate('OpenLP.Ui', 'Play Slides in Loop')
+        self.PlaySlidesToEnd = translate('OpenLP.Ui', 'Play Slides to End')
+        self.Preview = translate('OpenLP.Ui', 'Preview')
+        self.PreviewToolbar = translate('OpenLP.Ui', 'Preview Toolbar')
+        self.PrintService = translate('OpenLP.Ui', 'Print Service')
+        self.Projector = translate('OpenLP.Ui', 'Projector', 'Singular')
+        self.Projectors = translate('OpenLP.Ui', 'Projectors', 'Plural')
+        self.ReplaceBG = translate('OpenLP.Ui', 'Replace Background')
+        self.ReplaceLiveBG = translate('OpenLP.Ui', 'Replace live background.')
+        self.ReplaceLiveBGDisabled = translate('OpenLP.Ui', 'Replace live background is not available when the WebKit '
+                                                            'player is disabled.')
+        self.ResetBG = translate('OpenLP.Ui', 'Reset Background')
+        self.ResetLiveBG = translate('OpenLP.Ui', 'Reset live background.')
+        self.Seconds = translate('OpenLP.Ui', 's', 'The abbreviated unit for seconds')
+        self.SaveAndPreview = translate('OpenLP.Ui', 'Save && Preview')
+        self.Search = translate('OpenLP.Ui', 'Search')
+        self.SearchThemes = translate('OpenLP.Ui', 'Search Themes...', 'Search bar place holder text ')
+        self.SelectDelete = translate('OpenLP.Ui', 'You must select an item to delete.')
+        self.SelectEdit = translate('OpenLP.Ui', 'You must select an item to edit.')
+        self.Settings = translate('OpenLP.Ui', 'Settings')
+        self.SaveService = translate('OpenLP.Ui', 'Save Service')
+        self.Service = translate('OpenLP.Ui', 'Service')
+        self.Split = translate('OpenLP.Ui', 'Optional &Split')
+        self.SplitToolTip = translate('OpenLP.Ui',
+                                      'Split a slide into two only if it does not fit on the screen as one slide.')
+        self.StartTimeCode = translate('OpenLP.Ui', 'Start %s')
+        self.StopPlaySlidesInLoop = translate('OpenLP.Ui', 'Stop Play Slides in Loop')
+        self.StopPlaySlidesToEnd = translate('OpenLP.Ui', 'Stop Play Slides to End')
+        self.Theme = translate('OpenLP.Ui', 'Theme', 'Singular')
+        self.Themes = translate('OpenLP.Ui', 'Themes', 'Plural')
+        self.Tools = translate('OpenLP.Ui', 'Tools')
+        self.Top = translate('OpenLP.Ui', 'Top')
+        self.UnsupportedFile = translate('OpenLP.Ui', 'Unsupported File')
+        self.VersePerSlide = translate('OpenLP.Ui', 'Verse Per Slide')
+        self.VersePerLine = translate('OpenLP.Ui', 'Verse Per Line')
+        self.Version = translate('OpenLP.Ui', 'Version')
+        self.View = translate('OpenLP.Ui', 'View')
+        self.ViewMode = translate('OpenLP.Ui', 'View Mode')

=== added file 'openlp/core/common/versionchecker.py'
--- openlp/core/common/versionchecker.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/versionchecker.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,170 @@
+import logging
+import os
+import platform
+import sys
+import time
+import urllib.error
+import urllib.parse
+import urllib.request
+from datetime import datetime
+from distutils.version import LooseVersion
+from subprocess import Popen, PIPE
+
+from openlp.core.common import AppLocation, Settings
+
+from PyQt5 import QtCore
+
+log = logging.getLogger(__name__)
+
+APPLICATION_VERSION = {}
+CONNECTION_TIMEOUT = 30
+CONNECTION_RETRIES = 2
+
+
+class VersionThread(QtCore.QThread):
+    """
+    A special Qt thread class to fetch the version of OpenLP from the website.
+    This is threaded so that it doesn't affect the loading time of OpenLP.
+    """
+    def __init__(self, main_window):
+        """
+        Constructor for the thread class.
+
+        :param main_window: The main window Object.
+        """
+        log.debug("VersionThread - Initialise")
+        super(VersionThread, self).__init__(None)
+        self.main_window = main_window
+
+    def run(self):
+        """
+        Run the thread.
+        """
+        self.sleep(1)
+        log.debug('Version thread - run')
+        app_version = get_application_version()
+        version = check_latest_version(app_version)
+        log.debug("Versions %s and %s " % (LooseVersion(str(version)), LooseVersion(str(app_version['full']))))
+        if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])):
+            self.main_window.openlp_version_check.emit('%s' % version)
+
+
+def get_application_version():
+    """
+    Returns the application version of the running instance of OpenLP::
+
+        {'full': '1.9.4-bzr1249', 'version': '1.9.4', 'build': 'bzr1249'}
+    """
+    global APPLICATION_VERSION
+    if APPLICATION_VERSION:
+        return APPLICATION_VERSION
+    if '--dev-version' in sys.argv or '-d' in sys.argv:
+        # NOTE: The following code is a duplicate of the code in setup.py. Any fix applied here should also be applied
+        # there.
+
+        # Get the revision of this tree.
+        bzr = Popen(('bzr', 'revno'), stdout=PIPE)
+        tree_revision, error = bzr.communicate()
+        tree_revision = tree_revision.decode()
+        code = bzr.wait()
+        if code != 0:
+            raise Exception('Error running bzr log')
+
+        # Get all tags.
+        bzr = Popen(('bzr', 'tags'), stdout=PIPE)
+        output, error = bzr.communicate()
+        code = bzr.wait()
+        if code != 0:
+            raise Exception('Error running bzr tags')
+        tags = list(map(bytes.decode, output.splitlines()))
+        if not tags:
+            tag_version = '0.0.0'
+            tag_revision = '0'
+        else:
+            # Remove any tag that has "?" as revision number. A "?" as revision number indicates, that this tag is from
+            # another series.
+            tags = [tag for tag in tags if tag.split()[-1].strip() != '?']
+            # Get the last tag and split it in a revision and tag name.
+            tag_version, tag_revision = tags[-1].split()
+        # If they are equal, then this tree is tarball with the source for the release. We do not want the revision
+        # number in the full version.
+        if tree_revision == tag_revision:
+            full_version = tag_version.strip()
+        else:
+            full_version = '%s-bzr%s' % (tag_version.strip(), tree_revision.strip())
+    else:
+        # We're not running the development version, let's use the file.
+        file_path = AppLocation.get_directory(AppLocation.VersionDir)
+        file_path = os.path.join(file_path, '.version')
+        version_file = None
+        try:
+            version_file = open(file_path, 'r')
+            full_version = str(version_file.read()).rstrip()
+        except IOError:
+            log.exception('Error in version file.')
+            full_version = '0.0.0-bzr000'
+        finally:
+            if version_file:
+                version_file.close()
+    bits = full_version.split('-')
+    APPLICATION_VERSION = {
+        'full': full_version,
+        'version': bits[0],
+        'build': bits[1] if len(bits) > 1 else None
+    }
+    if APPLICATION_VERSION['build']:
+        log.info('Openlp version %s build %s', APPLICATION_VERSION['version'], APPLICATION_VERSION['build'])
+    else:
+        log.info('Openlp version %s' % APPLICATION_VERSION['version'])
+    return APPLICATION_VERSION
+
+
+def check_latest_version(current_version):
+    """
+    Check the latest version of OpenLP against the version file on the OpenLP
+    site.
+
+    **Rules around versions and version files:**
+
+    * If a version number has a build (i.e. -bzr1234), then it is a nightly.
+    * If a version number's minor version is an odd number, it is a development release.
+    * If a version number's minor version is an even number, it is a stable release.
+
+    :param current_version: The current version of OpenLP.
+    """
+    version_string = current_version['full']
+    # set to prod in the distribution config file.
+    settings = Settings()
+    settings.beginGroup('core')
+    last_test = settings.value('last version test')
+    this_test = str(datetime.now().date())
+    settings.setValue('last version test', this_test)
+    settings.endGroup()
+    if last_test != this_test:
+        if current_version['build']:
+            req = urllib.request.Request('http://www.openlp.org/files/nightly_version.txt')
+        else:
+            version_parts = current_version['version'].split('.')
+            if int(version_parts[1]) % 2 != 0:
+                req = urllib.request.Request('http://www.openlp.org/files/dev_version.txt')
+            else:
+                req = urllib.request.Request('http://www.openlp.org/files/version.txt')
+        req.add_header('User-Agent', 'OpenLP/%s %s/%s; ' % (current_version['full'], platform.system(),
+                                                            platform.release()))
+        remote_version = None
+        retries = 0
+        while True:
+            try:
+                remote_version = str(urllib.request.urlopen(req, None,
+                                                            timeout=CONNECTION_TIMEOUT).read().decode()).strip()
+            except (urllib.error.URLError, ConnectionError):
+                if retries > CONNECTION_RETRIES:
+                    log.exception('Failed to download the latest OpenLP version file')
+                else:
+                    retries += 1
+                    time.sleep(0.1)
+                    continue
+            break
+        if remote_version:
+            version_string = remote_version
+    return version_string

=== added directory 'openlp/core/lib'
=== added file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/__init__.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,335 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`lib` module contains most of the components and libraries that make
+OpenLP work.
+"""
+
+from distutils.version import LooseVersion
+import logging
+import os
+
+from PyQt5 import QtCore, QtGui, Qt, QtWidgets
+
+
+from openlp.core.common import translate
+
+log = logging.getLogger(__name__ + '.__init__')
+
+
+class ServiceItemContext(object):
+    """
+    The context in which a Service Item is being generated
+    """
+    Preview = 0
+    Live = 1
+    Service = 2
+
+
+class ImageSource(object):
+    """
+    This enumeration class represents different image sources. An image sources states where an image is used. This
+    enumeration class is need in the context of the :class:~openlp.core.lib.imagemanager`.
+
+    ``ImagePlugin``
+        This states that an image is being used by the image plugin.
+
+    ``Theme``
+        This says, that the image is used by a theme.
+    """
+    ImagePlugin = 1
+    Theme = 2
+
+
+class MediaType(object):
+    """
+    An enumeration class for types of media.
+    """
+    Audio = 1
+    Video = 2
+
+
+class ServiceItemAction(object):
+    """
+    Provides an enumeration for the required action moving between service items by left/right arrow keys
+    """
+    Previous = 1
+    PreviousLastSlide = 2
+    Next = 3
+
+
+def get_text_file_string(text_file):
+    """
+    Open a file and return its content as unicode string. If the supplied file name is not a file then the function
+    returns False. If there is an error loading the file or the content can't be decoded then the function will return
+    None.
+
+    :param text_file: The name of the file.
+    :return: The file as a single string
+    """
+    if not os.path.isfile(text_file):
+        return False
+    file_handle = None
+    content = None
+    try:
+        file_handle = open(text_file, 'r', encoding='utf-8')
+        if not file_handle.read(3) == '\xEF\xBB\xBF':
+            # no BOM was found
+            file_handle.seek(0)
+        content = file_handle.read()
+    except (IOError, UnicodeError):
+        log.exception('Failed to open text file %s' % text_file)
+    finally:
+        if file_handle:
+            file_handle.close()
+    return content
+
+
+def str_to_bool(string_value):
+    """
+    Convert a string version of a boolean into a real boolean.
+
+    :param string_value: The string value to examine and convert to a boolean type.
+    :return: The correct boolean value
+    """
+    if isinstance(string_value, bool):
+        return string_value
+    return str(string_value).strip().lower() in ('true', 'yes', 'y')
+
+
+def build_icon(icon):
+    """
+    Build a QIcon instance from an existing QIcon, a resource location, or a physical file location. If the icon is a
+    QIcon instance, that icon is simply returned. If not, it builds a QIcon instance from the resource or file name.
+
+    :param icon:
+        The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file
+        location like ``/path/to/file.png``. However, the **recommended** way is to specify a resource string.
+    :return: The build icon.
+    """
+    button_icon = QtGui.QIcon()
+    if isinstance(icon, QtGui.QIcon):
+        button_icon = icon
+    elif isinstance(icon, str):
+        if icon.startswith(':/'):
+            button_icon.addPixmap(QtGui.QPixmap(icon), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+        else:
+            button_icon.addPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(icon)), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+    elif isinstance(icon, QtGui.QImage):
+        button_icon.addPixmap(QtGui.QPixmap.fromImage(icon), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+    return button_icon
+
+
+def image_to_byte(image, base_64=True):
+    """
+    Resize an image to fit on the current screen for the web and returns it as a byte stream.
+
+    :param image: The image to converted.
+    :param base_64: If True returns the image as Base64 bytes, otherwise the image is returned as a byte array.
+        To preserve original intention, this defaults to True
+    """
+    log.debug('image_to_byte - start')
+    byte_array = QtCore.QByteArray()
+    # use buffer to store pixmap into byteArray
+    buffie = QtCore.QBuffer(byte_array)
+    buffie.open(QtCore.QIODevice.WriteOnly)
+    image.save(buffie, "PNG")
+    log.debug('image_to_byte - end')
+    if not base_64:
+        return byte_array
+    # convert to base64 encoding so does not get missed!
+    return bytes(byte_array.toBase64()).decode('utf-8')
+
+
+def create_thumb(image_path, thumb_path, return_icon=True, size=None):
+    """
+    Create a thumbnail from the given image path and depending on ``return_icon`` it returns an icon from this thumb.
+
+    :param image_path: The image file to create the icon from.
+    :param thumb_path: The filename to save the thumbnail to.
+    :param return_icon: States if an icon should be build and returned from the thumb. Defaults to ``True``.
+    :param size: Allows to state a own size (QtCore.QSize) to use. Defaults to ``None``, which means that a default
+     height of 88 is used.
+    :return: The final icon.
+    """
+    ext = os.path.splitext(thumb_path)[1].lower()
+    reader = QtGui.QImageReader(image_path)
+    if size is None:
+        ratio = reader.size().width() / reader.size().height()
+        reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
+    else:
+        reader.setScaledSize(size)
+    thumb = reader.read()
+    thumb.save(thumb_path, ext[1:])
+    if not return_icon:
+        return
+    if os.path.exists(thumb_path):
+        return build_icon(thumb_path)
+    # Fallback for files with animation support.
+    return build_icon(image_path)
+
+
+def validate_thumb(file_path, thumb_path):
+    """
+    Validates whether an file's thumb still exists and if is up to date. **Note**, you must **not** call this function,
+    before checking the existence of the file.
+
+    :param file_path: The path to the file. The file **must** exist!
+    :param thumb_path: The path to the thumb.
+    :return: True, False if the image has changed since the thumb was created.
+    """
+    if not os.path.exists(thumb_path):
+        return False
+    image_date = os.stat(file_path).st_mtime
+    thumb_date = os.stat(thumb_path).st_mtime
+    return image_date <= thumb_date
+
+
+def resize_image(image_path, width, height, background='#000000'):
+    """
+    Resize an image to fit on the current screen.
+
+    DO NOT REMOVE THE DEFAULT BACKGROUND VALUE!
+
+    :param image_path: The path to the image to resize.
+    :param width: The new image width.
+    :param height: The new image height.
+    :param background: The background colour. Defaults to black.
+    """
+    log.debug('resize_image - start')
+    reader = QtGui.QImageReader(image_path)
+    # The image's ratio.
+    image_ratio = reader.size().width() / reader.size().height()
+    resize_ratio = width / height
+    # Figure out the size we want to resize the image to (keep aspect ratio).
+    if image_ratio == resize_ratio:
+        size = QtCore.QSize(width, height)
+    elif image_ratio < resize_ratio:
+        # Use the image's height as reference for the new size.
+        size = QtCore.QSize(image_ratio * height, height)
+    else:
+        # Use the image's width as reference for the new size.
+        size = QtCore.QSize(width, 1 / (image_ratio / width))
+    reader.setScaledSize(size)
+    preview = reader.read()
+    if image_ratio == resize_ratio:
+        # We neither need to centre the image nor add "bars" to the image.
+        return preview
+    real_width = preview.width()
+    real_height = preview.height()
+    # and move it to the centre of the preview space
+    new_image = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32_Premultiplied)
+    painter = QtGui.QPainter(new_image)
+    painter.fillRect(new_image.rect(), QtGui.QColor(background))
+    painter.drawImage((width - real_width) // 2, (height - real_height) // 2, preview)
+    return new_image
+
+
+def check_item_selected(list_widget, message):
+    """
+    Check if a list item is selected so an action may be performed on it
+
+    :param list_widget: The list to check for selected items
+    :param message: The message to give the user if no item is selected
+    """
+    if not list_widget.selectedIndexes():
+        QtWidgets.QMessageBox.information(list_widget.parent(),
+                                          translate('OpenLP.MediaManagerItem', 'No Items Selected'), message)
+        return False
+    return True
+
+
+def clean_tags(text):
+    """
+    Remove Tags from text for display
+
+    :param text: Text to be cleaned
+    """
+    text = text.replace('<br>', '\n')
+    text = text.replace('{br}', '\n')
+    text = text.replace('&nbsp;', ' ')
+    for tag in FormattingTags.get_html_tags():
+        text = text.replace(tag['start tag'], '')
+        text = text.replace(tag['end tag'], '')
+    return text
+
+
+def expand_tags(text):
+    """
+    Expand tags HTML for display
+
+    :param text: The text to be expanded.
+    """
+    for tag in FormattingTags.get_html_tags():
+        text = text.replace(tag['start tag'], tag['start html'])
+        text = text.replace(tag['end tag'], tag['end html'])
+    return text
+
+
+def create_separated_list(string_list):
+    """
+    Returns a string that represents a join of a list of strings with a localized separator. This function corresponds
+
+    to QLocale::createSeparatedList which was introduced in Qt 4.8 and implements the algorithm from
+    http://www.unicode.org/reports/tr35/#ListPatterns
+
+     :param string_list: List of unicode strings
+    """
+    if LooseVersion(Qt.PYQT_VERSION_STR) >= LooseVersion('4.9') and LooseVersion(Qt.qVersion()) >= LooseVersion('4.8'):
+        return QtCore.QLocale().createSeparatedList(string_list)
+    if not string_list:
+        return ''
+    elif len(string_list) == 1:
+        return string_list[0]
+    elif len(string_list) == 2:
+        return translate('OpenLP.core.lib', '%s and %s',
+                         'Locale list separator: 2 items') % (string_list[0], string_list[1])
+    else:
+        merged = translate('OpenLP.core.lib', '%s, and %s',
+                           'Locale list separator: end') % (string_list[-2], string_list[-1])
+        for index in reversed(list(range(1, len(string_list) - 2))):
+            merged = translate('OpenLP.core.lib', '%s, %s',
+                               'Locale list separator: middle') % (string_list[index], merged)
+        return translate('OpenLP.core.lib', '%s, %s', 'Locale list separator: start') % (string_list[0], merged)
+
+
+from .colorbutton import ColorButton
+from .exceptions import ValidationError
+from .filedialog import FileDialog
+from .screen import ScreenList
+from .listwidgetwithdnd import ListWidgetWithDnD
+from .treewidgetwithdnd import TreeWidgetWithDnD
+from .formattingtags import FormattingTags
+from .spelltextedit import SpellTextEdit
+from .plugin import PluginStatus, StringContent, Plugin
+from .pluginmanager import PluginManager
+from .settingstab import SettingsTab
+from .serviceitem import ServiceItem, ServiceItemType, ItemCapabilities
+from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css
+from .toolbar import OpenLPToolbar
+from .dockwidget import OpenLPDockWidget
+from .imagemanager import ImageManager
+from .renderer import Renderer
+from .mediamanageritem import MediaManagerItem
+from .projector.db import ProjectorDB, Projector
+from .projector.pjlink1 import PJLink1
+from .projector.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING

=== added file 'openlp/core/lib/colorbutton.py'
--- openlp/core/lib/colorbutton.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/colorbutton.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+
+"""
+Provide a custom widget based on QPushButton for the selection of colors
+"""
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import translate
+
+
+class ColorButton(QtWidgets.QPushButton):
+    """
+    Subclasses QPushbutton to create a "Color Chooser" button
+    """
+
+    colorChanged = QtCore.pyqtSignal(str)
+
+    def __init__(self, parent=None):
+        """
+        Initialise the ColorButton
+        """
+        super(ColorButton, self).__init__()
+        self.parent = parent
+        self.change_color('#ffffff')
+        self.setToolTip(translate('OpenLP.ColorButton', 'Click to select a color.'))
+        self.clicked.connect(self.on_clicked)
+
+    def change_color(self, color):
+        """
+        Sets the _color variable and the background color.
+
+        :param color:  String representation of a hexidecimal color
+        """
+        self._color = color
+        self.setStyleSheet('background-color: %s' % color)
+
+    @property
+    def color(self):
+        """
+        Property method to return the color variable
+
+        :return:  String representation of a hexidecimal color
+        """
+        return self._color
+
+    @color.setter
+    def color(self, color):
+        """
+        Property setter to change the instance color
+
+        :param color:  String representation of a hexidecimal color
+        """
+        self.change_color(color)
+
+    def on_clicked(self):
+        """
+        Handle the PushButton clicked signal, showing the ColorDialog and validating the input
+        """
+        new_color = QtWidgets.QColorDialog.getColor(QtGui.QColor(self._color), self.parent)
+        if new_color.isValid() and self._color != new_color.name():
+            self.change_color(new_color.name())
+            self.colorChanged.emit(new_color.name())

=== added file 'openlp/core/lib/db.py'
--- openlp/core/lib/db.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/db.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,476 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+
+"""
+The :mod:`db` module provides the core database functionality for OpenLP
+"""
+import logging
+import os
+from urllib.parse import quote_plus as urlquote
+
+from sqlalchemy import Table, MetaData, Column, types, create_engine
+from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, DBAPIError, OperationalError
+from sqlalchemy.orm import scoped_session, sessionmaker, mapper
+from sqlalchemy.pool import NullPool
+from alembic.migration import MigrationContext
+from alembic.operations import Operations
+
+from openlp.core.common import AppLocation, Settings, translate, delete_file
+from openlp.core.lib.ui import critical_error_message_box
+
+log = logging.getLogger(__name__)
+
+
+def init_db(url, auto_flush=True, auto_commit=False, base=None):
+    """
+    Initialise and return the session and metadata for a database
+
+    :param url: The database to initialise connection with
+    :param auto_flush: Sets the flushing behaviour of the session
+    :param auto_commit: Sets the commit behaviour of the session
+    :param base: If using declarative, the base class to bind with
+    """
+    engine = create_engine(url, poolclass=NullPool)
+    if base is None:
+        metadata = MetaData(bind=engine)
+    else:
+        base.metadata.bind = engine
+        metadata = base.metadata
+    session = scoped_session(sessionmaker(autoflush=auto_flush, autocommit=auto_commit, bind=engine))
+    return session, metadata
+
+
+def get_db_path(plugin_name, db_file_name=None):
+    """
+    Create a path to a database from the plugin name and database name
+
+    :param plugin_name: Name of plugin
+    :param db_file_name: File name of database
+    :return: The path to the database as type str
+    """
+    if db_file_name is None:
+        return 'sqlite:///%s/%s.sqlite' % (AppLocation.get_section_data_path(plugin_name), plugin_name)
+    else:
+        return 'sqlite:///%s/%s' % (AppLocation.get_section_data_path(plugin_name), db_file_name)
+
+
+def handle_db_error(plugin_name, db_file_name):
+    """
+    Log and report to the user that a database cannot be loaded
+
+    :param plugin_name: Name of plugin
+    :param db_file_name: File name of database
+    :return: None
+    """
+    db_path = get_db_path(plugin_name, db_file_name)
+    log.exception('Error loading database: %s', db_path)
+    critical_error_message_box(translate('OpenLP.Manager', 'Database Error'),
+                               translate('OpenLP.Manager', 'OpenLP cannot load your database.\n\nDatabase: %s')
+                               % db_path)
+
+
+def init_url(plugin_name, db_file_name=None):
+    """
+    Return the database URL.
+
+    :param plugin_name: The name of the plugin for the database creation.
+    :param db_file_name: The database file name. Defaults to None resulting in the plugin_name being used.
+    """
+    settings = Settings()
+    settings.beginGroup(plugin_name)
+    db_type = settings.value('db type')
+    if db_type == 'sqlite':
+        db_url = get_db_path(plugin_name, db_file_name)
+    else:
+        db_url = '%s://%s:%s@%s/%s' % (db_type, urlquote(settings.value('db username')),
+                                       urlquote(settings.value('db password')),
+                                       urlquote(settings.value('db hostname')),
+                                       urlquote(settings.value('db database')))
+    settings.endGroup()
+    return db_url
+
+
+def get_upgrade_op(session):
+    """
+    Create a migration context and an operations object for performing upgrades.
+
+    :param session: The SQLAlchemy session object.
+    """
+    context = MigrationContext.configure(session.bind.connect())
+    return Operations(context)
+
+
+def upgrade_db(url, upgrade):
+    """
+    Upgrade a database.
+
+    :param url: The url of the database to upgrade.
+    :param upgrade: The python module that contains the upgrade instructions.
+    """
+    session, metadata = init_db(url)
+
+    class Metadata(BaseModel):
+        """
+        Provides a class for the metadata table.
+        """
+        pass
+
+    metadata_table = Table(
+        'metadata', metadata,
+        Column('key', types.Unicode(64), primary_key=True),
+        Column('value', types.UnicodeText(), default=None)
+    )
+    metadata_table.create(checkfirst=True)
+    mapper(Metadata, metadata_table)
+    version_meta = session.query(Metadata).get('version')
+    if version_meta is None:
+        # Tables have just been created - fill the version field with the most recent version
+        if session.query(Metadata).get('dbversion'):
+            version = 0
+        else:
+            version = upgrade.__version__
+        version_meta = Metadata.populate(key='version', value=version)
+        session.add(version_meta)
+        session.commit()
+    else:
+        version = int(version_meta.value)
+    if version > upgrade.__version__:
+        return version, upgrade.__version__
+    version += 1
+    try:
+        while hasattr(upgrade, 'upgrade_%d' % version):
+            log.debug('Running upgrade_%d', version)
+            try:
+                upgrade_func = getattr(upgrade, 'upgrade_%d' % version)
+                upgrade_func(session, metadata)
+                session.commit()
+                # Update the version number AFTER a commit so that we are sure the previous transaction happened
+                version_meta.value = str(version)
+                session.commit()
+                version += 1
+            except (SQLAlchemyError, DBAPIError):
+                log.exception('Could not run database upgrade script "upgrade_%s", upgrade process has been halted.',
+                              version)
+                break
+    except (SQLAlchemyError, DBAPIError):
+        version_meta = Metadata.populate(key='version', value=int(upgrade.__version__))
+        session.commit()
+    upgrade_version = upgrade.__version__
+    version_meta = int(version_meta.value)
+    session.close()
+    return version_meta, upgrade_version
+
+
+def delete_database(plugin_name, db_file_name=None):
+    """
+    Remove a database file from the system.
+
+    :param plugin_name: The name of the plugin to remove the database for
+    :param db_file_name: The database file name. Defaults to None resulting in the plugin_name being used.
+    """
+    if db_file_name:
+        db_file_path = os.path.join(AppLocation.get_section_data_path(plugin_name), db_file_name)
+    else:
+        db_file_path = os.path.join(AppLocation.get_section_data_path(plugin_name), plugin_name)
+    return delete_file(db_file_path)
+
+
+class BaseModel(object):
+    """
+    BaseModel provides a base object with a set of generic functions
+    """
+    @classmethod
+    def populate(cls, **kwargs):
+        """
+        Creates an instance of a class and populates it, returning the instance
+        """
+        instance = cls()
+        for key, value in kwargs.items():
+            instance.__setattr__(key, value)
+        return instance
+
+
+class Manager(object):
+    """
+    Provide generic object persistence management
+    """
+    def __init__(self, plugin_name, init_schema, db_file_name=None, upgrade_mod=None, session=None):
+        """
+        Runs the initialisation process that includes creating the connection to the database and the tables if they do
+        not exist.
+
+        :param plugin_name:  The name to setup paths and settings section names
+        :param init_schema: The init_schema function for this database
+        :param db_file_name: The upgrade_schema function for this database
+        :param upgrade_mod: The file name to use for this database. Defaults to None resulting in the plugin_name
+        being used.
+        """
+        self.is_dirty = False
+        self.session = None
+        self.db_url = None
+        if db_file_name:
+            log.debug('Manager: Creating new DB url')
+            self.db_url = init_url(plugin_name, db_file_name)
+        else:
+            self.db_url = init_url(plugin_name)
+        if upgrade_mod:
+            try:
+                db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod)
+            except (SQLAlchemyError, DBAPIError):
+                handle_db_error(plugin_name, db_file_name)
+                return
+            if db_ver > up_ver:
+                critical_error_message_box(
+                    translate('OpenLP.Manager', 'Database Error'),
+                    translate('OpenLP.Manager', 'The database being loaded was created in a more recent version of '
+                              'OpenLP. The database is version %d, while OpenLP expects version %d. The database will '
+                              'not be loaded.\n\nDatabase: %s') % (db_ver, up_ver, self.db_url)
+                )
+                return
+        if not session:
+            try:
+                self.session = init_schema(self.db_url)
+            except (SQLAlchemyError, DBAPIError):
+                handle_db_error(plugin_name, db_file_name)
+        else:
+            self.session = session
+
+    def save_object(self, object_instance, commit=True):
+        """
+        Save an object to the database
+
+        :param object_instance: The object to save
+        :param commit: Commit the session with this object
+        """
+        for try_count in range(3):
+            try:
+                self.session.add(object_instance)
+                if commit:
+                    self.session.commit()
+                self.is_dirty = True
+                return True
+            except OperationalError:
+                # This exception clause is for users running MySQL which likes to terminate connections on its own
+                # without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
+                # non-recoverable way. So we only retry 3 times.
+                log.exception('Probably a MySQL issue - "MySQL has gone away"')
+                self.session.rollback()
+                if try_count >= 2:
+                    raise
+            except InvalidRequestError:
+                self.session.rollback()
+                log.exception('Object list save failed')
+                return False
+            except:
+                self.session.rollback()
+                raise
+
+    def save_objects(self, object_list, commit=True):
+        """
+        Save a list of objects to the database
+
+        :param object_list: The list of objects to save
+        :param commit: Commit the session with this object
+        """
+        for try_count in range(3):
+            try:
+                self.session.add_all(object_list)
+                if commit:
+                    self.session.commit()
+                self.is_dirty = True
+                return True
+            except OperationalError:
+                # This exception clause is for users running MySQL which likes to terminate connections on its own
+                # without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
+                # non-recoverable way. So we only retry 3 times.
+                log.exception('Probably a MySQL issue, "MySQL has gone away"')
+                self.session.rollback()
+                if try_count >= 2:
+                    raise
+            except InvalidRequestError:
+                self.session.rollback()
+                log.exception('Object list save failed')
+                return False
+            except:
+                self.session.rollback()
+                raise
+
+    def get_object(self, object_class, key=None):
+        """
+        Return the details of an object
+
+        :param object_class:  The type of object to return
+        :param key: The unique reference or primary key for the instance to return
+        """
+        if not key:
+            return object_class()
+        else:
+            for try_count in range(3):
+                try:
+                    return self.session.query(object_class).get(key)
+                except OperationalError:
+                    # This exception clause is for users running MySQL which likes to terminate connections on its own
+                    # without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
+                    # non-recoverable way. So we only retry 3 times.
+                    log.exception('Probably a MySQL issue, "MySQL has gone away"')
+                    if try_count >= 2:
+                        raise
+
+    def get_object_filtered(self, object_class, filter_clause):
+        """
+        Returns an object matching specified criteria
+
+        :param object_class: The type of object to return
+        :param filter_clause: The criteria to select the object by
+        """
+        for try_count in range(3):
+            try:
+                return self.session.query(object_class).filter(filter_clause).first()
+            except OperationalError as oe:
+                # This exception clause is for users running MySQL which likes to terminate connections on its own
+                # without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
+                # non-recoverable way. So we only retry 3 times.
+                if try_count >= 2 or 'MySQL has gone away' in str(oe):
+                    raise
+                log.exception('Probably a MySQL issue, "MySQL has gone away"')
+
+    def get_all_objects(self, object_class, filter_clause=None, order_by_ref=None):
+        """
+        Returns all the objects from the database
+
+        :param object_class: The type of objects to return
+        :param filter_clause: The filter governing selection of objects to return. Defaults to None.
+        :param order_by_ref: Any parameters to order the returned objects by. Defaults to None.
+        """
+        query = self.session.query(object_class)
+        if filter_clause is not None:
+            query = query.filter(filter_clause)
+        if isinstance(order_by_ref, list):
+            query = query.order_by(*order_by_ref)
+        elif order_by_ref is not None:
+            query = query.order_by(order_by_ref)
+        for try_count in range(3):
+            try:
+                return query.all()
+            except OperationalError:
+                # This exception clause is for users running MySQL which likes to terminate connections on its own
+                # without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
+                # non-recoverable way. So we only retry 3 times.
+                log.exception('Probably a MySQL issue, "MySQL has gone away"')
+                if try_count >= 2:
+                    raise
+
+    def get_object_count(self, object_class, filter_clause=None):
+        """
+        Returns a count of the number of objects in the database.
+
+        :param object_class: The type of objects to return.
+        :param filter_clause: The filter governing selection of objects to return. Defaults to None.
+        """
+        query = self.session.query(object_class)
+        if filter_clause is not None:
+            query = query.filter(filter_clause)
+        for try_count in range(3):
+            try:
+                return query.count()
+            except OperationalError:
+                # This exception clause is for users running MySQL which likes to terminate connections on its own
+                # without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
+                # non-recoverable way. So we only retry 3 times.
+                log.exception('Probably a MySQL issue, "MySQL has gone away"')
+                if try_count >= 2:
+                    raise
+
+    def delete_object(self, object_class, key):
+        """
+        Delete an object from the database
+
+        :param object_class: The type of object to delete
+        :param key: The unique reference or primary key for the instance to be deleted
+        """
+        if key != 0:
+            object_instance = self.get_object(object_class, key)
+            for try_count in range(3):
+                try:
+                    self.session.delete(object_instance)
+                    self.session.commit()
+                    self.is_dirty = True
+                    return True
+                except OperationalError:
+                    # This exception clause is for users running MySQL which likes to terminate connections on its own
+                    # without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
+                    # non-recoverable way. So we only retry 3 times.
+                    log.exception('Probably a MySQL issue, "MySQL has gone away"')
+                    self.session.rollback()
+                    if try_count >= 2:
+                        raise
+                except InvalidRequestError:
+                    self.session.rollback()
+                    log.exception('Failed to delete object')
+                    return False
+                except:
+                    self.session.rollback()
+                    raise
+        else:
+            return True
+
+    def delete_all_objects(self, object_class, filter_clause=None):
+        """
+        Delete all object records. This method should only be used for simple tables and **not** ones with
+        relationships. The relationships are not deleted from the database and this will lead to database corruptions.
+
+        :param object_class:  The type of object to delete
+        :param filter_clause: The filter governing selection of objects to return. Defaults to None.
+        """
+        for try_count in range(3):
+            try:
+                query = self.session.query(object_class)
+                if filter_clause is not None:
+                    query = query.filter(filter_clause)
+                query.delete(synchronize_session=False)
+                self.session.commit()
+                self.is_dirty = True
+                return True
+            except OperationalError:
+                # This exception clause is for users running MySQL which likes to terminate connections on its own
+                # without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
+                # non-recoverable way. So we only retry 3 times.
+                log.exception('Probably a MySQL issue, "MySQL has gone away"')
+                self.session.rollback()
+                if try_count >= 2:
+                    raise
+            except InvalidRequestError:
+                self.session.rollback()
+                log.exception('Failed to delete %s records', object_class.__name__)
+                return False
+            except:
+                self.session.rollback()
+                raise
+
+    def finalise(self):
+        """
+        VACUUM the database on exit.
+        """
+        if self.is_dirty:
+            engine = create_engine(self.db_url)
+            if self.db_url.startswith('sqlite'):
+                engine.execute("vacuum")

=== added file 'openlp/core/lib/dockwidget.py'
--- openlp/core/lib/dockwidget.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/dockwidget.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+
+"""
+Provide additional functionality required by OpenLP from the inherited QDockWidget.
+"""
+
+import logging
+
+from PyQt5 import QtWidgets
+
+from openlp.core.lib import ScreenList, build_icon
+
+log = logging.getLogger(__name__)
+
+
+class OpenLPDockWidget(QtWidgets.QDockWidget):
+    """
+    Custom DockWidget class to handle events
+    """
+    def __init__(self, parent=None, name=None, icon=None):
+        """
+        Initialise the DockWidget
+        """
+        log.debug('Initialise the %s widget' % name)
+        super(OpenLPDockWidget, self).__init__(parent)
+        if name:
+            self.setObjectName(name)
+        if icon:
+            self.setWindowIcon(build_icon(icon))
+        # Sort out the minimum width.
+        screens = ScreenList()
+        main_window_docbars = screens.current['size'].width() // 5
+        if main_window_docbars > 300:
+            self.setMinimumWidth(300)
+        else:
+            self.setMinimumWidth(main_window_docbars)

=== added file 'openlp/core/lib/exceptions.py'
--- openlp/core/lib/exceptions.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/exceptions.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`~openlp.core.lib.exceptions` module contains custom exceptions
+"""
+
+
+class ValidationError(Exception):
+    """
+    The :class:`~openlp.core.lib.exceptions.ValidationError` exception provides a custom exception for validating
+    import files.
+    """
+    pass

=== added file 'openlp/core/lib/filedialog.py'
--- openlp/core/lib/filedialog.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/filedialog.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Provide a work around for a bug in QFileDialog <https://bugs.launchpad.net/openlp/+bug/1209515>
+"""
+import logging
+import os
+from urllib import parse
+
+from PyQt5 import QtWidgets
+
+from openlp.core.common import UiStrings
+
+log = logging.getLogger(__name__)
+
+
+class FileDialog(QtWidgets.QFileDialog):
+    """
+    Subclass QFileDialog to work round a bug
+    """
+    @staticmethod
+    def getOpenFileNames(parent, *args, **kwargs):
+        """
+        Reimplement getOpenFileNames to fix the way it returns some file names that url encoded when selecting multiple
+        files
+        """
+        files, filter_used = QtWidgets.QFileDialog.getOpenFileNames(parent, *args, **kwargs)
+        file_list = []
+        for file in files:
+            if not os.path.exists(file):
+                log.info('File not found. Attempting to unquote.')
+                file = parse.unquote(file)
+                if not os.path.exists(file):
+                    log.error('File %s not found.' % file)
+                    QtWidgets.QMessageBox.information(parent, UiStrings().FileNotFound,
+                                                      UiStrings().FileNotFoundMessage % file)
+                    continue
+            file_list.append(file)
+        return file_list

=== added file 'openlp/core/lib/formattingtags.py'
--- openlp/core/lib/formattingtags.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/formattingtags.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,198 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Provide HTML Tag management and Formatting Tag access class
+"""
+import json
+
+from openlp.core.common import Settings
+from openlp.core.lib import translate
+
+
+class FormattingTags(object):
+    """
+    Static Class for HTML Tags to be access around the code the list is managed by the Options Tab.
+    """
+    html_expands = []
+
+    @staticmethod
+    def get_html_tags():
+        """
+        Provide access to the html_expands list.
+        """
+        return FormattingTags.html_expands
+
+    @staticmethod
+    def save_html_tags(new_tags):
+        """
+        Saves all formatting tags except protected ones
+
+        `new_tags`
+            The tags to be saved..
+        """
+        # Formatting Tags were also known as display tags.
+        Settings().setValue('formattingTags/html_tags', json.dumps(new_tags) if new_tags else '')
+
+    @staticmethod
+    def load_tags():
+        """
+        Load the Tags from store so can be used in the system or used to update the display.
+        """
+        temporary_tags = [tag for tag in FormattingTags.html_expands if tag.get('temporary')]
+        FormattingTags.html_expands = []
+        base_tags = []
+        # Append the base tags.
+        base_tags.append({
+            'desc': translate('OpenLP.FormattingTags', 'Red'),
+            'start tag': '{r}',
+            'start html': '<span style="-webkit-text-fill-color:red">',
+            'end tag': '{/r}', 'end html': '</span>', 'protected': True,
+            'temporary': False})
+        base_tags.append({
+            'desc': translate('OpenLP.FormattingTags', 'Black'),
+            'start tag': '{b}',
+            'start html': '<span style="-webkit-text-fill-color:black">',
+            'end tag': '{/b}', 'end html': '</span>', 'protected': True,
+            'temporary': False})
+        base_tags.append({
+            'desc': translate('OpenLP.FormattingTags', 'Blue'),
+            'start tag': '{bl}',
+            'start html': '<span style="-webkit-text-fill-color:blue">',
+            'end tag': '{/bl}', 'end html': '</span>', 'protected': True,
+            'temporary': False})
+        base_tags.append({
+            'desc': translate('OpenLP.FormattingTags', 'Yellow'),
+            'start tag': '{y}',
+            'start html': '<span style="-webkit-text-fill-color:yellow">',
+            'end tag': '{/y}', 'end html': '</span>', 'protected': True,
+            'temporary': False})
+        base_tags.append({
+            'desc': translate('OpenLP.FormattingTags', 'Green'),
+            'start tag': '{g}',
+            'start html': '<span style="-webkit-text-fill-color:green">',
+            'end tag': '{/g}', 'end html': '</span>', 'protected': True,
+            'temporary': False})
+        base_tags.append({
+            'desc': translate('OpenLP.FormattingTags', 'Pink'),
+            'start tag': '{pk}',
+            'start html': '<span style="-webkit-text-fill-color:#FFC0CB">',
+            'end tag': '{/pk}', 'end html': '</span>', 'protected': True,
+            'temporary': False})
+        base_tags.append({
+            'desc': translate('OpenLP.FormattingTags', 'Orange'),
+            'start tag': '{o}',
+            'start html': '<span style="-webkit-text-fill-color:#FFA500">',
+            'end tag': '{/o}', 'end html': '</span>', 'protected': True,
+            'temporary': False})
+        base_tags.append({
+            'desc': translate('OpenLP.FormattingTags', 'Purple'),
+            'start tag': '{pp}',
+            'start html': '<span style="-webkit-text-fill-color:#800080">',
+            'end tag': '{/pp}', 'end html': '</span>', 'protected': True,
+            'temporary': False})
+        base_tags.append({
+            'desc': translate('OpenLP.FormattingTags', 'White'),
+            'start tag': '{w}',
+            'start html': '<span style="-webkit-text-fill-color:white">',
+            'end tag': '{/w}', 'end html': '</span>', 'protected': True,
+            'temporary': False})
+        base_tags.append({
+            'desc': translate('OpenLP.FormattingTags', 'Superscript'),
+            'start tag': '{su}', 'start html': '<sup>',
+            'end tag': '{/su}', 'end html': '</sup>', 'protected': True,
+            'temporary': False})
+        base_tags.append({
+            'desc': translate('OpenLP.FormattingTags', 'Subscript'),
+            'start tag': '{sb}', 'start html': '<sub>',
+            'end tag': '{/sb}', 'end html': '</sub>', 'protected': True,
+            'temporary': False})
+        base_tags.append({
+            'desc': translate('OpenLP.FormattingTags', 'Paragraph'),
+            'start tag': '{p}', 'start html': '<p>', 'end tag': '{/p}',
+            'end html': '</p>', 'protected': True,
+            'temporary': False})
+        base_tags.append({
+            'desc': translate('OpenLP.FormattingTags', 'Bold'),
+            'start tag': '{st}', 'start html': '<strong>',
+            'end tag': '{/st}', 'end html': '</strong>',
+            'protected': True, 'temporary': False})
+        base_tags.append({
+            'desc': translate('OpenLP.FormattingTags', 'Italics'),
+            'start tag': '{it}', 'start html': '<em>', 'end tag': '{/it}',
+            'end html': '</em>', 'protected': True, 'temporary': False})
+        base_tags.append({
+            'desc': translate('OpenLP.FormattingTags', 'Underline'),
+            'start tag': '{u}',
+            'start html': '<span style="text-decoration: underline;">',
+            'end tag': '{/u}', 'end html': '</span>', 'protected': True,
+            'temporary': False})
+        base_tags.append({
+            'desc': translate('OpenLP.FormattingTags', 'Break'),
+            'start tag': '{br}', 'start html': '<br>', 'end tag': '',
+            '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'))
+        # If we have some user ones added them as well
+        if user_expands_string:
+            user_tags = json.loads(user_expands_string)
+            FormattingTags.add_html_tags(user_tags)
+
+    @staticmethod
+    def add_html_tags(tags):
+        """
+        Add a list of tags to the list.
+
+        :param tags: The list with tags to add.
+            Each **tag** has to be a ``dict`` and should have the following keys:
+
+            * desc
+                The formatting tag's description, e. g. **Red**
+
+            * start tag
+                The start tag, e. g. ``{r}``
+
+            * end tag
+                The end tag, e. g. ``{/r}``
+
+            * start html
+                The start html tag. For instance ``<span style="-webkit-text-fill-color:red">``
+
+            * end html
+                The end html tag. For example ``</span>``
+
+            * protected
+                A boolean stating whether this is a build-in tag or not. Should be ``True`` in most cases.
+
+            * temporary
+                A temporary tag will not be saved, but is also considered when displaying text containing the tag. It
+                has to be a ``boolean``.
+        """
+        FormattingTags.html_expands.extend(tags)
+
+    @staticmethod
+    def remove_html_tag(tag_id):
+        """
+        Removes an individual html_expands tag.
+        """
+        FormattingTags.html_expands.pop(tag_id)

=== added file 'openlp/core/lib/htmlbuilder.py'
--- openlp/core/lib/htmlbuilder.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/htmlbuilder.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,756 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+This module is responsible for generating the HTML for :class:`~openlp.core.ui.maindisplay`. The ``build_html`` function
+is the function which has to be called from outside. The generated and returned HTML will look similar to this::
+
+        <!DOCTYPE html>
+        <html>
+        <head>
+        <title>OpenLP Display</title>
+        <style>
+        *{
+            margin: 0;
+            padding: 0;
+            border: 0;
+            overflow: hidden;
+            -webkit-user-select: none;
+        }
+        body {
+            background-color: #000000;
+        }
+        .size {
+            position: absolute;
+            left: 0px;
+            top: 0px;
+            width: 100%;
+            height: 100%;
+        }
+        #black {
+            z-index: 8;
+            background-color: black;
+            display: none;
+        }
+        #bgimage {
+            z-index: 1;
+        }
+        #image {
+            z-index: 2;
+        }
+
+        #videobackboard {
+            z-index:3;
+            background-color: #000000;
+        }
+        #video {
+            background-color: #000000;
+            z-index:4;
+        }
+
+        #flash {
+            z-index:5;
+        }
+
+            #alert {
+                position: absolute;
+                left: 0px;
+                top: 0px;
+                z-index: 10;
+                width: 100%;
+                vertical-align: bottom;
+                font-family: DejaVu Sans;
+                font-size: 40pt;
+                color: #ffffff;
+                background-color: #660000;
+                word-wrap: break-word;
+            }
+
+        #footer {
+            position: absolute;
+            z-index: 6;
+
+            left: 10px;
+            bottom: 0px;
+            width: 1580px;
+            font-family: Nimbus Sans L;
+            font-size: 12pt;
+            color: #FFFFFF;
+            text-align: left;
+            white-space: nowrap;
+
+        }
+        /* lyric css */
+
+        .lyricstable {
+            z-index: 5;
+            position: absolute;
+            display: table;
+            left: 10px; top: 0px;
+        }
+        .lyricscell {
+            display: table-cell;
+            word-wrap: break-word;
+            -webkit-transition: opacity 0.4s ease;
+            white-space:pre-wrap; word-wrap: break-word; text-align: left; vertical-align: top; font-family: Nimbus
+            Sans L; font-size: 40pt; color: #FFFFFF; line-height: 100%; margin: 0;padding: 0; padding-bottom: 0;
+            padding-left: 4px; width: 1580px; height: 810px;
+        }
+        .lyricsmain {
+             -webkit-text-stroke: 0.125em #000000; -webkit-text-fill-color: #FFFFFF;  text-shadow: #000000 5px 5px;
+        }
+
+        sup {
+            font-size: 0.6em;
+            vertical-align: top;
+            position: relative;
+            top: -0.3em;
+        }
+        </style>
+        <script>
+            var timer = null;
+            var transition = false;
+
+            function show_video(state, path, volume, loop, variable_value){
+                // Sometimes  video.currentTime stops slightly short of video.duration and video.ended is intermittent!
+
+                var video = document.getElementById('video');
+                if(volume != null){
+                    video.volume = volume;
+                }
+                switch(state){
+                    case 'load':
+                        video.src = 'file:///' + path;
+                        if(loop == true) {
+                            video.loop = true;
+                        }
+                        video.load();
+                        break;
+                    case 'play':
+                        video.play();
+                        break;
+                    case 'pause':
+                        video.pause();
+                        break;
+                    case 'stop':
+                        show_video('pause');
+                        video.currentTime = 0;
+                        break;
+                    case 'close':
+                        show_video('stop');
+                        video.src = '';
+                        break;
+                    case 'length':
+                        return video.duration;
+                    case 'current_time':
+                        return video.currentTime;
+                    case 'seek':
+                        video.currentTime = variable_value;
+                        break;
+                    case 'isEnded':
+                        return video.ended;
+                    case 'setVisible':
+                        video.style.visibility = variable_value;
+                        break;
+                    case 'setBackBoard':
+                        var back = document.getElementById('videobackboard');
+                        back.style.visibility = variable_value;
+                        break;
+               }
+            }
+
+            function getFlashMovieObject(movieName)
+            {
+                if (window.document[movieName]){
+                    return window.document[movieName];
+                }
+                if (document.embeds && document.embeds[movieName]){
+                    return document.embeds[movieName];
+                }
+            }
+
+            function show_flash(state, path, volume, variable_value){
+                var text = document.getElementById('flash');
+                var flashMovie = getFlashMovieObject("OpenLPFlashMovie");
+                var src = "src = 'file:///" + path + "'";
+                var view_parm = " wmode='opaque'" + " width='100%%'" + " height='100%%'";
+                var swf_parm = " name='OpenLPFlashMovie'" + " autostart='true' loop='false' play='true'" +
+                    " hidden='false' swliveconnect='true' allowscriptaccess='always'" + " volume='" + volume + "'";
+
+                switch(state){
+                    case 'load':
+                        text.innerHTML = "<embed " + src + view_parm + swf_parm + "/>";
+                        flashMovie = getFlashMovieObject("OpenLPFlashMovie");
+                        flashMovie.Play();
+                        break;
+                    case 'play':
+                        flashMovie.Play();
+                        break;
+                    case 'pause':
+                        flashMovie.StopPlay();
+                        break;
+                    case 'stop':
+                        flashMovie.StopPlay();
+                        tempHtml = text.innerHTML;
+                        text.innerHTML = '';
+                        text.innerHTML = tempHtml;
+                        break;
+                    case 'close':
+                        flashMovie.StopPlay();
+                        text.innerHTML = '';
+                        break;
+                    case 'length':
+                        return flashMovie.TotalFrames();
+                    case 'current_time':
+                        return flashMovie.CurrentFrame();
+                    case 'seek':
+        //                flashMovie.GotoFrame(variable_value);
+                        break;
+                    case 'isEnded':
+                        //TODO check flash end
+                        return false;
+                    case 'setVisible':
+                        text.style.visibility = variable_value;
+                        break;
+                }
+            }
+
+            function show_alert(alerttext, position){
+                var text = document.getElementById('alert');
+                text.innerHTML = alerttext;
+                if(alerttext == '') {
+                    text.style.visibility = 'hidden';
+                    return 0;
+                }
+                if(position == ''){
+                    position = getComputedStyle(text, '').verticalAlign;
+                }
+                switch(position)
+                {
+                    case 'top':
+                        text.style.top = '0px';
+                        break;
+                    case 'middle':
+                        text.style.top = ((window.innerHeight - text.clientHeight) / 2)
+                            + 'px';
+                        break;
+                    case 'bottom':
+                        text.style.top = (window.innerHeight - text.clientHeight)
+                            + 'px';
+                        break;
+                }
+                text.style.visibility = 'visible';
+                return text.clientHeight;
+            }
+
+            function update_css(align, font, size, color, bgcolor){
+                var text = document.getElementById('alert');
+                text.style.fontSize = size + "pt";
+                text.style.fontFamily = font;
+                text.style.color = color;
+                text.style.backgroundColor = bgcolor;
+                switch(align)
+                {
+                    case 'top':
+                        text.style.top = '0px';
+                        break;
+                    case 'middle':
+                        text.style.top = ((window.innerHeight - text.clientHeight) / 2)
+                            + 'px';
+                        break;
+                    case 'bottom':
+                        text.style.top = (window.innerHeight - text.clientHeight)
+                            + 'px';
+                        break;
+                }
+            }
+
+
+            function show_image(src){
+                var img = document.getElementById('image');
+                img.src = src;
+                if(src == '')
+                    img.style.display = 'none';
+                else
+                    img.style.display = 'block';
+            }
+
+            function show_blank(state){
+                var black = 'none';
+                var lyrics = '';
+                switch(state){
+                    case 'theme':
+                        lyrics = 'hidden';
+                        break;
+                    case 'black':
+                        black = 'block';
+                        break;
+                    case 'desktop':
+                        break;
+                }
+                document.getElementById('black').style.display = black;
+                document.getElementById('lyricsmain').style.visibility = lyrics;
+                document.getElementById('image').style.visibility = lyrics;
+                document.getElementById('footer').style.visibility = lyrics;
+            }
+
+            function show_footer(footertext){
+                document.getElementById('footer').innerHTML = footertext;
+            }
+
+            function show_text(new_text){
+                var match = /-webkit-text-fill-color:[^;"]+/gi;
+                if(timer != null)
+                    clearTimeout(timer);
+                /*
+                QtWebkit bug with outlines and justify causing outline alignment
+                problems. (Bug 859950) Surround each word with a <span> to workaround,
+                but only in this scenario.
+                */
+                var txt = document.getElementById('lyricsmain');
+                if(window.getComputedStyle(txt).textAlign == 'justify'){
+                    if(window.getComputedStyle(txt).webkitTextStrokeWidth != '0px'){
+                        new_text = new_text.replace(/(\s|&nbsp;)+(?![^<]*>)/g,
+                            function(match) {
+                                return '</span>' + match + '<span>';
+                            });
+                        new_text = '<span>' + new_text + '</span>';
+                    }
+                }
+                text_fade('lyricsmain', new_text);
+            }
+
+            function text_fade(id, new_text){
+                /*
+                Show the text.
+                */
+                var text = document.getElementById(id);
+                if(text == null) return;
+                if(!transition){
+                    text.innerHTML = new_text;
+                    return;
+                }
+                // Fade text out. 0.1 to minimize the time "nothing" is shown on the screen.
+                text.style.opacity = '0.1';
+                // Fade new text in after the old text has finished fading out.
+                timer = window.setTimeout(function(){_show_text(text, new_text)}, 400);
+            }
+
+            function _show_text(text, new_text) {
+                /*
+                Helper function to show the new_text delayed.
+                */
+                text.innerHTML = new_text;
+                text.style.opacity = '1';
+                // Wait until the text is completely visible. We want to save the timer id, to be able to call
+                // clearTimeout(timer) when the text has changed before finishing fading.
+                timer = window.setTimeout(function(){timer = null;}, 400);
+            }
+
+            function show_text_completed(){
+                return (timer == null);
+            }
+        </script>
+        </head>
+        <body>
+        <img id="bgimage" class="size" style="display:none;" />
+        <img id="image" class="size" style="display:none;" />
+
+        <div id="videobackboard" class="size" style="visibility:hidden"></div>
+        <video id="video" class="size" style="visibility:hidden" autobuffer preload></video>
+
+        <div id="flash" class="size" style="visibility:hidden"></div>
+
+            <div id="alert" style="visibility:hidden"></div>
+
+        <div class="lyricstable"><div id="lyricsmain" style="opacity:1" class="lyricscell lyricsmain"></div></div>
+        <div id="footer" class="footer"></div>
+        <div id="black" class="size"></div>
+        </body>
+        </html>
+"""
+import logging
+
+from PyQt5 import QtWebKit
+
+from openlp.core.common import Settings
+from openlp.core.lib.theme import BackgroundType, BackgroundGradientType, VerticalType, HorizontalType
+
+log = logging.getLogger(__name__)
+
+HTMLSRC = """
+<!DOCTYPE html>
+<html>
+<head>
+<title>OpenLP Display</title>
+<style>
+*{
+    margin: 0;
+    padding: 0;
+    border: 0;
+    overflow: hidden;
+    -webkit-user-select: none;
+}
+body {
+    %s;
+}
+.size {
+    position: absolute;
+    left: 0px;
+    top: 0px;
+    width: 100%%;
+    height: 100%%;
+}
+#black {
+    z-index: 8;
+    background-color: black;
+    display: none;
+}
+#bgimage {
+    z-index: 1;
+}
+#image {
+    z-index: 2;
+}
+%s
+#footer {
+    position: absolute;
+    z-index: 6;
+    %s
+}
+/* lyric css */
+%s
+sup {
+    font-size: 0.6em;
+    vertical-align: top;
+    position: relative;
+    top: -0.3em;
+}
+</style>
+<script>
+    var timer = null;
+    var transition = %s;
+    %s
+
+    function show_image(src){
+        var img = document.getElementById('image');
+        img.src = src;
+        if(src == '')
+            img.style.display = 'none';
+        else
+            img.style.display = 'block';
+    }
+
+    function show_blank(state){
+        var black = 'none';
+        var lyrics = '';
+        switch(state){
+            case 'theme':
+                lyrics = 'hidden';
+                break;
+            case 'black':
+                black = 'block';
+                break;
+            case 'desktop':
+                break;
+        }
+        document.getElementById('black').style.display = black;
+        document.getElementById('lyricsmain').style.visibility = lyrics;
+        document.getElementById('image').style.visibility = lyrics;
+        document.getElementById('footer').style.visibility = lyrics;
+    }
+
+    function show_footer(footertext){
+        document.getElementById('footer').innerHTML = footertext;
+    }
+
+    function show_text(new_text){
+        var match = /-webkit-text-fill-color:[^;\"]+/gi;
+        if(timer != null)
+            clearTimeout(timer);
+        /*
+        QtWebkit bug with outlines and justify causing outline alignment
+        problems. (Bug 859950) Surround each word with a <span> to workaround,
+        but only in this scenario.
+        */
+        var txt = document.getElementById('lyricsmain');
+        if(window.getComputedStyle(txt).textAlign == 'justify'){
+            if(window.getComputedStyle(txt).webkitTextStrokeWidth != '0px'){
+                new_text = new_text.replace(/(\s|&nbsp;)+(?![^<]*>)/g,
+                    function(match) {
+                        return '</span>' + match + '<span>';
+                    });
+                new_text = '<span>' + new_text + '</span>';
+            }
+        }
+        text_fade('lyricsmain', new_text);
+    }
+
+    function text_fade(id, new_text){
+        /*
+        Show the text.
+        */
+        var text = document.getElementById(id);
+        if(text == null) return;
+        if(!transition){
+            text.innerHTML = new_text;
+            return;
+        }
+        // Fade text out. 0.1 to minimize the time "nothing" is shown on the screen.
+        text.style.opacity = '0.1';
+        // Fade new text in after the old text has finished fading out.
+        timer = window.setTimeout(function(){_show_text(text, new_text)}, 400);
+    }
+
+    function _show_text(text, new_text) {
+        /*
+        Helper function to show the new_text delayed.
+        */
+        text.innerHTML = new_text;
+        text.style.opacity = '1';
+        // Wait until the text is completely visible. We want to save the timer id, to be able to call
+        // clearTimeout(timer) when the text has changed before finishing fading.
+        timer = window.setTimeout(function(){timer = null;}, 400);
+    }
+
+    function show_text_completed(){
+        return (timer == null);
+    }
+</script>
+</head>
+<body>
+<img id="bgimage" class="size" %s />
+<img id="image" class="size" %s />
+%s
+<div class="lyricstable"><div id="lyricsmain" style="opacity:1" class="lyricscell lyricsmain"></div></div>
+<div id="footer" class="footer"></div>
+<div id="black" class="size"></div>
+</body>
+</html>
+"""
+
+
+def build_html(item, screen, is_live, background, image=None, plugins=None):
+    """
+    Build the full web paged structure for display
+
+    :param item: Service Item to be displayed
+    :param screen: Current display information
+    :param is_live: Item is going live, rather than preview/theme building
+    :param background:  Theme background image - bytes
+    :param image: Image media item - bytes
+    :param plugins: The List of available plugins
+    """
+    width = screen['size'].width()
+    height = screen['size'].height()
+    theme_data = item.theme_data
+    # Image generated and poked in
+    if background:
+        bgimage_src = 'src="data:image/png;base64,%s"' % background
+    elif item.bg_image_bytes:
+        bgimage_src = 'src="data:image/png;base64,%s"' % item.bg_image_bytes
+    else:
+        bgimage_src = 'style="display:none;"'
+    if image:
+        image_src = 'src="data:image/png;base64,%s"' % image
+    else:
+        image_src = 'style="display:none;"'
+    css_additions = ''
+    js_additions = ''
+    html_additions = ''
+    if plugins:
+        for plugin in plugins:
+            css_additions += plugin.get_display_css()
+            js_additions += plugin.get_display_javascript()
+            html_additions += plugin.get_display_html()
+    html = HTMLSRC % (
+        build_background_css(item, width),
+        css_additions,
+        build_footer_css(item, height),
+        build_lyrics_css(item),
+        'true' if theme_data and theme_data.display_slide_transition and is_live else 'false',
+        js_additions,
+        bgimage_src,
+        image_src,
+        html_additions
+    )
+    return html
+
+
+def webkit_version():
+    """
+    Return the Webkit version in use. Note method added relatively recently, so return 0 if prior to this
+    """
+    try:
+        webkit_ver = float(QtWebKit.qWebKitVersion())
+        log.debug('Webkit version = %s' % webkit_ver)
+    except AttributeError:
+        webkit_ver = 0
+    return webkit_ver
+
+
+def build_background_css(item, width):
+    """
+    Build the background css
+
+    :param item: Service Item containing theme and location information
+    :param width:
+    """
+    width = int(width) // 2
+    theme = item.theme_data
+    background = 'background-color: black'
+    if theme:
+        if theme.background_type == BackgroundType.to_string(BackgroundType.Transparent):
+            background = ''
+        elif theme.background_type == BackgroundType.to_string(BackgroundType.Solid):
+            background = 'background-color: %s' % theme.background_color
+        else:
+            if theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.Horizontal):
+                background = 'background: -webkit-gradient(linear, left top, left bottom, from(%s), to(%s)) fixed' \
+                    % (theme.background_start_color, theme.background_end_color)
+            elif theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.LeftTop):
+                background = 'background: -webkit-gradient(linear, left top, right bottom, from(%s), to(%s)) fixed' \
+                    % (theme.background_start_color, theme.background_end_color)
+            elif theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.LeftBottom):
+                background = 'background: -webkit-gradient(linear, left bottom, right top, from(%s), to(%s)) fixed' \
+                    % (theme.background_start_color, theme.background_end_color)
+            elif theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.Vertical):
+                background = 'background: -webkit-gradient(linear, left top, right top, from(%s), to(%s)) fixed' % \
+                    (theme.background_start_color, theme.background_end_color)
+            else:
+                background = 'background: -webkit-gradient(radial, %s 50%%, 100, %s 50%%, %s, from(%s), to(%s)) fixed'\
+                    % (width, width, width, theme.background_start_color, theme.background_end_color)
+    return background
+
+
+def build_lyrics_css(item):
+    """
+    Build the lyrics display css
+
+    :param item: Service Item containing theme and location information
+    """
+    style = """
+.lyricstable {
+    z-index: 5;
+    position: absolute;
+    display: table;
+    %s
+}
+.lyricscell {
+    display: table-cell;
+    word-wrap: break-word;
+    -webkit-transition: opacity 0.4s ease;
+    %s
+}
+.lyricsmain {
+    %s
+}
+"""
+    theme_data = item.theme_data
+    lyricstable = ''
+    lyrics = ''
+    lyricsmain = ''
+    if theme_data and item.main:
+        lyricstable = 'left: %spx; top: %spx;' % (item.main.x(), item.main.y())
+        lyrics = build_lyrics_format_css(theme_data, item.main.width(), item.main.height())
+        lyricsmain += build_lyrics_outline_css(theme_data)
+        if theme_data.font_main_shadow:
+            lyricsmain += ' text-shadow: %s %spx %spx;' % \
+                (theme_data.font_main_shadow_color, theme_data.font_main_shadow_size, theme_data.font_main_shadow_size)
+    lyrics_css = style % (lyricstable, lyrics, lyricsmain)
+    return lyrics_css
+
+
+def build_lyrics_outline_css(theme_data):
+    """
+    Build the css which controls the theme outline. Also used by renderer for splitting verses
+
+    :param theme_data: Object containing theme information
+    """
+    if theme_data.font_main_outline:
+        size = float(theme_data.font_main_outline_size) / 16
+        fill_color = theme_data.font_main_color
+        outline_color = theme_data.font_main_outline_color
+        return ' -webkit-text-stroke: %sem %s; -webkit-text-fill-color: %s; ' % (size, outline_color, fill_color)
+    return ''
+
+
+def build_lyrics_format_css(theme_data, width, height):
+    """
+    Build the css which controls the theme format. Also used by renderer for splitting verses
+
+    :param theme_data: Object containing theme information
+    :param width: Width of the lyrics block
+    :param height: Height of the lyrics block
+    """
+    align = HorizontalType.Names[theme_data.display_horizontal_align]
+    valign = VerticalType.Names[theme_data.display_vertical_align]
+    if theme_data.font_main_outline:
+        left_margin = int(theme_data.font_main_outline_size) * 2
+    else:
+        left_margin = 0
+    justify = 'white-space:pre-wrap;'
+    # fix tag incompatibilities
+    if theme_data.display_horizontal_align == HorizontalType.Justify:
+        justify = ''
+    if theme_data.display_vertical_align == VerticalType.Bottom:
+        padding_bottom = '0.5em'
+    else:
+        padding_bottom = '0'
+    lyrics = '%s word-wrap: break-word; ' \
+             'text-align: %s; vertical-align: %s; font-family: %s; ' \
+             'font-size: %spt; color: %s; line-height: %d%%; margin: 0;' \
+             'padding: 0; padding-bottom: %s; padding-left: %spx; width: %spx; height: %spx; ' % \
+        (justify, align, valign, theme_data.font_main_name, theme_data.font_main_size,
+         theme_data.font_main_color, 100 + int(theme_data.font_main_line_adjustment), padding_bottom,
+         left_margin, width, height)
+    if theme_data.font_main_italics:
+        lyrics += 'font-style:italic; '
+    if theme_data.font_main_bold:
+        lyrics += 'font-weight:bold; '
+    return lyrics
+
+
+def build_footer_css(item, height):
+    """
+    Build the display of the item footer
+
+    :param item: Service Item to be processed.
+    :param height:
+    """
+    style = """
+    left: %spx;
+    bottom: %spx;
+    width: %spx;
+    font-family: %s;
+    font-size: %spt;
+    color: %s;
+    text-align: left;
+    white-space: %s;
+    """
+    theme = item.theme_data
+    if not theme or not item.footer:
+        return ''
+    bottom = height - int(item.footer.y()) - int(item.footer.height())
+    whitespace = 'normal' if Settings().value('themes/wrap footer') else 'nowrap'
+    lyrics_html = style % (item.footer.x(), bottom, item.footer.width(),
+                           theme.font_footer_name, theme.font_footer_size, theme.font_footer_color, whitespace)
+    return lyrics_html

=== added file 'openlp/core/lib/imagemanager.py'
--- openlp/core/lib/imagemanager.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/imagemanager.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,321 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Provides the store and management for Images automatically caching them and resizing them when needed. Only one copy of
+each image is needed in the system. A Thread is used to convert the image to a byte array so the user does not need to
+wait for the conversion to happen.
+"""
+import logging
+import os
+import time
+import queue
+
+from PyQt5 import QtCore
+
+from openlp.core.common import Registry
+from openlp.core.lib import ScreenList, resize_image, image_to_byte
+
+log = logging.getLogger(__name__)
+
+
+class ImageThread(QtCore.QThread):
+    """
+    A special Qt thread class to speed up the display of images. This is threaded so it loads the frames and generates
+    byte stream in background.
+    """
+    def __init__(self, manager):
+        """
+        Constructor for the thread class.
+
+        ``manager``
+            The image manager.
+        """
+        super(ImageThread, self).__init__(None)
+        self.image_manager = manager
+
+    def run(self):
+        """
+        Run the thread.
+        """
+        self.image_manager._process()
+
+
+class Priority(object):
+    """
+    Enumeration class for different priorities.
+
+    ``Lowest``
+        Only the image's byte stream has to be generated. But neither the ``QImage`` nor the byte stream has been
+        requested yet.
+
+    ``Low``
+        Only the image's byte stream has to be generated. Because the image's ``QImage`` has been requested previously
+        it is reasonable to assume that the byte stream will be needed before the byte stream of other images whose
+        ``QImage`` were not generated due to a request.
+
+    ``Normal``
+        The image's byte stream as well as the image has to be generated. Neither the ``QImage`` nor the byte stream has
+        been requested yet.
+
+    ``High``
+        The image's byte stream as well as the image has to be generated. The ``QImage`` for this image has been
+        requested. **Note**, this priority is only set when the ``QImage`` has not been generated yet.
+
+    ``Urgent``
+        The image's byte stream as well as the image has to be generated. The byte stream for this image has been
+        requested. **Note**, this priority is only set when the byte stream has not been generated yet.
+    """
+    Lowest = 4
+    Low = 3
+    Normal = 2
+    High = 1
+    Urgent = 0
+
+
+class Image(object):
+    """
+    This class represents an image. To mark an image as *dirty* call the :class:`ImageManager`'s ``_reset_image`` method
+    with the Image instance as argument.
+    """
+    secondary_priority = 0
+
+    def __init__(self, path, source, background, width=-1, height=-1):
+        """
+        Create an image for the :class:`ImageManager`'s cache.
+
+        :param path: The image's file path. This should be an existing file path.
+        :param source: The source describes the image's origin. Possible values are described in the
+            :class:`~openlp.core.lib.ImageSource` class.
+        :param background: A ``QtGui.QColor`` object specifying the colour to be used to fill the gabs if the image's
+            ratio does not match with the display ratio.
+        :param width: The width of the image, defaults to -1 meaning that the screen width will be used.
+        :param height: The height of the image, defaults to -1 meaning that the screen height will be used.
+        """
+        self.path = path
+        self.image = None
+        self.image_bytes = None
+        self.priority = Priority.Normal
+        self.source = source
+        self.background = background
+        self.timestamp = 0
+        self.width = width
+        self.height = height
+        # FIXME: We assume that the path exist. The caller has to take care that it exists!
+        if os.path.exists(path):
+            self.timestamp = os.stat(path).st_mtime
+        self.secondary_priority = Image.secondary_priority
+        Image.secondary_priority += 1
+
+
+class PriorityQueue(queue.PriorityQueue):
+    """
+    Customised ``Queue.PriorityQueue``.
+
+    Each item in the queue must be a tuple with three values. The first value is the :class:`Image`'s ``priority``
+    attribute, the second value the :class:`Image`'s ``secondary_priority`` attribute. The last value the :class:`Image`
+    instance itself::
+
+        (image.priority, image.secondary_priority, image)
+
+    Doing this, the :class:`Queue.PriorityQueue` will sort the images according to their priorities, but also according
+    to there number. However, the number only has an impact on the result if there are more images with the same
+    priority. In such case the image which has been added earlier is privileged.
+    """
+    def modify_priority(self, image, new_priority):
+        """
+        Modifies the priority of the given ``image``.
+
+        :param image: The image to remove. This should be an :class:`Image` instance.
+        :param new_priority: The image's new priority. See the :class:`Priority` class for priorities.
+        """
+        self.remove(image)
+        image.priority = new_priority
+        self.put((image.priority, image.secondary_priority, image))
+
+    def remove(self, image):
+        """
+        Removes the given ``image`` from the queue.
+
+        :param image:  The image to remove. This should be an ``Image`` instance.
+        """
+        if (image.priority, image.secondary_priority, image) in self.queue:
+            self.queue.remove((image.priority, image.secondary_priority, image))
+
+
+class ImageManager(QtCore.QObject):
+    """
+    Image Manager handles the conversion and sizing of images.
+    """
+    log.info('Image Manager loaded')
+
+    def __init__(self):
+        """
+        Constructor for the image manager.
+        """
+        super(ImageManager, self).__init__()
+        Registry().register('image_manager', self)
+        current_screen = ScreenList().current
+        self.width = current_screen['size'].width()
+        self.height = current_screen['size'].height()
+        self._cache = {}
+        self.image_thread = ImageThread(self)
+        self._conversion_queue = PriorityQueue()
+        self.stop_manager = False
+        Registry().register_function('images_regenerate', self.process_updates)
+
+    def update_display(self):
+        """
+        Screen has changed size so rebuild the cache to new size.
+        """
+        log.debug('update_display')
+        current_screen = ScreenList().current
+        self.width = current_screen['size'].width()
+        self.height = current_screen['size'].height()
+        # Mark the images as dirty for a rebuild by setting the image and byte stream to None.
+        for image in list(self._cache.values()):
+            self._reset_image(image)
+
+    def update_images_border(self, source, background):
+        """
+        Border has changed so update all the images affected.
+        """
+        log.debug('update_images_border')
+        # Mark the images as dirty for a rebuild by setting the image and byte stream to None.
+        for image in list(self._cache.values()):
+            if image.source == source:
+                image.background = background
+                self._reset_image(image)
+
+    def update_image_border(self, path, source, background, width=-1, height=-1):
+        """
+        Border has changed so update the image affected.
+        """
+        log.debug('update_image_border')
+        # Mark the image as dirty for a rebuild by setting the image and byte stream to None.
+        image = self._cache[(path, source, width, height)]
+        if image.source == source:
+            image.background = background
+            self._reset_image(image)
+
+    def _reset_image(self, image):
+        """
+        Mark the given :class:`Image` instance as dirty by setting its ``image`` and ``image_bytes`` attributes to None.
+        """
+        image.image = None
+        image.image_bytes = None
+        self._conversion_queue.modify_priority(image, Priority.Normal)
+
+    def process_updates(self):
+        """
+        Flush the queue to updated any data to update
+        """
+        # We want only one thread.
+        if not self.image_thread.isRunning():
+            self.image_thread.start()
+
+    def get_image(self, path, source, width=-1, height=-1):
+        """
+        Return the ``QImage`` from the cache. If not present wait for the background thread to process it.
+        """
+        log.debug('getImage %s' % path)
+        image = self._cache[(path, source, width, height)]
+        if image.image is None:
+            self._conversion_queue.modify_priority(image, Priority.High)
+            # make sure we are running and if not give it a kick
+            self.process_updates()
+            while image.image is None:
+                log.debug('getImage - waiting')
+                time.sleep(0.1)
+        elif image.image_bytes is None:
+            # Set the priority to Low, because the image was requested but the byte stream was not generated yet.
+            # However, we only need to do this, when the image was generated before it was requested (otherwise this is
+            # already taken care of).
+            self._conversion_queue.modify_priority(image, Priority.Low)
+        return image.image
+
+    def get_image_bytes(self, path, source, width=-1, height=-1):
+        """
+        Returns the byte string for an image. If not present wait for the background thread to process it.
+        """
+        log.debug('get_image_bytes %s' % path)
+        image = self._cache[(path, source, width, height)]
+        if image.image_bytes is None:
+            self._conversion_queue.modify_priority(image, Priority.Urgent)
+            # make sure we are running and if not give it a kick
+            self.process_updates()
+            while image.image_bytes is None:
+                log.debug('getImageBytes - waiting')
+                time.sleep(0.1)
+        return image.image_bytes
+
+    def add_image(self, path, source, background, width=-1, height=-1):
+        """
+        Add image to cache if it is not already there.
+        """
+        log.debug('add_image %s' % path)
+        if not (path, source, width, height) in self._cache:
+            image = Image(path, source, background, width, height)
+            self._cache[(path, source, width, height)] = image
+            self._conversion_queue.put((image.priority, image.secondary_priority, image))
+        # Check if the there are any images with the same path and check if the timestamp has changed.
+        for image in list(self._cache.values()):
+            if os.path.exists(path):
+                if image.path == path and image.timestamp != os.stat(path).st_mtime:
+                    image.timestamp = os.stat(path).st_mtime
+                    self._reset_image(image)
+        # We want only one thread.
+        if not self.image_thread.isRunning():
+            self.image_thread.start()
+
+    def _process(self):
+        """
+        Controls the processing called from a ``QtCore.QThread``.
+        """
+        log.debug('_process - started')
+        while not self._conversion_queue.empty() and not self.stop_manager:
+            self._process_cache()
+        log.debug('_process - ended')
+
+    def _process_cache(self):
+        """
+        Actually does the work.
+        """
+        log.debug('_processCache')
+        image = self._conversion_queue.get()[2]
+        # Generate the QImage for the image.
+        if image.image is None:
+            # Let's see if the image was requested with specific dimensions
+            width = self.width if image.width == -1 else image.width
+            height = self.height if image.height == -1 else image.height
+            image.image = resize_image(image.path, width, height, image.background)
+            # Set the priority to Lowest and stop here as we need to process more important images first.
+            if image.priority == Priority.Normal:
+                self._conversion_queue.modify_priority(image, Priority.Lowest)
+                return
+            # For image with high priority we set the priority to Low, as the byte stream might be needed earlier the
+            # byte stream of image with Normal priority. We stop here as we need to process more important images first.
+            elif image.priority == Priority.High:
+                self._conversion_queue.modify_priority(image, Priority.Low)
+                return
+        # Generate the byte stream for the image.
+        if image.image_bytes is None:
+            image.image_bytes = image_to_byte(image.image)

=== added directory 'openlp/core/lib/json'
=== added file 'openlp/core/lib/json/theme.json'
--- openlp/core/lib/json/theme.json	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/json/theme.json	2016-04-05 20:22:40 +0000
@@ -0,0 +1,59 @@
+{
+    "background" : {
+        "border_color": "#000000",
+        "color": "#000000",
+        "direction": "vertical",
+        "end_color": "#000000",
+        "filename": "",
+        "start_color": "#000000",
+        "type": "solid"
+    },
+    "display" :{
+        "horizontal_align": 0,
+        "slide_transition": false,
+        "vertical_align": 0
+    },
+    "font": {
+        "footer": {
+            "bold": false,
+            "color": "#FFFFFF",
+            "height": 78,
+            "italics": false,
+            "line_adjustment": 0,
+            "location": "",
+            "name": "Arial",
+            "outline": false,
+            "outline_color": "#000000",
+            "outline_size": 2,
+            "override": false,
+            "shadow": true,
+            "shadow_color": "#000000",
+            "shadow_size": 5,
+            "size": 12,
+            "width": 1004,
+            "x": 10,
+            "y": 690
+            },
+        "main": {
+            "bold": false,
+            "color": "#FFFFFF",
+            "height": 690,
+            "italics": false,
+            "line_adjustment": 0,
+            "location": "",
+            "name": "Arial",
+            "outline": false,
+            "outline_color": "#000000",
+            "outline_size": 2,
+            "override": false,
+            "shadow": true,
+            "shadow_color": "#000000",
+            "shadow_size": 5,
+            "size": 40,
+            "width": 1004,
+            "x": 10,
+            "y": 10
+        }
+    },
+    "theme_name": ""
+}

=== added file 'openlp/core/lib/listwidgetwithdnd.py'
--- openlp/core/lib/listwidgetwithdnd.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/listwidgetwithdnd.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Extend QListWidget to handle drag and drop functionality
+"""
+import os
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import Registry
+
+
+class ListWidgetWithDnD(QtWidgets.QListWidget):
+    """
+    Provide a list widget to store objects and handle drag and drop events
+    """
+    def __init__(self, parent=None, name=''):
+        """
+        Initialise the list widget
+        """
+        super(ListWidgetWithDnD, self).__init__(parent)
+        self.mime_data_text = name
+
+    def activateDnD(self):
+        """
+        Activate DnD of widget
+        """
+        self.setAcceptDrops(True)
+        self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
+        Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file)
+
+    def mouseMoveEvent(self, event):
+        """
+        Drag and drop event does not care what data is selected as the recipient will use events to request the data
+        move just tell it what plugin to call
+        """
+        if event.buttons() != QtCore.Qt.LeftButton:
+            event.ignore()
+            return
+        if not self.selectedItems():
+            event.ignore()
+            return
+        drag = QtGui.QDrag(self)
+        mime_data = QtCore.QMimeData()
+        drag.setMimeData(mime_data)
+        mime_data.setText(self.mime_data_text)
+        drag.exec(QtCore.Qt.CopyAction)
+
+    def dragEnterEvent(self, event):
+        """
+        When something is dragged into this object, check if you should be able to drop it in here.
+        """
+        if event.mimeData().hasUrls():
+            event.accept()
+        else:
+            event.ignore()
+
+    def dragMoveEvent(self, event):
+        """
+        Make an object droppable, and set it to copy the contents of the object, not move it.
+        """
+        if event.mimeData().hasUrls():
+            event.setDropAction(QtCore.Qt.CopyAction)
+            event.accept()
+        else:
+            event.ignore()
+
+    def dropEvent(self, event):
+        """
+        Receive drop event check if it is a file and process it if it is.
+
+        :param event:  Handle of the event pint passed
+        """
+        if event.mimeData().hasUrls():
+            event.setDropAction(QtCore.Qt.CopyAction)
+            event.accept()
+            files = []
+            for url in event.mimeData().urls():
+                local_file = os.path.normpath(url.toLocalFile())
+                if os.path.isfile(local_file):
+                    files.append(local_file)
+                elif os.path.isdir(local_file):
+                    listing = os.listdir(local_file)
+                    for file in listing:
+                        files.append(os.path.join(local_file, file))
+            Registry().execute('%s_dnd' % self.mime_data_text, {'files': files, 'target': self.itemAt(event.pos())})
+        else:
+            event.ignore()

=== added file 'openlp/core/lib/mediamanageritem.py'
--- openlp/core/lib/mediamanageritem.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/mediamanageritem.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,682 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Provides the generic functions for interfacing plugins with the Media Manager.
+"""
+import logging
+import os
+import re
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate
+from openlp.core.lib import FileDialog, OpenLPToolbar, ServiceItem, StringContent, ListWidgetWithDnD, \
+    ServiceItemContext
+from openlp.core.lib.searchedit import SearchEdit
+from openlp.core.lib.ui import create_widget_action, critical_error_message_box
+
+log = logging.getLogger(__name__)
+
+
+class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
+    """
+    MediaManagerItem is a helper widget for plugins.
+
+    None of the following *need* to be used, feel free to override them completely in your plugin's implementation.
+    Alternatively, call them from your plugin before or after you've done extra things that you need to.
+
+    **Constructor Parameters**
+
+    ``parent``
+        The parent widget. Usually this will be the *Media Manager* itself. This needs to be a class descended from
+        ``QWidget``.
+
+    ``plugin``
+        The plugin widget. Usually this will be the *Plugin* itself. This needs to be a class descended from ``Plugin``.
+
+    **Member Variables**
+
+    When creating a descendant class from this class for your plugin, the following member variables should be set.
+
+     ``self.on_new_prompt``
+
+        Defaults to *'Select Image(s)'*.
+
+     ``self.on_new_file_masks``
+        Defaults to *'Images (*.jpg *jpeg *.gif *.png *.bmp)'*. This assumes that the new action is to load a file. If
+        not, you need to override the ``OnNew`` method.
+
+     ``self.PreviewFunction``
+        This must be a method which returns a QImage to represent the item (usually a preview). No scaling is required,
+        that is performed automatically by OpenLP when necessary. If this method is not defined, a default will be used
+        (treat the filename as an image).
+    """
+    log.info('Media Item loaded')
+
+    def __init__(self, parent=None, plugin=None):
+        """
+        Constructor to create the media manager item.
+        """
+        super(MediaManagerItem, self).__init__(parent)
+        self.plugin = plugin
+        self._setup()
+        self.setup_item()
+
+    def _setup(self):
+        """
+        Run some initial setup. This method is separate from __init__ in order to mock it out in tests.
+        """
+        self.hide()
+        self.whitespace = re.compile(r'[\W_]+', re.UNICODE)
+        visible_title = self.plugin.get_string(StringContent.VisibleName)
+        self.title = str(visible_title['title'])
+        Registry().register(self.plugin.name, self)
+        self.settings_section = self.plugin.name
+        self.toolbar = None
+        self.remote_triggered = None
+        self.single_service_item = True
+        self.quick_preview_allowed = False
+        self.has_search = False
+        self.page_layout = QtWidgets.QVBoxLayout(self)
+        self.page_layout.setSpacing(0)
+        self.page_layout.setContentsMargins(0, 0, 0, 0)
+        self.required_icons()
+        self.setupUi()
+        self.retranslateUi()
+        self.auto_select_id = -1
+
+    def setup_item(self):
+        """
+        Override this for additional Plugin setup
+        """
+        pass
+
+    def required_icons(self):
+        """
+        This method is called to define the icons for the plugin. It provides a default set and the plugin is able to
+        override the if required.
+        """
+        self.has_import_icon = False
+        self.has_new_icon = True
+        self.has_edit_icon = True
+        self.has_file_icon = False
+        self.has_delete_icon = True
+        self.add_to_service_item = False
+
+    def retranslateUi(self):
+        """
+        This method is called automatically to provide OpenLP with the opportunity to translate the ``MediaManagerItem``
+        to another language.
+        """
+        pass
+
+    def add_toolbar(self):
+        """
+        A method to help developers easily add a toolbar to the media manager item.
+        """
+        if self.toolbar is None:
+            self.toolbar = OpenLPToolbar(self)
+            self.page_layout.addWidget(self.toolbar)
+
+    def setupUi(self):
+        """
+        This method sets up the interface on the button. Plugin developers use this to add and create toolbars, and the
+        rest of the interface of the media manager item.
+        """
+        # Add a toolbar
+        self.add_toolbar()
+        # Allow the plugin to define buttons at start of bar
+        self.add_start_header_bar()
+        # Add the middle of the tool bar (pre defined)
+        self.add_middle_header_bar()
+        # Allow the plugin to define buttons at end of bar
+        self.add_end_header_bar()
+        # Add the list view
+        self.add_list_view_to_toolbar()
+
+    def add_middle_header_bar(self):
+        """
+        Create buttons for the media item toolbar
+        """
+        toolbar_actions = []
+        # Import Button
+        if self.has_import_icon:
+            toolbar_actions.append(['Import', StringContent.Import,
+                                    ':/general/general_import.png', self.on_import_click])
+        # Load Button
+        if self.has_file_icon:
+            toolbar_actions.append(['Load', StringContent.Load, ':/general/general_open.png', self.on_file_click])
+        # New Button
+        if self.has_new_icon:
+            toolbar_actions.append(['New', StringContent.New, ':/general/general_new.png', self.on_new_click])
+        # Edit Button
+        if self.has_edit_icon:
+            toolbar_actions.append(['Edit', StringContent.Edit, ':/general/general_edit.png', self.on_edit_click])
+        # Delete Button
+        if self.has_delete_icon:
+            toolbar_actions.append(['Delete', StringContent.Delete,
+                                    ':/general/general_delete.png', self.on_delete_click])
+        # Preview
+        toolbar_actions.append(['Preview', StringContent.Preview,
+                                ':/general/general_preview.png', self.on_preview_click])
+        # Live Button
+        toolbar_actions.append(['Live', StringContent.Live, ':/general/general_live.png', self.on_live_click])
+        # Add to service Button
+        toolbar_actions.append(['Service', StringContent.Service, ':/general/general_add.png', self.on_add_click])
+        for action in toolbar_actions:
+            if action[0] == StringContent.Preview:
+                self.toolbar.addSeparator()
+            self.toolbar.add_toolbar_action('%s%sAction' % (self.plugin.name, action[0]),
+                                            text=self.plugin.get_string(action[1])['title'], icon=action[2],
+                                            tooltip=self.plugin.get_string(action[1])['tooltip'],
+                                            triggers=action[3])
+
+    def add_list_view_to_toolbar(self):
+        """
+        Creates the main widget for listing items the media item is tracking
+        """
+        # Add the List widget
+        self.list_view = ListWidgetWithDnD(self, self.plugin.name)
+        self.list_view.setSpacing(1)
+        self.list_view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+        self.list_view.setAlternatingRowColors(True)
+        self.list_view.setObjectName('%sListView' % self.plugin.name)
+        # Add to page_layout
+        self.page_layout.addWidget(self.list_view)
+        # define and add the context menu
+        self.list_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+        if self.has_edit_icon:
+            create_widget_action(self.list_view,
+                                 text=self.plugin.get_string(StringContent.Edit)['title'],
+                                 icon=':/general/general_edit.png',
+                                 triggers=self.on_edit_click)
+            create_widget_action(self.list_view, separator=True)
+        create_widget_action(self.list_view,
+                             'listView%s%sItem' % (self.plugin.name.title(), StringContent.Preview.title()),
+                             text=self.plugin.get_string(StringContent.Preview)['title'],
+                             icon=':/general/general_preview.png',
+                             can_shortcuts=True,
+                             triggers=self.on_preview_click)
+        create_widget_action(self.list_view,
+                             'listView%s%sItem' % (self.plugin.name.title(), StringContent.Live.title()),
+                             text=self.plugin.get_string(StringContent.Live)['title'],
+                             icon=':/general/general_live.png',
+                             can_shortcuts=True,
+                             triggers=self.on_live_click)
+        create_widget_action(self.list_view,
+                             'listView%s%sItem' % (self.plugin.name.title(), StringContent.Service.title()),
+                             can_shortcuts=True,
+                             text=self.plugin.get_string(StringContent.Service)['title'],
+                             icon=':/general/general_add.png',
+                             triggers=self.on_add_click)
+        if self.has_delete_icon:
+            create_widget_action(self.list_view, separator=True)
+            create_widget_action(self.list_view,
+                                 'listView%s%sItem' % (self.plugin.name.title(), StringContent.Delete.title()),
+                                 text=self.plugin.get_string(StringContent.Delete)['title'],
+                                 icon=':/general/general_delete.png',
+                                 can_shortcuts=True, triggers=self.on_delete_click)
+        if self.add_to_service_item:
+            create_widget_action(self.list_view, separator=True)
+            create_widget_action(self.list_view,
+                                 text=translate('OpenLP.MediaManagerItem', '&Add to selected Service Item'),
+                                 icon=':/general/general_add.png',
+                                 triggers=self.on_add_edit_click)
+        self.add_custom_context_actions()
+        # Create the context menu and add all actions from the list_view.
+        self.menu = QtWidgets.QMenu()
+        self.menu.addActions(self.list_view.actions())
+        self.list_view.doubleClicked.connect(self.on_double_clicked)
+        self.list_view.itemSelectionChanged.connect(self.on_selection_change)
+        self.list_view.customContextMenuRequested.connect(self.context_menu)
+
+    def add_search_to_toolbar(self):
+        """
+        Creates a search field with button and related signal handling.
+        """
+        self.search_widget = QtWidgets.QWidget(self)
+        self.search_widget.setObjectName('search_widget')
+        self.search_layout = QtWidgets.QVBoxLayout(self.search_widget)
+        self.search_layout.setObjectName('search_layout')
+        self.search_text_layout = QtWidgets.QFormLayout()
+        self.search_text_layout.setObjectName('search_text_layout')
+        self.search_text_label = QtWidgets.QLabel(self.search_widget)
+        self.search_text_label.setObjectName('search_text_label')
+        self.search_text_edit = SearchEdit(self.search_widget)
+        self.search_text_edit.setObjectName('search_text_edit')
+        self.search_text_label.setBuddy(self.search_text_edit)
+        self.search_text_layout.addRow(self.search_text_label, self.search_text_edit)
+        self.search_layout.addLayout(self.search_text_layout)
+        self.search_button_layout = QtWidgets.QHBoxLayout()
+        self.search_button_layout.setObjectName('search_button_layout')
+        self.search_button_layout.addStretch()
+        self.search_text_button = QtWidgets.QPushButton(self.search_widget)
+        self.search_text_button.setObjectName('search_text_button')
+        self.search_button_layout.addWidget(self.search_text_button)
+        self.search_layout.addLayout(self.search_button_layout)
+        self.page_layout.addWidget(self.search_widget)
+        # Signals and slots
+        self.search_text_edit.returnPressed.connect(self.on_search_text_button_clicked)
+        self.search_text_button.clicked.connect(self.on_search_text_button_clicked)
+        self.search_text_edit.textChanged.connect(self.on_search_text_edit_changed)
+
+    def add_custom_context_actions(self):
+        """
+        Implement this method in your descendant media manager item to add any context menu items.
+        This method is called automatically.
+        """
+        pass
+
+    def initialise(self):
+        """
+        Implement this method in your descendant media manager item to do any UI or other initialisation.
+        This method is called automatically.
+        """
+        pass
+
+    def add_start_header_bar(self):
+        """
+        Slot at start of toolbar for plugin to add widgets
+        """
+        pass
+
+    def add_end_header_bar(self):
+        """
+        Slot at end of toolbar for plugin to add widgets
+        """
+        pass
+
+    def on_file_click(self):
+        """
+        Add a file to the list widget to make it available for showing
+        """
+        files = FileDialog.getOpenFileNames(self, self.on_new_prompt,
+                                            Settings().value(self.settings_section + '/last directory'),
+                                            self.on_new_file_masks)
+        log.info('New files(s) %s' % files)
+        if files:
+            self.application.set_busy_cursor()
+            self.validate_and_load(files)
+        self.application.set_normal_cursor()
+
+    def load_file(self, data):
+        """
+        Turn file from Drag and Drop into an array so the Validate code can run it.
+
+        :param data: A dictionary containing the list of files to be loaded and the target
+        """
+        new_files = []
+        error_shown = False
+        for file_name in data['files']:
+            file_type = file_name.split('.')[-1]
+            if file_type.lower() not in self.on_new_file_masks:
+                if not error_shown:
+                    critical_error_message_box(translate('OpenLP.MediaManagerItem', 'Invalid File Type'),
+                                               translate('OpenLP.MediaManagerItem',
+                                                         'Invalid File %s.\nSuffix not supported') % file_name)
+                    error_shown = True
+            else:
+                new_files.append(file_name)
+        if new_files:
+            self.validate_and_load(new_files, data['target'])
+
+    def dnd_move_internal(self, target):
+        """
+        Handle internal moving of media manager items
+
+        :param target: The target of the DnD action
+        """
+        pass
+
+    def validate_and_load(self, files, target_group=None):
+        """
+        Process a list for files either from the File Dialog or from Drag and
+        Drop
+
+        :param files: The files to be loaded.
+        :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
+        """
+        names = []
+        full_list = []
+        for count in range(self.list_view.count()):
+            names.append(self.list_view.item(count).text())
+            full_list.append(self.list_view.item(count).data(QtCore.Qt.UserRole))
+        duplicates_found = False
+        files_added = False
+        for file_path in files:
+            if file_path in full_list:
+                duplicates_found = True
+            else:
+                files_added = True
+                full_list.append(file_path)
+        if full_list and files_added:
+            if target_group is None:
+                self.list_view.clear()
+            self.load_list(full_list, target_group)
+            last_dir = os.path.split(files[0])[0]
+            Settings().setValue(self.settings_section + '/last directory', last_dir)
+            Settings().setValue('%s/%s files' % (self.settings_section, self.settings_section), self.get_file_list())
+        if duplicates_found:
+            critical_error_message_box(UiStrings().Duplicate,
+                                       translate('OpenLP.MediaManagerItem',
+                                                 'Duplicate files were found on import and were ignored.'))
+
+    def context_menu(self, point):
+        """
+        Display a context menu
+
+        :param point: The point the cursor was at
+        """
+        item = self.list_view.itemAt(point)
+        # Decide if we have to show the context menu or not.
+        if item is None:
+            return
+        if not item.flags() & QtCore.Qt.ItemIsSelectable:
+            return
+        self.menu.exec(self.list_view.mapToGlobal(point))
+
+    def get_file_list(self):
+        """
+        Return the current list of files
+        """
+        file_list = []
+        for index in range(self.list_view.count()):
+            list_item = self.list_view.item(index)
+            filename = list_item.data(QtCore.Qt.UserRole)
+            file_list.append(filename)
+        return file_list
+
+    def load_list(self, load_list, target_group):
+        """
+        Load a list. Needs to be implemented by the plugin.
+
+        :param load_list: List object to load
+        :param target_group: Group to load
+        """
+        raise NotImplementedError('MediaManagerItem.loadList needs to be defined by the plugin')
+
+    def on_new_click(self):
+        """
+        Hook for plugins to define behaviour for adding new items.
+        """
+        pass
+
+    def on_edit_click(self):
+        """
+        Hook for plugins to define behaviour for editing items.
+        """
+        pass
+
+    def on_delete_click(self):
+        """
+        Delete an item. Needs to be implemented by the plugin.
+        """
+        raise NotImplementedError('MediaManagerItem.on_delete_click needs to be defined by the plugin')
+
+    def on_focus(self):
+        """
+        Run when a tab in the media manager gains focus. This gives the media
+        item a chance to focus any elements it wants to.
+        """
+        pass
+
+    def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
+                            context=ServiceItemContext.Live):
+        """
+        Generate the slide data. Needs to be implemented by the plugin.
+        :param service_item: The service Item to be processed
+        :param item: The database item to be used to build the service item
+        :param xml_version:
+        :param remote: Was this remote triggered (False)
+        :param context: The service context
+        """
+        raise NotImplementedError('MediaManagerItem.generate_slide_data needs to be defined by the plugin')
+
+    def on_double_clicked(self):
+        """
+        Allows the list click action to be determined dynamically
+        """
+        if Settings().value('advanced/double click live'):
+            self.on_live_click()
+        elif not Settings().value('advanced/single click preview'):
+            # NOTE: The above check is necessary to prevent bug #1419300
+            self.on_preview_click()
+
+    def on_selection_change(self):
+        """
+        Allows the change of current item in the list to be actioned
+        """
+        if Settings().value('advanced/single click preview') and self.quick_preview_allowed \
+                and self.list_view.selectedIndexes() and self.auto_select_id == -1:
+            self.on_preview_click(True)
+
+    def on_preview_click(self, keep_focus=False):
+        """
+        Preview an item by building a service item then adding that service item to the preview slide controller.
+
+        :param keep_focus: Do we keep focus (False)
+        """
+        if not self.list_view.selectedIndexes() and not self.remote_triggered:
+            QtWidgets.QMessageBox.information(self, UiStrings().NISp,
+                                              translate('OpenLP.MediaManagerItem',
+                                                        'You must select one or more items to preview.'))
+        else:
+            log.debug('%s Preview requested' % self.plugin.name)
+            service_item = self.build_service_item()
+            if service_item:
+                service_item.from_plugin = True
+                self.preview_controller.add_service_item(service_item)
+                if not keep_focus:
+                    self.preview_controller.preview_widget.setFocus()
+
+    def on_live_click(self):
+        """
+        Send an item live by building a service item then adding that service item to the live slide controller.
+        """
+        if not self.list_view.selectedIndexes():
+            QtWidgets.QMessageBox.information(self, UiStrings().NISp,
+                                              translate('OpenLP.MediaManagerItem',
+                                                        'You must select one or more items to send live.'))
+        else:
+            self.go_live()
+
+    def go_live_remote(self, message):
+        """
+        Remote Call wrapper
+
+        :param message: The passed data item_id:Remote.
+        """
+        self.go_live(message[0], remote=message[1])
+
+    def go_live(self, item_id=None, remote=False):
+        """
+        Make the currently selected item go live.
+
+        :param item_id: item to make live
+        :param remote: From Remote
+        """
+        log.debug('%s Live requested', self.plugin.name)
+        item = None
+        if item_id:
+            item = self.create_item_from_id(item_id)
+        service_item = self.build_service_item(item, remote=remote)
+        if service_item:
+            if not item_id:
+                service_item.from_plugin = True
+            if remote:
+                service_item.will_auto_start = True
+            self.live_controller.add_service_item(service_item)
+            self.live_controller.preview_widget.setFocus()
+
+    def create_item_from_id(self, item_id):
+        """
+        Create a media item from an item id.
+
+        :param item_id: Id to make live
+        """
+        item = QtWidgets.QListWidgetItem()
+        item.setData(QtCore.Qt.UserRole, item_id)
+        return item
+
+    def on_add_click(self):
+        """
+        Add a selected item to the current service
+        """
+        if not self.list_view.selectedIndexes():
+            QtWidgets.QMessageBox.information(self, UiStrings().NISp,
+                                              translate('OpenLP.MediaManagerItem',
+                                                        'You must select one or more items to add.'))
+        else:
+            # Is it possible to process multiple list items to generate
+            # multiple service items?
+            if self.single_service_item:
+                log.debug('%s Add requested', self.plugin.name)
+                self.add_to_service(replace=self.remote_triggered)
+            else:
+                items = self.list_view.selectedIndexes()
+                drop_position = self.service_manager.get_drop_position()
+                for item in items:
+                    self.add_to_service(item, position=drop_position)
+                    if drop_position != -1:
+                        drop_position += 1
+
+    def add_to_service_remote(self, message):
+        """
+        Remote Call wrapper
+
+        :param message: The passed data item:Remote.
+        """
+        self.add_to_service(message[0], remote=message[1])
+
+    def add_to_service(self, item=None, replace=None, remote=False, position=-1):
+        """
+        Add this item to the current service.
+
+        :param item: Item to be processed
+        :param replace: Replace the existing item
+        :param remote: Triggered from remote
+        :param position: Position to place item
+        """
+        service_item = self.build_service_item(item, True, remote=remote, context=ServiceItemContext.Service)
+        if service_item:
+            service_item.from_plugin = False
+            self.service_manager.add_service_item(service_item, replace=replace, position=position)
+
+    def on_add_edit_click(self):
+        """
+        Add a selected item to an existing item in the current service.
+        """
+        if not self.list_view.selectedIndexes() and not self.remote_triggered:
+            QtWidgets.QMessageBox.information(self, UiStrings().NISp,
+                                              translate('OpenLP.MediaManagerItem',
+                                                        'You must select one or more items.'))
+        else:
+            log.debug('%s Add requested', self.plugin.name)
+            service_item = self.service_manager.get_service_item()
+            if not service_item:
+                QtWidgets.QMessageBox.information(self, UiStrings().NISs,
+                                                  translate('OpenLP.MediaManagerItem',
+                                                            'You must select an existing service item to add to.'))
+            elif self.plugin.name == service_item.name:
+                self.generate_slide_data(service_item)
+                self.service_manager.add_service_item(service_item, replace=True)
+            else:
+                # Turn off the remote edit update message indicator
+                QtWidgets.QMessageBox.information(self, translate('OpenLP.MediaManagerItem', 'Invalid Service Item'),
+                                                  translate('OpenLP.MediaManagerItem',
+                                                            'You must select a %s service item.') % self.title)
+
+    def build_service_item(self, item=None, xml_version=False, remote=False, context=ServiceItemContext.Live):
+        """
+        Common method for generating a service item
+        :param item: Service Item to be built.
+        :param xml_version: version of XML (False)
+        :param remote: Remote triggered (False)
+        :param context: The context on which this is called
+        """
+        service_item = ServiceItem(self.plugin)
+        service_item.add_icon(self.plugin.icon_path)
+        if self.generate_slide_data(service_item, item, xml_version, remote, context):
+            return service_item
+        else:
+            return None
+
+    def service_load(self, item):
+        """
+        Method to add processing when a service has been loaded and individual service items need to be processed by the
+        plugins.
+
+        :param item: The item to be processed and returned.
+        """
+        return item
+
+    def check_search_result(self):
+        """
+        Checks if the list_view is empty and adds a "No Search Results" item.
+        """
+        if self.list_view.count():
+            return
+        message = translate('OpenLP.MediaManagerItem', 'No Search Results')
+        item = QtWidgets.QListWidgetItem(message)
+        item.setFlags(QtCore.Qt.NoItemFlags)
+        font = QtGui.QFont()
+        font.setItalic(True)
+        item.setFont(font)
+        self.list_view.addItem(item)
+
+    def _get_id_of_item_to_generate(self, item, remote_item):
+        """
+        Utility method to check items being submitted for slide generation.
+
+        :param item: The item to check.
+        :param remote_item: The id to assign if the slide generation was remotely triggered.
+        """
+        if item is None:
+            if self.remote_triggered is None:
+                item = self.list_view.currentItem()
+                if item is None:
+                    return False
+                item_id = item.data(QtCore.Qt.UserRole)
+            else:
+                item_id = remote_item
+        else:
+            item_id = item.data(QtCore.Qt.UserRole)
+        return item_id
+
+    def save_auto_select_id(self):
+        """
+        Sorts out, what item to select after loading a list.
+        """
+        # The item to select has not been set.
+        if self.auto_select_id == -1:
+            item = self.list_view.currentItem()
+            if item:
+                self.auto_select_id = item.data(QtCore.Qt.UserRole)
+
+    def search(self, string, show_error=True):
+        """
+        Performs a plugin specific search for items containing ``string``
+
+        :param string: String to be displayed
+        :param show_error: Should the error be shown (True)
+        """
+        raise NotImplementedError('Plugin.search needs to be defined by the plugin')

=== added file 'openlp/core/lib/plugin.py'
--- openlp/core/lib/plugin.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/plugin.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,385 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Provide the generic plugin functionality for OpenLP plugins.
+"""
+import logging
+
+from PyQt5 import QtCore
+
+from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings
+from openlp.core.common.versionchecker import get_application_version
+
+log = logging.getLogger(__name__)
+
+
+class PluginStatus(object):
+    """
+    Defines the status of the plugin
+    """
+    Active = 1
+    Inactive = 0
+    Disabled = -1
+
+
+class StringContent(object):
+    """
+    Provide standard strings for objects to use.
+    """
+    Name = 'name'
+    Import = 'import'
+    Load = 'load'
+    New = 'new'
+    Edit = 'edit'
+    Delete = 'delete'
+    Preview = 'preview'
+    Live = 'live'
+    Service = 'service'
+    VisibleName = 'visible_name'
+
+
+class Plugin(QtCore.QObject, RegistryProperties):
+    """
+    Base class for openlp plugins to inherit from.
+
+    **Basic Attributes**
+
+    ``name``
+        The name that should appear in the plugins list.
+
+    ``version``
+        The version number of this iteration of the plugin.
+
+    ``settings_section``
+        The namespace to store settings for the plugin.
+
+    ``icon``
+        An instance of QIcon, which holds an icon for this plugin.
+
+    ``log``
+        A log object used to log debugging messages. This is pre-instantiated.
+
+    ``weight``
+        A numerical value used to order the plugins.
+
+    **Hook Functions**
+
+    ``check_pre_conditions()``
+        Provides the Plugin with a handle to check if it can be loaded.
+
+    ``create_media_manager_item()``
+        Creates a new instance of MediaManagerItem to be used in the Media
+        Manager.
+
+    ``add_import_menu_item(import_menu)``
+        Add an item to the Import menu.
+
+    ``add_export_menu_item(export_menu)``
+        Add an item to the Export menu.
+
+    ``create_settings_tab()``
+        Creates a new instance of SettingsTabItem to be used in the Settings
+        dialog.
+
+    ``add_to_menu(menubar)``
+        A method to add a menu item to anywhere in the menu, given the menu bar.
+
+    ``handle_event(event)``
+        A method use to handle events, given an Event object.
+
+    ``about()``
+        Used in the plugin manager, when a person clicks on the 'About' button.
+
+    """
+    log.info('loaded')
+
+    def __init__(self, name, default_settings, media_item_class=None, settings_tab_class=None, version=None):
+        """
+        This is the constructor for the plugin object. This provides an easy way for descendant plugins to populate
+         common data. This method *must*
+
+        be overridden, like so::
+
+            class MyPlugin(Plugin):
+                def __init__(self):
+                    super(MyPlugin, self).__init__('MyPlugin', version='0.1')
+
+        :param name: Defaults to *None*. The name of the plugin.
+        :param default_settings: A dict containing the plugin's settings. The value to each key is the default value
+        to be used.
+        :param media_item_class: The class name of the plugin's media item.
+        :param settings_tab_class: The class name of the plugin's settings tab.
+        :param version: Defaults to *None*, which means that the same version number is used as OpenLP's version number.
+        """
+        log.debug('Plugin %s initialised' % name)
+        super(Plugin, self).__init__()
+        self.name = name
+        self.text_strings = {}
+        self.set_plugin_text_strings()
+        self.name_strings = self.text_strings[StringContent.Name]
+        if version:
+            self.version = version
+        else:
+            self.version = get_application_version()['version']
+        self.settings_section = self.name
+        self.icon = None
+        self.media_item_class = media_item_class
+        self.settings_tab_class = settings_tab_class
+        self.settings_tab = None
+        self.media_item = None
+        self.weight = 0
+        self.status = PluginStatus.Inactive
+        # Add the default status to the default settings.
+        default_settings[name + '/status'] = PluginStatus.Inactive
+        default_settings[name + '/last directory'] = ''
+        # Append a setting for files in the mediamanager (note not all plugins
+        # which have a mediamanager need this).
+        if media_item_class is not None:
+            default_settings['%s/%s files' % (name, name)] = []
+        # Add settings to the dict of all settings.
+        Settings.extend_default_settings(default_settings)
+        Registry().register_function('%s_add_service_item' % self.name, self.process_add_service_event)
+        Registry().register_function('%s_config_updated' % self.name, self.config_update)
+
+    def check_pre_conditions(self):
+        """
+        Provides the Plugin with a handle to check if it can be loaded.
+        Failing Preconditions does not stop a settings Tab being created
+
+        Returns ``True`` or ``False``.
+        """
+        return True
+
+    def set_status(self):
+        """
+        Sets the status of the plugin
+        """
+        self.status = Settings().value(self.settings_section + '/status')
+
+    def toggle_status(self, new_status):
+        """
+        Changes the status of the plugin and remembers it
+        """
+        self.status = new_status
+        Settings().setValue(self.settings_section + '/status', self.status)
+        if new_status == PluginStatus.Active:
+            self.initialise()
+        elif new_status == PluginStatus.Inactive:
+            self.finalise()
+
+    def is_active(self):
+        """
+        Indicates if the plugin is active
+
+        Returns True or False.
+        """
+        return self.status == PluginStatus.Active
+
+    def create_media_manager_item(self):
+        """
+        Construct a MediaManagerItem object with all the buttons and things
+        you need, and return it for integration into OpenLP.
+        """
+        if self.media_item_class:
+            self.media_item = self.media_item_class(self.main_window.media_dock_manager.media_dock, self)
+
+    def upgrade_settings(self, settings):
+        """
+        Upgrade the settings of this plugin.
+
+        :param settings: The Settings object containing the old settings.
+        """
+        pass
+
+    def add_import_menu_item(self, import_menu):
+        """
+        Create a menu item and add it to the "Import" menu.
+
+        :param import_menu: The Import menu.
+        """
+        pass
+
+    def add_export_menu_item(self, export_menu):
+        """
+        Create a menu item and add it to the "Export" menu.
+
+        :param export_menu: The Export menu
+        """
+        pass
+
+    def add_tools_menu_item(self, tools_menu):
+        """
+        Create a menu item and add it to the "Tools" menu.
+
+        :param tools_menu: The Tools menu
+        """
+        pass
+
+    def create_settings_tab(self, parent):
+        """
+        Create a tab for the settings window to display the configurable options
+        for this plugin to the user.
+        """
+        if self.settings_tab_class:
+            self.settings_tab = self.settings_tab_class(parent, self.name,
+                                                        self.get_string(StringContent.VisibleName)['title'],
+                                                        self.icon_path)
+
+    def add_to_menu(self, menubar):
+        """
+        Add menu items to the menu, given the menubar.
+
+        :param menubar: The application's menu bar.
+        """
+        pass
+
+    def process_add_service_event(self, replace=False):
+        """
+        Generic Drag and drop handler triggered from service_manager.
+        """
+        log.debug('process_add_service_event event called for plugin %s' % self.name)
+        if replace:
+            self.media_item.on_add_edit_click()
+        else:
+            self.media_item.on_add_click()
+
+    @staticmethod
+    def about():
+        """
+        Show a dialog when the user clicks on the 'About' button in the plugin manager.
+        """
+        raise NotImplementedError('Plugin.about needs to be defined by the plugin')
+
+    def initialise(self):
+        """
+        Called by the plugin Manager to initialise anything it needs.
+        """
+        if self.media_item:
+            self.media_item.initialise()
+            self.main_window.media_dock_manager.add_item_to_dock(self.media_item)
+
+    def finalise(self):
+        """
+        Called by the plugin Manager to cleanup things.
+        """
+        if self.media_item:
+            self.main_window.media_dock_manager.remove_dock(self.media_item)
+
+    def app_startup(self):
+        """
+        Perform tasks on application startup
+        """
+        pass
+
+    def uses_theme(self, theme):
+        """
+        Called to find out if a plugin is currently using a theme.
+
+        Returns True if the theme is being used, otherwise returns False.
+        """
+        return False
+
+    def rename_theme(self, old_theme, new_theme):
+        """
+        Renames a theme a plugin is using making the plugin use the new name.
+
+        :param old_theme:  The name of the theme the plugin should stop using.
+        :param new_theme: The new name the plugin should now use
+        """
+        pass
+
+    def get_string(self, name):
+        """
+        Encapsulate access of plugins translated text strings
+        """
+        return self.text_strings[name]
+
+    def set_plugin_ui_text_strings(self, tooltips):
+        """
+        Called to define all translatable texts of the plugin
+
+        :param tooltips:
+        """
+        # Load Action
+        self.__set_name_text_string(StringContent.Load, UiStrings().Load, tooltips['load'])
+        # Import Action
+        self.__set_name_text_string(StringContent.Import, UiStrings().Import, tooltips['import'])
+        # New Action
+        self.__set_name_text_string(StringContent.New, UiStrings().Add, tooltips['new'])
+        # Edit Action
+        self.__set_name_text_string(StringContent.Edit, UiStrings().Edit, tooltips['edit'])
+        # Delete Action
+        self.__set_name_text_string(StringContent.Delete, UiStrings().Delete, tooltips['delete'])
+        # Preview Action
+        self.__set_name_text_string(StringContent.Preview, UiStrings().Preview, tooltips['preview'])
+        # Send Live Action
+        self.__set_name_text_string(StringContent.Live, UiStrings().Live, tooltips['live'])
+        # Add to Service Action
+        self.__set_name_text_string(StringContent.Service, UiStrings().Service, tooltips['service'])
+
+    def __set_name_text_string(self, name, title, tooltip):
+        """
+        Utility method for creating a plugin's text_strings. This method makes use of the singular name of the
+        plugin object so must only be called after this has been set.
+        """
+        self.text_strings[name] = {'title': title, 'tooltip': tooltip}
+
+    def get_display_css(self):
+        """
+        Add css style sheets to htmlbuilder.
+        """
+        return ''
+
+    def get_display_javascript(self):
+        """
+        Add javascript functions to htmlbuilder.
+        """
+        return ''
+
+    def refresh_css(self, frame):
+        """
+        Allow plugins to refresh javascript on displayed screen.
+
+        ``frame``
+            The Web frame holding the page.
+        """
+        return ''
+
+    def get_display_html(self):
+        """
+        Add html code to htmlbuilder.
+        """
+        return ''
+
+    def config_update(self):
+        """
+        Called when Config is changed to restart values dependent on configuration.
+        """
+        log.info('config update processed')
+        if self.media_item:
+            self.media_item.config_update()
+
+    def new_service_created(self):
+        """
+        The plugin's needs to handle a new song creation
+        """
+        pass

=== added file 'openlp/core/lib/pluginmanager.py'
--- openlp/core/lib/pluginmanager.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/pluginmanager.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,207 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Provide plugin management
+"""
+import os
+import sys
+import imp
+
+from openlp.core.lib import Plugin, PluginStatus
+from openlp.core.common import AppLocation, RegistryProperties, OpenLPMixin, RegistryMixin
+
+
+class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
+    """
+    This is the Plugin manager, which loads all the plugins,
+    and executes all the hooks, as and when necessary.
+    """
+    def __init__(self, parent=None):
+        """
+        The constructor for the plugin manager. Passes the controllers on to
+        the plugins for them to interact with via their ServiceItems.
+        """
+        super(PluginManager, self).__init__(parent)
+        self.log_info('Plugin manager Initialising')
+        self.base_path = os.path.abspath(AppLocation.get_directory(AppLocation.PluginsDir))
+        self.log_debug('Base path %s ' % self.base_path)
+        self.plugins = []
+        self.log_info('Plugin manager Initialised')
+
+    def bootstrap_initialise(self):
+        """
+        Bootstrap all the plugin manager functions
+        """
+        self.find_plugins()
+        # hook methods have to happen after find_plugins. Find plugins needs
+        # the controllers hence the hooks have moved from setupUI() to here
+        # Find and insert settings tabs
+        self.hook_settings_tabs()
+        # Find and insert media manager items
+        self.hook_media_manager()
+        # Call the hook method to pull in import menus.
+        self.hook_import_menu()
+        # Call the hook method to pull in export menus.
+        self.hook_export_menu()
+        # Call the hook method to pull in tools menus.
+        self.hook_tools_menu()
+        # Call the initialise method to setup plugins.
+        self.initialise_plugins()
+
+    def find_plugins(self):
+        """
+        Scan a directory for objects inheriting from the ``Plugin`` class.
+        """
+        start_depth = len(os.path.abspath(self.base_path).split(os.sep))
+        present_plugin_dir = os.path.join(self.base_path, 'presentations')
+        self.log_debug('finding plugins in %s at depth %d' % (self.base_path, start_depth))
+        for root, dirs, files in os.walk(self.base_path):
+            for name in files:
+                if name.endswith('.py') and not name.startswith('__'):
+                    path = os.path.abspath(os.path.join(root, name))
+                    this_depth = len(path.split(os.sep))
+                    if this_depth - start_depth > 2:
+                        # skip anything lower down
+                        break
+                    module_name = name[:-3]
+                    # import the modules
+                    self.log_debug('Importing %s from %s. Depth %d' % (module_name, root, this_depth))
+                    try:
+                        # Use the "imp" library to try to get around a problem with the PyUNO library which
+                        # monkey-patches the __import__ function to do some magic. This causes issues with our tests.
+                        # First, try to find the module we want to import, searching the directory in root
+                        fp, path_name, description = imp.find_module(module_name, [root])
+                        # Then load the module (do the actual import) using the details from find_module()
+                        imp.load_module(module_name, fp, path_name, description)
+                    except ImportError as e:
+                        self.log_exception('Failed to import module %s on path %s: %s'
+                                           % (module_name, path, e.args[0]))
+        plugin_classes = Plugin.__subclasses__()
+        plugin_objects = []
+        for p in plugin_classes:
+            try:
+                plugin = p()
+                self.log_debug('Loaded plugin %s' % str(p))
+                plugin_objects.append(plugin)
+            except TypeError:
+                self.log_exception('Failed to load plugin %s' % str(p))
+        plugins_list = sorted(plugin_objects, key=lambda plugin: plugin.weight)
+        for plugin in plugins_list:
+            if plugin.check_pre_conditions():
+                self.log_debug('Plugin %s active' % str(plugin.name))
+                plugin.set_status()
+            else:
+                plugin.status = PluginStatus.Disabled
+            self.plugins.append(plugin)
+
+    def hook_media_manager(self):
+        """
+        Create the plugins' media manager items.
+        """
+        for plugin in self.plugins:
+            if plugin.status is not PluginStatus.Disabled:
+                plugin.create_media_manager_item()
+
+    def hook_settings_tabs(self):
+        """
+        Loop through all the plugins. If a plugin has a valid settings tab
+        item, add it to the settings tab.
+        Tabs are set for all plugins not just Active ones
+
+        """
+        for plugin in self.plugins:
+            if plugin.status is not PluginStatus.Disabled:
+                plugin.create_settings_tab(self.settings_form)
+
+    def hook_import_menu(self):
+        """
+        Loop through all the plugins and give them an opportunity to add an
+        item to the import menu.
+
+        """
+        for plugin in self.plugins:
+            if plugin.status is not PluginStatus.Disabled:
+                plugin.add_import_menu_item(self.main_window.file_import_menu)
+
+    def hook_export_menu(self):
+        """
+        Loop through all the plugins and give them an opportunity to add an
+        item to the export menu.
+        """
+        for plugin in self.plugins:
+            if plugin.status is not PluginStatus.Disabled:
+                plugin.add_export_menu_item(self.main_window.file_export_menu)
+
+    def hook_tools_menu(self):
+        """
+        Loop through all the plugins and give them an opportunity to add an
+        item to the tools menu.
+        """
+        for plugin in self.plugins:
+            if plugin.status is not PluginStatus.Disabled:
+                plugin.add_tools_menu_item(self.main_window.tools_menu)
+
+    def hook_upgrade_plugin_settings(self, settings):
+        """
+        Loop through all the plugins and give them an opportunity to upgrade their settings.
+
+        :param settings: The Settings object containing the old settings.
+        """
+        for plugin in self.plugins:
+            if plugin.status is not PluginStatus.Disabled:
+                plugin.upgrade_settings(settings)
+
+    def initialise_plugins(self):
+        """
+        Loop through all the plugins and give them an opportunity to initialise themselves.
+        """
+        for plugin in self.plugins:
+            self.log_info('initialising plugins %s in a %s state' % (plugin.name, plugin.is_active()))
+            if plugin.is_active():
+                plugin.initialise()
+                self.log_info('Initialisation Complete for %s ' % plugin.name)
+
+    def finalise_plugins(self):
+        """
+        Loop through all the plugins and give them an opportunity to clean themselves up
+        """
+        for plugin in self.plugins:
+            if plugin.is_active():
+                plugin.finalise()
+                self.log_info('Finalisation Complete for %s ' % plugin.name)
+
+    def get_plugin_by_name(self, name):
+        """
+        Return the plugin which has a name with value ``name``.
+        """
+        for plugin in self.plugins:
+            if plugin.name == name:
+                return plugin
+        return None
+
+    def new_service_created(self):
+        """
+        Loop through all the plugins and give them an opportunity to handle a new service
+        """
+        for plugin in self.plugins:
+            if plugin.is_active():
+                plugin.new_service_created()

=== added directory 'openlp/core/lib/projector'
=== added file 'openlp/core/lib/projector/__init__.py'
--- openlp/core/lib/projector/__init__.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/projector/__init__.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+    :mod:`openlp.core.ui.projector`
+
+    Initialization for the openlp.core.ui.projector modules.
+"""
+
+
+class DialogSourceStyle(object):
+    """
+    An enumeration for projector dialog box type.
+    """
+    Tabbed = 0
+    Single = 1

=== added file 'openlp/core/lib/projector/constants.py'
--- openlp/core/lib/projector/constants.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/projector/constants.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,353 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+    :mod:`openlp.core.lib.projector.constants` module
+
+    Provides the constants used for projector errors/status/defaults
+"""
+
+import logging
+log = logging.getLogger(__name__)
+log.debug('projector_constants loaded')
+
+from openlp.core.common import translate
+
+
+__all__ = ['S_OK', 'E_GENERAL', 'E_NOT_CONNECTED', 'E_FAN', 'E_LAMP', 'E_TEMP',
+           'E_COVER', 'E_FILTER', 'E_AUTHENTICATION', 'E_NO_AUTHENTICATION',
+           'E_UNDEFINED', 'E_PARAMETER', 'E_UNAVAILABLE', 'E_PROJECTOR',
+           'E_INVALID_DATA', 'E_WARN', 'E_ERROR', 'E_CLASS', 'E_PREFIX',
+           'E_CONNECTION_REFUSED', 'E_REMOTE_HOST_CLOSED_CONNECTION', 'E_HOST_NOT_FOUND',
+           'E_SOCKET_ACCESS', 'E_SOCKET_RESOURCE', 'E_SOCKET_TIMEOUT', 'E_DATAGRAM_TOO_LARGE',
+           'E_NETWORK', 'E_ADDRESS_IN_USE', 'E_SOCKET_ADDRESS_NOT_AVAILABLE',
+           'E_UNSUPPORTED_SOCKET_OPERATION', 'E_PROXY_AUTHENTICATION_REQUIRED',
+           'E_SLS_HANDSHAKE_FAILED', 'E_UNFINISHED_SOCKET_OPERATION', 'E_PROXY_CONNECTION_REFUSED',
+           'E_PROXY_CONNECTION_CLOSED', 'E_PROXY_CONNECTION_TIMEOUT', 'E_PROXY_NOT_FOUND',
+           'E_PROXY_PROTOCOL', 'E_UNKNOWN_SOCKET_ERROR',
+           'S_NOT_CONNECTED', 'S_CONNECTING', 'S_CONNECTED',
+           'S_STATUS', 'S_OFF', 'S_INITIALIZE', 'S_STANDBY', 'S_WARMUP', 'S_ON', 'S_COOLDOWN',
+           'S_INFO', 'S_NETWORK_SENDING', 'S_NETWORK_RECEIVED',
+           'ERROR_STRING', 'CR', 'LF', 'PJLINK_ERST_STATUS', 'PJLINK_POWR_STATUS',
+           'PJLINK_PORT', 'PJLINK_MAX_PACKET', 'TIMEOUT', 'ERROR_MSG', 'PJLINK_ERRORS',
+           'STATUS_STRING', 'PJLINK_VALID_CMD', 'CONNECTION_ERRORS']
+
+# Set common constants.
+CR = chr(0x0D)  # \r
+LF = chr(0x0A)  # \n
+PJLINK_PORT = 4352
+TIMEOUT = 30.0
+PJLINK_MAX_PACKET = 136
+PJLINK_VALID_CMD = {'1': ['PJLINK',  # Initial connection
+                          'POWR',  # Power option
+                          'INPT',  # Video sources option
+                          'AVMT',  # Shutter option
+                          'ERST',  # Error status option
+                          'LAMP',  # Lamp(s) query (Includes fans)
+                          'INST',  # Input sources available query
+                          'NAME',  # Projector name query
+                          'INF1',  # Manufacturer name query
+                          'INF2',  # Product name query
+                          'INFO',  # Other information query
+                          'CLSS'   # PJLink class support query
+                          ]}
+
+# Error and status codes
+S_OK = E_OK = 0  # E_OK included since I sometimes forget
+# Error codes. Start at 200 so we don't duplicate system error codes.
+E_GENERAL = 200  # Unknown error
+E_NOT_CONNECTED = 201
+E_FAN = 202
+E_LAMP = 203
+E_TEMP = 204
+E_COVER = 205
+E_FILTER = 206
+E_NO_AUTHENTICATION = 207  # PIN set and no authentication set on projector
+E_UNDEFINED = 208       # ERR1
+E_PARAMETER = 209       # ERR2
+E_UNAVAILABLE = 210     # ERR3
+E_PROJECTOR = 211       # ERR4
+E_INVALID_DATA = 212
+E_WARN = 213
+E_ERROR = 214
+E_AUTHENTICATION = 215  # ERRA
+E_CLASS = 216
+E_PREFIX = 217
+
+# Remap Qt socket error codes to projector error codes
+E_CONNECTION_REFUSED = 230
+E_REMOTE_HOST_CLOSED_CONNECTION = 231
+E_HOST_NOT_FOUND = 232
+E_SOCKET_ACCESS = 233
+E_SOCKET_RESOURCE = 234
+E_SOCKET_TIMEOUT = 235
+E_DATAGRAM_TOO_LARGE = 236
+E_NETWORK = 237
+E_ADDRESS_IN_USE = 238
+E_SOCKET_ADDRESS_NOT_AVAILABLE = 239
+E_UNSUPPORTED_SOCKET_OPERATION = 240
+E_PROXY_AUTHENTICATION_REQUIRED = 241
+E_SLS_HANDSHAKE_FAILED = 242
+E_UNFINISHED_SOCKET_OPERATION = 243
+E_PROXY_CONNECTION_REFUSED = 244
+E_PROXY_CONNECTION_CLOSED = 245
+E_PROXY_CONNECTION_TIMEOUT = 246
+E_PROXY_NOT_FOUND = 247
+E_PROXY_PROTOCOL = 248
+E_UNKNOWN_SOCKET_ERROR = -1
+
+# Status codes start at 300
+S_NOT_CONNECTED = 300
+S_CONNECTING = 301
+S_CONNECTED = 302
+S_INITIALIZE = 303
+S_STATUS = 304
+S_OFF = 305
+S_STANDBY = 306
+S_WARMUP = 307
+S_ON = 308
+S_COOLDOWN = 309
+S_INFO = 310
+
+# Information that does not affect status
+S_NETWORK_SENDING = 400
+S_NETWORK_RECEIVED = 401
+
+CONNECTION_ERRORS = {E_NOT_CONNECTED, E_NO_AUTHENTICATION, E_AUTHENTICATION, E_CLASS,
+                     E_PREFIX, E_CONNECTION_REFUSED, E_REMOTE_HOST_CLOSED_CONNECTION,
+                     E_HOST_NOT_FOUND, E_SOCKET_ACCESS, E_SOCKET_RESOURCE, E_SOCKET_TIMEOUT,
+                     E_DATAGRAM_TOO_LARGE, E_NETWORK, E_ADDRESS_IN_USE, E_SOCKET_ADDRESS_NOT_AVAILABLE,
+                     E_UNSUPPORTED_SOCKET_OPERATION, E_PROXY_AUTHENTICATION_REQUIRED,
+                     E_SLS_HANDSHAKE_FAILED, E_UNFINISHED_SOCKET_OPERATION, E_PROXY_CONNECTION_REFUSED,
+                     E_PROXY_CONNECTION_CLOSED, E_PROXY_CONNECTION_TIMEOUT, E_PROXY_NOT_FOUND,
+                     E_PROXY_PROTOCOL, E_UNKNOWN_SOCKET_ERROR
+                     }
+
+PJLINK_ERRORS = {'ERRA': E_AUTHENTICATION,   # Authentication error
+                 'ERR1': E_UNDEFINED,        # Undefined command error
+                 'ERR2': E_PARAMETER,        # Invalid parameter error
+                 'ERR3': E_UNAVAILABLE,      # Projector busy
+                 'ERR4': E_PROJECTOR,        # Projector or display failure
+                 E_AUTHENTICATION: 'ERRA',
+                 E_UNDEFINED: 'ERR1',
+                 E_PARAMETER: 'ERR2',
+                 E_UNAVAILABLE: 'ERR3',
+                 E_PROJECTOR: 'ERR4'}
+
+# Map error/status codes to string
+ERROR_STRING = {0: 'S_OK',
+                E_GENERAL: 'E_GENERAL',
+                E_NOT_CONNECTED: 'E_NOT_CONNECTED',
+                E_FAN: 'E_FAN',
+                E_LAMP: 'E_LAMP',
+                E_TEMP: 'E_TEMP',
+                E_COVER: 'E_COVER',
+                E_FILTER: 'E_FILTER',
+                E_AUTHENTICATION: 'E_AUTHENTICATION',
+                E_NO_AUTHENTICATION: 'E_NO_AUTHENTICATION',
+                E_UNDEFINED: 'E_UNDEFINED',
+                E_PARAMETER: 'E_PARAMETER',
+                E_UNAVAILABLE: 'E_UNAVAILABLE',
+                E_PROJECTOR: 'E_PROJECTOR',
+                E_INVALID_DATA: 'E_INVALID_DATA',
+                E_WARN: 'E_WARN',
+                E_ERROR: 'E_ERROR',
+                E_CLASS: 'E_CLASS',
+                E_PREFIX: 'E_PREFIX',  # Last projector error
+                E_CONNECTION_REFUSED: 'E_CONNECTION_REFUSED',  # First QtSocket error
+                E_REMOTE_HOST_CLOSED_CONNECTION: 'E_REMOTE_HOST_CLOSED_CONNECTION',
+                E_HOST_NOT_FOUND: 'E_HOST_NOT_FOUND',
+                E_SOCKET_ACCESS: 'E_SOCKET_ACCESS',
+                E_SOCKET_RESOURCE: 'E_SOCKET_RESOURCE',
+                E_SOCKET_TIMEOUT: 'E_SOCKET_TIMEOUT',
+                E_DATAGRAM_TOO_LARGE: 'E_DATAGRAM_TOO_LARGE',
+                E_NETWORK: 'E_NETWORK',
+                E_ADDRESS_IN_USE: 'E_ADDRESS_IN_USE',
+                E_SOCKET_ADDRESS_NOT_AVAILABLE: 'E_SOCKET_ADDRESS_NOT_AVAILABLE',
+                E_UNSUPPORTED_SOCKET_OPERATION: 'E_UNSUPPORTED_SOCKET_OPERATION',
+                E_PROXY_AUTHENTICATION_REQUIRED: 'E_PROXY_AUTHENTICATION_REQUIRED',
+                E_SLS_HANDSHAKE_FAILED: 'E_SLS_HANDSHAKE_FAILED',
+                E_UNFINISHED_SOCKET_OPERATION: 'E_UNFINISHED_SOCKET_OPERATION',
+                E_PROXY_CONNECTION_REFUSED: 'E_PROXY_CONNECTION_REFUSED',
+                E_PROXY_CONNECTION_CLOSED: 'E_PROXY_CONNECTION_CLOSED',
+                E_PROXY_CONNECTION_TIMEOUT: 'E_PROXY_CONNECTION_TIMEOUT',
+                E_PROXY_NOT_FOUND: 'E_PROXY_NOT_FOUND',
+                E_PROXY_PROTOCOL: 'E_PROXY_PROTOCOL',
+                E_UNKNOWN_SOCKET_ERROR: 'E_UNKNOWN_SOCKET_ERROR'}
+
+STATUS_STRING = {S_NOT_CONNECTED: 'S_NOT_CONNECTED',
+                 S_CONNECTING: 'S_CONNECTING',
+                 S_CONNECTED: 'S_CONNECTED',
+                 S_STATUS: 'S_STATUS',
+                 S_OFF: 'S_OFF',
+                 S_INITIALIZE: 'S_INITIALIZE',
+                 S_STANDBY: 'S_STANDBY',
+                 S_WARMUP: 'S_WARMUP',
+                 S_ON: 'S_ON',
+                 S_COOLDOWN: 'S_COOLDOWN',
+                 S_INFO: 'S_INFO',
+                 S_NETWORK_SENDING: 'S_NETWORK_SENDING',
+                 S_NETWORK_RECEIVED: 'S_NETWORK_RECEIVED'}
+
+# Map error/status codes to message strings
+ERROR_MSG = {E_OK: translate('OpenLP.ProjectorConstants', 'OK'),  # E_OK | S_OK
+             E_GENERAL: translate('OpenLP.ProjectorConstants', 'General projector error'),
+             E_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected error'),
+             E_LAMP: translate('OpenLP.ProjectorConstants', 'Lamp error'),
+             E_FAN: translate('OpenLP.ProjectorConstants', 'Fan error'),
+             E_TEMP: translate('OpenLP.ProjectorConstants', 'High temperature detected'),
+             E_COVER: translate('OpenLP.ProjectorConstants', 'Cover open detected'),
+             E_FILTER: translate('OpenLP.ProjectorConstants', 'Check filter'),
+             E_AUTHENTICATION: translate('OpenLP.ProjectorConstants', 'Authentication Error'),
+             E_UNDEFINED: translate('OpenLP.ProjectorConstants', 'Undefined Command'),
+             E_PARAMETER: translate('OpenLP.ProjectorConstants', 'Invalid Parameter'),
+             E_UNAVAILABLE: translate('OpenLP.ProjectorConstants', 'Projector Busy'),
+             E_PROJECTOR: translate('OpenLP.ProjectorConstants', 'Projector/Display Error'),
+             E_INVALID_DATA: translate('OpenLP.ProjectorConstants', 'Invalid packet received'),
+             E_WARN: translate('OpenLP.ProjectorConstants', 'Warning condition detected'),
+             E_ERROR: translate('OpenLP.ProjectorConstants', 'Error condition detected'),
+             E_CLASS: translate('OpenLP.ProjectorConstants', 'PJLink class not supported'),
+             E_PREFIX: translate('OpenLP.ProjectorConstants', 'Invalid prefix character'),
+             E_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants',
+                                             'The connection was refused by the peer (or timed out)'),
+             E_REMOTE_HOST_CLOSED_CONNECTION: translate('OpenLP.ProjectorConstants',
+                                                        'The remote host closed the connection'),
+             E_HOST_NOT_FOUND: translate('OpenLP.ProjectorConstants', 'The host address was not found'),
+             E_SOCKET_ACCESS: translate('OpenLP.ProjectorConstants',
+                                        'The socket operation failed because the application '
+                                        'lacked the required privileges'),
+             E_SOCKET_RESOURCE: translate('OpenLP.ProjectorConstants',
+                                          'The local system ran out of resources (e.g., too many sockets)'),
+             E_SOCKET_TIMEOUT: translate('OpenLP.ProjectorConstants',
+                                         'The socket operation timed out'),
+             E_DATAGRAM_TOO_LARGE: translate('OpenLP.ProjectorConstants',
+                                             'The datagram was larger than the operating system\'s limit'),
+             E_NETWORK: translate('OpenLP.ProjectorConstants',
+                                  'An error occurred with the network (Possibly someone pulled the plug?)'),
+             E_ADDRESS_IN_USE: translate('OpenLP.ProjectorConstants',
+                                         'The address specified with socket.bind() '
+                                         'is already in use and was set to be exclusive'),
+             E_SOCKET_ADDRESS_NOT_AVAILABLE: translate('OpenLP.ProjectorConstants',
+                                                       'The address specified to socket.bind() '
+                                                       'does not belong to the host'),
+             E_UNSUPPORTED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants',
+                                                       'The requested socket operation is not supported by the local '
+                                                       'operating system (e.g., lack of IPv6 support)'),
+             E_PROXY_AUTHENTICATION_REQUIRED: translate('OpenLP.ProjectorConstants',
+                                                        'The socket is using a proxy, '
+                                                        'and the proxy requires authentication'),
+             E_SLS_HANDSHAKE_FAILED: translate('OpenLP.ProjectorConstants',
+                                               'The SSL/TLS handshake failed'),
+             E_UNFINISHED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants',
+                                                      'The last operation attempted has not finished yet '
+                                                      '(still in progress in the background)'),
+             E_PROXY_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants',
+                                                   'Could not contact the proxy server because the connection '
+                                                   'to that server was denied'),
+             E_PROXY_CONNECTION_CLOSED: translate('OpenLP.ProjectorConstants',
+                                                  'The connection to the proxy server was closed unexpectedly '
+                                                  '(before the connection to the final peer was established)'),
+             E_PROXY_CONNECTION_TIMEOUT: translate('OpenLP.ProjectorConstants',
+                                                   'The connection to the proxy server timed out or the proxy '
+                                                   'server stopped responding in the authentication phase.'),
+             E_PROXY_NOT_FOUND: translate('OpenLP.ProjectorConstants',
+                                          'The proxy address set with setProxy() was not found'),
+             E_PROXY_PROTOCOL: translate('OpenLP.ProjectorConstants',
+                                         'The connection negotiation with the proxy server failed because the '
+                                         'response from the proxy server could not be understood'),
+             E_UNKNOWN_SOCKET_ERROR: translate('OpenLP.ProjectorConstants', 'An unidentified error occurred'),
+             S_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected'),
+             S_CONNECTING: translate('OpenLP.ProjectorConstants', 'Connecting'),
+             S_CONNECTED: translate('OpenLP.ProjectorConstants', 'Connected'),
+             S_STATUS: translate('OpenLP.ProjectorConstants', 'Getting status'),
+             S_OFF: translate('OpenLP.ProjectorConstants', 'Off'),
+             S_INITIALIZE: translate('OpenLP.ProjectorConstants', 'Initialize in progress'),
+             S_STANDBY: translate('OpenLP.ProjectorConstants', 'Power in standby'),
+             S_WARMUP: translate('OpenLP.ProjectorConstants', 'Warmup in progress'),
+             S_ON: translate('OpenLP.ProjectorConstants', 'Power is on'),
+             S_COOLDOWN: translate('OpenLP.ProjectorConstants', 'Cooldown in progress'),
+             S_INFO: translate('OpenLP.ProjectorConstants', 'Projector Information available'),
+             S_NETWORK_SENDING: translate('OpenLP.ProjectorConstants', 'Sending data'),
+             S_NETWORK_RECEIVED: translate('OpenLP.ProjectorConstants', 'Received data')}
+
+# Map for ERST return codes to string
+PJLINK_ERST_STATUS = {'0': ERROR_STRING[E_OK],
+                      '1': ERROR_STRING[E_WARN],
+                      '2': ERROR_STRING[E_ERROR]}
+
+# Map for POWR return codes to status code
+PJLINK_POWR_STATUS = {'0': S_STANDBY,
+                      '1': S_ON,
+                      '2': S_COOLDOWN,
+                      '3': S_WARMUP}
+
+PJLINK_DEFAULT_SOURCES = {'1': translate('OpenLP.DB', 'RGB'),
+                          '2': translate('OpenLP.DB', 'Video'),
+                          '3': translate('OpenLP.DB', 'Digital'),
+                          '4': translate('OpenLP.DB', 'Storage'),
+                          '5': translate('OpenLP.DB', 'Network')}
+
+PJLINK_DEFAULT_CODES = {'11': translate('OpenLP.DB', 'RGB 1'),
+                        '12': translate('OpenLP.DB', 'RGB 2'),
+                        '13': translate('OpenLP.DB', 'RGB 3'),
+                        '14': translate('OpenLP.DB', 'RGB 4'),
+                        '15': translate('OpenLP.DB', 'RGB 5'),
+                        '16': translate('OpenLP.DB', 'RGB 6'),
+                        '17': translate('OpenLP.DB', 'RGB 7'),
+                        '18': translate('OpenLP.DB', 'RGB 8'),
+                        '19': translate('OpenLP.DB', 'RGB 9'),
+                        '21': translate('OpenLP.DB', 'Video 1'),
+                        '22': translate('OpenLP.DB', 'Video 2'),
+                        '23': translate('OpenLP.DB', 'Video 3'),
+                        '24': translate('OpenLP.DB', 'Video 4'),
+                        '25': translate('OpenLP.DB', 'Video 5'),
+                        '26': translate('OpenLP.DB', 'Video 6'),
+                        '27': translate('OpenLP.DB', 'Video 7'),
+                        '28': translate('OpenLP.DB', 'Video 8'),
+                        '29': translate('OpenLP.DB', 'Video 9'),
+                        '31': translate('OpenLP.DB', 'Digital 1'),
+                        '32': translate('OpenLP.DB', 'Digital 2'),
+                        '33': translate('OpenLP.DB', 'Digital 3'),
+                        '34': translate('OpenLP.DB', 'Digital 4'),
+                        '35': translate('OpenLP.DB', 'Digital 5'),
+                        '36': translate('OpenLP.DB', 'Digital 6'),
+                        '37': translate('OpenLP.DB', 'Digital 7'),
+                        '38': translate('OpenLP.DB', 'Digital 8'),
+                        '39': translate('OpenLP.DB', 'Digital 9'),
+                        '41': translate('OpenLP.DB', 'Storage 1'),
+                        '42': translate('OpenLP.DB', 'Storage 2'),
+                        '43': translate('OpenLP.DB', 'Storage 3'),
+                        '44': translate('OpenLP.DB', 'Storage 4'),
+                        '45': translate('OpenLP.DB', 'Storage 5'),
+                        '46': translate('OpenLP.DB', 'Storage 6'),
+                        '47': translate('OpenLP.DB', 'Storage 7'),
+                        '48': translate('OpenLP.DB', 'Storage 8'),
+                        '49': translate('OpenLP.DB', 'Storage 9'),
+                        '51': translate('OpenLP.DB', 'Network 1'),
+                        '52': translate('OpenLP.DB', 'Network 2'),
+                        '53': translate('OpenLP.DB', 'Network 3'),
+                        '54': translate('OpenLP.DB', 'Network 4'),
+                        '55': translate('OpenLP.DB', 'Network 5'),
+                        '56': translate('OpenLP.DB', 'Network 6'),
+                        '57': translate('OpenLP.DB', 'Network 7'),
+                        '58': translate('OpenLP.DB', 'Network 8'),
+                        '59': translate('OpenLP.DB', 'Network 9')
+                        }

=== added file 'openlp/core/lib/projector/db.py'
--- openlp/core/lib/projector/db.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/projector/db.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,429 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+:mod:`openlp.core.lib.projector.db` module
+
+Provides the database functions for the Projector module.
+
+The Manufacturer, Model, Source tables keep track of the video source
+strings used for display of input sources. The Source table maps
+manufacturer-defined or user-defined strings from PJLink default strings
+to end-user readable strings; ex: PJLink code 11 would map "RGB 1"
+default string to "RGB PC (analog)" string.
+(Future feature).
+
+The Projector table keeps track of entries for controlled projectors.
+"""
+
+import logging
+log = logging.getLogger(__name__)
+log.debug('projector.lib.db module loaded')
+
+from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, and_
+from sqlalchemy.ext.declarative import declarative_base, declared_attr
+from sqlalchemy.orm import backref, relationship
+
+from openlp.core.lib.db import Manager, init_db, init_url
+from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES
+
+metadata = MetaData()
+Base = declarative_base(metadata)
+
+
+class CommonBase(object):
+    """
+    Base class to automate table name and ID column.
+    """
+    @declared_attr
+    def __tablename__(cls):
+        return cls.__name__.lower()
+
+    id = Column(Integer, primary_key=True)
+
+
+class Manufacturer(CommonBase, Base):
+    """
+    Projector manufacturer table.
+
+    Manufacturer:
+        name:   Column(String(30))
+        models: Relationship(Model.id)
+
+    Model table is related.
+    """
+    def __repr__(self):
+        """
+        Returns a basic representation of a Manufacturer table entry.
+        """
+        return '<Manufacturer(name="%s")>' % self.name
+
+    name = Column(String(30))
+    models = relationship('Model',
+                          order_by='Model.name',
+                          backref='manufacturer',
+                          cascade='all, delete-orphan',
+                          primaryjoin='Manufacturer.id==Model.manufacturer_id',
+                          lazy='joined')
+
+
+class Model(CommonBase, Base):
+    """
+    Projector model table.
+
+    Model:
+        name:               Column(String(20))
+        sources:            Relationship(Source.id)
+        manufacturer_id:    Foreign_key(Manufacturer.id)
+
+    Manufacturer table links here.
+    Source table is related.
+    """
+    def __repr__(self):
+        """
+        Returns a basic representation of a Model table entry.
+        """
+        return '<Model(name=%s)>' % self.name
+
+    manufacturer_id = Column(Integer, ForeignKey('manufacturer.id'))
+    name = Column(String(20))
+    sources = relationship('Source',
+                           order_by='Source.pjlink_name',
+                           backref='model',
+                           cascade='all, delete-orphan',
+                           primaryjoin='Model.id==Source.model_id',
+                           lazy='joined')
+
+
+class Source(CommonBase, Base):
+    """
+    Projector video source table.
+
+    Source:
+        pjlink_name:    Column(String(15))
+        pjlink_code:    Column(String(2))
+        text:           Column(String(30))
+        model_id:       Foreign_key(Model.id)
+
+    Model table links here.
+
+    These entries map PJLink input video source codes to text strings.
+    """
+    def __repr__(self):
+        """
+        Return basic representation of Source table entry.
+        """
+        return '<Source(pjlink_name="%s", pjlink_code="%s", text="%s")>' % \
+            (self.pjlink_name, self.pjlink_code, self.text)
+    model_id = Column(Integer, ForeignKey('model.id'))
+    pjlink_name = Column(String(15))
+    pjlink_code = Column(String(2))
+    text = Column(String(30))
+
+
+class Projector(CommonBase, Base):
+    """
+    Projector table.
+
+    Projector:
+        ip:             Column(String(100))  # Allow for IPv6 or FQDN
+        port:           Column(String(8))
+        pin:            Column(String(20))   # Allow for test strings
+        name:           Column(String(20))
+        location:       Column(String(30))
+        notes:          Column(String(200))
+        pjlink_name:    Column(String(128))  # From projector (future)
+        manufacturer:   Column(String(128))  # From projector (future)
+        model:          Column(String(128))  # From projector (future)
+        other:          Column(String(128))  # From projector (future)
+        sources:        Column(String(128))  # From projector (future)
+
+        ProjectorSource relates
+    """
+    def __repr__(self):
+        """
+        Return basic representation of Source table entry.
+        """
+        return '< Projector(id="%s", ip="%s", port="%s", pin="%s", name="%s", location="%s",' \
+            'notes="%s", pjlink_name="%s", manufacturer="%s", model="%s", other="%s",' \
+            'sources="%s", source_list="%s") >' % (self.id, self.ip, self.port, self.pin, self.name, self.location,
+                                                   self.notes, self.pjlink_name, self.manufacturer, self.model,
+                                                   self.other, self.sources, self.source_list)
+    ip = Column(String(100))
+    port = Column(String(8))
+    pin = Column(String(20))
+    name = Column(String(20))
+    location = Column(String(30))
+    notes = Column(String(200))
+    pjlink_name = Column(String(128))
+    manufacturer = Column(String(128))
+    model = Column(String(128))
+    other = Column(String(128))
+    sources = Column(String(128))
+    source_list = relationship('ProjectorSource',
+                               order_by='ProjectorSource.code',
+                               backref='projector',
+                               cascade='all, delete-orphan',
+                               primaryjoin='Projector.id==ProjectorSource.projector_id',
+                               lazy='joined')
+
+
+class ProjectorSource(CommonBase, Base):
+    """
+    Projector local source table
+    This table allows mapping specific projector source input to a local
+    connection; i.e., '11': 'DVD Player'
+
+    Projector Source:
+        projector_id:   Foreign_key(Column(Projector.id))
+        code:           Column(String(3)) #  PJLink source code
+        text:           Column(String(20))  # Text to display
+
+    Projector table links here
+    """
+    def __repr__(self):
+        """
+        Return basic representation of Source table entry.
+        """
+        return '<ProjectorSource(id="%s", code="%s", text="%s", projector_id="%s")>' % (self.id,
+                                                                                        self.code,
+                                                                                        self.text,
+                                                                                        self.projector_id)
+    code = Column(String(3))
+    text = Column(String(20))
+    projector_id = Column(Integer, ForeignKey('projector.id'))
+
+
+class ProjectorDB(Manager):
+    """
+    Class to access the projector database.
+    """
+    def __init__(self, *args, **kwargs):
+        log.debug('ProjectorDB().__init__(args="%s", kwargs="%s")' % (args, kwargs))
+        super().__init__(plugin_name='projector', init_schema=self.init_schema)
+        log.debug('ProjectorDB() Initialized using db url %s' % self.db_url)
+        log.debug('Session: %s', self.session)
+
+    def init_schema(self, *args, **kwargs):
+        """
+        Setup the projector database and initialize the schema.
+
+        Declarative uses table classes to define schema.
+        """
+        self.db_url = init_url('projector')
+        session, metadata = init_db(self.db_url, base=Base)
+        metadata.create_all(checkfirst=True)
+        return session
+
+    def get_projector_by_id(self, dbid):
+        """
+        Locate a DB record by record ID.
+
+        :param dbid: DB record id
+        :returns: Projector() instance
+        """
+        log.debug('get_projector_by_id(id="%s")' % dbid)
+        projector = self.get_object_filtered(Projector, Projector.id == dbid)
+        if projector is None:
+            # Not found
+            log.warn('get_projector_by_id() did not find %s' % id)
+            return None
+        log.debug('get_projectorby_id() returning 1 entry for "%s" id="%s"' % (dbid, projector.id))
+        return projector
+
+    def get_projector_all(self):
+        """
+        Retrieve all projector entries.
+
+        :returns: List with Projector() instances used in Manager() QListWidget.
+        """
+        log.debug('get_all() called')
+        return_list = []
+        new_list = self.get_all_objects(Projector)
+        if new_list is None or new_list.count == 0:
+            return return_list
+        for new_projector in new_list:
+            return_list.append(new_projector)
+        log.debug('get_all() returning %s item(s)' % len(return_list))
+        return return_list
+
+    def get_projector_by_ip(self, ip):
+        """
+        Locate a projector by host IP/Name.
+
+        :param ip: Host IP/Name
+        :returns: Projector() instance
+        """
+        log.debug('get_projector_by_ip(ip="%s")' % ip)
+        projector = self.get_object_filtered(Projector, Projector.ip == ip)
+        if projector is None:
+            # Not found
+            log.warn('get_projector_by_ip() did not find %s' % ip)
+            return None
+        log.debug('get_projectorby_ip() returning 1 entry for "%s" id="%s"' % (ip, projector.id))
+        return projector
+
+    def get_projector_by_name(self, name):
+        """
+        Locate a projector by name field
+
+        :param name: Name of projector
+        :returns: Projector() instance
+        """
+        log.debug('get_projector_by_name(name="%s")' % name)
+        projector = self.get_object_filtered(Projector, Projector.name == name)
+        if projector is None:
+            # Not found
+            log.warn('get_projector_by_name() did not find "%s"' % name)
+            return None
+        log.debug('get_projector_by_name() returning one entry for "%s" id="%s"' % (name, projector.id))
+        return projector
+
+    def add_projector(self, projector):
+        """
+        Add a new projector entry
+
+        :param projector: Projector() instance to add
+        :returns: bool
+                  True if entry added
+                  False if entry already in DB or db error
+        """
+        old_projector = self.get_object_filtered(Projector, Projector.ip == projector.ip)
+        if old_projector is not None:
+            log.warn('add_new() skipping entry ip="%s" (Already saved)' % old_projector.ip)
+            return False
+        log.debug('add_new() saving new entry')
+        log.debug('ip="%s", name="%s", location="%s"' % (projector.ip,
+                                                         projector.name,
+                                                         projector.location))
+        log.debug('notes="%s"' % projector.notes)
+        return self.save_object(projector)
+
+    def update_projector(self, projector=None):
+        """
+        Update projector entry
+
+        :param projector: Projector() instance with new information
+        :returns: bool
+                  True if DB record updated
+                  False if entry not in DB or DB error
+        """
+        if projector is None:
+            log.error('No Projector() instance to update - cancelled')
+            return False
+        old_projector = self.get_object_filtered(Projector, Projector.id == projector.id)
+        if old_projector is None:
+            log.error('Edit called on projector instance not in database - cancelled')
+            return False
+        log.debug('(%s) Updating projector with dbid=%s' % (projector.ip, projector.id))
+        old_projector.ip = projector.ip
+        old_projector.name = projector.name
+        old_projector.location = projector.location
+        old_projector.pin = projector.pin
+        old_projector.port = projector.port
+        old_projector.pjlink_name = projector.pjlink_name
+        old_projector.manufacturer = projector.manufacturer
+        old_projector.model = projector.model
+        old_projector.other = projector.other
+        old_projector.sources = projector.sources
+        return self.save_object(old_projector)
+
+    def delete_projector(self, projector):
+        """
+        Delete an entry by record id
+
+        :param projector: Projector() instance to delete
+        :returns: bool
+                  True if record deleted
+                  False if DB error
+        """
+        deleted = self.delete_object(Projector, projector.id)
+        if deleted:
+            log.debug('delete_by_id() Removed entry id="%s"' % projector.id)
+        else:
+            log.error('delete_by_id() Entry id="%s" not deleted for some reason' % projector.id)
+        return deleted
+
+    def get_source_list(self, projector):
+        """
+        Retrieves the source inputs pjlink code-to-text if available based on
+        manufacturer and model.
+        If not available, then returns the PJLink code to default text.
+
+        :param projector: Projector instance
+        :returns: dict
+                  key: (str) PJLink code for source
+                  value: (str) From ProjectorSource, Sources tables or PJLink default code list
+        """
+        source_dict = {}
+        # Get default list first
+        for key in projector.source_available:
+            item = self.get_object_filtered(ProjectorSource,
+                                            and_(ProjectorSource.code == key,
+                                                 ProjectorSource.projector_id == projector.dbid))
+            if item is None:
+                source_dict[key] = PJLINK_DEFAULT_CODES[key]
+            else:
+                source_dict[key] = item.text
+        return source_dict
+
+    def get_source_by_id(self, source):
+        """
+        Retrieves the ProjectorSource by ProjectorSource.id
+
+        :param source: ProjectorSource id
+        :returns: ProjetorSource instance or None
+        """
+        source_entry = self.get_object_filtered(ProjetorSource, ProjectorSource.id == source)
+        if source_entry is None:
+            # Not found
+            log.warn('get_source_by_id() did not find "%s"' % source)
+            return None
+        log.debug('get_source_by_id() returning one entry for "%s""' % (source))
+        return source_entry
+
+    def get_source_by_code(self, code, projector_id):
+        """
+        Retrieves the ProjectorSource by ProjectorSource.id
+
+        :param source: PJLink ID
+        :param projector_id: Projector.id
+        :returns: ProjetorSource instance or None
+        """
+        source_entry = self.get_object_filtered(ProjectorSource,
+                                                and_(ProjectorSource.code == code,
+                                                     ProjectorSource.projector_id == projector_id))
+        if source_entry is None:
+            # Not found
+            log.warn('get_source_by_id() did not find code="%s" projector_id="%s"' % (code, projector_id))
+            return None
+        log.debug('get_source_by_id() returning one entry for code="%s" projector_id="%s"' % (code, projector_id))
+        return source_entry
+
+    def add_source(self, source):
+        """
+        Add a new ProjectorSource record
+
+        :param source: ProjectorSource() instance to add
+        """
+        log.debug('Saving ProjectorSource(projector_id="%s" code="%s" text="%s")' % (source.projector_id,
+                                                                                     source.code, source.text))
+        return self.save_object(source)

=== added file 'openlp/core/lib/projector/pjlink1.py'
--- openlp/core/lib/projector/pjlink1.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/projector/pjlink1.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,912 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+    :mod:`openlp.core.lib.projector.pjlink1` module
+    Provides the necessary functions for connecting to a PJLink-capable projector.
+
+    See PJLink Class 1 Specifications for details.
+    http://pjlink.jbmia.or.jp/english/dl.html
+
+        Section 5-1 PJLink Specifications
+
+        Section 5-5 Guidelines for Input Terminals
+
+    NOTE:
+      Function names follow  the following syntax:
+            def process_CCCC(...):
+      WHERE:
+            CCCC = PJLink command being processed.
+"""
+
+import logging
+log = logging.getLogger(__name__)
+
+log.debug('pjlink1 loaded')
+
+__all__ = ['PJLink1']
+
+from codecs import decode
+
+from PyQt5.QtCore import pyqtSignal, pyqtSlot
+from PyQt5.QtNetwork import QAbstractSocket, QTcpSocket
+
+from openlp.core.common import translate, qmd5_hash
+from openlp.core.lib.projector.constants import *
+
+# Shortcuts
+SocketError = QAbstractSocket.SocketError
+SocketSTate = QAbstractSocket.SocketState
+
+PJLINK_PREFIX = '%'
+PJLINK_CLASS = '1'
+PJLINK_HEADER = '%s%s' % (PJLINK_PREFIX, PJLINK_CLASS)
+PJLINK_SUFFIX = CR
+
+
+class PJLink1(QTcpSocket):
+    """
+    Socket service for connecting to a PJLink-capable projector.
+    """
+    # Signals sent by this module
+    changeStatus = pyqtSignal(str, int, str)
+    projectorNetwork = pyqtSignal(int)  # Projector network activity
+    projectorStatus = pyqtSignal(int)  # Status update
+    projectorAuthentication = pyqtSignal(str)  # Authentication error
+    projectorNoAuthentication = pyqtSignal(str)  # PIN set and no authentication needed
+    projectorReceivedData = pyqtSignal()  # Notify when received data finished processing
+    projectorUpdateIcons = pyqtSignal()  # Update the status icons on toolbar
+
+    def __init__(self, name=None, ip=None, port=PJLINK_PORT, pin=None, *args, **kwargs):
+        """
+        Setup for instance.
+
+        :param name: Display name
+        :param ip: IP address to connect to
+        :param port: Port to use. Default to PJLINK_PORT
+        :param pin: Access pin (if needed)
+
+        Optional parameters
+        :param dbid: Database ID number
+        :param location: Location where projector is physically located
+        :param notes: Extra notes about the projector
+        :param poll_time: Time (in seconds) to poll connected projector
+        :param socket_timeout: Time (in seconds) to abort the connection if no response
+        """
+        log.debug('PJlink(args="%s" kwargs="%s")' % (args, kwargs))
+        self.name = name
+        self.ip = ip
+        self.port = port
+        self.pin = pin
+        super(PJLink1, self).__init__()
+        self.dbid = None
+        self.location = None
+        self.notes = None
+        self.dbid = None if 'dbid' not in kwargs else kwargs['dbid']
+        self.location = None if 'location' not in kwargs else kwargs['location']
+        self.notes = None if 'notes' not in kwargs else kwargs['notes']
+        # Poll time 20 seconds unless called with something else
+        self.poll_time = 20000 if 'poll_time' not in kwargs else kwargs['poll_time'] * 1000
+        # Timeout 5 seconds unless called with something else
+        self.socket_timeout = 5000 if 'socket_timeout' not in kwargs else kwargs['socket_timeout'] * 1000
+        # In case we're called from somewhere that only wants information
+        self.no_poll = 'no_poll' in kwargs
+        self.i_am_running = False
+        self.status_connect = S_NOT_CONNECTED
+        self.last_command = ''
+        self.projector_status = S_NOT_CONNECTED
+        self.error_status = S_OK
+        # Socket information
+        # Add enough space to input buffer for extraneous \n \r
+        self.maxSize = PJLINK_MAX_PACKET + 2
+        self.setReadBufferSize(self.maxSize)
+        # PJLink information
+        self.pjlink_class = '1'  # Default class
+        self.reset_information()
+        # Set from ProjectorManager.add_projector()
+        self.widget = None  # QListBox entry
+        self.timer = None  # Timer that calls the poll_loop
+        self.send_queue = []
+        self.send_busy = False
+        # Socket timer for some possible brain-dead projectors or network cable pulled
+        self.socket_timer = None
+        # Map command to function
+        self.PJLINK1_FUNC = {'AVMT': self.process_avmt,
+                             'CLSS': self.process_clss,
+                             'ERST': self.process_erst,
+                             'INFO': self.process_info,
+                             'INF1': self.process_inf1,
+                             'INF2': self.process_inf2,
+                             'INPT': self.process_inpt,
+                             'INST': self.process_inst,
+                             'LAMP': self.process_lamp,
+                             'NAME': self.process_name,
+                             'PJLINK': self.check_login,
+                             'POWR': self.process_powr
+                             }
+
+    def reset_information(self):
+        """
+        Reset projector-specific information to default
+        """
+        log.debug('(%s) reset_information() connect status is %s' % (self.ip, self.state()))
+        self.power = S_OFF
+        self.pjlink_name = None
+        self.manufacturer = None
+        self.model = None
+        self.shutter = None
+        self.mute = None
+        self.lamp = None
+        self.fan = None
+        self.source_available = None
+        self.source = None
+        self.other_info = None
+        if hasattr(self, 'timer'):
+            self.timer.stop()
+        if hasattr(self, 'socket_timer'):
+            self.socket_timer.stop()
+        self.send_queue = []
+        self.send_busy = False
+
+    def thread_started(self):
+        """
+        Connects signals to methods when thread is started.
+        """
+        log.debug('(%s) Thread starting' % self.ip)
+        self.i_am_running = True
+        self.connected.connect(self.check_login)
+        self.disconnected.connect(self.disconnect_from_host)
+        self.error.connect(self.get_error)
+
+    def thread_stopped(self):
+        """
+        Cleanups when thread is stopped.
+        """
+        log.debug('(%s) Thread stopped' % self.ip)
+        try:
+            self.connected.disconnect(self.check_login)
+        except TypeError:
+            pass
+        try:
+            self.disconnected.disconnect(self.disconnect_from_host)
+        except TypeError:
+            pass
+        try:
+            self.error.disconnect(self.get_error)
+        except TypeError:
+            pass
+        try:
+            self.projectorReceivedData.disconnect(self._send_command)
+        except TypeError:
+            pass
+        self.disconnect_from_host()
+        self.deleteLater()
+        self.i_am_running = False
+
+    def socket_abort(self):
+        """
+        Aborts connection and closes socket in case of brain-dead projectors.
+        Should normally be called by socket_timer().
+        """
+        log.debug('(%s) socket_abort() - Killing connection' % self.ip)
+        self.disconnect_from_host(abort=True)
+
+    def poll_loop(self):
+        """
+        Retrieve information from projector that changes.
+        Normally called by timer().
+        """
+        if self.state() != self.ConnectedState:
+            return
+        log.debug('(%s) Updating projector status' % self.ip)
+        # Reset timer in case we were called from a set command
+        if self.timer.interval() < self.poll_time:
+            # Reset timer to 5 seconds
+            self.timer.setInterval(self.poll_time)
+        # Restart timer
+        self.timer.start()
+        # These commands may change during connetion
+        for command in ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']:
+            self.send_command(command, queue=True)
+        # The following commands do not change, so only check them once
+        if self.power == S_ON and self.source_available is None:
+            self.send_command('INST', queue=True)
+        if self.other_info is None:
+            self.send_command('INFO', queue=True)
+        if self.manufacturer is None:
+            self.send_command('INF1', queue=True)
+        if self.model is None:
+            self.send_command('INF2', queue=True)
+        if self.pjlink_name is None:
+            self.send_command('NAME', queue=True)
+        if self.power == S_ON and self.source_available is None:
+            self.send_command('INST', queue=True)
+
+    def _get_status(self, status):
+        """
+        Helper to retrieve status/error codes and convert to strings.
+
+        :param status: Status/Error code
+        :returns: (Status/Error code, String)
+        """
+        if status in ERROR_STRING:
+            return ERROR_STRING[status], ERROR_MSG[status]
+        elif status in STATUS_STRING:
+            return STATUS_STRING[status], ERROR_MSG[status]
+        else:
+            return status, translate('OpenLP.PJLink1', 'Unknown status')
+
+    def change_status(self, status, msg=None):
+        """
+        Check connection/error status, set status for projector, then emit status change signal
+        for gui to allow changing the icons.
+
+        :param status: Status code
+        :param msg: Optional message
+        """
+        message = translate('OpenLP.PJLink1', 'No message') if msg is None else msg
+        (code, message) = self._get_status(status)
+        if msg is not None:
+            message = msg
+        if status in CONNECTION_ERRORS:
+            # Projector, connection state
+            self.projector_status = self.error_status = self.status_connect = E_NOT_CONNECTED
+        elif status >= S_NOT_CONNECTED and status < S_STATUS:
+            self.status_connect = status
+            self.projector_status = S_NOT_CONNECTED
+        elif status < S_NETWORK_SENDING:
+            self.status_connect = S_CONNECTED
+            self.projector_status = status
+        (status_code, status_message) = self._get_status(self.status_connect)
+        log.debug('(%s) status_connect: %s: %s' % (self.ip, status_code, status_message if msg is None else msg))
+        (status_code, status_message) = self._get_status(self.projector_status)
+        log.debug('(%s) projector_status: %s: %s' % (self.ip, status_code, status_message if msg is None else msg))
+        (status_code, status_message) = self._get_status(self.error_status)
+        log.debug('(%s) error_status: %s: %s' % (self.ip, status_code, status_message if msg is None else msg))
+        self.changeStatus.emit(self.ip, status, message)
+
+    @pyqtSlot()
+    def check_login(self, data=None):
+        """
+        Processes the initial connection and authentication (if needed).
+        Starts poll timer if connection is established.
+
+        :param data: Optional data if called from another routine
+        """
+        log.debug('(%s) check_login(data="%s")' % (self.ip, data))
+        if data is None:
+            # Reconnected setup?
+            if not self.waitForReadyRead(2000):
+                # Possible timeout issue
+                log.error('(%s) Socket timeout waiting for login' % self.ip)
+                self.change_status(E_SOCKET_TIMEOUT)
+                return
+            read = self.readLine(self.maxSize)
+            dontcare = self.readLine(self.maxSize)  # Clean out the trailing \r\n
+            if read is None:
+                log.warn('(%s) read is None - socket error?' % self.ip)
+                return
+            elif len(read) < 8:
+                log.warn('(%s) Not enough data read)' % self.ip)
+                return
+            data = decode(read, 'ascii')
+            # Possibility of extraneous data on input when reading.
+            # Clean out extraneous characters in buffer.
+            dontcare = self.readLine(self.maxSize)
+            log.debug('(%s) check_login() read "%s"' % (self.ip, data.strip()))
+        # At this point, we should only have the initial login prompt with
+        # possible authentication
+        # PJLink initial login will be:
+        # 'PJLink 0' - Unauthenticated login - no extra steps required.
+        # 'PJLink 1 XXXXXX' Authenticated login - extra processing required.
+        if not data.upper().startswith('PJLINK'):
+            # Invalid response
+            return self.disconnect_from_host()
+        if '=' in data:
+            # Processing a login reply
+            data_check = data.strip().split('=')
+        else:
+            # Process initial connection
+            data_check = data.strip().split(' ')
+        log.debug('(%s) data_check="%s"' % (self.ip, data_check))
+        # Check for projector reporting an error
+        if data_check[1].upper() == 'ERRA':
+            # Authentication error
+            self.disconnect_from_host()
+            self.change_status(E_AUTHENTICATION)
+            log.debug('(%s) emitting projectorAuthentication() signal' % self.name)
+            return
+        elif data_check[1] == '0' and self.pin is not None:
+            # Pin set and no authentication needed
+            self.disconnect_from_host()
+            self.change_status(E_AUTHENTICATION)
+            log.debug('(%s) emitting projectorNoAuthentication() signal' % self.name)
+            self.projectorNoAuthentication.emit(self.name)
+            return
+        elif data_check[1] == '1':
+            # Authenticated login with salt
+            log.debug('(%s) Setting hash with salt="%s"' % (self.ip, data_check[2]))
+            log.debug('(%s) pin="%s"' % (self.ip, self.pin))
+            salt = qmd5_hash(salt=data_check[2].encode('ascii'), data=self.pin.encode('ascii'))
+        else:
+            salt = None
+        # We're connected at this point, so go ahead and do regular I/O
+        self.readyRead.connect(self.get_data)
+        self.projectorReceivedData.connect(self._send_command)
+        # Initial data we should know about
+        self.send_command(cmd='CLSS', salt=salt)
+        self.waitForReadyRead()
+        if (not self.no_poll) and (self.state() == self.ConnectedState):
+            log.debug('(%s) Starting timer' % self.ip)
+            self.timer.setInterval(2000)  # Set 2 seconds for initial information
+            self.timer.start()
+
+    @pyqtSlot()
+    def get_data(self):
+        """
+        Socket interface to retrieve data.
+        """
+        log.debug('(%s) get_data(): Reading data' % self.ip)
+        if self.state() != self.ConnectedState:
+            log.debug('(%s) get_data(): Not connected - returning' % self.ip)
+            self.send_busy = False
+            return
+        read = self.readLine(self.maxSize)
+        if read == -1:
+            # No data available
+            log.debug('(%s) get_data(): No data available (-1)' % self.ip)
+            self.send_busy = False
+            self.projectorReceivedData.emit()
+            return
+        self.socket_timer.stop()
+        self.projectorNetwork.emit(S_NETWORK_RECEIVED)
+        data_in = decode(read, 'ascii')
+        data = data_in.strip()
+        if len(data) < 7:
+            # Not enough data for a packet
+            log.debug('(%s) get_data(): Packet length < 7: "%s"' % (self.ip, data))
+            self.send_busy = False
+            self.projectorReceivedData.emit()
+            return
+        log.debug('(%s) get_data(): Checking new data "%s"' % (self.ip, data))
+        if data.upper().startswith('PJLINK'):
+            # Reconnected from remote host disconnect ?
+            self.check_login(data)
+            self.send_busy = False
+            self.projectorReceivedData.emit()
+            return
+        elif '=' not in data:
+            log.warn('(%s) get_data(): Invalid packet received' % self.ip)
+            self.send_busy = False
+            self.projectorReceivedData.emit()
+            return
+        data_split = data.split('=')
+        try:
+            (prefix, class_, cmd, data) = (data_split[0][0], data_split[0][1], data_split[0][2:], data_split[1])
+        except ValueError as e:
+            log.warn('(%s) get_data(): Invalid packet - expected header + command + data' % self.ip)
+            log.warn('(%s) get_data(): Received data: "%s"' % (self.ip, read))
+            self.change_status(E_INVALID_DATA)
+            self.send_busy = False
+            self.projectorReceivedData.emit()
+            return
+
+        if not (self.pjlink_class in PJLINK_VALID_CMD and cmd in PJLINK_VALID_CMD[self.pjlink_class]):
+            log.warn('(%s) get_data(): Invalid packet - unknown command "%s"' % (self.ip, cmd))
+            self.send_busy = False
+            self.projectorReceivedData.emit()
+            return
+        return self.process_command(cmd, data)
+
+    @pyqtSlot(int)
+    def get_error(self, err):
+        """
+        Process error from SocketError signal.
+        Remaps system error codes to projector error codes.
+
+        :param err: Error code
+        """
+        log.debug('(%s) get_error(err=%s): %s' % (self.ip, err, self.errorString()))
+        if err <= 18:
+            # QSocket errors. Redefined in projector.constants so we don't mistake
+            # them for system errors
+            check = err + E_CONNECTION_REFUSED
+            self.timer.stop()
+        else:
+            check = err
+        if check < E_GENERAL:
+            # Some system error?
+            self.change_status(err, self.errorString())
+        else:
+            self.change_status(E_NETWORK, self.errorString())
+        self.projectorUpdateIcons.emit()
+        if self.status_connect == E_NOT_CONNECTED:
+            self.abort()
+            self.reset_information()
+        return
+
+    def send_command(self, cmd, opts='?', salt=None, queue=False):
+        """
+        Add command to output queue if not already in queue.
+
+        :param cmd: Command to send
+        :param opts: Command option (if any) - defaults to '?' (get information)
+        :param salt: Optional  salt for md5 hash initial authentication
+        :param queue: Option to force add to queue rather than sending directly
+        """
+        if self.state() != self.ConnectedState:
+            log.warn('(%s) send_command(): Not connected - returning' % self.ip)
+            self.send_queue = []
+            return
+        self.projectorNetwork.emit(S_NETWORK_SENDING)
+        log.debug('(%s) send_command(): Building cmd="%s" opts="%s" %s' % (self.ip,
+                                                                           cmd,
+                                                                           opts,
+                                                                           '' if salt is None else 'with hash'))
+        if salt is None:
+            out = '%s%s %s%s' % (PJLINK_HEADER, cmd, opts, CR)
+        else:
+            out = '%s%s%s %s%s' % (salt, PJLINK_HEADER, cmd, opts, CR)
+        if out in self.send_queue:
+            # Already there, so don't add
+            log.debug('(%s) send_command(out="%s") Already in queue - skipping' % (self.ip, out.strip()))
+        elif not queue and len(self.send_queue) == 0:
+            # Nothing waiting to send, so just send it
+            log.debug('(%s) send_command(out="%s") Sending data' % (self.ip, out.strip()))
+            return self._send_command(data=out)
+        else:
+            log.debug('(%s) send_command(out="%s") adding to queue' % (self.ip, out.strip()))
+            self.send_queue.append(out)
+            self.projectorReceivedData.emit()
+        log.debug('(%s) send_command(): send_busy is %s' % (self.ip, self.send_busy))
+        if not self.send_busy:
+            log.debug('(%s) send_command() calling _send_string()')
+            self._send_command()
+
+    @pyqtSlot()
+    def _send_command(self, data=None):
+        """
+        Socket interface to send data. If data=None, then check queue.
+
+        :param data: Immediate data to send
+        """
+        log.debug('(%s) _send_string()' % self.ip)
+        log.debug('(%s) _send_string(): Connection status: %s' % (self.ip, self.state()))
+        if self.state() != self.ConnectedState:
+            log.debug('(%s) _send_string() Not connected - abort' % self.ip)
+            self.send_queue = []
+            self.send_busy = False
+            return
+        if self.send_busy:
+            # Still waiting for response from last command sent
+            return
+        if data is not None:
+            out = data
+            log.debug('(%s) _send_string(data=%s)' % (self.ip, out.strip()))
+        elif len(self.send_queue) != 0:
+            out = self.send_queue.pop(0)
+            log.debug('(%s) _send_string(queued data=%s)' % (self.ip, out.strip()))
+        else:
+            # No data to send
+            log.debug('(%s) _send_string(): No data to send' % self.ip)
+            self.send_busy = False
+            return
+        self.send_busy = True
+        log.debug('(%s) _send_string(): Sending "%s"' % (self.ip, out.strip()))
+        log.debug('(%s) _send_string(): Queue = %s' % (self.ip, self.send_queue))
+        self.socket_timer.start()
+        self.projectorNetwork.emit(S_NETWORK_SENDING)
+        sent = self.write(out.encode('ascii'))
+        self.waitForBytesWritten(2000)  # 2 seconds should be enough
+        if sent == -1:
+            # Network error?
+            self.change_status(E_NETWORK,
+                               translate('OpenLP.PJLink1', 'Error while sending data to projector'))
+
+    def process_command(self, cmd, data):
+        """
+        Verifies any return error code. Calls the appropriate command handler.
+
+        :param cmd: Command to process
+        :param data: Data being processed
+        """
+        log.debug('(%s) Processing command "%s"' % (self.ip, cmd))
+        if data in PJLINK_ERRORS:
+            # Oops - projector error
+            if data.upper() == 'ERRA':
+                # Authentication error
+                self.disconnect_from_host()
+                self.change_status(E_AUTHENTICATION)
+                log.debug('(%s) emitting projectorAuthentication() signal' % self.ip)
+                self.projectorAuthentication.emit(self.name)
+            elif data.upper() == 'ERR1':
+                # Undefined command
+                self.change_status(E_UNDEFINED, '%s "%s"' %
+                                   (translate('OpenLP.PJLink1', 'Undefined command:'), cmd))
+            elif data.upper() == 'ERR2':
+                # Invalid parameter
+                self.change_status(E_PARAMETER)
+            elif data.upper() == 'ERR3':
+                # Projector busy
+                self.change_status(E_UNAVAILABLE)
+            elif data.upper() == 'ERR4':
+                # Projector/display error
+                self.change_status(E_PROJECTOR)
+            self.send_busy = False
+            self.projectorReceivedData.emit()
+            return
+        # Command succeeded - no extra information
+        elif data.upper() == 'OK':
+            log.debug('(%s) Command returned OK' % self.ip)
+            # A command returned successfully, recheck data
+            self.send_busy = False
+            self.projectorReceivedData.emit()
+            return
+
+        if cmd in self.PJLINK1_FUNC:
+            self.PJLINK1_FUNC[cmd](data)
+        else:
+            log.warn('(%s) Invalid command %s' % (self.ip, cmd))
+        self.send_busy = False
+        self.projectorReceivedData.emit()
+
+    def process_lamp(self, data):
+        """
+        Lamp(s) status. See PJLink Specifications for format.
+        Data may have more than 1 lamp to process.
+        Update self.lamp dictionary with lamp status.
+
+        :param data: Lamp(s) status.
+        """
+        lamps = []
+        data_dict = data.split()
+        while data_dict:
+            try:
+                fill = {'Hours': int(data_dict[0]), 'On': False if data_dict[1] == '0' else True}
+            except ValueError:
+                # In case of invalid entry
+                log.warn('(%s) process_lamp(): Invalid data "%s"' % (self.ip, data))
+                return
+            lamps.append(fill)
+            data_dict.pop(0)  # Remove lamp hours
+            data_dict.pop(0)  # Remove lamp on/off
+        self.lamp = lamps
+        return
+
+    def process_powr(self, data):
+        """
+        Power status. See PJLink specification for format.
+        Update self.power with status. Update icons if change from previous setting.
+
+        :param data: Power status
+        """
+        if data in PJLINK_POWR_STATUS:
+            power = PJLINK_POWR_STATUS[data]
+            update_icons = self.power != power
+            self.power = power
+            self.change_status(PJLINK_POWR_STATUS[data])
+            if update_icons:
+                self.projectorUpdateIcons.emit()
+                # Update the input sources available
+                if power == S_ON:
+                    self.send_command('INST')
+        else:
+            # Log unknown status response
+            log.warn('Unknown power response: %s' % data)
+        return
+
+    def process_avmt(self, data):
+        """
+        Process shutter and speaker status. See PJLink specification for format.
+        Update self.mute (audio) and self.shutter (video shutter).
+
+        :param data: Shutter and audio status
+        """
+        shutter = self.shutter
+        mute = self.mute
+        if data == '11':
+            shutter = True
+            mute = False
+        elif data == '21':
+            shutter = False
+            mute = True
+        elif data == '30':
+            shutter = False
+            mute = False
+        elif data == '31':
+            shutter = True
+            mute = True
+        else:
+            log.warn('Unknown shutter response: %s' % data)
+        update_icons = shutter != self.shutter
+        update_icons = update_icons or mute != self.mute
+        self.shutter = shutter
+        self.mute = mute
+        if update_icons:
+            self.projectorUpdateIcons.emit()
+        return
+
+    def process_inpt(self, data):
+        """
+        Current source input selected. See PJLink specification for format.
+        Update self.source
+
+        :param data: Currently selected source
+        """
+        self.source = data
+        return
+
+    def process_clss(self, data):
+        """
+        PJLink class that this projector supports. See PJLink specification for format.
+        Updates self.class.
+
+        :param data: Class that projector supports.
+        """
+        # bug 1550891: Projector returns non-standard class response:
+        #            : Expected: %1CLSS=1
+        #            : Received: %1CLSS=Class 1
+        if len(data) > 1:
+            # Split non-standard information from response
+            clss = data.split()[-1]
+        else:
+            clss = data
+        self.pjlink_class = clss
+        log.debug('(%s) Setting pjlink_class for this projector to "%s"' % (self.ip, self.pjlink_class))
+        return
+
+    def process_name(self, data):
+        """
+        Projector name set in projector.
+        Updates self.pjlink_name
+
+        :param data: Projector name
+        """
+        self.pjlink_name = data
+        return
+
+    def process_inf1(self, data):
+        """
+        Manufacturer name set in projector.
+        Updates self.manufacturer
+
+        :param data: Projector manufacturer
+        """
+        self.manufacturer = data
+        return
+
+    def process_inf2(self, data):
+        """
+        Projector Model set in projector.
+        Updates self.model.
+
+        :param data: Model name
+        """
+        self.model = data
+        return
+
+    def process_info(self, data):
+        """
+        Any extra info set in projector.
+        Updates self.other_info.
+
+        :param data: Projector other info
+        """
+        self.other_info = data
+        return
+
+    def process_inst(self, data):
+        """
+        Available source inputs. See PJLink specification for format.
+        Updates self.source_available
+
+        :param data: Sources list
+        """
+        sources = []
+        check = data.split()
+        for source in check:
+            sources.append(source)
+        sources.sort()
+        self.source_available = sources
+        self.projectorUpdateIcons.emit()
+        return
+
+    def process_erst(self, data):
+        """
+        Error status. See PJLink Specifications for format.
+        Updates self.projector_errors
+
+        :param data: Error status
+        """
+        try:
+            datacheck = int(data)
+        except ValueError:
+            # Bad data - ignore
+            return
+        if datacheck == 0:
+            self.projector_errors = None
+        else:
+            self.projector_errors = {}
+            # Fan
+            if data[0] != '0':
+                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Fan')] = \
+                    PJLINK_ERST_STATUS[data[0]]
+            # Lamp
+            if data[1] != '0':
+                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Lamp')] =  \
+                    PJLINK_ERST_STATUS[data[1]]
+            # Temp
+            if data[2] != '0':
+                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Temperature')] =  \
+                    PJLINK_ERST_STATUS[data[2]]
+            # Cover
+            if data[3] != '0':
+                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Cover')] =  \
+                    PJLINK_ERST_STATUS[data[3]]
+            # Filter
+            if data[4] != '0':
+                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Filter')] =  \
+                    PJLINK_ERST_STATUS[data[4]]
+            # Other
+            if data[5] != '0':
+                self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Other')] =  \
+                    PJLINK_ERST_STATUS[data[5]]
+        return
+
+    def connect_to_host(self):
+        """
+        Initiate connection to projector.
+        """
+        if self.state() == self.ConnectedState:
+            log.warn('(%s) connect_to_host(): Already connected - returning' % self.ip)
+            return
+        self.change_status(S_CONNECTING)
+        self.connectToHost(self.ip, self.port if type(self.port) is int else int(self.port))
+
+    @pyqtSlot()
+    def disconnect_from_host(self, abort=False):
+        """
+        Close socket and cleanup.
+        """
+        if abort or self.state() != self.ConnectedState:
+            if abort:
+                log.warn('(%s) disconnect_from_host(): Aborting connection' % self.ip)
+            else:
+                log.warn('(%s) disconnect_from_host(): Not connected - returning' % self.ip)
+            self.reset_information()
+        self.disconnectFromHost()
+        try:
+            self.readyRead.disconnect(self.get_data)
+        except TypeError:
+            pass
+        if abort:
+            self.change_status(E_NOT_CONNECTED)
+        else:
+            log.debug('(%s) disconnect_from_host() Current status %s' % (self.ip,
+                                                                         self._get_status(self.status_connect)[0]))
+            if self.status_connect != E_NOT_CONNECTED:
+                self.change_status(S_NOT_CONNECTED)
+        self.reset_information()
+        self.projectorUpdateIcons.emit()
+
+    def get_available_inputs(self):
+        """
+        Send command to retrieve available source inputs.
+        """
+        return self.send_command(cmd='INST')
+
+    def get_error_status(self):
+        """
+        Send command to retrieve currently known errors.
+        """
+        return self.send_command(cmd='ERST')
+
+    def get_input_source(self):
+        """
+        Send command to retrieve currently selected source input.
+        """
+        return self.send_command(cmd='INPT')
+
+    def get_lamp_status(self):
+        """
+        Send command to return the lap status.
+        """
+        return self.send_command(cmd='LAMP')
+
+    def get_manufacturer(self):
+        """
+        Send command to retrieve manufacturer name.
+        """
+        return self.send_command(cmd='INF1')
+
+    def get_model(self):
+        """
+        Send command to retrieve the model name.
+        """
+        return self.send_command(cmd='INF2')
+
+    def get_name(self):
+        """
+        Send command to retrieve name as set by end-user (if set).
+        """
+        return self.send_command(cmd='NAME')
+
+    def get_other_info(self):
+        """
+        Send command to retrieve extra info set by manufacturer.
+        """
+        return self.send_command(cmd='INFO')
+
+    def get_power_status(self):
+        """
+        Send command to retrieve power status.
+        """
+        return self.send_command(cmd='POWR')
+
+    def get_shutter_status(self):
+        """
+        Send command to retrieve shutter status.
+        """
+        return self.send_command(cmd='AVMT')
+
+    def set_input_source(self, src=None):
+        """
+        Verify input source available as listed in 'INST' command,
+        then send the command to select the input source.
+
+        :param src: Video source to select in projector
+        """
+        log.debug('(%s) set_input_source(src=%s)' % (self.ip, src))
+        if self.source_available is None:
+            return
+        elif src not in self.source_available:
+            return
+        log.debug('(%s) Setting input source to %s' % (self.ip, src))
+        self.send_command(cmd='INPT', opts=src)
+        self.poll_loop()
+
+    def set_power_on(self):
+        """
+        Send command to turn power to on.
+        """
+        self.send_command(cmd='POWR', opts='1')
+        self.poll_loop()
+
+    def set_power_off(self):
+        """
+        Send command to turn power to standby.
+        """
+        self.send_command(cmd='POWR', opts='0')
+        self.poll_loop()
+
+    def set_shutter_closed(self):
+        """
+        Send command to set shutter to closed position.
+        """
+        self.send_command(cmd='AVMT', opts='11')
+        self.poll_loop()
+
+    def set_shutter_open(self):
+        """
+        Send command to set shutter to open position.
+        """
+        self.send_command(cmd='AVMT', opts='10')
+        self.poll_loop()

=== added file 'openlp/core/lib/renderer.py'
--- openlp/core/lib/renderer.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/renderer.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,569 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+
+import re
+
+from PyQt5 import QtGui, QtCore, QtWebKitWidgets
+
+from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings
+from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \
+    build_lyrics_format_css, build_lyrics_outline_css
+from openlp.core.common import ThemeLevel
+from openlp.core.ui import MainDisplay
+
+VERSE = 'The Lord said to {r}Noah{/r}: \n' \
+    'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n' \
+    'The Lord said to {g}Noah{/g}:\n' \
+    'There\'s gonna be a {st}floody{/st}, {it}floody{/it}\n' \
+    'Get those children out of the muddy, muddy \n' \
+    '{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}' \
+    'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
+VERSE_FOR_LINE_COUNT = '\n'.join(map(str, range(100)))
+FOOTER = ['Arky Arky (Unknown)', 'Public Domain', 'CCLI 123456']
+
+
+class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
+    """
+    Class to pull all Renderer interactions into one place. The plugins will call helper methods to do the rendering but
+    this class will provide display defense code.
+    """
+
+    def __init__(self):
+        """
+        Initialise the renderer.
+        """
+        super(Renderer, self).__init__(None)
+        # Need live behaviour if this is also working as a pseudo MainDisplay.
+        self.screens = ScreenList()
+        self.theme_level = ThemeLevel.Global
+        self.global_theme_name = ''
+        self.service_theme_name = ''
+        self.item_theme_name = ''
+        self.force_page = False
+        self._theme_dimensions = {}
+        self._calculate_default()
+        self.web = QtWebKitWidgets.QWebView()
+        self.web.setVisible(False)
+        self.web_frame = self.web.page().mainFrame()
+        Registry().register_function('theme_update_global', self.set_global_theme)
+
+    def bootstrap_initialise(self):
+        """
+        Initialise functions
+        """
+        self.display = MainDisplay(self)
+        self.display.setup()
+
+    def update_display(self):
+        """
+        Updates the renderer's information about the current screen.
+        """
+        self._calculate_default()
+        if self.display:
+            self.display.close()
+        self.display = MainDisplay(self)
+        self.display.setup()
+        self._theme_dimensions = {}
+
+    def update_theme(self, theme_name, old_theme_name=None, only_delete=False):
+        """
+        This method updates the theme in ``_theme_dimensions`` when a theme has been edited or renamed.
+
+        :param theme_name: The current theme name.
+        :param old_theme_name: The old theme name. Has only to be passed, when the theme has been renamed.
+            Defaults to *None*.
+        :param only_delete: Only remove the given ``theme_name`` from the ``_theme_dimensions`` list. This can be
+            used when a theme is permanently deleted.
+        """
+        if old_theme_name is not None and old_theme_name in self._theme_dimensions:
+            del self._theme_dimensions[old_theme_name]
+        if theme_name in self._theme_dimensions:
+            del self._theme_dimensions[theme_name]
+        if not only_delete and theme_name:
+            self._set_theme(theme_name)
+
+    def _set_theme(self, theme_name):
+        """
+        Helper method to save theme names and theme data.
+
+        :param theme_name: The theme name
+        """
+        self.log_debug("_set_theme with theme %s" % theme_name)
+        if theme_name not in self._theme_dimensions:
+            theme_data = self.theme_manager.get_theme_data(theme_name)
+            main_rect = self.get_main_rectangle(theme_data)
+            footer_rect = self.get_footer_rectangle(theme_data)
+            self._theme_dimensions[theme_name] = [theme_data, main_rect, footer_rect]
+        else:
+            theme_data, main_rect, footer_rect = self._theme_dimensions[theme_name]
+        # if No file do not update cache
+        if theme_data.background_filename:
+            self.image_manager.add_image(theme_data.background_filename,
+                                         ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
+
+    def pre_render(self, override_theme_data=None):
+        """
+        Set up the theme to be used before rendering an item.
+
+        :param override_theme_data: The theme data should be passed, when we want to use our own theme data, regardless
+         of the theme level. This should for example be used in the theme manager. **Note**, this is **not** to
+         be mixed up with the ``set_item_theme`` method.
+        """
+        # Just assume we use the global theme.
+        theme_to_use = self.global_theme_name
+        # The theme level is either set to Service or Item. Use the service theme if one is set. We also have to use the
+        # service theme, even when the theme level is set to Item, because the item does not necessarily have to have a
+        # theme.
+        if self.theme_level != ThemeLevel.Global:
+            # When the theme level is at Service and we actually have a service theme then use it.
+            if self.service_theme_name:
+                theme_to_use = self.service_theme_name
+        # If we have Item level and have an item theme then use it.
+        if self.theme_level == ThemeLevel.Song and self.item_theme_name:
+            theme_to_use = self.item_theme_name
+        if override_theme_data is None:
+            if theme_to_use not in self._theme_dimensions:
+                self._set_theme(theme_to_use)
+            theme_data, main_rect, footer_rect = self._theme_dimensions[theme_to_use]
+        else:
+            # Ignore everything and use own theme data.
+            theme_data = override_theme_data
+            main_rect = self.get_main_rectangle(override_theme_data)
+            footer_rect = self.get_footer_rectangle(override_theme_data)
+        self._set_text_rectangle(theme_data, main_rect, footer_rect)
+        return theme_data, self._rect, self._rect_footer
+
+    def set_theme_level(self, theme_level):
+        """
+        Sets the theme level.
+
+        :param theme_level: The theme level to be used.
+        """
+        self.theme_level = theme_level
+
+    def set_global_theme(self):
+        """
+        Set the global-level theme name.
+        """
+        global_theme_name = Settings().value('themes/global theme')
+        self._set_theme(global_theme_name)
+        self.global_theme_name = global_theme_name
+
+    def set_service_theme(self, service_theme_name):
+        """
+        Set the service-level theme.
+
+        :param service_theme_name: The service level theme's name.
+        """
+        self._set_theme(service_theme_name)
+        self.service_theme_name = service_theme_name
+
+    def set_item_theme(self, item_theme_name):
+        """
+        Set the item-level theme. **Note**, this has to be done for each item we are rendering.
+
+        :param item_theme_name: The item theme's name.
+        """
+        self.log_debug("set_item_theme with theme %s" % item_theme_name)
+        self._set_theme(item_theme_name)
+        self.item_theme_name = item_theme_name
+
+    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 force_page: Flag to tell message lines per page need to be generated.
+        """
+        # save value for use in format_slide
+        self.force_page = force_page
+        # build a service item to generate preview
+        service_item = ServiceItem()
+        if self.force_page:
+            # make big page for theme edit dialog to get line count
+            service_item.add_from_text(VERSE_FOR_LINE_COUNT)
+        else:
+            service_item.add_from_text(VERSE)
+        service_item.raw_footer = FOOTER
+        # if No file do not update cache
+        if theme_data.background_filename:
+            self.image_manager.add_image(
+                theme_data.background_filename, ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
+        theme_data, main, footer = self.pre_render(theme_data)
+        service_item.theme_data = theme_data
+        service_item.main = main
+        service_item.footer = footer
+        service_item.render(True)
+        if not self.force_page:
+            self.display.build_html(service_item)
+            raw_html = service_item.get_rendered_frame(0)
+            self.display.text(raw_html, False)
+            preview = self.display.preview()
+            return preview
+        self.force_page = False
+
+    def format_slide(self, text, item):
+        """
+        Calculate how much text can fit on a slide.
+
+        :param text:  The words to go on the slides.
+        :param item: The :class:`~openlp.core.lib.serviceitem.ServiceItem` item object.
+
+        """
+        self.log_debug('format slide')
+        # Add line endings after each line of text used for bibles.
+        line_end = '<br>'
+        if item.is_capable(ItemCapabilities.NoLineBreaks):
+            line_end = ' '
+        # Bibles
+        if item.is_capable(ItemCapabilities.CanWordSplit):
+            pages = self._paginate_slide_words(text.split('\n'), line_end)
+        # Songs and Custom
+        elif item.is_capable(ItemCapabilities.CanSoftBreak):
+            pages = []
+            if '[---]' in text:
+                # Remove two or more option slide breaks next to each other (causing infinite loop).
+                while '\n[---]\n[---]\n' in text:
+                    text = text.replace('\n[---]\n[---]\n', '\n[---]\n')
+                while ' [---]' in text:
+                    text = text.replace(' [---]', '[---]')
+                while '[---] ' in text:
+                    text = text.replace('[---] ', '[---]')
+                count = 0
+                # only loop 5 times as there will never be more than 5 incorrect logical splits on a single slide.
+                while True and count < 5:
+                    slides = text.split('\n[---]\n', 2)
+                    # If there are (at least) two occurrences of [---] we use the first two slides (and neglect the last
+                    # for now).
+                    if len(slides) == 3:
+                        html_text = expand_tags('\n'.join(slides[:2]))
+                    # We check both slides to determine if the optional split is needed (there is only one optional
+                    # split).
+                    else:
+                        html_text = expand_tags('\n'.join(slides))
+                    html_text = html_text.replace('\n', '<br>')
+                    if self._text_fits_on_slide(html_text):
+                        # The first two optional slides fit (as a whole) on one slide. Replace the first occurrence
+                        # of [---].
+                        text = text.replace('\n[---]', '', 1)
+                    else:
+                        # The first optional slide fits, which means we have to render the first optional slide.
+                        text_contains_split = '[---]' in text
+                        if text_contains_split:
+                            try:
+                                text_to_render, text = text.split('\n[---]\n', 1)
+                            except ValueError:
+                                text_to_render = text.split('\n[---]\n')[0]
+                                text = ''
+                            text_to_render, raw_tags, html_tags = get_start_tags(text_to_render)
+                            if text:
+                                text = raw_tags + text
+                        else:
+                            text_to_render = text
+                            text = ''
+                        lines = text_to_render.strip('\n').split('\n')
+                        slides = self._paginate_slide(lines, line_end)
+                        if len(slides) > 1 and text:
+                            # Add all slides apart from the last one the list.
+                            pages.extend(slides[:-1])
+                            if text_contains_split:
+                                text = slides[-1] + '\n[---]\n' + text
+                            else:
+                                text = slides[-1] + '\n' + text
+                            text = text.replace('<br>', '\n')
+                        else:
+                            pages.extend(slides)
+                    if '[---]' not in text:
+                        lines = text.strip('\n').split('\n')
+                        pages.extend(self._paginate_slide(lines, line_end))
+                        break
+                    count += 1
+            else:
+                # Clean up line endings.
+                pages = self._paginate_slide(text.split('\n'), line_end)
+        else:
+            pages = self._paginate_slide(text.split('\n'), line_end)
+        new_pages = []
+        for page in pages:
+            while page.endswith('<br>'):
+                page = page[:-4]
+            new_pages.append(page)
+        return new_pages
+
+    def _calculate_default(self):
+        """
+        Calculate the default dimensions of the screen.
+        """
+        screen_size = self.screens.current['size']
+        self.width = screen_size.width()
+        self.height = screen_size.height()
+        self.screen_ratio = self.height / self.width
+        self.log_debug('_calculate default %s, %f' % (screen_size, self.screen_ratio))
+        # 90% is start of footer
+        self.footer_start = int(self.height * 0.90)
+
+    def get_main_rectangle(self, theme_data):
+        """
+        Calculates the placement and size of the main rectangle.
+
+        :param theme_data: The theme information
+        """
+        if not theme_data.font_main_override:
+            return QtCore.QRect(10, 0, self.width - 20, self.footer_start)
+        else:
+            return QtCore.QRect(theme_data.font_main_x, theme_data.font_main_y,
+                                theme_data.font_main_width - 1, theme_data.font_main_height - 1)
+
+    def get_footer_rectangle(self, theme_data):
+        """
+        Calculates the placement and size of the footer rectangle.
+
+        :param theme_data: The theme data.
+        """
+        if not theme_data.font_footer_override:
+            return QtCore.QRect(10, self.footer_start, self.width - 20, self.height - self.footer_start)
+        else:
+            return QtCore.QRect(theme_data.font_footer_x,
+                                theme_data.font_footer_y, theme_data.font_footer_width - 1,
+                                theme_data.font_footer_height - 1)
+
+    def _set_text_rectangle(self, theme_data, rect_main, rect_footer):
+        """
+        Sets the rectangle within which text should be rendered.
+
+        :param theme_data: The theme data.
+        :param rect_main: The main text block.
+        :param rect_footer: The footer text block.
+        """
+        self.log_debug('_set_text_rectangle %s , %s' % (rect_main, rect_footer))
+        self._rect = rect_main
+        self._rect_footer = rect_footer
+        self.page_width = self._rect.width()
+        self.page_height = self._rect.height()
+        if theme_data.font_main_shadow:
+            self.page_width -= int(theme_data.font_main_shadow_size)
+            self.page_height -= int(theme_data.font_main_shadow_size)
+        # For the life of my I don't know why we have to completely kill the QWebView in order for the display to work
+        # properly, but we do. See bug #1041366 for an example of what happens if we take this out.
+        self.web = None
+        self.web = QtWebKitWidgets.QWebView()
+        self.web.setVisible(False)
+        self.web.resize(self.page_width, self.page_height)
+        self.web_frame = self.web.page().mainFrame()
+        # Adjust width and height to account for shadow. outline done in css.
+        html = """<!DOCTYPE html><html><head><script>
+            function show_text(newtext) {
+                var main = document.getElementById('main');
+                main.innerHTML = newtext;
+                // We need to be sure that the page is loaded, that is why we
+                // return the element's height (even though we do not use the
+                // returned value).
+                return main.offsetHeight;
+            }
+            </script><style>*{margin: 0; padding: 0; border: 0;}
+            #main {position: absolute; top: 0px; %s %s}</style></head><body>
+            <div id="main"></div></body></html>""" % \
+            (build_lyrics_format_css(theme_data, self.page_width, self.page_height),
+             build_lyrics_outline_css(theme_data))
+        self.web.setHtml(html)
+        self.empty_height = self.web_frame.contentsSize().height()
+
+    def _paginate_slide(self, lines, line_end):
+        """
+        Figure out how much text can appear on a slide, using the current theme settings.
+
+        **Note:** The smallest possible "unit" of text for a slide is one line. If the line is too long it will be cut
+        off when displayed.
+
+        :param lines: The text to be fitted on the slide split into lines.
+        :param line_end: The text added after each line. Either ``' '`` or ``'<br>``.
+        """
+        formatted = []
+        previous_html = ''
+        previous_raw = ''
+        separator = '<br>'
+        html_lines = list(map(expand_tags, lines))
+        # Text too long so go to next page.
+        if not self._text_fits_on_slide(separator.join(html_lines)):
+            html_text, previous_raw = self._binary_chop(
+                formatted, previous_html, previous_raw, html_lines, lines, separator, '')
+        else:
+            previous_raw = separator.join(lines)
+        formatted.append(previous_raw)
+        return formatted
+
+    def _paginate_slide_words(self, lines, line_end):
+        """
+        Figure out how much text can appear on a slide, using the current theme settings.
+
+        **Note:** The smallest possible "unit" of text for a slide is one word. If one line is too long it will be
+        processed word by word. This is sometimes need for **bible** verses.
+
+        :param lines: The text to be fitted on the slide split into lines.
+        :param line_end: The text added after each line. Either ``' '`` or ``'<br>``. This is needed for **bibles**.
+        """
+        formatted = []
+        previous_html = ''
+        previous_raw = ''
+        for line in lines:
+            line = line.strip()
+            html_line = expand_tags(line)
+            # Text too long so go to next page.
+            if not self._text_fits_on_slide(previous_html + html_line):
+                # Check if there was a verse before the current one and append it, when it fits on the page.
+                if previous_html:
+                    if self._text_fits_on_slide(previous_html):
+                        formatted.append(previous_raw)
+                        previous_html = ''
+                        previous_raw = ''
+                        # Now check if the current verse will fit, if it does not we have to start to process the verse
+                        # word by word.
+                        if self._text_fits_on_slide(html_line):
+                            previous_html = html_line + line_end
+                            previous_raw = line + line_end
+                            continue
+                # Figure out how many words of the line will fit on screen as the line will not fit as a whole.
+                raw_words = words_split(line)
+                html_words = list(map(expand_tags, raw_words))
+                previous_html, previous_raw = \
+                    self._binary_chop(formatted, previous_html, previous_raw, html_words, raw_words, ' ', line_end)
+            else:
+                previous_html += html_line + line_end
+                previous_raw += line + line_end
+        formatted.append(previous_raw)
+        return formatted
+
+    def _binary_chop(self, formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end):
+        """
+        This implements the binary chop algorithm for faster rendering. This algorithm works line based (line by line)
+        and word based (word by word). It is assumed that this method is **only** called, when the lines/words to be
+        rendered do **not** fit as a whole.
+
+        :param formatted: The list to append any slides.
+        :param previous_html: The html text which is know to fit on a slide, but is not yet added to the list of
+        slides. (unicode string)
+        :param previous_raw: The raw text (with formatting tags) which is know to fit on a slide, but is not yet added
+        to the list of slides. (unicode string)
+        :param html_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
+        The text contains html.
+        :param raw_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
+        The elements can contain formatting tags.
+        :param separator: The separator for the elements. For lines this is ``'<br>'`` and for words this is ``' '``.
+        :param line_end: The text added after each "element line". Either ``' '`` or ``'<br>``. This is needed for
+         bibles.
+        """
+        smallest_index = 0
+        highest_index = len(html_list) - 1
+        index = highest_index // 2
+        while True:
+            if not self._text_fits_on_slide(previous_html + separator.join(html_list[:index + 1]).strip()):
+                # We know that it does not fit, so change/calculate the new index and highest_index accordingly.
+                highest_index = index
+                index = index - (index - smallest_index) // 2
+            else:
+                smallest_index = index
+                index = index + (highest_index - index) // 2
+            # We found the number of words which will fit.
+            if smallest_index == index or highest_index == index:
+                index = smallest_index
+                text = previous_raw.rstrip('<br>') + separator.join(raw_list[:index + 1])
+                text, raw_tags, html_tags = get_start_tags(text)
+                formatted.append(text)
+                previous_html = ''
+                previous_raw = ''
+                # Stop here as the theme line count was requested.
+                if self.force_page:
+                    Registry().execute('theme_line_count', index + 1)
+                    break
+            else:
+                continue
+            # Check if the remaining elements fit on the slide.
+            if self._text_fits_on_slide(html_tags + separator.join(html_list[index + 1:]).strip()):
+                previous_html = html_tags + separator.join(html_list[index + 1:]).strip() + line_end
+                previous_raw = raw_tags + separator.join(raw_list[index + 1:]).strip() + line_end
+                break
+            else:
+                # The remaining elements do not fit, thus reset the indexes, create a new list and continue.
+                raw_list = raw_list[index + 1:]
+                raw_list[0] = raw_tags + raw_list[0]
+                html_list = html_list[index + 1:]
+                html_list[0] = html_tags + html_list[0]
+                smallest_index = 0
+                highest_index = len(html_list) - 1
+                index = highest_index // 2
+        return previous_html, previous_raw
+
+    def _text_fits_on_slide(self, text):
+        """
+        Checks if the given ``text`` fits on a slide. If it does ``True`` is returned, otherwise ``False``.
+
+        :param text:  The text to check. It may contain HTML tags.
+        """
+        self.web_frame.evaluateJavaScript('show_text("%s")' % text.replace('\\', '\\\\').replace('\"', '\\\"'))
+        return self.web_frame.contentsSize().height() <= self.empty_height
+
+
+def words_split(line):
+    """
+    Split the slide up by word so can wrap better
+
+    :param line: Line to be split
+    """
+    # this parse we are to be wordy
+    return re.split('\s+', line)
+
+
+def get_start_tags(raw_text):
+    """
+    Tests the given text for not closed formatting tags and returns a tuple consisting of three unicode strings::
+
+        ('{st}{r}Text text text{/r}{/st}', '{st}{r}', '<strong><span style="-webkit-text-fill-color:red">')
+
+    The first unicode string is the text, with correct closing tags. The second unicode string are OpenLP's opening
+    formatting tags and the third unicode string the html opening formatting tags.
+
+    :param raw_text: The text to test. The text must **not** contain html tags, only OpenLP formatting tags
+    are allowed::
+            {st}{r}Text text text
+    """
+    raw_tags = []
+    html_tags = []
+    for tag in FormattingTags.get_html_tags():
+        if tag['start tag'] == '{br}':
+            continue
+        if raw_text.count(tag['start tag']) != raw_text.count(tag['end tag']):
+            raw_tags.append((raw_text.find(tag['start tag']), tag['start tag'], tag['end tag']))
+            html_tags.append((raw_text.find(tag['start tag']), tag['start html']))
+    # Sort the lists, so that the tags which were opened first on the first slide (the text we are checking) will be
+    # opened first on the next slide as well.
+    raw_tags.sort(key=lambda tag: tag[0])
+    html_tags.sort(key=lambda tag: tag[0])
+    # Create a list with closing tags for the raw_text.
+    end_tags = []
+    start_tags = []
+    for tag in raw_tags:
+        start_tags.append(tag[1])
+        end_tags.append(tag[2])
+    end_tags.reverse()
+    # Remove the indexes.
+    html_tags = [tag[1] for tag in html_tags]
+    return raw_text + ''.join(end_tags), ''.join(start_tags), ''.join(html_tags)

=== added file 'openlp/core/lib/screen.py'
--- openlp/core/lib/screen.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/screen.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`screen` module provides management functionality for a machines'
+displays.
+"""
+
+import logging
+import copy
+
+from PyQt5 import QtCore
+
+from openlp.core.common import Registry, Settings, translate
+
+log = logging.getLogger(__name__)
+
+
+class ScreenList(object):
+    """
+    Wrapper to handle the parameters of the display screen.
+
+    To get access to the screen list call ``ScreenList()``.
+    """
+    log.info('Screen loaded')
+    __instance__ = None
+
+    def __new__(cls):
+        """
+        Re-implement __new__ to create a true singleton.
+        """
+        if not cls.__instance__:
+            cls.__instance__ = object.__new__(cls)
+        return cls.__instance__
+
+    @classmethod
+    def create(cls, desktop):
+        """
+        Initialise the screen list.
+
+        :param desktop:  A QDesktopWidget object.
+        """
+        screen_list = cls()
+        screen_list.desktop = desktop
+        screen_list.preview = None
+        screen_list.current = None
+        screen_list.override = None
+        screen_list.screen_list = []
+        screen_list.display_count = 0
+        screen_list.screen_count_changed()
+        screen_list.load_screen_settings()
+        desktop.resized.connect(screen_list.screen_resolution_changed)
+        desktop.screenCountChanged.connect(screen_list.screen_count_changed)
+        return screen_list
+
+    def screen_resolution_changed(self, number):
+        """
+        Called when the resolution of a screen has changed.
+
+        ``number``
+            The number of the screen, which size has changed.
+        """
+        log.info('screen_resolution_changed %d' % number)
+        for screen in self.screen_list:
+            if number == screen['number']:
+                new_screen = {
+                    'number': number,
+                    'size': self.desktop.screenGeometry(number),
+                    'primary': self.desktop.primaryScreen() == number
+                }
+                self.remove_screen(number)
+                self.add_screen(new_screen)
+                # The screen's default size is used, that is why we have to
+                # update the override screen.
+                if screen == self.override:
+                    self.override = copy.deepcopy(new_screen)
+                    self.set_override_display()
+                Registry().execute('config_screen_changed')
+                break
+
+    def screen_count_changed(self, changed_screen=-1):
+        """
+        Called when a screen has been added or removed.
+
+        ``changed_screen``
+            The screen's number which has been (un)plugged.
+        """
+        # Do not log at start up.
+        if changed_screen != -1:
+            log.info('screen_count_changed %d' % self.desktop.screenCount())
+        # Remove unplugged screens.
+        for screen in copy.deepcopy(self.screen_list):
+            if screen['number'] == self.desktop.screenCount():
+                self.remove_screen(screen['number'])
+        # Add new screens.
+        for number in range(self.desktop.screenCount()):
+            if not self.screen_exists(number):
+                self.add_screen({
+                    'number': number,
+                    'size': self.desktop.screenGeometry(number),
+                    'primary': (self.desktop.primaryScreen() == number)
+                })
+        # We do not want to send this message at start up.
+        if changed_screen != -1:
+            # Reload setting tabs to apply possible changes.
+            Registry().execute('config_screen_changed')
+
+    def get_screen_list(self):
+        """
+        Returns a list with the screens. This should only be used to display
+        available screens to the user::
+
+            ['Screen 1 (primary)', 'Screen 2']
+        """
+        screen_list = []
+        for screen in self.screen_list:
+            screen_name = '%s %d' % (translate('OpenLP.ScreenList', 'Screen'), screen['number'] + 1)
+            if screen['primary']:
+                screen_name = '%s (%s)' % (screen_name, translate('OpenLP.ScreenList', 'primary'))
+            screen_list.append(screen_name)
+        return screen_list
+
+    def add_screen(self, screen):
+        """
+        Add a screen to the list of known screens.
+
+        :param screen: A dict with the screen properties:
+
+            ::
+
+                {
+                    'primary': True,
+                    'number': 0,
+                    'size': PyQt5.QtCore.QRect(0, 0, 1024, 768)
+                }
+        """
+        log.info('Screen %d found with resolution %s' % (screen['number'], screen['size']))
+        if screen['primary']:
+            self.current = screen
+            self.override = copy.deepcopy(self.current)
+        self.screen_list.append(screen)
+        self.display_count += 1
+
+    def remove_screen(self, number):
+        """
+        Remove a screen from the list of known screens.
+
+        :param number: The screen number (int).
+        """
+        log.info('remove_screen %d' % number)
+        for screen in self.screen_list:
+            if screen['number'] == number:
+                self.screen_list.remove(screen)
+                self.display_count -= 1
+                break
+
+    def screen_exists(self, number):
+        """
+        Confirms a screen is known.
+
+        :param number: The screen number (int).
+        """
+        for screen in self.screen_list:
+            if screen['number'] == number:
+                return True
+        return False
+
+    def set_current_display(self, number):
+        """
+        Set up the current screen dimensions.
+
+        :param number: The screen number (int).
+        """
+        log.debug('set_current_display %s' % number)
+        if number + 1 > self.display_count:
+            self.current = self.screen_list[0]
+        else:
+            self.current = self.screen_list[number]
+            self.preview = copy.deepcopy(self.current)
+        self.override = copy.deepcopy(self.current)
+        if self.display_count == 1:
+            self.preview = self.screen_list[0]
+
+    def set_override_display(self):
+        """
+        Replace the current size with the override values, as the user wants to have their own screen attributes.
+        """
+        log.debug('set_override_display')
+        self.current = copy.deepcopy(self.override)
+        self.preview = copy.deepcopy(self.current)
+
+    def reset_current_display(self):
+        """
+        Replace the current values with the correct values, as the user wants to use the correct screen attributes.
+        """
+        log.debug('reset_current_display')
+        self.set_current_display(self.current['number'])
+
+    def which_screen(self, window):
+        """
+        Return the screen number that the centre of the passed window is in.
+
+        :param window: A QWidget we are finding the location of.
+        """
+        x = window.x() + (window.width() // 2)
+        y = window.y() + (window.height() // 2)
+        for screen in self.screen_list:
+            size = screen['size']
+            if x >= size.x() and x <= (size.x() + size.width()) and y >= size.y() and y <= (size.y() + size.height()):
+                return screen['number']
+
+    def load_screen_settings(self):
+        """
+        Loads the screen size and the monitor number from the settings.
+        """
+        # Add the screen settings to the settings dict. This has to be done here due to cyclic dependency.
+        # Do not do this anywhere else.
+        screen_settings = {
+            'core/x position': self.current['size'].x(),
+            'core/y position': self.current['size'].y(),
+            'core/monitor': self.display_count - 1,
+            'core/height': self.current['size'].height(),
+            'core/width': self.current['size'].width()
+        }
+        Settings.extend_default_settings(screen_settings)
+        settings = Settings()
+        settings.beginGroup('core')
+        monitor = settings.value('monitor')
+        self.set_current_display(monitor)
+        self.display = settings.value('display on monitor')
+        override_display = settings.value('override position')
+        x = settings.value('x position')
+        y = settings.value('y position')
+        width = settings.value('width')
+        height = settings.value('height')
+        self.override['size'] = QtCore.QRect(x, y, width, height)
+        self.override['primary'] = False
+        settings.endGroup()
+        if override_display:
+            self.set_override_display()
+        else:
+            self.reset_current_display()

=== added file 'openlp/core/lib/searchedit.py'
--- openlp/core/lib/searchedit.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/searchedit.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,179 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+
+import logging
+
+from PyQt5 import QtCore, QtWidgets
+
+from openlp.core.lib import build_icon
+from openlp.core.lib.ui import create_widget_action
+
+log = logging.getLogger(__name__)
+
+
+class SearchEdit(QtWidgets.QLineEdit):
+    """
+    This is a specialised QLineEdit with a "clear" button inside for searches.
+    """
+    searchTypeChanged = QtCore.pyqtSignal(QtCore.QVariant)
+    cleared = QtCore.pyqtSignal()
+
+    def __init__(self, parent):
+        """
+        Constructor.
+        """
+        super(SearchEdit, self).__init__(parent)
+        self._current_search_type = -1
+        self.clear_button = QtWidgets.QToolButton(self)
+        self.clear_button.setIcon(build_icon(':/system/clear_shortcut.png'))
+        self.clear_button.setCursor(QtCore.Qt.ArrowCursor)
+        self.clear_button.setStyleSheet('QToolButton { border: none; padding: 0px; }')
+        self.clear_button.resize(18, 18)
+        self.clear_button.hide()
+        self.clear_button.clicked.connect(self._on_clear_button_clicked)
+        self.textChanged.connect(self._on_search_edit_text_changed)
+        self._update_style_sheet()
+        self.setAcceptDrops(False)
+
+    def _update_style_sheet(self):
+        """
+        Internal method to update the stylesheet depending on which widgets are available and visible.
+        """
+        frame_width = self.style().pixelMetric(QtWidgets.QStyle.PM_DefaultFrameWidth)
+        right_padding = self.clear_button.width() + frame_width
+        if hasattr(self, 'menu_button'):
+            left_padding = self.menu_button.width()
+            stylesheet = 'QLineEdit { padding-left: %spx; padding-right: %spx; } ' % (left_padding, right_padding)
+        else:
+            stylesheet = 'QLineEdit { padding-right: %spx; } ' % right_padding
+        self.setStyleSheet(stylesheet)
+        msz = self.minimumSizeHint()
+        self.setMinimumSize(max(msz.width(), self.clear_button.width() + (frame_width * 2) + 2),
+                            max(msz.height(), self.clear_button.height() + (frame_width * 2) + 2))
+
+    def resizeEvent(self, event):
+        """
+        Reimplemented method to react to resizing of the widget.
+
+        :param event: The event that happened.
+        """
+        size = self.clear_button.size()
+        frame_width = self.style().pixelMetric(QtWidgets.QStyle.PM_DefaultFrameWidth)
+        self.clear_button.move(self.rect().right() - frame_width - size.width(),
+                               (self.rect().bottom() + 1 - size.height()) // 2)
+        if hasattr(self, 'menu_button'):
+            size = self.menu_button.size()
+            self.menu_button.move(self.rect().left() + frame_width + 2, (self.rect().bottom() + 1 - size.height()) // 2)
+
+    def current_search_type(self):
+        """
+        Readonly property to return the current search type.
+        """
+        return self._current_search_type
+
+    def set_current_search_type(self, identifier):
+        """
+        Set a new current search type.
+
+        :param identifier: The search type identifier (int).
+        """
+        menu = self.menu_button.menu()
+        for action in menu.actions():
+            if identifier == action.data():
+                # setPlaceholderText has been implemented in Qt 4.7 and in at least PyQt 4.9 (I am not sure, if it was
+                # implemented in PyQt 4.8).
+                try:
+                    self.setPlaceholderText(action.placeholder_text)
+                except AttributeError:
+                    pass
+                self.menu_button.setDefaultAction(action)
+                self._current_search_type = identifier
+                self.searchTypeChanged.emit(identifier)
+                return True
+
+    def set_search_types(self, items):
+        """
+        A list of tuples to be used in the search type menu. The first item in the list will be preselected as the
+        default.
+
+         :param items:     The list of tuples to use. The tuples should contain an integer identifier, an icon (QIcon
+             instance or string) and a title for the item in the menu. In short, they should look like this::
+
+                    (<identifier>, <icon>, <title>, <place holder text>)
+
+                For instance::
+
+                    (1, <QIcon instance>, "Titles", "Search Song Titles...")
+
+                Or::
+
+                    (2, ":/songs/authors.png", "Authors", "Search Authors...")
+        """
+        menu = QtWidgets.QMenu(self)
+        first = None
+        for identifier, icon, title, placeholder in items:
+            action = create_widget_action(
+                menu, text=title, icon=icon, data=identifier, triggers=self._on_menu_action_triggered)
+            action.placeholder_text = placeholder
+            if first is None:
+                first = action
+                self._current_search_type = identifier
+        if not hasattr(self, 'menu_button'):
+            self.menu_button = QtWidgets.QToolButton(self)
+            self.menu_button.setIcon(build_icon(':/system/clear_shortcut.png'))
+            self.menu_button.setCursor(QtCore.Qt.ArrowCursor)
+            self.menu_button.setPopupMode(QtWidgets.QToolButton.InstantPopup)
+            self.menu_button.setStyleSheet('QToolButton { border: none; padding: 0px 10px 0px 0px; }')
+            self.menu_button.resize(QtCore.QSize(28, 18))
+        self.menu_button.setMenu(menu)
+        self.menu_button.setDefaultAction(first)
+        self.menu_button.show()
+        self._update_style_sheet()
+
+    def _on_search_edit_text_changed(self, text):
+        """
+        Internally implemented slot to react to when the text in the line edit has changed so that we can show or hide
+        the clear button.
+
+        :param text: A :class:`~PyQt5.QtCore.QString` instance which represents the text in the line edit.
+        """
+        self.clear_button.setVisible(bool(text))
+
+    def _on_clear_button_clicked(self):
+        """
+        Internally implemented slot to react to the clear button being clicked to clear the line edit. Once it has
+        cleared the line edit, it emits the ``cleared()`` signal so that an application can react to the clearing of the
+        line edit.
+        """
+        self.clear()
+        self.cleared.emit()
+
+    def _on_menu_action_triggered(self):
+        """
+        Internally implemented slot to react to the select of one of the search types in the menu. Once it has set the
+        correct action on the button, and set the current search type (using the list of identifiers provided by the
+        developer), the ``searchTypeChanged(int)`` signal is emitted with the identifier.
+        """
+        for action in self.menu_button.menu().actions():
+            # Why is this needed?
+            action.setChecked(False)
+        self.set_current_search_type(self.sender().data())

=== added file 'openlp/core/lib/serviceitem.py'
--- openlp/core/lib/serviceitem.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/serviceitem.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,672 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`serviceitem` provides the service item functionality including the
+type and capability of an item.
+"""
+
+import datetime
+import html
+import logging
+import os
+import uuid
+import ntpath
+
+from PyQt5 import QtGui
+
+from openlp.core.common import RegistryProperties, Settings, translate, AppLocation, md5_hash
+from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, create_thumb
+
+log = logging.getLogger(__name__)
+
+
+class ServiceItemType(object):
+    """
+    Defines the type of service item
+    """
+    Text = 1
+    Image = 2
+    Command = 3
+
+
+class ItemCapabilities(object):
+    """
+    Provides an enumeration of a service item's capabilities
+
+    ``CanPreview``
+            The capability to allow the ServiceManager to add to the preview tab when making the previous item live.
+
+    ``CanEdit``
+            The capability to allow the ServiceManager to allow the item to be edited
+
+    ``CanMaintain``
+            The capability to allow the ServiceManager to allow the item to be reordered.
+
+    ``RequiresMedia``
+            Determines is the service_item needs a Media Player
+
+    ``CanLoop``
+            The capability to allow the SlideController to allow the loop processing.
+
+    ``CanAppend``
+            The capability to allow the ServiceManager to add leaves to the
+            item
+
+    ``NoLineBreaks``
+            The capability to remove lines breaks in the renderer
+
+    ``OnLoadUpdate``
+            The capability to update MediaManager when a service Item is loaded.
+
+    ``AddIfNewItem``
+            Not Used
+
+    ``ProvidesOwnDisplay``
+            The capability to tell the SlideController the service Item has a different display.
+
+    ``HasDetailedTitleDisplay``
+            Being Removed and decommissioned.
+
+    ``HasVariableStartTime``
+            The capability to tell the ServiceManager that a change to start time is possible.
+
+    ``CanSoftBreak``
+            The capability to tell the renderer that Soft Break is allowed
+
+    ``CanWordSplit``
+            The capability to tell the renderer that it can split words is
+            allowed
+
+    ``HasBackgroundAudio``
+            That a audio file is present with the text.
+
+    ``CanAutoStartForLive``
+            The capability to ignore the do not play if display blank flag.
+
+    ``CanEditTitle``
+            The capability to edit the title of the item
+
+    ``IsOptical``
+            Determines is the service_item is based on an optical device
+
+    ``HasDisplayTitle``
+            The item contains 'displaytitle' on every frame which should be
+            preferred over 'title' when displaying the item
+
+    ``HasNotes``
+            The item contains 'notes'
+
+    ``HasThumbnails``
+            The item has related thumbnails available
+
+    """
+    CanPreview = 1
+    CanEdit = 2
+    CanMaintain = 3
+    RequiresMedia = 4
+    CanLoop = 5
+    CanAppend = 6
+    NoLineBreaks = 7
+    OnLoadUpdate = 8
+    AddIfNewItem = 9
+    ProvidesOwnDisplay = 10
+    # HasDetailedTitleDisplay = 11
+    HasVariableStartTime = 12
+    CanSoftBreak = 13
+    CanWordSplit = 14
+    HasBackgroundAudio = 15
+    CanAutoStartForLive = 16
+    CanEditTitle = 17
+    IsOptical = 18
+    HasDisplayTitle = 19
+    HasNotes = 20
+    HasThumbnails = 21
+
+
+class ServiceItem(RegistryProperties):
+    """
+    The service item is a base class for the plugins to use to interact with
+    the service manager, the slide controller, and the projection screen
+    compositor.
+    """
+    log.info('Service Item created')
+
+    def __init__(self, plugin=None):
+        """
+        Set up the service item.
+
+        :param plugin: The plugin that this service item belongs to.
+        """
+        if plugin:
+            self.name = plugin.name
+        self.title = ''
+        self.processor = None
+        self.audit = ''
+        self.items = []
+        self.iconic_representation = None
+        self.raw_footer = []
+        self.foot_text = ''
+        self.theme = None
+        self.service_item_type = None
+        self._raw_frames = []
+        self._display_frames = []
+        self.unique_identifier = 0
+        self.notes = ''
+        self.from_plugin = False
+        self.capabilities = []
+        self.is_valid = True
+        self.icon = None
+        self.theme_data = None
+        self.main = None
+        self.footer = None
+        self.bg_image_bytes = None
+        self.search_string = ''
+        self.data_string = ''
+        self.edit_id = None
+        self.xml_version = None
+        self.start_time = 0
+        self.end_time = 0
+        self.media_length = 0
+        self.from_service = False
+        self.image_border = '#000000'
+        self.background_audio = []
+        self.theme_overwritten = False
+        self.temporary_edit = False
+        self.auto_play_slides_once = False
+        self.auto_play_slides_loop = False
+        self.timed_slide_interval = 0
+        self.will_auto_start = False
+        self.has_original_files = True
+        self._new_item()
+
+    def _new_item(self):
+        """
+        Method to set the internal id of the item. This is used to compare service items to see if they are the same.
+        """
+        self.unique_identifier = str(uuid.uuid1())
+        self.validate_item()
+
+    def add_capability(self, capability):
+        """
+        Add an ItemCapability to a ServiceItem
+
+        :param capability: The capability to add
+        """
+        self.capabilities.append(capability)
+
+    def is_capable(self, capability):
+        """
+        Tell the caller if a ServiceItem has a capability
+
+        :param capability: The capability to test for
+        """
+        return capability in self.capabilities
+
+    def add_icon(self, icon):
+        """
+        Add an icon to the service item. This is used when displaying the service item in the service manager.
+
+        :param icon: A string to an icon in the resources or on disk.
+        """
+        self.icon = icon
+        self.iconic_representation = build_icon(icon)
+
+    def render(self, provides_own_theme_data=False):
+        """
+        The render method is what generates the frames for the screen and obtains the display information from the
+        renderer. At this point all slides are built for the given display size.
+
+        :param provides_own_theme_data: This switch disables the usage of the item's theme. However, this is
+            disabled by default. If this is used, it has to be taken care, that
+            the renderer knows the correct theme data. However, this is needed
+            for the theme manager.
+        """
+        log.debug('Render called')
+        self._display_frames = []
+        self.bg_image_bytes = None
+        if not provides_own_theme_data:
+            self.renderer.set_item_theme(self.theme)
+            self.theme_data, self.main, self.footer = self.renderer.pre_render()
+        if self.service_item_type == ServiceItemType.Text:
+            log.debug('Formatting slides: %s' % self.title)
+            # Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
+            # the dict instead of rendering them again.
+            previous_pages = {}
+            for slide in self._raw_frames:
+                verse_tag = slide['verseTag']
+                if verse_tag in previous_pages and previous_pages[verse_tag][0] == slide['raw_slide']:
+                    pages = previous_pages[verse_tag][1]
+                else:
+                    pages = self.renderer.format_slide(slide['raw_slide'], self)
+                    previous_pages[verse_tag] = (slide['raw_slide'], pages)
+                for page in pages:
+                    page = page.replace('<br>', '{br}')
+                    html_data = expand_tags(html.escape(page.rstrip()))
+                    self._display_frames.append({
+                        'title': clean_tags(page),
+                        'text': clean_tags(page.rstrip()),
+                        'html': html_data.replace('&amp;nbsp;', '&nbsp;'),
+                        'verseTag': verse_tag
+                    })
+        elif self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command:
+            pass
+        else:
+            log.error('Invalid value renderer: %s' % self.service_item_type)
+        self.title = clean_tags(self.title)
+        # The footer should never be None, but to be compatible with a few
+        # nightly builds between 1.9.4 and 1.9.5, we have to correct this to
+        # avoid tracebacks.
+        if self.raw_footer is None:
+            self.raw_footer = []
+        self.foot_text = '<br>'.join([_f for _f in self.raw_footer if _f])
+
+    def add_from_image(self, path, title, background=None, thumbnail=None):
+        """
+        Add an image slide to the service item.
+
+        :param path: The directory in which the image file is located.
+        :param title: A title for the slide in the service item.
+        :param background:
+        :param thumbnail: Optional alternative thumbnail, used for remote thumbnails.
+        """
+        if background:
+            self.image_border = background
+        self.service_item_type = ServiceItemType.Image
+        if not thumbnail:
+            self._raw_frames.append({'title': title, 'path': path})
+        else:
+            self._raw_frames.append({'title': title, 'path': path, 'image': thumbnail})
+        self.image_manager.add_image(path, ImageSource.ImagePlugin, self.image_border)
+        self._new_item()
+
+    def add_from_text(self, raw_slide, verse_tag=None):
+        """
+        Add a text slide to the service item.
+
+        :param raw_slide: The raw text of the slide.
+        :param verse_tag:
+        """
+        if verse_tag:
+            verse_tag = verse_tag.upper()
+        self.service_item_type = ServiceItemType.Text
+        title = raw_slide[:30].split('\n')[0]
+        self._raw_frames.append({'title': title, 'raw_slide': raw_slide, 'verseTag': verse_tag})
+        self._new_item()
+
+    def add_from_command(self, path, file_name, image, display_title=None, notes=None):
+        """
+        Add a slide from a command.
+
+        :param path: The title of the slide in the service item.
+        :param file_name: The title of the slide in the service item.
+        :param image: The command of/for the slide.
+        :param display_title: Title to show in gui/webinterface, optional.
+        :param notes: Notes to show in the webinteface, optional.
+        """
+        self.service_item_type = ServiceItemType.Command
+        # If the item should have a display title but this frame doesn't have one, we make one up
+        if self.is_capable(ItemCapabilities.HasDisplayTitle) and not display_title:
+            display_title = translate('OpenLP.ServiceItem', '[slide %d]') % (len(self._raw_frames) + 1)
+        # Update image path to match servicemanager location if file was loaded from service
+        if image and not self.has_original_files and self.name == 'presentations':
+            file_location = os.path.join(path, file_name)
+            file_location_hash = md5_hash(file_location.encode('utf-8'))
+            image = os.path.join(AppLocation.get_section_data_path(self.name), 'thumbnails',
+                                 file_location_hash, ntpath.basename(image))
+        self._raw_frames.append({'title': file_name, 'image': image, 'path': path,
+                                 'display_title': display_title, 'notes': notes})
+        self._new_item()
+
+    def get_service_repr(self, lite_save):
+        """
+        This method returns some text which can be saved into the service file to represent this item.
+        """
+        service_header = {
+            'name': self.name,
+            'plugin': self.name,
+            'theme': self.theme,
+            'title': self.title,
+            'icon': self.icon,
+            'footer': self.raw_footer,
+            'type': self.service_item_type,
+            'audit': self.audit,
+            'notes': self.notes,
+            'from_plugin': self.from_plugin,
+            'capabilities': self.capabilities,
+            'search': self.search_string,
+            'data': self.data_string,
+            'xml_version': self.xml_version,
+            'auto_play_slides_once': self.auto_play_slides_once,
+            'auto_play_slides_loop': self.auto_play_slides_loop,
+            'timed_slide_interval': self.timed_slide_interval,
+            'start_time': self.start_time,
+            'end_time': self.end_time,
+            'media_length': self.media_length,
+            'background_audio': self.background_audio,
+            'theme_overwritten': self.theme_overwritten,
+            'will_auto_start': self.will_auto_start,
+            'processor': self.processor
+        }
+        service_data = []
+        if self.service_item_type == ServiceItemType.Text:
+            service_data = [slide for slide in self._raw_frames]
+        elif self.service_item_type == ServiceItemType.Image:
+            if lite_save:
+                for slide in self._raw_frames:
+                    service_data.append({'title': slide['title'], 'path': slide['path']})
+            else:
+                service_data = [slide['title'] for slide in self._raw_frames]
+        elif self.service_item_type == ServiceItemType.Command:
+            for slide in self._raw_frames:
+                service_data.append({'title': slide['title'], 'image': slide['image'], 'path': slide['path'],
+                                     'display_title': slide['display_title'], 'notes': slide['notes']})
+        return {'header': service_header, 'data': service_data}
+
+    def set_from_service(self, service_item, path=None):
+        """
+        This method takes a service item from a saved service file (passed from the ServiceManager) and extracts the
+        data actually required.
+
+        :param service_item: The item to extract data from.
+        :param path: Defaults to *None*. This is the service manager path for things which have their files saved
+            with them or None when the saved service is lite and the original file paths need to be preserved.
+        """
+        log.debug('set_from_service called with path %s' % path)
+        header = service_item['serviceitem']['header']
+        self.title = header['title']
+        self.name = header['name']
+        self.service_item_type = header['type']
+        self.theme = header['theme']
+        self.add_icon(header['icon'])
+        self.raw_footer = header['footer']
+        self.audit = header['audit']
+        self.notes = header['notes']
+        self.from_plugin = header['from_plugin']
+        self.capabilities = header['capabilities']
+        # Added later so may not be present in older services.
+        self.search_string = header.get('search', '')
+        self.data_string = header.get('data', '')
+        self.xml_version = header.get('xml_version')
+        self.start_time = header.get('start_time', 0)
+        self.end_time = header.get('end_time', 0)
+        self.media_length = header.get('media_length', 0)
+        self.auto_play_slides_once = header.get('auto_play_slides_once', False)
+        self.auto_play_slides_loop = header.get('auto_play_slides_loop', False)
+        self.timed_slide_interval = header.get('timed_slide_interval', 0)
+        self.will_auto_start = header.get('will_auto_start', False)
+        self.processor = header.get('processor', None)
+        self.has_original_files = True
+        if 'background_audio' in header:
+            self.background_audio = []
+            for filename in header['background_audio']:
+                # Give them real file paths.
+                filepath = filename
+                if path:
+                    # Windows can handle both forward and backward slashes, so we use ntpath to get the basename
+                    filepath = os.path.join(path, ntpath.basename(filename))
+                self.background_audio.append(filepath)
+        self.theme_overwritten = header.get('theme_overwritten', False)
+        if self.service_item_type == ServiceItemType.Text:
+            for slide in service_item['serviceitem']['data']:
+                self._raw_frames.append(slide)
+        elif self.service_item_type == ServiceItemType.Image:
+            settings_section = service_item['serviceitem']['header']['name']
+            background = QtGui.QColor(Settings().value(settings_section + '/background color'))
+            if path:
+                self.has_original_files = False
+                for text_image in service_item['serviceitem']['data']:
+                    filename = os.path.join(path, text_image)
+                    self.add_from_image(filename, text_image, background)
+            else:
+                for text_image in service_item['serviceitem']['data']:
+                    self.add_from_image(text_image['path'], text_image['title'], background)
+        elif self.service_item_type == ServiceItemType.Command:
+            for text_image in service_item['serviceitem']['data']:
+                if not self.title:
+                    self.title = text_image['title']
+                if self.is_capable(ItemCapabilities.IsOptical):
+                    self.has_original_files = False
+                    self.add_from_command(text_image['path'], text_image['title'], text_image['image'])
+                elif path:
+                    self.has_original_files = False
+                    self.add_from_command(path, text_image['title'], text_image['image'],
+                                          text_image.get('display_title', ''), text_image.get('notes', ''))
+                else:
+                    self.add_from_command(text_image['path'], text_image['title'], text_image['image'])
+        self._new_item()
+
+    def get_display_title(self):
+        """
+        Returns the title of the service item.
+        """
+        if self.is_text() or self.is_capable(ItemCapabilities.IsOptical) \
+                or self.is_capable(ItemCapabilities.CanEditTitle):
+            return self.title
+        else:
+            if len(self._raw_frames) > 1:
+                return self.title
+            else:
+                return self._raw_frames[0]['title']
+
+    def merge(self, other):
+        """
+        Updates the unique_identifier with the value from the original one
+        The unique_identifier is unique for a given service item but this allows one to replace an original version.
+
+        :param other: The service item to be merged with
+        """
+        self.unique_identifier = other.unique_identifier
+        self.notes = other.notes
+        self.temporary_edit = other.temporary_edit
+        # Copy theme over if present.
+        if other.theme is not None:
+            self.theme = other.theme
+            self._new_item()
+        self.render()
+        if self.is_capable(ItemCapabilities.HasBackgroundAudio):
+            log.debug(self.background_audio)
+
+    def __eq__(self, other):
+        """
+        Confirms the service items are for the same instance
+        """
+        if not other:
+            return False
+        return self.unique_identifier == other.unique_identifier
+
+    def __ne__(self, other):
+        """
+        Confirms the service items are not for the same instance
+        """
+        return self.unique_identifier != other.unique_identifier
+
+    def __hash__(self):
+        """
+        Return the hash for the service item.
+        """
+        return self.unique_identifier
+
+    def is_media(self):
+        """
+        Confirms if the ServiceItem is media
+        """
+        return ItemCapabilities.RequiresMedia in self.capabilities
+
+    def is_command(self):
+        """
+        Confirms if the ServiceItem is a command
+        """
+        return self.service_item_type == ServiceItemType.Command
+
+    def is_image(self):
+        """
+        Confirms if the ServiceItem is an image
+        """
+        return self.service_item_type == ServiceItemType.Image
+
+    def uses_file(self):
+        """
+        Confirms if the ServiceItem uses a file
+        """
+        return self.service_item_type == ServiceItemType.Image or \
+            (self.service_item_type == ServiceItemType.Command and not self.is_capable(ItemCapabilities.IsOptical))
+
+    def is_text(self):
+        """
+        Confirms if the ServiceItem is text
+        """
+        return self.service_item_type == ServiceItemType.Text
+
+    def set_media_length(self, length):
+        """
+        Stores the media length of the item
+
+        :param length: The length of the media item
+        """
+        self.media_length = length
+        if length > 0:
+            self.add_capability(ItemCapabilities.HasVariableStartTime)
+
+    def get_frames(self):
+        """
+        Returns the frames for the ServiceItem
+        """
+        if self.service_item_type == ServiceItemType.Text:
+            return self._display_frames
+        else:
+            return self._raw_frames
+
+    def get_rendered_frame(self, row):
+        """
+        Returns the correct frame for a given list and renders it if required.
+
+        :param row: The service item slide to be returned
+        """
+        if self.service_item_type == ServiceItemType.Text:
+            return self._display_frames[row]['html'].split('\n')[0]
+        elif self.service_item_type == ServiceItemType.Image:
+            return self._raw_frames[row]['path']
+        else:
+            return self._raw_frames[row]['image']
+
+    def get_frame_title(self, row=0):
+        """
+        Returns the title of the raw frame
+        """
+        try:
+            return self._raw_frames[row]['title']
+        except IndexError:
+            return ''
+
+    def get_frame_path(self, row=0, frame=None):
+        """
+        Returns the path of the raw frame
+        """
+        if not frame:
+            try:
+                frame = self._raw_frames[row]
+            except IndexError:
+                return ''
+        if self.is_image() or self.is_capable(ItemCapabilities.IsOptical):
+            path_from = frame['path']
+        else:
+            path_from = os.path.join(frame['path'], frame['title'])
+        return path_from
+
+    def remove_frame(self, frame):
+        """
+        Remove the specified frame from the item
+        """
+        if frame in self._raw_frames:
+            self._raw_frames.remove(frame)
+
+    def get_media_time(self):
+        """
+        Returns the start and finish time for a media item
+        """
+        start = None
+        end = None
+        if self.start_time != 0:
+            start = translate('OpenLP.ServiceItem', '<strong>Start</strong>: %s') % \
+                str(datetime.timedelta(seconds=self.start_time))
+        if self.media_length != 0:
+            end = translate('OpenLP.ServiceItem', '<strong>Length</strong>: %s') % \
+                str(datetime.timedelta(seconds=self.media_length))
+        if not start and not end:
+            return ''
+        elif start and not end:
+            return start
+        elif not start and end:
+            return end
+        else:
+            return '%s <br>%s' % (start, end)
+
+    def update_theme(self, theme):
+        """
+        updates the theme in the service item
+
+        :param theme: The new theme to be replaced in the service item
+        """
+        self.theme_overwritten = (theme is None)
+        self.theme = theme
+        self._new_item()
+        self.render()
+
+    def remove_invalid_frames(self, invalid_paths=None):
+        """
+        Remove invalid frames, such as ones where the file no longer exists.
+        """
+        if self.uses_file():
+            for frame in self.get_frames():
+                if self.get_frame_path(frame=frame) in invalid_paths:
+                    self.remove_frame(frame)
+
+    def missing_frames(self):
+        """
+        Returns if there are any frames in the service item
+        """
+        return not bool(self._raw_frames)
+
+    def validate_item(self, suffix_list=None):
+        """
+        Validates a service item to make sure it is valid
+        """
+        self.is_valid = True
+        for frame in self._raw_frames:
+            if self.is_image() and not os.path.exists(frame['path']):
+                self.is_valid = False
+                break
+            elif self.is_command():
+                if self.is_capable(ItemCapabilities.IsOptical):
+                    if not os.path.exists(frame['title']):
+                        self.is_valid = False
+                        break
+                else:
+                    file_name = os.path.join(frame['path'], frame['title'])
+                    if not os.path.exists(file_name):
+                        self.is_valid = False
+                        break
+                    if suffix_list and not self.is_text():
+                        file_suffix = frame['title'].split('.')[-1]
+                        if file_suffix.lower() not in suffix_list:
+                            self.is_valid = False
+                            break

=== added file 'openlp/core/lib/settingstab.py'
--- openlp/core/lib/settingstab.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/settingstab.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,138 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`~openlp.core.lib.settingstab` module contains the base SettingsTab class which plugins use for adding their
+own tab to the settings dialog.
+"""
+
+
+from PyQt5 import QtWidgets
+
+
+from openlp.core.common import RegistryProperties
+
+
+class SettingsTab(QtWidgets.QWidget, RegistryProperties):
+    """
+    SettingsTab is a helper widget for plugins to define Tabs for the settings dialog.
+    """
+    def __init__(self, parent, title, visible_title=None, icon_path=None):
+        """
+        Constructor to create the Settings tab item.
+
+        :param parent:
+        :param title: The title of the tab, which is used internally for the tab handling.
+        :param visible_title: The title of the tab, which is usually displayed on the tab.
+        :param icon_path:
+        """
+        super(SettingsTab, self).__init__(parent)
+        self.tab_title = title
+        self.tab_title_visible = visible_title
+        self.settings_section = self.tab_title.lower()
+        self.tab_visited = False
+        if icon_path:
+            self.icon_path = icon_path
+        self._setup()
+
+    def _setup(self):
+        """
+        Run some initial setup. This method is separate from __init__ in order to mock it out in tests.
+        """
+        self.setupUi()
+        self.retranslateUi()
+        self.initialise()
+        self.load()
+
+    def setupUi(self):
+        """
+        Setup the tab's interface.
+        """
+        self.tab_layout = QtWidgets.QHBoxLayout(self)
+        self.tab_layout.setObjectName('tab_layout')
+        self.left_column = QtWidgets.QWidget(self)
+        self.left_column.setObjectName('left_column')
+        self.left_layout = QtWidgets.QVBoxLayout(self.left_column)
+        self.left_layout.setContentsMargins(0, 0, 0, 0)
+        self.left_layout.setObjectName('left_layout')
+        self.tab_layout.addWidget(self.left_column)
+        self.right_column = QtWidgets.QWidget(self)
+        self.right_column.setObjectName('right_column')
+        self.right_layout = QtWidgets.QVBoxLayout(self.right_column)
+        self.right_layout.setContentsMargins(0, 0, 0, 0)
+        self.right_layout.setObjectName('right_layout')
+        self.tab_layout.addWidget(self.right_column)
+
+    def resizeEvent(self, event=None):
+        """
+        Resize the sides in two equal halves if the layout allows this.
+        """
+        if event:
+            QtWidgets.QWidget.resizeEvent(self, event)
+        width = self.width() - self.tab_layout.spacing() - \
+            self.tab_layout.contentsMargins().left() - self.tab_layout.contentsMargins().right()
+        left_width = min(width - self.right_column.minimumSizeHint().width(), width // 2)
+        left_width = max(left_width, self.left_column.minimumSizeHint().width())
+        self.left_column.setFixedWidth(left_width)
+
+    def retranslateUi(self):
+        """
+        Setup the interface translation strings.
+        """
+        pass
+
+    def initialise(self):
+        """
+        Do any extra initialisation here.
+        """
+        pass
+
+    def load(self):
+        """
+        Load settings from disk.
+        """
+        pass
+
+    def save(self):
+        """
+        Save settings to disk.
+        """
+        pass
+
+    def cancel(self):
+        """
+        Reset any settings if cancel triggered
+        """
+        self.load()
+
+    def post_set_up(self, post_update=False):
+        """
+        Changes which need to be made after setup of application
+
+        :param post_update: Indicates if called before or after updates.
+        """
+        pass
+
+    def tab_visible(self):
+        """
+        Tab has just been made visible to the user
+        """
+        self.tab_visited = True

=== added file 'openlp/core/lib/spelltextedit.py'
--- openlp/core/lib/spelltextedit.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/spelltextedit.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,203 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`~openlp.core.lib.spelltextedit` module contains a classes to add spell checking to an edit widget.
+"""
+
+import logging
+import re
+
+try:
+    import enchant
+    from enchant import DictNotFoundError
+    from enchant.errors import Error
+    ENCHANT_AVAILABLE = True
+except ImportError:
+    ENCHANT_AVAILABLE = False
+
+# based on code from http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.lib import translate, FormattingTags
+from openlp.core.lib.ui import create_action
+
+log = logging.getLogger(__name__)
+
+
+class SpellTextEdit(QtWidgets.QPlainTextEdit):
+    """
+    Spell checking widget based on QPlanTextEdit.
+    """
+    def __init__(self, parent=None, formatting_tags_allowed=True):
+        """
+        Constructor.
+        """
+        global ENCHANT_AVAILABLE
+        super(SpellTextEdit, self).__init__(parent)
+        self.formatting_tags_allowed = formatting_tags_allowed
+        # Default dictionary based on the current locale.
+        if ENCHANT_AVAILABLE:
+            try:
+                self.dictionary = enchant.Dict()
+                self.highlighter = Highlighter(self.document())
+                self.highlighter.spelling_dictionary = self.dictionary
+            except (Error, DictNotFoundError):
+                ENCHANT_AVAILABLE = False
+                log.debug('Could not load default dictionary')
+
+    def mousePressEvent(self, event):
+        """
+        Handle mouse clicks within the text edit region.
+        """
+        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)
+        QtWidgets.QPlainTextEdit.mousePressEvent(self, event)
+
+    def contextMenuEvent(self, event):
+        """
+        Provide the context menu for the text edit region.
+        """
+        popup_menu = self.createStandardContextMenu()
+        # Select the word under the cursor.
+        cursor = self.textCursor()
+        # only select text if not already selected
+        if not cursor.hasSelection():
+            cursor.select(QtGui.QTextCursor.WordUnderCursor)
+        self.setTextCursor(cursor)
+        # Add menu with available languages.
+        if ENCHANT_AVAILABLE:
+            lang_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Language:'))
+            for lang in enchant.list_languages():
+                action = create_action(lang_menu, lang, text=lang, checked=lang == self.dictionary.tag)
+                lang_menu.addAction(action)
+            popup_menu.insertSeparator(popup_menu.actions()[0])
+            popup_menu.insertMenu(popup_menu.actions()[0], lang_menu)
+            lang_menu.triggered.connect(self.set_language)
+        # Check if the selected word is misspelled and offer spelling suggestions if it is.
+        if ENCHANT_AVAILABLE and self.textCursor().hasSelection():
+            text = self.textCursor().selectedText()
+            if not self.dictionary.check(text):
+                spell_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Spelling Suggestions'))
+                for word in self.dictionary.suggest(text):
+                    action = SpellAction(word, spell_menu)
+                    action.correct.connect(self.correct_word)
+                    spell_menu.addAction(action)
+                # Only add the spelling suggests to the menu if there are suggestions.
+                if spell_menu.actions():
+                    popup_menu.insertMenu(popup_menu.actions()[0], spell_menu)
+        tag_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Formatting Tags'))
+        if self.formatting_tags_allowed:
+            for html in FormattingTags.get_html_tags():
+                action = SpellAction(html['desc'], tag_menu)
+                action.correct.connect(self.html_tag)
+                tag_menu.addAction(action)
+            popup_menu.insertSeparator(popup_menu.actions()[0])
+            popup_menu.insertMenu(popup_menu.actions()[0], tag_menu)
+        popup_menu.exec(event.globalPos())
+
+    def set_language(self, action):
+        """
+        Changes the language for this spelltextedit.
+
+        :param action: The action.
+        """
+        self.dictionary = enchant.Dict(action.text())
+        self.highlighter.spelling_dictionary = self.dictionary
+        self.highlighter.highlightBlock(self.toPlainText())
+        self.highlighter.rehighlight()
+
+    def correct_word(self, word):
+        """
+        Replaces the selected text with word.
+        """
+        cursor = self.textCursor()
+        cursor.beginEditBlock()
+        cursor.removeSelectedText()
+        cursor.insertText(word)
+        cursor.endEditBlock()
+
+    def html_tag(self, tag):
+        """
+        Replaces the selected text with word.
+        """
+        for html in FormattingTags.get_html_tags():
+            if tag == html['desc']:
+                cursor = self.textCursor()
+                if self.textCursor().hasSelection():
+                    text = cursor.selectedText()
+                    cursor.beginEditBlock()
+                    cursor.removeSelectedText()
+                    cursor.insertText(html['start tag'])
+                    cursor.insertText(text)
+                    cursor.insertText(html['end tag'])
+                    cursor.endEditBlock()
+                else:
+                    cursor = self.textCursor()
+                    cursor.insertText(html['start tag'])
+                    cursor.insertText(html['end tag'])
+
+
+class Highlighter(QtGui.QSyntaxHighlighter):
+    """
+    Provides a text highlighter for pointing out spelling errors in text.
+    """
+    WORDS = '(?iu)[\w\']+'
+
+    def __init__(self, *args):
+        """
+        Constructor
+        """
+        super(Highlighter, self).__init__(*args)
+        self.spelling_dictionary = None
+
+    def highlightBlock(self, text):
+        """
+        Highlight mis spelt words in a block of text.
+
+        Note, this is a Qt hook.
+        """
+        if not self.spelling_dictionary:
+            return
+        text = str(text)
+        char_format = QtGui.QTextCharFormat()
+        char_format.setUnderlineColor(QtCore.Qt.red)
+        char_format.setUnderlineStyle(QtGui.QTextCharFormat.SpellCheckUnderline)
+        for word_object in re.finditer(self.WORDS, text):
+            if not self.spelling_dictionary.check(word_object.group()):
+                self.setFormat(word_object.start(), word_object.end() - word_object.start(), char_format)
+
+
+class SpellAction(QtWidgets.QAction):
+    """
+    A special QAction that returns the text in a signal.
+    """
+    correct = QtCore.pyqtSignal(str)
+
+    def __init__(self, *args):
+        """
+        Constructor
+        """
+        super(SpellAction, self).__init__(*args)
+        self.triggered.connect(lambda x: self.correct.emit(self.text()))

=== added file 'openlp/core/lib/theme.py'
--- openlp/core/lib/theme.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/theme.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,559 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Provide the theme XML and handling functions for OpenLP v2 themes.
+"""
+import os
+import re
+import logging
+import json
+
+from xml.dom.minidom import Document
+from lxml import etree, objectify
+from openlp.core.common import AppLocation, de_hump
+
+from openlp.core.lib import str_to_bool, ScreenList, get_text_file_string
+
+log = logging.getLogger(__name__)
+
+
+class BackgroundType(object):
+    """
+    Type enumeration for backgrounds.
+    """
+    Solid = 0
+    Gradient = 1
+    Image = 2
+    Transparent = 3
+
+    @staticmethod
+    def to_string(background_type):
+        """
+        Return a string representation of a background type.
+        """
+        if background_type == BackgroundType.Solid:
+            return 'solid'
+        elif background_type == BackgroundType.Gradient:
+            return 'gradient'
+        elif background_type == BackgroundType.Image:
+            return 'image'
+        elif background_type == BackgroundType.Transparent:
+            return 'transparent'
+
+    @staticmethod
+    def from_string(type_string):
+        """
+        Return a background type for the given string.
+        """
+        if type_string == 'solid':
+            return BackgroundType.Solid
+        elif type_string == 'gradient':
+            return BackgroundType.Gradient
+        elif type_string == 'image':
+            return BackgroundType.Image
+        elif type_string == 'transparent':
+            return BackgroundType.Transparent
+
+
+class BackgroundGradientType(object):
+    """
+    Type enumeration for background gradients.
+    """
+    Horizontal = 0
+    Vertical = 1
+    Circular = 2
+    LeftTop = 3
+    LeftBottom = 4
+
+    @staticmethod
+    def to_string(gradient_type):
+        """
+        Return a string representation of a background gradient type.
+        """
+        if gradient_type == BackgroundGradientType.Horizontal:
+            return 'horizontal'
+        elif gradient_type == BackgroundGradientType.Vertical:
+            return 'vertical'
+        elif gradient_type == BackgroundGradientType.Circular:
+            return 'circular'
+        elif gradient_type == BackgroundGradientType.LeftTop:
+            return 'leftTop'
+        elif gradient_type == BackgroundGradientType.LeftBottom:
+            return 'leftBottom'
+
+    @staticmethod
+    def from_string(type_string):
+        """
+        Return a background gradient type for the given string.
+        """
+        if type_string == 'horizontal':
+            return BackgroundGradientType.Horizontal
+        elif type_string == 'vertical':
+            return BackgroundGradientType.Vertical
+        elif type_string == 'circular':
+            return BackgroundGradientType.Circular
+        elif type_string == 'leftTop':
+            return BackgroundGradientType.LeftTop
+        elif type_string == 'leftBottom':
+            return BackgroundGradientType.LeftBottom
+
+
+class HorizontalType(object):
+    """
+    Type enumeration for horizontal alignment.
+    """
+    Left = 0
+    Right = 1
+    Center = 2
+    Justify = 3
+
+    Names = ['left', 'right', 'center', 'justify']
+
+
+class VerticalType(object):
+    """
+    Type enumeration for vertical alignment.
+    """
+    Top = 0
+    Middle = 1
+    Bottom = 2
+
+    Names = ['top', 'middle', 'bottom']
+
+
+BOOLEAN_LIST = ['bold', 'italics', 'override', 'outline', 'shadow', 'slide_transition']
+
+INTEGER_LIST = ['size', 'line_adjustment', 'x', 'height', 'y', 'width', 'shadow_size', 'outline_size',
+                'horizontal_align', 'vertical_align', 'wrap_style']
+
+
+class ThemeXML(object):
+    """
+    A class to encapsulate the Theme XML.
+    """
+    def __init__(self):
+        """
+        Initialise the theme object.
+        """
+        # basic theme object with defaults
+        json_dir = os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'core', 'lib', 'json')
+        json_file = os.path.join(json_dir, 'theme.json')
+        jsn = get_text_file_string(json_file)
+        jsn = json.loads(jsn)
+        self.expand_json(jsn)
+
+    def expand_json(self, var, prev=None):
+        """
+        Expand the json objects and make into variables.
+
+        :param var: The array list to be processed.
+        :param prev: The preceding string to add to the key to make the variable.
+        """
+        for key, value in var.items():
+            if prev:
+                key = prev + "_" + key
+            else:
+                key = key
+            if isinstance(value, dict):
+                self.expand_json(value, key)
+            else:
+                setattr(self, key, value)
+
+    def extend_image_filename(self, path):
+        """
+        Add the path name to the image name so the background can be rendered.
+
+        :param path: The path name to be added.
+        """
+        if self.background_type == 'image':
+            if self.background_filename and path:
+                self.theme_name = self.theme_name.strip()
+                self.background_filename = self.background_filename.strip()
+                self.background_filename = os.path.join(path, self.theme_name, self.background_filename)
+
+    def _new_document(self, name):
+        """
+        Create a new theme XML document.
+        """
+        self.theme_xml = Document()
+        self.theme = self.theme_xml.createElement('theme')
+        self.theme_xml.appendChild(self.theme)
+        self.theme.setAttribute('version', '2.0')
+        self.name = self.theme_xml.createElement('name')
+        text_node = self.theme_xml.createTextNode(name)
+        self.name.appendChild(text_node)
+        self.theme.appendChild(self.name)
+
+    def add_background_transparent(self):
+        """
+        Add a transparent background.
+        """
+        background = self.theme_xml.createElement('background')
+        background.setAttribute('type', 'transparent')
+        self.theme.appendChild(background)
+
+    def add_background_solid(self, bkcolor):
+        """
+        Add a Solid background.
+
+        :param bkcolor: The color of the background.
+        """
+        background = self.theme_xml.createElement('background')
+        background.setAttribute('type', 'solid')
+        self.theme.appendChild(background)
+        self.child_element(background, 'color', str(bkcolor))
+
+    def add_background_gradient(self, startcolor, endcolor, direction):
+        """
+        Add a gradient background.
+
+        :param startcolor: The gradient's starting colour.
+        :param endcolor: The gradient's ending colour.
+        :param direction: The direction of the gradient.
+        """
+        background = self.theme_xml.createElement('background')
+        background.setAttribute('type', 'gradient')
+        self.theme.appendChild(background)
+        # Create startColor element
+        self.child_element(background, 'startColor', str(startcolor))
+        # Create endColor element
+        self.child_element(background, 'endColor', str(endcolor))
+        # Create direction element
+        self.child_element(background, 'direction', str(direction))
+
+    def add_background_image(self, filename, border_color):
+        """
+        Add a image background.
+
+        :param filename: The file name of the image.
+        :param border_color:
+        """
+        background = self.theme_xml.createElement('background')
+        background.setAttribute('type', 'image')
+        self.theme.appendChild(background)
+        # Create Filename element
+        self.child_element(background, 'filename', filename)
+        # Create endColor element
+        self.child_element(background, 'borderColor', str(border_color))
+
+    def add_font(self, name, color, size, override, fonttype='main', bold='False', italics='False',
+                 line_adjustment=0, xpos=0, ypos=0, width=0, height=0, outline='False', outline_color='#ffffff',
+                 outline_pixel=2, shadow='False', shadow_color='#ffffff', shadow_pixel=5):
+        """
+        Add a Font.
+
+        :param name: The name of the font.
+        :param color: The colour of the font.
+        :param size: The size of the font.
+        :param override: Whether or not to override the default positioning of the theme.
+        :param fonttype: The type of font, ``main`` or ``footer``. Defaults to ``main``.
+        :param bold:
+        :param italics: The weight of then font Defaults to 50 Normal
+        :param line_adjustment: Does the font render to italics Defaults to 0 Normal
+        :param xpos: The X position of the text block.
+        :param ypos: The Y position of the text block.
+        :param width: The width of the text block.
+        :param height: The height of the text block.
+        :param outline: Whether or not to show an outline.
+        :param outline_color: The colour of the outline.
+        :param outline_pixel:  How big the Shadow is
+        :param shadow: Whether or not to show a shadow.
+        :param shadow_color: The colour of the shadow.
+        :param shadow_pixel: How big the Shadow is
+        """
+        background = self.theme_xml.createElement('font')
+        background.setAttribute('type', fonttype)
+        self.theme.appendChild(background)
+        # Create Font name element
+        self.child_element(background, 'name', name)
+        # Create Font color element
+        self.child_element(background, 'color', str(color))
+        # Create Proportion name element
+        self.child_element(background, 'size', str(size))
+        # Create weight name element
+        self.child_element(background, 'bold', str(bold))
+        # Create italics name element
+        self.child_element(background, 'italics', str(italics))
+        # Create indentation name element
+        self.child_element(background, 'line_adjustment', str(line_adjustment))
+        # Create Location element
+        element = self.theme_xml.createElement('location')
+        element.setAttribute('override', str(override))
+        element.setAttribute('x', str(xpos))
+        element.setAttribute('y', str(ypos))
+        element.setAttribute('width', str(width))
+        element.setAttribute('height', str(height))
+        background.appendChild(element)
+        # Shadow
+        element = self.theme_xml.createElement('shadow')
+        element.setAttribute('shadowColor', str(shadow_color))
+        element.setAttribute('shadowSize', str(shadow_pixel))
+        value = self.theme_xml.createTextNode(str(shadow))
+        element.appendChild(value)
+        background.appendChild(element)
+        # Outline
+        element = self.theme_xml.createElement('outline')
+        element.setAttribute('outlineColor', str(outline_color))
+        element.setAttribute('outlineSize', str(outline_pixel))
+        value = self.theme_xml.createTextNode(str(outline))
+        element.appendChild(value)
+        background.appendChild(element)
+
+    def add_display(self, horizontal, vertical, transition):
+        """
+        Add a Display options.
+
+        :param horizontal: The horizontal alignment of the text.
+        :param vertical: The vertical alignment of the text.
+        :param transition: Whether the slide transition is active.
+        """
+        background = self.theme_xml.createElement('display')
+        self.theme.appendChild(background)
+        # Horizontal alignment
+        element = self.theme_xml.createElement('horizontalAlign')
+        value = self.theme_xml.createTextNode(str(horizontal))
+        element.appendChild(value)
+        background.appendChild(element)
+        # Vertical alignment
+        element = self.theme_xml.createElement('verticalAlign')
+        value = self.theme_xml.createTextNode(str(vertical))
+        element.appendChild(value)
+        background.appendChild(element)
+        # Slide Transition
+        element = self.theme_xml.createElement('slideTransition')
+        value = self.theme_xml.createTextNode(str(transition))
+        element.appendChild(value)
+        background.appendChild(element)
+
+    def child_element(self, element, tag, value):
+        """
+        Generic child element creator.
+        """
+        child = self.theme_xml.createElement(tag)
+        child.appendChild(self.theme_xml.createTextNode(value))
+        element.appendChild(child)
+        return child
+
+    def set_default_header_footer(self):
+        """
+        Set the header and footer size into the current primary screen.
+        10 px on each side is removed to allow for a border.
+        """
+        current_screen = ScreenList().current
+        self.font_main_y = 0
+        self.font_main_width = current_screen['size'].width() - 20
+        self.font_main_height = current_screen['size'].height() * 9 / 10
+        self.font_footer_width = current_screen['size'].width() - 20
+        self.font_footer_y = current_screen['size'].height() * 9 / 10
+        self.font_footer_height = current_screen['size'].height() / 10
+
+    def dump_xml(self):
+        """
+        Dump the XML to file used for debugging
+        """
+        return self.theme_xml.toprettyxml(indent='  ')
+
+    def extract_xml(self):
+        """
+        Print out the XML string.
+        """
+        self._build_xml_from_attrs()
+        return self.theme_xml.toxml('utf-8').decode('utf-8')
+
+    def extract_formatted_xml(self):
+        """
+        Pull out the XML string formatted for human consumption
+        """
+        self._build_xml_from_attrs()
+        return self.theme_xml.toprettyxml(indent='    ', newl='\n', encoding='utf-8')
+
+    def parse(self, xml):
+        """
+        Read in an XML string and parse it.
+
+        :param xml: The XML string to parse.
+        """
+        self.parse_xml(str(xml))
+
+    def parse_xml(self, xml):
+        """
+        Parse an XML string.
+
+        :param xml: The XML string to parse.
+        """
+        # remove encoding string
+        line = xml.find('?>')
+        if line:
+            xml = xml[line + 2:]
+        try:
+            theme_xml = objectify.fromstring(xml)
+        except etree.XMLSyntaxError:
+            log.exception('Invalid xml %s', xml)
+            return
+        xml_iter = theme_xml.getiterator()
+        for element in xml_iter:
+            master = ''
+            if element.tag == 'background':
+                if element.attrib:
+                    for attr in element.attrib:
+                        self._create_attr(element.tag, attr, element.attrib[attr])
+            parent = element.getparent()
+            if parent is not None:
+                if parent.tag == 'font':
+                    master = parent.tag + '_' + parent.attrib['type']
+                # set up Outline and Shadow Tags and move to font_main
+                if parent.tag == 'display':
+                    if element.tag.startswith('shadow') or element.tag.startswith('outline'):
+                        self._create_attr('font_main', element.tag, element.text)
+                    master = parent.tag
+                if parent.tag == 'background':
+                    master = parent.tag
+            if master:
+                self._create_attr(master, element.tag, element.text)
+                if element.attrib:
+                    for attr in element.attrib:
+                        base_element = attr
+                        # correction for the shadow and outline tags
+                        if element.tag == 'shadow' or element.tag == 'outline':
+                            if not attr.startswith(element.tag):
+                                base_element = element.tag + '_' + attr
+                        self._create_attr(master, base_element, element.attrib[attr])
+            else:
+                if element.tag == 'name':
+                    self._create_attr('theme', element.tag, element.text)
+
+    def _translate_tags(self, master, element, value):
+        """
+        Clean up XML removing and redefining tags
+        """
+        master = master.strip().lstrip()
+        element = element.strip().lstrip()
+        value = str(value).strip().lstrip()
+        if master == 'display':
+            if element == 'wrapStyle':
+                return True, None, None, None
+            if element.startswith('shadow') or element.startswith('outline'):
+                master = 'font_main'
+        # fix bold font
+        if element == 'weight':
+            element = 'bold'
+            if value == 'Normal':
+                value = False
+            else:
+                value = True
+        if element == 'proportion':
+            element = 'size'
+        return False, master, element, value
+
+    def _create_attr(self, master, element, value):
+        """
+        Create the attributes with the correct data types and name format
+        """
+        reject, master, element, value = self._translate_tags(master, element, value)
+        if reject:
+            return
+        field = de_hump(element)
+        tag = master + '_' + field
+        if field in BOOLEAN_LIST:
+            setattr(self, tag, str_to_bool(value))
+        elif field in INTEGER_LIST:
+            setattr(self, tag, int(value))
+        else:
+            # make string value unicode
+            if not isinstance(value, str):
+                value = str(str(value), 'utf-8')
+            # None means an empty string so lets have one.
+            if value == 'None':
+                value = ''
+            setattr(self, tag, str(value).strip().lstrip())
+
+    def __str__(self):
+        """
+        Return a string representation of this object.
+        """
+        theme_strings = []
+        for key in dir(self):
+            if key[0:1] != '_':
+                theme_strings.append('%30s: %s' % (key, getattr(self, key)))
+        return '\n'.join(theme_strings)
+
+    def _build_xml_from_attrs(self):
+        """
+        Build the XML from the varables in the object
+        """
+        self._new_document(self.theme_name)
+        if self.background_type == BackgroundType.to_string(BackgroundType.Solid):
+            self.add_background_solid(self.background_color)
+        elif self.background_type == BackgroundType.to_string(BackgroundType.Gradient):
+            self.add_background_gradient(
+                self.background_start_color,
+                self.background_end_color,
+                self.background_direction
+            )
+        elif self.background_type == BackgroundType.to_string(BackgroundType.Image):
+            filename = os.path.split(self.background_filename)[1]
+            self.add_background_image(filename, self.background_border_color)
+        elif self.background_type == BackgroundType.to_string(BackgroundType.Transparent):
+            self.add_background_transparent()
+        self.add_font(
+            self.font_main_name,
+            self.font_main_color,
+            self.font_main_size,
+            self.font_main_override, 'main',
+            self.font_main_bold,
+            self.font_main_italics,
+            self.font_main_line_adjustment,
+            self.font_main_x,
+            self.font_main_y,
+            self.font_main_width,
+            self.font_main_height,
+            self.font_main_outline,
+            self.font_main_outline_color,
+            self.font_main_outline_size,
+            self.font_main_shadow,
+            self.font_main_shadow_color,
+            self.font_main_shadow_size
+        )
+        self.add_font(
+            self.font_footer_name,
+            self.font_footer_color,
+            self.font_footer_size,
+            self.font_footer_override, 'footer',
+            self.font_footer_bold,
+            self.font_footer_italics,
+            0,  # line adjustment
+            self.font_footer_x,
+            self.font_footer_y,
+            self.font_footer_width,
+            self.font_footer_height,
+            self.font_footer_outline,
+            self.font_footer_outline_color,
+            self.font_footer_outline_size,
+            self.font_footer_shadow,
+            self.font_footer_shadow_color,
+            self.font_footer_shadow_size
+        )
+        self.add_display(
+            self.display_horizontal_align,
+            self.display_vertical_align,
+            self.display_slide_transition
+        )

=== added file 'openlp/core/lib/toolbar.py'
--- openlp/core/lib/toolbar.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/toolbar.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Provide common toolbar handling for OpenLP
+"""
+import logging
+
+from PyQt5 import QtCore, QtWidgets
+
+from openlp.core.lib.ui import create_widget_action
+
+log = logging.getLogger(__name__)
+
+
+class OpenLPToolbar(QtWidgets.QToolBar):
+    """
+    Lots of toolbars around the place, so it makes sense to have a common way to manage them. This is the base toolbar
+    class.
+    """
+    def __init__(self, parent):
+        """
+        Initialise the toolbar.
+        """
+        super(OpenLPToolbar, self).__init__(parent)
+        # useful to be able to reuse button icons...
+        self.setIconSize(QtCore.QSize(20, 20))
+        self.actions = {}
+        log.debug('Init done for %s' % parent.__class__.__name__)
+
+    def add_toolbar_action(self, name, **kwargs):
+        """
+        A method to help developers easily add a button to the toolbar. A new QAction is created by calling
+        ``create_action()``. The action is added to the toolbar and the toolbar is set as parent. For more details
+        please look at openlp.core.lib.ui.create_action()
+        """
+        action = create_widget_action(self, name, **kwargs)
+        self.actions[name] = action
+        return action
+
+    def add_toolbar_widget(self, widget):
+        """
+        Add a widget and store it's handle under the widgets object name.
+        """
+        action = self.addWidget(widget)
+        self.actions[widget.objectName()] = action
+
+    def set_widget_visible(self, widgets, visible=True):
+        """
+        Set the visibility for a widget or a list of widgets.
+
+        :param widgets: A list of string with widget object names.
+        :param visible: The new state as bool.
+        """
+        for handle in widgets:
+            if handle in self.actions:
+                self.actions[handle].setVisible(visible)
+            else:
+                log.warning('No handle "%s" in actions list.', str(handle))
+
+    def set_widget_enabled(self, widgets, enabled=True):
+        """
+        Set the enabled state for a widget or a list of widgets.
+
+        :param widgets: A list of string with widget object names.
+        :param enabled: The new state as bool.
+        """
+        for handle in widgets:
+            if handle in self.actions:
+                self.actions[handle].setEnabled(enabled)
+            else:
+                log.warning('No handle "%s" in actions list.', str(handle))

=== added file 'openlp/core/lib/treewidgetwithdnd.py'
--- openlp/core/lib/treewidgetwithdnd.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/treewidgetwithdnd.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Extend QTreeWidget to handle drag and drop functionality
+"""
+import os
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import Registry
+
+
+class TreeWidgetWithDnD(QtWidgets.QTreeWidget):
+    """
+    Provide a tree widget to store objects and handle drag and drop events
+    """
+    def __init__(self, parent=None, name=''):
+        """
+        Initialise the tree widget
+        """
+        super(TreeWidgetWithDnD, self).__init__(parent)
+        self.mime_data_text = name
+        self.allow_internal_dnd = False
+        self.header().close()
+        self.default_indentation = self.indentation()
+        self.setIndentation(0)
+        self.setAnimated(True)
+
+    def activateDnD(self):
+        """
+        Activate DnD of widget
+        """
+        self.setAcceptDrops(True)
+        self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
+        Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file)
+        Registry().register_function(('%s_dnd_internal' % self.mime_data_text), self.parent().dnd_move_internal)
+
+    def mouseMoveEvent(self, event):
+        """
+        Drag and drop event does not care what data is selected as the recipient will use events to request the data
+        move just tell it what plugin to call
+
+        :param event: The event that occurred
+        """
+        if event.buttons() != QtCore.Qt.LeftButton:
+            event.ignore()
+            return
+        if not self.selectedItems():
+            event.ignore()
+            return
+        drag = QtGui.QDrag(self)
+        mime_data = QtCore.QMimeData()
+        drag.setMimeData(mime_data)
+        mime_data.setText(self.mime_data_text)
+        drag.exec(QtCore.Qt.CopyAction)
+
+    def dragEnterEvent(self, event):
+        """
+        Receive drag enter event, check if it is a file or internal object and allow it if it is.
+
+        :param event:  The event that occurred
+        """
+        if event.mimeData().hasUrls():
+            event.accept()
+        elif self.allow_internal_dnd:
+            event.accept()
+        else:
+            event.ignore()
+
+    def dragMoveEvent(self, event):
+        """
+        Receive drag move event, check if it is a file or internal object and allow it if it is.
+
+        :param event: The event that occurred
+        """
+        QtWidgets.QTreeWidget.dragMoveEvent(self, event)
+        if event.mimeData().hasUrls():
+            event.setDropAction(QtCore.Qt.CopyAction)
+            event.accept()
+        elif self.allow_internal_dnd:
+            event.setDropAction(QtCore.Qt.CopyAction)
+            event.accept()
+        else:
+            event.ignore()
+
+    def dropEvent(self, event):
+        """
+        Receive drop event, check if it is a file or internal object and process it if it is.
+
+        :param event: Handle of the event pint passed
+        """
+        if event.mimeData().hasUrls():
+            event.setDropAction(QtCore.Qt.CopyAction)
+            event.accept()
+            files = []
+            for url in event.mimeData().urls():
+                local_file = url.toLocalFile()
+                if os.path.isfile(local_file):
+                    files.append(local_file)
+                elif os.path.isdir(local_file):
+                    listing = os.listdir(local_file)
+                    for file_name in listing:
+                        files.append(os.path.join(local_file, file_name))
+            Registry().execute('%s_dnd' % self.mime_data_text, {'files': files, 'target': self.itemAt(event.pos())})
+        elif self.allow_internal_dnd:
+            event.setDropAction(QtCore.Qt.CopyAction)
+            event.accept()
+            Registry().execute('%s_dnd_internal' % self.mime_data_text, self.itemAt(event.pos()))
+        else:
+            event.ignore()
+
+    # Convenience methods for emulating a QListWidget. This helps keeping MediaManagerItem simple.
+    def addItem(self, item):
+        self.addTopLevelItem(item)
+
+    def count(self):
+        return self.topLevelItemCount()
+
+    def item(self, index):
+        return self.topLevelItem(index)

=== added file 'openlp/core/lib/ui.py'
--- openlp/core/lib/ui.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/ui.py	2016-04-05 20:22:40 +0000
@@ -0,0 +1,326 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`ui` module provides standard UI components for OpenLP.
+"""
+import logging
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import Registry, UiStrings, translate, is_macosx
+from openlp.core.common.actions import ActionList
+from openlp.core.lib import build_icon
+
+
+log = logging.getLogger(__name__)
+
+
+def add_welcome_page(parent, image):
+    """
+    Generate an opening welcome page for a wizard using a provided image.
+
+    :param parent: A ``QWizard`` object to add the welcome page to.
+    :param image: A splash image for the wizard.
+    """
+    parent.welcome_page = QtWidgets.QWizardPage()
+    parent.welcome_page.setPixmap(QtWidgets.QWizard.WatermarkPixmap, QtGui.QPixmap(image))
+    parent.welcome_page.setObjectName('welcome_page')
+    parent.welcome_layout = QtWidgets.QVBoxLayout(parent.welcome_page)
+    parent.welcome_layout.setObjectName('WelcomeLayout')
+    parent.title_label = QtWidgets.QLabel(parent.welcome_page)
+    parent.title_label.setObjectName('title_label')
+    parent.welcome_layout.addWidget(parent.title_label)
+    parent.welcome_layout.addSpacing(40)
+    parent.information_label = QtWidgets.QLabel(parent.welcome_page)
+    parent.information_label.setWordWrap(True)
+    parent.information_label.setObjectName('information_label')
+    parent.welcome_layout.addWidget(parent.information_label)
+    parent.welcome_layout.addStretch()
+    parent.addPage(parent.welcome_page)
+
+
+def create_button_box(dialog, name, standard_buttons, custom_buttons=None):
+    """
+    Creates a QDialogButtonBox with the given buttons. The ``accepted()`` and ``rejected()`` signals of the button box
+    are connected with the dialogs ``accept()`` and ``reject()`` slots.
+
+    :param dialog: The parent object. This has to be a ``QDialog`` descendant.
+    :param name: A string which is set as object name.
+    :param standard_buttons: A list of strings for the used buttons. It might contain: ``ok``, ``save``, ``cancel``,
+        ``close``, and ``defaults``.
+    :param custom_buttons: A list of additional buttons. If an item is an instance of QtWidgets.QAbstractButton it is
+    added with QDialogButtonBox.ActionRole. Otherwise the item has to be a tuple of a Button and a ButtonRole.
+    """
+    if custom_buttons is None:
+        custom_buttons = []
+    if standard_buttons is None:
+        standard_buttons = []
+    buttons = QtWidgets.QDialogButtonBox.NoButton
+    if 'ok' in standard_buttons:
+        buttons |= QtWidgets.QDialogButtonBox.Ok
+    if 'save' in standard_buttons:
+        buttons |= QtWidgets.QDialogButtonBox.Save
+    if 'cancel' in standard_buttons:
+        buttons |= QtWidgets.QDialogButtonBox.Cancel
+    if 'close' in standard_buttons:
+        buttons |= QtWidgets.QDialogButtonBox.Close
+    if 'defaults' in standard_buttons:
+        buttons |= QtWidgets.QDialogButtonBox.RestoreDefaults
+    button_box = QtWidgets.QDialogButtonBox(dialog)
+    button_box.setObjectName(name)
+    button_box.setStandardButtons(buttons)
+    for button in custom_buttons:
+        if isinstance(button, QtWidgets.QAbstractButton):
+            button_box.addButton(button, QtWidgets.QDialogButtonBox.ActionRole)
+        else:
+            button_box.addButton(*button)
+    button_box.accepted.connect(dialog.accept)
+    button_box.rejected.connect(dialog.reject)
+    return button_box
+
+
+def critical_error_message_box(title=None, message=None, parent=None, question=False):
+    """
+    Provides a standard critical message box for errors that OpenLP displays to users.
+
+    :param title: The title for the message box.
+    :param message: The message to display to the user.
+    :param parent: The parent UI element to attach the dialog to.
+    :param question: Should this message box question the user.
+    """
+    if question:
+        return QtWidgets.QMessageBox.critical(parent, UiStrings().Error, message,
+                                              QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
+                                                                                    QtWidgets.QMessageBox.No))
+    return Registry().get('main_window').error_message(title if title else UiStrings().Error, message)
+
+
+def create_horizontal_adjusting_combo_box(parent, name):
+    """
+    Creates a QComboBox with adapting width for media items.
+
+    :param parent: The parent widget.
+    :param name: A string set as object name for the combo box.
+    """
+    combo = QtWidgets.QComboBox(parent)
+    combo.setObjectName(name)
+    combo.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToMinimumContentsLength)
+    combo.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
+    return combo
+
+
+def create_button(parent, name, **kwargs):
+    """
+    Return an button with the object name set and the given parameters.
+
+    :param parent:  A QtCore.QWidget for the buttons parent (required).
+    :param name: A string which is set as object name (required).
+    :param kwargs:
+
+    ``role``
+        A string which can have one value out of ``delete``, ``up``, and ``down``. This decides about default values
+        for properties like text, icon, or tooltip.
+
+    ``text``
+        A string for the action text.
+
+    ``icon``
+        Either a QIcon, a resource string, or a file location string for the action icon.
+
+    ``tooltip``
+        A string for the action tool tip.
+
+    ``enabled``
+        False in case the button should be disabled.
+
+    """
+    if 'role' in kwargs:
+        role = kwargs.pop('role')
+        if role == 'delete':
+            kwargs.setdefault('text', UiStrings().Delete)
+            kwargs.setdefault('tooltip', translate('OpenLP.Ui', 'Delete the selected item.'))
+        elif role == 'up':
+            kwargs.setdefault('icon', ':/services/service_up.png')
+            kwargs.setdefault('tooltip', translate('OpenLP.Ui', 'Move selection up one position.'))
+        elif role == 'down':
+            kwargs.setdefault('icon', ':/services/service_down.png')
+            kwargs.setdefault('tooltip', translate('OpenLP.Ui', 'Move selection down one position.'))
+        else:
+            log.warning('The role "%s" is not defined in create_push_button().', role)
+    if kwargs.pop('btn_class', '') == 'toolbutton':
+        button = QtWidgets.QToolButton(parent)
+    else:
+        button = QtWidgets.QPushButton(parent)
+    button.setObjectName(name)
+    if kwargs.get('text'):
+        button.setText(kwargs.pop('text'))
+    if kwargs.get('icon'):
+        button.setIcon(build_icon(kwargs.pop('icon')))
+    if kwargs.get('tooltip'):
+        button.setToolTip(kwargs.pop('tooltip'))
+    if not kwargs.pop('enabled', True):
+        button.setEnabled(False)
+    if kwargs.get('click'):
+        button.clicked.connect(kwargs.pop('click'))
+    for key in list(kwargs.keys()):
+        if key not in ['text', 'icon', 'tooltip', 'click']:
+            log.warning('Parameter %s was not consumed in create_button().', key)
+    return button
+
+
+def create_action(parent, name, **kwargs):
+    """
+    Return an action with the object name set and the given parameters.
+
+    :param parent:  A QtCore.QObject for the actions parent (required).
+    :param name:  A string which is set as object name (required).
+    :param kwargs:
+
+    ``text``
+        A string for the action text.
+
+    ``icon``
+        Either a QIcon, a resource string, or a file location string for the
+        action icon.
+
+    ``tooltip``
+        A string for the action tool tip.
+
+    ``statustip``
+        A string for the action status tip.
+
+    ``checked``
+        A bool for the state. If ``None`` the Action is not checkable.
+
+    ``enabled``
+        False in case the action should be disabled.
+
+    ``visible``
+        False in case the action should be hidden.
+
+    ``separator``
+        True in case the action will be considered a separator.
+
+    ``data``
+        The action's data.
+
+    ``can_shortcuts``
+        Capability stating if this action can have shortcuts. If ``True`` the action is added to shortcut dialog
+
+        otherwise it it not. Define your shortcut in the :class:`~openlp.core.lib.Settings` class. *Note*: When *not*
+        ``True`` you *must not* set a shortcuts at all.
+
+    ``context``
+        A context for the shortcut execution.
+
+    ``category``
+        A category the action should be listed in the shortcut dialog.
+
+    ``triggers``
+        A slot which is connected to the actions ``triggered()`` slot.
+    """
+    action = QtWidgets.QAction(parent)
+    action.setObjectName(name)
+    if is_macosx():
+        action.setIconVisibleInMenu(False)
+    if kwargs.get('text'):
+        action.setText(kwargs.pop('text'))
+    if kwargs.get('icon'):
+        action.setIcon(