← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~alisonken1/openlp/projector-2.1-merge into lp:openlp

 

Ken Roberts has proposed merging lp:~alisonken1/openlp/projector-2.1-merge into lp:openlp.

Requested reviews:
  Jonathan Springer (springermac)
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~alisonken1/openlp/projector-2.1-merge/+merge/239901

Changes per springermac
Fix typos in Projector and ProjectorSource tables
Fix regression in source select dialog

Add basic remote control of networked projectors using PJLink control format.
See http://pjlink.jbmia.or.jp/english/dl.html section 5-1. PJLink Specifications

Creates ~/.openlp/data/projector/projector.sqlite database
Creates Projector Manager window below Theme Manager

Basic functions:
Create/edit/delete projector entry for control
Connect/disconnect to/from projector
Power on/standby projector
Blank/show projector screen
Select source input

lp:~alisonken1/openlp/projector-2.1-merge (revision 2529)
[SUCCESS] http://ci.openlp.org/job/Branch-01-Pull/719/
[SUCCESS] http://ci.openlp.org/job/Branch-02-Functional-Tests/662/
[SUCCESS] http://ci.openlp.org/job/Branch-03-Interface-Tests/606/
[SUCCESS] http://ci.openlp.org/job/Branch-04a-Windows_Functional_Tests/546/
[SUCCESS] http://ci.openlp.org/job/Branch-04b-Windows_Interface_Tests/155/
[SUCCESS] http://ci.openlp.org/job/Branch-05a-Code_Analysis/360/
[SUCCESS] http://ci.openlp.org/job/Branch-05b-Test_Coverage/234/


-- 
https://code.launchpad.net/~alisonken1/openlp/projector-2.1-merge/+merge/239901
Your team OpenLP Core is requested to review the proposed merge of lp:~alisonken1/openlp/projector-2.1-merge into lp:openlp.
=== modified file '.bzrignore'
--- .bzrignore	2014-07-11 11:35:56 +0000
+++ .bzrignore	2014-10-28 19:17:32 +0000
@@ -33,3 +33,10 @@
 __pycache__
 *.dll
 .directory
+*.kate-swp
+# Git files
+.git
+.gitignore
+# Rejected diff's
+*.rej
+*.~\?~

=== modified file 'openlp/core/common/__init__.py'
--- openlp/core/common/__init__.py	2014-08-27 23:18:06 +0000
+++ openlp/core/common/__init__.py	2014-10-28 19:17:32 +0000
@@ -30,13 +30,17 @@
 The :mod:`common` module contains most of the components and libraries that make
 OpenLP work.
 """
+import hashlib
 import re
 import os
 import logging
 import sys
 import traceback
+from ipaddress import IPv4Address, IPv6Address, AddressValueError
+from codecs import decode, encode
 
 from PyQt4 import QtCore
+from PyQt4.QtCore import QCryptographicHash as QHash
 
 log = logging.getLogger(__name__ + '.__init__')
 
@@ -154,6 +158,81 @@
     """
     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):
+    """
+    Returns the hashed output of md5sum on salt,data
+    using Python3 hashlib
+
+    :param salt: Initial salt
+    :param data: Data to hash
+    :returns: str
+    """
+    log.debug('md5_hash(salt="%s")' % salt)
+    hash_obj = hashlib.new('md5')
+    hash_obj.update(salt.encode('ascii'))
+    hash_obj.update(data.encode('ascii'))
+    hash_value = hash_obj.hexdigest()
+    log.debug('md5_hash() returning "%s"' % hash_value)
+    return hash_value
+
+
+def qmd5_hash(salt, data):
+    """
+    Returns the hashed output of MD5Sum on salt, data
+    using PyQt4.QCryptographicHash.
+
+    :param salt: Initial salt
+    :param data: 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 decode(hash_value.data(), 'ascii')
+
+
 from .openlpmixin import OpenLPMixin
 from .registry import Registry
 from .registrymixin import RegistryMixin

=== modified file 'openlp/core/common/registryproperties.py'
--- openlp/core/common/registryproperties.py	2014-08-27 23:18:06 +0000
+++ openlp/core/common/registryproperties.py	2014-10-28 19:17:32 +0000
@@ -148,3 +148,12 @@
         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

=== modified file 'openlp/core/common/settings.py'
--- openlp/core/common/settings.py	2014-10-14 07:52:59 +0000
+++ openlp/core/common/settings.py	2014-10-28 19:17:32 +0000
@@ -276,6 +276,7 @@
         'shortcuts/toolsAddToolItem': [],
         'shortcuts/updateThemeImages': [],
         'shortcuts/up': [QtGui.QKeySequence(QtCore.Qt.Key_Up)],
+        'shortcuts/viewProjectorManagerItem': [QtGui.QKeySequence('F6')],
         'shortcuts/viewThemeManagerItem': [QtGui.QKeySequence('F10')],
         'shortcuts/viewMediaManagerItem': [QtGui.QKeySequence('F8')],
         'shortcuts/viewPreviewPanel': [QtGui.QKeySequence('F11')],
@@ -296,7 +297,15 @@
         '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()
+        'user interface/preview splitter geometry': QtCore.QByteArray(),
+        'projector/db type': 'sqlite',
+        '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__ = [

=== modified file 'openlp/core/common/uistrings.py'
--- openlp/core/common/uistrings.py	2014-03-20 19:10:31 +0000
+++ openlp/core/common/uistrings.py	2014-10-28 19:17:32 +0000
@@ -99,6 +99,10 @@
         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')
@@ -118,6 +122,8 @@
         self.PlaySlidesToEnd = translate('OpenLP.Ui', 'Play Slides to End')
         self.Preview = translate('OpenLP.Ui', 'Preview')
         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.ResetBG = translate('OpenLP.Ui', 'Reset Background')

=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	2014-07-11 11:35:56 +0000
+++ openlp/core/lib/__init__.py	2014-10-28 19:17:32 +0000
@@ -334,3 +334,6 @@
 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

=== modified file 'openlp/core/lib/db.py'
--- openlp/core/lib/db.py	2014-07-17 21:04:58 +0000
+++ openlp/core/lib/db.py	2014-10-28 19:17:32 +0000
@@ -48,20 +48,53 @@
 log = logging.getLogger(__name__)
 
 
-def init_db(url, auto_flush=True, auto_commit=False):
+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)
-    metadata = MetaData(bind=engine)
+    if base is None:
+        metadata = MetaData(bind=engine)
+    else:
+        base.metadata.bind = engine
+        metadata = None
     session = scoped_session(sessionmaker(autoflush=auto_flush, autocommit=auto_commit, bind=engine))
     return session, metadata
 
 
+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_url = ''
+    db_type = settings.value('db type')
+    if db_type == 'sqlite':
+        if db_file_name is None:
+            db_url = 'sqlite:///%s/%s.sqlite' % (AppLocation.get_section_data_path(plugin_name), plugin_name)
+        else:
+            db_url = 'sqlite:///%s/%s' % (AppLocation.get_section_data_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')))
+        if db_type == 'mysql':
+            db_encoding = settings.value('db encoding')
+            db_url += '?charset=%s' % urlquote(db_encoding)
+    settings.endGroup()
+    return db_url
+
+
 def get_upgrade_op(session):
     """
     Create a migration context and an operations object for performing upgrades.
@@ -159,7 +192,7 @@
     """
     Provide generic object persistence management
     """
-    def __init__(self, plugin_name, init_schema, db_file_name=None, upgrade_mod=None):
+    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.
@@ -170,26 +203,15 @@
         :param upgrade_mod: The file name to use for this database. Defaults to None resulting in the plugin_name
         being used.
         """
-        settings = Settings()
-        settings.beginGroup(plugin_name)
-        self.db_url = ''
         self.is_dirty = False
         self.session = None
-        db_type = settings.value('db type')
-        if db_type == 'sqlite':
-            if db_file_name:
-                self.db_url = 'sqlite:///%s/%s' % (AppLocation.get_section_data_path(plugin_name), db_file_name)
-            else:
-                self.db_url = 'sqlite:///%s/%s.sqlite' % (AppLocation.get_section_data_path(plugin_name), plugin_name)
+        # See if we're using declarative_base with a pre-existing session.
+        log.debug('Manager: Testing for pre-existing session')
+        if session is not None:
+            log.debug('Manager: Using existing session')
         else:
-            self.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')))
-            if db_type == 'mysql':
-                db_encoding = settings.value('db encoding')
-                self.db_url += '?charset=%s' % urlquote(db_encoding)
-        settings.endGroup()
+            log.debug('Manager: Creating new session')
+            self.db_url = init_url(plugin_name, db_file_name)
         if upgrade_mod:
             try:
                 db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod)

=== 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	2014-10-28 19:17:32 +0000
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
+# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder,               #
+# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble,             #
+# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann           #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+    :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	2014-10-28 19:17:32 +0000
@@ -0,0 +1,366 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
+# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder,               #
+# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble,             #
+# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann           #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+    :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: translate('OpenLP.ProjectorConstants', 'ERRA'),
+                 E_UNDEFINED: translate('OpenLP.ProjectorConstants', 'ERR1'),
+                 E_PARAMETER: translate('OpenLP.ProjectorConstants', 'ERR2'),
+                 E_UNAVAILABLE: translate('OpenLP.ProjectorConstants', 'ERR3'),
+                 E_PROJECTOR: translate('OpenLP.ProjectorConstants', 'ERR4')}
+
+# Map error/status codes to string
+ERROR_STRING = {0: translate('OpenLP.ProjectorConstants', 'S_OK'),
+                E_GENERAL: translate('OpenLP.ProjectorConstants', 'E_GENERAL'),
+                E_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'E_NOT_CONNECTED'),
+                E_FAN: translate('OpenLP.ProjectorConstants', 'E_FAN'),
+                E_LAMP: translate('OpenLP.ProjectorConstants', 'E_LAMP'),
+                E_TEMP: translate('OpenLP.ProjectorConstants', 'E_TEMP'),
+                E_COVER: translate('OpenLP.ProjectorConstants', 'E_COVER'),
+                E_FILTER: translate('OpenLP.ProjectorConstants', 'E_FILTER'),
+                E_AUTHENTICATION: translate('OpenLP.ProjectorConstants', 'E_AUTHENTICATION'),
+                E_NO_AUTHENTICATION: translate('OpenLP.ProjectorConstants', 'E_NO_AUTHENTICATION'),
+                E_UNDEFINED: translate('OpenLP.ProjectorConstants', 'E_UNDEFINED'),
+                E_PARAMETER: translate('OpenLP.ProjectorConstants', 'E_PARAMETER'),
+                E_UNAVAILABLE: translate('OpenLP.ProjectorConstants', 'E_UNAVAILABLE'),
+                E_PROJECTOR: translate('OpenLP.ProjectorConstants', 'E_PROJECTOR'),
+                E_INVALID_DATA: translate('OpenLP.ProjectorConstants', 'E_INVALID_DATA'),
+                E_WARN: translate('OpenLP.ProjectorConstants', 'E_WARN'),
+                E_ERROR: translate('OpenLP.ProjectorConstants', 'E_ERROR'),
+                E_CLASS: translate('OpenLP.ProjectorConstants', 'E_CLASS'),
+                E_PREFIX: translate('OpenLP.ProjectorConstants', 'E_PREFIX'),  # Last projector error
+                E_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants',
+                                                'E_CONNECTION_REFUSED'),  # First QtSocket error
+                E_REMOTE_HOST_CLOSED_CONNECTION: translate('OpenLP.ProjectorConstants',
+                                                           'E_REMOTE_HOST_CLOSED_CONNECTION'),
+                E_HOST_NOT_FOUND: translate('OpenLP.ProjectorConstants', 'E_HOST_NOT_FOUND'),
+                E_SOCKET_ACCESS: translate('OpenLP.ProjectorConstants', 'E_SOCKET_ACCESS'),
+                E_SOCKET_RESOURCE: translate('OpenLP.ProjectorConstants', 'E_SOCKET_RESOURCE'),
+                E_SOCKET_TIMEOUT: translate('OpenLP.ProjectorConstants', 'E_SOCKET_TIMEOUT'),
+                E_DATAGRAM_TOO_LARGE: translate('OpenLP.ProjectorConstants', 'E_DATAGRAM_TOO_LARGE'),
+                E_NETWORK: translate('OpenLP.ProjectorConstants', 'E_NETWORK'),
+                E_ADDRESS_IN_USE: translate('OpenLP.ProjectorConstants', 'E_ADDRESS_IN_USE'),
+                E_SOCKET_ADDRESS_NOT_AVAILABLE: translate('OpenLP.ProjectorConstants',
+                                                          'E_SOCKET_ADDRESS_NOT_AVAILABLE'),
+                E_UNSUPPORTED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants',
+                                                          'E_UNSUPPORTED_SOCKET_OPERATION'),
+                E_PROXY_AUTHENTICATION_REQUIRED: translate('OpenLP.ProjectorConstants',
+                                                           'E_PROXY_AUTHENTICATION_REQUIRED'),
+                E_SLS_HANDSHAKE_FAILED: translate('OpenLP.ProjectorConstants', 'E_SLS_HANDSHAKE_FAILED'),
+                E_UNFINISHED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants',
+                                                         'E_UNFINISHED_SOCKET_OPERATION'),
+                E_PROXY_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants', 'E_PROXY_CONNECTION_REFUSED'),
+                E_PROXY_CONNECTION_CLOSED: translate('OpenLP.ProjectorConstants', 'E_PROXY_CONNECTION_CLOSED'),
+                E_PROXY_CONNECTION_TIMEOUT: translate('OpenLP.ProjectorConstants', 'E_PROXY_CONNECTION_TIMEOUT'),
+                E_PROXY_NOT_FOUND: translate('OpenLP.ProjectorConstants', 'E_PROXY_NOT_FOUND'),
+                E_PROXY_PROTOCOL: translate('OpenLP.ProjectorConstants', 'E_PROXY_PROTOCOL'),
+                E_UNKNOWN_SOCKET_ERROR: translate('OpenLP.ProjectorConstants', 'E_UNKNOWN_SOCKET_ERROR')}
+
+STATUS_STRING = {S_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'S_NOT_CONNECTED'),
+                 S_CONNECTING: translate('OpenLP.ProjectorConstants', 'S_CONNECTING'),
+                 S_CONNECTED: translate('OpenLP.ProjectorConstants', 'S_CONNECTED'),
+                 S_STATUS: translate('OpenLP.ProjectorConstants', 'S_STATUS'),
+                 S_OFF: translate('OpenLP.ProjectorConstants', 'S_OFF'),
+                 S_INITIALIZE: translate('OpenLP.ProjectorConstants', 'S_INITIALIZE'),
+                 S_STANDBY: translate('OpenLP.ProjectorConstants', 'S_STANDBY'),
+                 S_WARMUP: translate('OpenLP.ProjectorConstants', 'S_WARMUP'),
+                 S_ON: translate('OpenLP.ProjectorConstants', 'S_ON'),
+                 S_COOLDOWN: translate('OpenLP.ProjectorConstants', 'S_COOLDOWN'),
+                 S_INFO: translate('OpenLP.ProjectorConstants', 'S_INFO'),
+                 S_NETWORK_SENDING: translate('OpenLP.ProjectorConstants', 'S_NETWORK_SENDING'),
+                 S_NETWORK_RECEIVED: translate('OpenLP.ProjectorConstants', '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 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	2014-10-28 19:17:32 +0000
@@ -0,0 +1,436 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
+# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder,               #
+# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble,             #
+# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann           #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+    :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)
+
+    def init_schema(*args, **kwargs):
+        """
+        Setup the projector database and initialize the schema.
+
+        Declarative uses table classes to define schema.
+        """
+        url = init_url('projector')
+        session, metadata = init_db(url, base=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	2014-10-28 19:17:32 +0000
@@ -0,0 +1,913 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
+# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder,               #
+# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble,             #
+# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann           #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+    :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 PyQt4.QtCore import pyqtSignal, pyqtSlot
+from PyQt4.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['notes']
+        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], data=self.pin)
+        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()
+        try:
+            self.projectorNetwork.emit(S_NETWORK_SENDING)
+            sent = self.write(out)
+            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'))
+        except SocketError as e:
+            self.disconnect_from_host(abort=True)
+            self.changeStatus(E_NETWORK, '%s : %s' % (e.error(), e.errorString()))
+
+    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.
+        """
+        self.pjlink_class = data
+        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()

=== modified file 'openlp/core/lib/toolbar.py'
--- openlp/core/lib/toolbar.py	2014-06-30 20:59:22 +0000
+++ openlp/core/lib/toolbar.py	2014-10-28 19:17:32 +0000
@@ -82,3 +82,16 @@
                 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))

=== modified file 'openlp/core/ui/__init__.py'
--- openlp/core/ui/__init__.py	2014-02-27 21:39:44 +0000
+++ openlp/core/ui/__init__.py	2014-10-28 19:17:32 +0000
@@ -124,9 +124,13 @@
 from .mediadockmanager import MediaDockManager
 from .servicemanager import ServiceManager
 from .thememanager import ThemeManager
+from .projector.manager import ProjectorManager
+from .projector.tab import ProjectorTab
+from .projector.editform import ProjectorEditForm
 
 __all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', 'ThemeForm',
            'ThemeManager', 'MediaDockManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm',
            'Display', 'ServiceNoteForm', 'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay',
            'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm',
-           'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'SingleColumnTableWidget']
+           'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'SingleColumnTableWidget',
+           'ProjectorManager', 'ProjectorTab', 'ProjectorEditForm']

=== modified file 'openlp/core/ui/mainwindow.py'
--- openlp/core/ui/mainwindow.py	2014-10-22 20:47:47 +0000
+++ openlp/core/ui/mainwindow.py	2014-10-28 19:17:32 +0000
@@ -53,6 +53,7 @@
 from openlp.core.utils import LanguageManager, add_actions, get_application_version
 from openlp.core.utils.actions import ActionList, CategoryOrder
 from openlp.core.ui.firsttimeform import FirstTimeForm
+from openlp.core.ui.projector.manager import ProjectorManager
 
 log = logging.getLogger(__name__)
 
@@ -178,6 +179,14 @@
         self.theme_manager_contents.setObjectName('theme_manager_contents')
         self.theme_manager_dock.setWidget(self.theme_manager_contents)
         main_window.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.theme_manager_dock)
+        # Create the projector manager
+        self.projector_manager_dock = OpenLPDockWidget(parent=main_window,
+                                                       name='projector_manager_dock',
+                                                       icon=':/projector/projector_manager.png')
+        self.projector_manager_contents = ProjectorManager(self.projector_manager_dock)
+        self.projector_manager_contents.setObjectName('projector_manager_contents')
+        self.projector_manager_dock.setWidget(self.projector_manager_contents)
+        main_window.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.projector_manager_dock)
         # Create the menu items
         action_list = ActionList.get_instance()
         action_list.add_category(UiStrings().File, CategoryOrder.standard_menu)
@@ -210,6 +219,16 @@
                                                can_shortcuts=True)
         self.export_language_item = create_action(main_window, 'exportLanguageItem')
         action_list.add_category(UiStrings().View, CategoryOrder.standard_menu)
+        # Projector items
+        self.import_projector_item = create_action(main_window, 'importProjectorItem', category=UiStrings().Import,
+                                                   can_shortcuts=False)
+        action_list.add_category(UiStrings().Import, CategoryOrder.standard_menu)
+        self.view_projector_manager_item = create_action(main_window, 'viewProjectorManagerItem',
+                                                         icon=':/projector/projector_manager.png',
+                                                         checked=self.projector_manager_dock.isVisible(),
+                                                         can_shortcuts=True,
+                                                         category=UiStrings().View,
+                                                         triggers=self.toggle_projector_manager)
         self.view_media_manager_item = create_action(main_window, 'viewMediaManagerItem',
                                                      icon=':/system/system_mediamanager.png',
                                                      checked=self.media_manager_dock.isVisible(),
@@ -310,6 +329,11 @@
                                                     'searchShortcut', can_shortcuts=True,
                                                     category=translate('OpenLP.MainWindow', 'General'),
                                                     triggers=self.on_search_shortcut_triggered)
+        '''
+        Leave until the import projector options are finished
+        add_actions(self.file_import_menu, (self.settings_import_item, self.import_theme_item,
+                    self.import_projector_item, self.import_language_item, None))
+        '''
         add_actions(self.file_import_menu, (self.settings_import_item, self.import_theme_item,
                     self.import_language_item, None))
         add_actions(self.file_export_menu, (self.settings_export_item, self.export_theme_item,
@@ -320,8 +344,8 @@
                     self.print_service_order_item, self.file_exit_item))
         add_actions(self.view_mode_menu, (self.mode_default_item, self.mode_setup_item, self.mode_live_item))
         add_actions(self.view_menu, (self.view_mode_menu.menuAction(), None, self.view_media_manager_item,
-                    self.view_service_manager_item, self.view_theme_manager_item, None, self.view_preview_panel,
-                    self.view_live_panel, None, self.lock_panel))
+                    self.view_projector_manager_item, self.view_service_manager_item, self.view_theme_manager_item,
+                    None, self.view_preview_panel, self.view_live_panel, None, self.lock_panel))
         # i18n add Language Actions
         add_actions(self.settings_language_menu, (self.auto_language_item, None))
         add_actions(self.settings_language_menu, self.language_group.actions())
@@ -375,6 +399,7 @@
         self.media_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Library'))
         self.service_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Service Manager'))
         self.theme_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Theme Manager'))
+        self.projector_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Projector Manager'))
         self.file_new_item.setText(translate('OpenLP.MainWindow', '&New'))
         self.file_new_item.setToolTip(UiStrings().NewService)
         self.file_new_item.setStatusTip(UiStrings().CreateService)
@@ -406,6 +431,10 @@
             translate('OpenLP.MainWindow', 'Import OpenLP settings from a specified *.config file previously '
                                            'exported on this or another machine'))
         self.settings_import_item.setText(translate('OpenLP.MainWindow', 'Settings'))
+        self.view_projector_manager_item.setText(translate('OPenLP.MainWindow', '&ProjectorManager'))
+        self.view_projector_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Toggle Projector Manager'))
+        self.view_projector_manager_item.setStatusTip(translate('OpenLP.MainWindow',
+                                                                'Toggle the visibility of the Projector Manager'))
         self.view_media_manager_item.setText(translate('OpenLP.MainWindow', '&Media Manager'))
         self.view_media_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Toggle Media Manager'))
         self.view_media_manager_item.setStatusTip(translate('OpenLP.MainWindow',
@@ -485,6 +514,7 @@
         self.service_manager_settings_section = 'servicemanager'
         self.songs_settings_section = 'songs'
         self.themes_settings_section = 'themes'
+        self.projector_settings_section = 'projector'
         self.players_settings_section = 'players'
         self.display_tags_section = 'displayTags'
         self.header_section = 'SettingsImport'
@@ -514,6 +544,7 @@
         self.media_manager_dock.visibilityChanged.connect(self.view_media_manager_item.setChecked)
         self.service_manager_dock.visibilityChanged.connect(self.view_service_manager_item.setChecked)
         self.theme_manager_dock.visibilityChanged.connect(self.view_theme_manager_item.setChecked)
+        self.projector_manager_dock.visibilityChanged.connect(self.view_projector_manager_item.setChecked)
         self.import_theme_item.triggered.connect(self.theme_manager_contents.on_import_theme)
         self.export_theme_item.triggered.connect(self.theme_manager_contents.on_export_theme)
         self.web_site_item.triggered.connect(self.on_help_web_site_clicked)
@@ -826,6 +857,7 @@
         setting_sections.extend([self.shortcuts_settings_section])
         setting_sections.extend([self.service_manager_settings_section])
         setting_sections.extend([self.themes_settings_section])
+        setting_sections.extend([self.projector_settings_section])
         setting_sections.extend([self.players_settings_section])
         setting_sections.extend([self.display_tags_section])
         setting_sections.extend([self.header_section])
@@ -1115,6 +1147,12 @@
         """
         self.media_manager_dock.setVisible(not self.media_manager_dock.isVisible())
 
+    def toggle_projector_manager(self):
+        """
+        Toggle visibility of the projector manager
+        """
+        self.projector_manager_dock.setVisible(not self.projector_manager_dock.isVisible())
+
     def toggle_service_manager(self):
         """
         Toggle the visibility of the service manager

=== added directory 'openlp/core/ui/projector'
=== added file 'openlp/core/ui/projector/editform.py'
--- openlp/core/ui/projector/editform.py	1970-01-01 00:00:00 +0000
+++ openlp/core/ui/projector/editform.py	2014-10-28 19:17:32 +0000
@@ -0,0 +1,269 @@
+
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
+# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder,               #
+# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble,             #
+# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann           #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+    :mod: `openlp.core.ui.projector.editform` module
+
+    Provides the functions for adding/editing entries in the projector database.
+"""
+
+import logging
+log = logging.getLogger(__name__)
+log.debug('editform loaded')
+
+from PyQt4 import QtCore, QtGui
+from PyQt4.QtCore import pyqtSlot, pyqtSignal
+from PyQt4.QtGui import QDialog, QPlainTextEdit, QLineEdit, QDialogButtonBox, QLabel, QGridLayout
+
+from openlp.core.common import translate, verify_ip_address
+from openlp.core.lib import build_icon
+from openlp.core.lib.projector.db import Projector
+from openlp.core.lib.projector.constants import PJLINK_PORT
+
+
+class Ui_ProjectorEditForm(object):
+    """
+    The :class:`~openlp.core.lib.ui.projector.editform.Ui_ProjectorEditForm` class defines
+    the user interface for the ProjectorEditForm dialog.
+    """
+    def setupUi(self, edit_projector_dialog):
+        """
+        Create the interface layout.
+        """
+        edit_projector_dialog.setObjectName('edit_projector_dialog')
+        edit_projector_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo-32x32.png'))
+        edit_projector_dialog.setMinimumWidth(400)
+        edit_projector_dialog.setModal(True)
+        # Define the basic layout
+        self.dialog_layout = QGridLayout(edit_projector_dialog)
+        self.dialog_layout.setObjectName('dialog_layout')
+        self.dialog_layout.setSpacing(8)
+        self.dialog_layout.setContentsMargins(8, 8, 8, 8)
+        # IP Address
+        self.ip_label = QLabel(edit_projector_dialog)
+        self.ip_label.setObjectName('projector_edit_ip_label')
+        self.ip_text = QLineEdit(edit_projector_dialog)
+        self.ip_text.setObjectName('projector_edit_ip_text')
+        self.dialog_layout.addWidget(self.ip_label, 0, 0)
+        self.dialog_layout.addWidget(self.ip_text, 0, 1)
+        # Port number
+        self.port_label = QLabel(edit_projector_dialog)
+        self.port_label.setObjectName('projector_edit_ip_label')
+        self.port_text = QLineEdit(edit_projector_dialog)
+        self.port_text.setObjectName('projector_edit_port_text')
+        self.dialog_layout.addWidget(self.port_label, 1, 0)
+        self.dialog_layout.addWidget(self.port_text, 1, 1)
+        # PIN
+        self.pin_label = QLabel(edit_projector_dialog)
+        self.pin_label.setObjectName('projector_edit_pin_label')
+        self.pin_text = QLineEdit(edit_projector_dialog)
+        self.pin_label.setObjectName('projector_edit_pin_text')
+        self.dialog_layout.addWidget(self.pin_label, 2, 0)
+        self.dialog_layout.addWidget(self.pin_text, 2, 1)
+        # Name
+        self.name_label = QLabel(edit_projector_dialog)
+        self.name_label.setObjectName('projector_edit_name_label')
+        self.name_text = QLineEdit(edit_projector_dialog)
+        self.name_text.setObjectName('projector_edit_name_text')
+        self.dialog_layout.addWidget(self.name_label, 3, 0)
+        self.dialog_layout.addWidget(self.name_text, 3, 1)
+        # Location
+        self.location_label = QLabel(edit_projector_dialog)
+        self.location_label.setObjectName('projector_edit_location_label')
+        self.location_text = QLineEdit(edit_projector_dialog)
+        self.location_text.setObjectName('projector_edit_location_text')
+        self.dialog_layout.addWidget(self.location_label, 4, 0)
+        self.dialog_layout.addWidget(self.location_text, 4, 1)
+        # Notes
+        self.notes_label = QLabel(edit_projector_dialog)
+        self.notes_label.setObjectName('projector_edit_notes_label')
+        self.notes_text = QPlainTextEdit(edit_projector_dialog)
+        self.notes_text.setObjectName('projector_edit_notes_text')
+        self.dialog_layout.addWidget(self.notes_label, 5, 0, alignment=QtCore.Qt.AlignTop)
+        self.dialog_layout.addWidget(self.notes_text, 5, 1)
+        # Time for the buttons
+        self.button_box = QDialogButtonBox(QDialogButtonBox.Help |
+                                           QDialogButtonBox.Save |
+                                           QDialogButtonBox.Cancel)
+        self.dialog_layout.addWidget(self.button_box, 8, 0, 1, 2)
+
+    def retranslateUi(self, edit_projector_dialog):
+        if self.new_projector:
+            title = translate('OpenLP.ProjectorEditForm', 'Add New Projector')
+            self.projector.port = PJLINK_PORT
+        else:
+            title = translate('OpenLP.ProjectorEditForm', 'Edit Projector')
+        edit_projector_dialog.setWindowTitle(title)
+        self.ip_label.setText(translate('OpenLP.ProjectorEditForm', 'IP Address'))
+        self.ip_text.setText(self.projector.ip)
+        self.ip_text.setFocus()
+        self.port_label.setText(translate('OpenLP.ProjectorEditForm', 'Port Number'))
+        self.port_text.setText(str(self.projector.port))
+        self.pin_label.setText(translate('OpenLP.ProjectorEditForm', 'PIN'))
+        self.pin_text.setText(self.projector.pin)
+        self.name_label.setText(translate('OpenLP.ProjectorEditForm', 'Name'))
+        self.name_text.setText(self.projector.name)
+        self.location_label.setText(translate('OpenLP.ProjectorEditForm', 'Location'))
+        self.location_text.setText(self.projector.location)
+        self.notes_label.setText(translate('OpenLP.ProjectorEditForm', 'Notes'))
+        self.notes_text.insertPlainText(self.projector.notes)
+
+
+class ProjectorEditForm(QDialog, Ui_ProjectorEditForm):
+    """
+    Class to add or edit a projector entry in the database.
+
+    Fields that are editable:
+        ip = Column(String(100))
+        port = Column(String(8))
+        pin = Column(String(20))
+        name = Column(String(20))
+        location = Column(String(30))
+        notes = Column(String(200))
+    """
+    newProjector = pyqtSignal(str)
+    editProjector = pyqtSignal(object)
+
+    def __init__(self, parent=None, projectordb=None):
+        super(ProjectorEditForm, self).__init__(parent=parent)
+        self.projectordb = projectordb
+        self.setupUi(self)
+        self.button_box.accepted.connect(self.accept_me)
+        self.button_box.helpRequested.connect(self.help_me)
+        self.button_box.rejected.connect(self.cancel_me)
+
+    def exec_(self, projector=None):
+        if projector is None:
+            self.projector = Projector()
+            self.new_projector = True
+        else:
+            self.projector = projector
+            self.new_projector = False
+        self.retranslateUi(self)
+        reply = QDialog.exec_(self)
+        self.projector = None
+        return reply
+
+    @pyqtSlot()
+    def accept_me(self):
+        """
+        Validate input before accepting input.
+        """
+        log.debug('accept_me() signal received')
+        if len(self.name_text.text().strip()) < 1:
+            QtGui.QMessageBox.warning(self,
+                                      translate('OpenLP.ProjectorEdit', 'Name Not Set'),
+                                      translate('OpenLP.ProjectorEdit',
+                                                'You must enter a name for this entry.<br />'
+                                                'Please enter a new name for this entry.'))
+            valid = False
+            return
+        name = self.name_text.text().strip()
+        record = self.projectordb.get_projector_by_name(name)
+        if record is not None and record.id != self.projector.id:
+            QtGui.QMessageBox.warning(self,
+                                      translate('OpenLP.ProjectorEdit', 'Duplicate Name'),
+                                      translate('OpenLP.ProjectorEdit',
+                                                'There is already an entry with name "%s" in '
+                                                'the database as ID "%s". <br />'
+                                                'Please enter a different name.' % (name, record.id)))
+            valid = False
+            return
+        adx = self.ip_text.text()
+        valid = verify_ip_address(adx)
+        if valid:
+            ip = self.projectordb.get_projector_by_ip(adx)
+            if ip is None:
+                valid = True
+                self.new_projector = True
+            elif ip.id != self.projector.id:
+                QtGui.QMessageBox.warning(self,
+                                          translate('OpenLP.ProjectorWizard', 'Duplicate IP Address'),
+                                          translate('OpenLP.ProjectorWizard',
+                                                    'IP address "%s"<br />is already in the database as ID %s.'
+                                                    '<br /><br />Please Enter a different IP address.' % (adx, ip.id)))
+                valid = False
+                return
+        else:
+            QtGui.QMessageBox.warning(self,
+                                      translate('OpenLP.ProjectorWizard', 'Invalid IP Address'),
+                                      translate('OpenLP.ProjectorWizard',
+                                                'IP address "%s"<br>is not a valid IP address.'
+                                                '<br /><br />Please enter a valid IP address.' % adx))
+            valid = False
+            return
+        port = int(self.port_text.text())
+        if port < 1000 or port > 32767:
+            QtGui.QMessageBox.warning(self,
+                                      translate('OpenLP.ProjectorWizard', 'Invalid Port Number'),
+                                      translate('OpenLP.ProjectorWizard',
+                                                'Port numbers below 1000 are reserved for admin use only, '
+                                                '<br />and port numbers above 32767 are not currently usable.'
+                                                '<br /><br />Please enter a valid port number between '
+                                                ' 1000 and 32767.'
+                                                '<br /><br />Default PJLink port is %s' % PJLINK_PORT))
+            valid = False
+        if valid:
+            self.projector.ip = self.ip_text.text()
+            self.projector.pin = self.pin_text.text()
+            self.projector.port = int(self.port_text.text())
+            self.projector.name = self.name_text.text()
+            self.projector.location = self.location_text.text()
+            self.projector.notes = self.notes_text.toPlainText()
+            if self.new_projector:
+                saved = self.projectordb.add_projector(self.projector)
+            else:
+                saved = self.projectordb.update_projector(self.projector)
+            if not saved:
+                QtGui.QMessageBox.warning(self,
+                                          translate('OpenLP.ProjectorEditForm', 'Database Error'),
+                                          translate('OpenLP.ProjectorEditForm',
+                                                    'There was an error saving projector '
+                                                    'information. See the log for the error'))
+                return saved
+            if self.new_projector:
+                self.newProjector.emit(adx)
+            else:
+                self.editProjector.emit(self.projector)
+            self.close()
+
+    @pyqtSlot()
+    def help_me(self):
+        """
+        Show a help message about the input fields.
+        """
+        log.debug('help_me() signal received')
+
+    @pyqtSlot()
+    def cancel_me(self):
+        """
+        Cancel button clicked - just close.
+        """
+        log.debug('cancel_me() signal received')
+        self.close()

=== added file 'openlp/core/ui/projector/manager.py'
--- openlp/core/ui/projector/manager.py	1970-01-01 00:00:00 +0000
+++ openlp/core/ui/projector/manager.py	2014-10-28 19:17:32 +0000
@@ -0,0 +1,983 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
+# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder,               #
+# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble,             #
+# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann           #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+    :mod: openlp.core.ui.projector.manager` module
+
+    Provides the functions for the display/control of Projectors.
+"""
+
+import logging
+log = logging.getLogger(__name__)
+log.debug('projectormanager loaded')
+
+from PyQt4 import QtCore, QtGui
+from PyQt4.QtCore import QObject, QThread, pyqtSlot
+from PyQt4.QtGui import QWidget
+
+from openlp.core.common import RegistryProperties, Settings, OpenLPMixin, \
+    RegistryMixin, translate
+from openlp.core.lib import OpenLPToolbar
+from openlp.core.lib.ui import create_widget_action
+from openlp.core.lib.projector import DialogSourceStyle
+from openlp.core.lib.projector.constants import *
+from openlp.core.lib.projector.db import ProjectorDB
+from openlp.core.lib.projector.pjlink1 import PJLink1
+from openlp.core.ui.projector.editform import ProjectorEditForm
+from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle
+
+# Dict for matching projector status to display icon
+STATUS_ICONS = {S_NOT_CONNECTED:  ':/projector/projector_item_disconnect.png',
+                S_CONNECTING:  ':/projector/projector_item_connect.png',
+                S_CONNECTED:  ':/projector/projector_off.png',
+                S_OFF:  ':/projector/projector_off.png',
+                S_INITIALIZE:  ':/projector/projector_off.png',
+                S_STANDBY:  ':/projector/projector_off.png',
+                S_WARMUP:  ':/projector/projector_warmup.png',
+                S_ON:  ':/projector/projector_on.png',
+                S_COOLDOWN:  ':/projector/projector_cooldown.png',
+                E_ERROR:  ':/projector/projector_error.png',
+                E_NETWORK:  ':/projector/projector_not_connected_error.png',
+                E_AUTHENTICATION:  ':/projector/projector_not_connected_error.png',
+                E_UNKNOWN_SOCKET_ERROR: ':/projector/projector_not_connected_error.png',
+                E_NOT_CONNECTED: ':/projector/projector_not_connected_error.png'
+                }
+
+
+class Ui_ProjectorManager(object):
+    """
+    UI part of the Projector Manager
+    """
+    def setup_ui(self, widget):
+        """
+        Define the UI
+
+        :param widget: The screen object the dialog is to be attached to.
+        """
+        log.debug('setup_ui()')
+        # Create ProjectorManager box
+        self.layout = QtGui.QVBoxLayout(widget)
+        self.layout.setSpacing(0)
+        self.layout.setMargin(0)
+        self.layout.setObjectName('layout')
+        # Add one selection toolbar
+        self.one_toolbar = OpenLPToolbar(widget)
+        self.one_toolbar.add_toolbar_action('new_projector',
+                                            text=translate('OpenLP.ProjectorManager', 'Add Projector'),
+                                            icon=':/projector/projector_new.png',
+                                            tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'),
+                                            triggers=self.on_add_projector)
+        # Show edit/delete when projector not connected
+        self.one_toolbar.add_toolbar_action('edit_projector',
+                                            text=translate('OpenLP.ProjectorManager', 'Edit Projector'),
+                                            icon=':/general/general_edit.png',
+                                            tooltip=translate('OpenLP.ProjectorManager', 'Edit selected projector'),
+                                            triggers=self.on_edit_projector)
+        self.one_toolbar.add_toolbar_action('delete_projector',
+                                            text=translate('OpenLP.ProjectorManager', 'Delete Projector'),
+                                            icon=':/general/general_delete.png',
+                                            tooltip=translate('OpenLP.ProjectorManager', 'Delete selected projector'),
+                                            triggers=self.on_delete_projector)
+        # Show source/view when projector connected
+        self.one_toolbar.add_toolbar_action('source_view_projector',
+                                            text=translate('OpenLP.ProjectorManager', 'Select Input Source'),
+                                            icon=':/projector/projector_hdmi.png',
+                                            tooltip=translate('OpenLP.ProjectorManager',
+                                                              'Choose input source on selected projector'),
+                                            triggers=self.on_select_input)
+        self.one_toolbar.add_toolbar_action('view_projector',
+                                            text=translate('OpenLP.ProjectorManager', 'View Projector'),
+                                            icon=':/system/system_about.png',
+                                            tooltip=translate('OpenLP.ProjectorManager',
+                                                              'View selected projector information'),
+                                            triggers=self.on_status_projector)
+        self.one_toolbar.addSeparator()
+        self.one_toolbar.add_toolbar_action('connect_projector',
+                                            text=translate('OpenLP.ProjectorManager',
+                                                           'Connect to selected projector'),
+                                            icon=':/projector/projector_connect.png',
+                                            tootip=translate('OpenLP.ProjectorManager',
+                                                             'Connect to selected projector'),
+                                            triggers=self.on_connect_projector)
+        self.one_toolbar.add_toolbar_action('connect_projector_multiple',
+                                            text=translate('OpenLP.ProjectorManager',
+                                                           'Connect to selected projectors'),
+                                            icon=':/projector/projector_connect_tiled.png',
+                                            tootip=translate('OpenLP.ProjectorManager',
+                                                             'Connect to selected projector'),
+                                            triggers=self.on_connect_projector)
+        self.one_toolbar.add_toolbar_action('disconnect_projector',
+                                            text=translate('OpenLP.ProjectorManager',
+                                                           'Disconnect from selected projectors'),
+                                            icon=':/projector/projector_disconnect.png',
+                                            tooltip=translate('OpenLP.ProjectorManager',
+                                                              'Disconnect from selected projector'),
+                                            triggers=self.on_disconnect_projector)
+        self.one_toolbar.add_toolbar_action('disconnect_projector_multiple',
+                                            text=translate('OpenLP.ProjectorManager',
+                                                           'Disconnect from selected projector'),
+                                            icon=':/projector/projector_disconnect_tiled.png',
+                                            tooltip=translate('OpenLP.ProjectorManager',
+                                                              'Disconnect from selected projector'),
+                                            triggers=self.on_disconnect_projector)
+        self.one_toolbar.addSeparator()
+        self.one_toolbar.add_toolbar_action('poweron_projector',
+                                            text=translate('OpenLP.ProjectorManager',
+                                                           'Power on selected projector'),
+                                            icon=':/projector/projector_power_on.png',
+                                            tooltip=translate('OpenLP.ProjectorManager',
+                                                              'Power on selected projector'),
+                                            triggers=self.on_poweron_projector)
+        self.one_toolbar.add_toolbar_action('poweron_projector_multiple',
+                                            text=translate('OpenLP.ProjectorManager',
+                                                           'Power on selected projector'),
+                                            icon=':/projector/projector_power_on_tiled.png',
+                                            tooltip=translate('OpenLP.ProjectorManager',
+                                                              'Power on selected projector'),
+                                            triggers=self.on_poweron_projector)
+        self.one_toolbar.add_toolbar_action('poweroff_projector',
+                                            text=translate('OpenLP.ProjectorManager', 'Standby selected projector'),
+                                            icon=':/projector/projector_power_off.png',
+                                            tooltip=translate('OpenLP.ProjectorManager',
+                                                              'Put selected projector in standby'),
+                                            triggers=self.on_poweroff_projector)
+        self.one_toolbar.add_toolbar_action('poweroff_projector_multiple',
+                                            text=translate('OpenLP.ProjectorManager', 'Standby selected projector'),
+                                            icon=':/projector/projector_power_off_tiled.png',
+                                            tooltip=translate('OpenLP.ProjectorManager',
+                                                              'Put selected projector in standby'),
+                                            triggers=self.on_poweroff_projector)
+        self.one_toolbar.addSeparator()
+        self.one_toolbar.add_toolbar_action('blank_projector',
+                                            text=translate('OpenLP.ProjectorManager',
+                                                           'Blank selected projector screen'),
+                                            icon=':/projector/projector_blank.png',
+                                            tooltip=translate('OpenLP.ProjectorManager',
+                                                              'Blank selected projector screen'),
+                                            triggers=self.on_blank_projector)
+        self.one_toolbar.add_toolbar_action('blank_projector_multiple',
+                                            text=translate('OpenLP.ProjectorManager',
+                                                           'Blank selected projector screen'),
+                                            icon=':/projector/projector_blank_tiled.png',
+                                            tooltip=translate('OpenLP.ProjectorManager',
+                                                              'Blank selected projector screen'),
+                                            triggers=self.on_blank_projector)
+        self.one_toolbar.add_toolbar_action('show_projector',
+                                            ext=translate('OpenLP.ProjectorManager',
+                                                          'Show selected projector screen'),
+                                            icon=':/projector/projector_show.png',
+                                            tooltip=translate('OpenLP.ProjectorManager',
+                                                              'Show selected projector screen'),
+                                            triggers=self.on_show_projector)
+        self.one_toolbar.add_toolbar_action('show_projector_multiple',
+                                            ext=translate('OpenLP.ProjectorManager',
+                                                          'Show selected projector screen'),
+                                            icon=':/projector/projector_show_tiled.png',
+                                            tooltip=translate('OpenLP.ProjectorManager',
+                                                              'Show selected projector screen'),
+                                            triggers=self.on_show_projector)
+        self.layout.addWidget(self.one_toolbar)
+        self.projector_one_widget = QtGui.QWidgetAction(self.one_toolbar)
+        self.projector_one_widget.setObjectName('projector_one_toolbar_widget')
+        # Create projector manager list
+        self.projector_list_widget = QtGui.QListWidget(widget)
+        self.projector_list_widget.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
+        self.projector_list_widget.setAlternatingRowColors(True)
+        self.projector_list_widget.setIconSize(QtCore.QSize(90, 50))
+        self.projector_list_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+        self.projector_list_widget.setObjectName('projector_list_widget')
+        self.layout.addWidget(self.projector_list_widget)
+        self.projector_list_widget.customContextMenuRequested.connect(self.context_menu)
+        self.projector_list_widget.itemDoubleClicked.connect(self.on_doubleclick_item)
+        # Build the context menu
+        self.menu = QtGui.QMenu()
+        self.status_action = create_widget_action(self.menu,
+                                                  text=translate('OpenLP.ProjectorManager',
+                                                                 '&View Projector Information'),
+                                                  icon=':/system/system_about.png',
+                                                  triggers=self.on_status_projector)
+        self.edit_action = create_widget_action(self.menu,
+                                                text=translate('OpenLP.ProjectorManager',
+                                                               '&Edit Projector'),
+                                                icon=':/projector/projector_edit.png',
+                                                triggers=self.on_edit_projector)
+        self.menu.addSeparator()
+        self.connect_action = create_widget_action(self.menu,
+                                                   text=translate('OpenLP.ProjectorManager',
+                                                                  '&Connect Projector'),
+                                                   icon=':/projector/projector_connect.png',
+                                                   triggers=self.on_connect_projector)
+        self.disconnect_action = create_widget_action(self.menu,
+                                                      text=translate('OpenLP.ProjectorManager',
+                                                                     'D&isconnect Projector'),
+                                                      icon=':/projector/projector_disconnect.png',
+                                                      triggers=self.on_disconnect_projector)
+        self.menu.addSeparator()
+        self.poweron_action = create_widget_action(self.menu,
+                                                   text=translate('OpenLP.ProjectorManager',
+                                                                  'Power &On Projector'),
+                                                   icon=':/projector/projector_power_on.png',
+                                                   triggers=self.on_poweron_projector)
+        self.poweroff_action = create_widget_action(self.menu,
+                                                    text=translate('OpenLP.ProjectorManager',
+                                                                   'Power O&ff Projector'),
+                                                    icon=':/projector/projector_power_off.png',
+                                                    triggers=self.on_poweroff_projector)
+        self.menu.addSeparator()
+        self.select_input_action = create_widget_action(self.menu,
+                                                        text=translate('OpenLP.ProjectorManager',
+                                                                       'Select &Input'),
+                                                        icon=':/projector/projector_hdmi.png',
+                                                        triggers=self.on_select_input)
+        self.edit_input_action = create_widget_action(self.menu,
+                                                      text=translate('OpenLP.ProjectorManager',
+                                                                     'Edit Input Source'),
+                                                      icon=':/general/general_edit.png',
+                                                      triggers=self.on_edit_input)
+        self.blank_action = create_widget_action(self.menu,
+                                                 text=translate('OpenLP.ProjectorManager',
+                                                                '&Blank Projector Screen'),
+                                                 icon=':/projector/projector_blank.png',
+                                                 triggers=self.on_blank_projector)
+        self.show_action = create_widget_action(self.menu,
+                                                text=translate('OpenLP.ProjectorManager',
+                                                               '&Show Projector Screen'),
+                                                icon=':/projector/projector_show.png',
+                                                triggers=self.on_show_projector)
+        self.menu.addSeparator()
+        self.delete_action = create_widget_action(self.menu,
+                                                  text=translate('OpenLP.ProjectorManager',
+                                                                 '&Delete Projector'),
+                                                  icon=':/general/general_delete.png',
+                                                  triggers=self.on_delete_projector)
+        self.update_icons()
+
+
+class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, RegistryProperties):
+    """
+    Manage the projectors.
+    """
+    def __init__(self, parent=None, projectordb=None):
+        """
+        Basic initialization.
+
+        :param parent: Who I belong to.
+        :param projectordb: Database session inherited from superclass.
+        """
+        log.debug('__init__()')
+        super().__init__(parent)
+        self.settings_section = 'projector'
+        self.projectordb = projectordb
+        self.projector_list = []
+        self.source_select_form = None
+
+    def bootstrap_initialise(self):
+        """
+        Pre-initialize setups.
+        """
+        self.setup_ui(self)
+        if self.projectordb is None:
+            # Work around for testing creating a ~/.openlp.data.projector.projector.sql file
+            log.debug('Creating new ProjectorDB() instance')
+            self.projectordb = ProjectorDB()
+        else:
+            log.debug('Using existing ProjectorDB() instance')
+        self.get_settings()
+
+    def bootstrap_post_set_up(self):
+        """
+        Post-initialize setups.
+        """
+        # Set 1.5 second delay before loading all projectors
+        if self.autostart:
+            log.debug('Delaying 1.5 seconds before loading all projectors')
+            QtCore.QTimer().singleShot(1500, self._load_projectors)
+        else:
+            log.debug('Loading all projectors')
+            self._load_projectors()
+        self.projector_form = ProjectorEditForm(self, projectordb=self.projectordb)
+        self.projector_form.newProjector.connect(self.add_projector_from_wizard)
+        self.projector_form.editProjector.connect(self.edit_projector_from_wizard)
+        self.projector_list_widget.itemSelectionChanged.connect(self.update_icons)
+
+    def get_settings(self):
+        """
+        Retrieve the saved settings
+        """
+        settings = Settings()
+        settings.beginGroup(self.settings_section)
+        self.autostart = settings.value('connect on start')
+        self.poll_time = settings.value('poll time')
+        self.socket_timeout = settings.value('socket timeout')
+        self.source_select_dialog_type = settings.value('source dialog type')
+        settings.endGroup()
+        del settings
+
+    def context_menu(self, point):
+        """
+        Build the Right Click Context menu and set state.
+
+        :param point: The position of the mouse so the correct item can be found.
+        """
+        # QListWidgetItem to build menu for.
+        item = self.projector_list_widget.itemAt(point)
+        if item is None:
+            return
+        real_projector = item.data(QtCore.Qt.UserRole)
+        projector_name = str(item.text())
+        visible = real_projector.link.status_connect >= S_CONNECTED
+        log.debug('(%s) Building menu - visible = %s' % (projector_name, visible))
+        self.delete_action.setVisible(True)
+        self.edit_action.setVisible(True)
+        self.connect_action.setVisible(not visible)
+        self.disconnect_action.setVisible(visible)
+        self.status_action.setVisible(visible)
+        if visible:
+            self.select_input_action.setVisible(real_projector.link.power == S_ON)
+            self.edit_input_action.setVisible(real_projector.link.power == S_ON)
+            self.poweron_action.setVisible(real_projector.link.power == S_STANDBY)
+            self.poweroff_action.setVisible(real_projector.link.power == S_ON)
+            self.blank_action.setVisible(real_projector.link.power == S_ON and
+                                         not real_projector.link.shutter)
+            self.show_action.setVisible(real_projector.link.power == S_ON and
+                                        real_projector.link.shutter)
+        else:
+            self.select_input_action.setVisible(False)
+            self.edit_input_action.setVisible(False)
+            self.poweron_action.setVisible(False)
+            self.poweroff_action.setVisible(False)
+            self.blank_action.setVisible(False)
+            self.show_action.setVisible(False)
+        self.menu.projector = real_projector
+        self.menu.exec_(self.projector_list_widget.mapToGlobal(point))
+
+    def on_edit_input(self, opt=None):
+        self.on_select_input(opt=opt, edit=True)
+
+    def on_select_input(self, opt=None, edit=False):
+        """
+        Builds menu for 'Select Input' option, then calls the selected projector
+        item to change input source.
+
+        :param opt: Needed by PyQt4
+        """
+        self.get_settings()  # In case the dialog interface setting was changed
+        list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow())
+        projector = list_item.data(QtCore.Qt.UserRole)
+        # QTabwidget for source select
+        source = 100
+        while source > 99:
+            if self.source_select_dialog_type == DialogSourceStyle.Tabbed:
+                source_select_form = SourceSelectTabs(parent=self,
+                                                      projectordb=self.projectordb,
+                                                      edit=edit)
+            else:
+                source_select_form = SourceSelectSingle(parent=self,
+                                                        projectordb=self.projectordb,
+                                                        edit=edit)
+            source = source_select_form.exec_(projector.link)
+        log.debug('(%s) source_select_form() returned %s' % (projector.link.ip, source))
+        if source is not None and source > 0:
+            projector.link.set_input_source(str(source))
+        return
+
+    def on_add_projector(self, opt=None):
+        """
+        Calls edit dialog to add a new projector to the database
+
+        :param opt: Needed by PyQt4
+        """
+        self.projector_form.exec_()
+
+    def on_blank_projector(self, opt=None):
+        """
+        Calls projector thread to send blank screen command
+
+        :param opt: Needed by PyQt4
+        """
+        try:
+            ip = opt.link.ip
+            projector = opt
+            projector.link.set_shutter_closed()
+        except AttributeError:
+            for list_item in self.projector_list_widget.selectedItems():
+                if list_item is None:
+                    return
+                projector = list_item.data(QtCore.Qt.UserRole)
+                try:
+                    projector.link.set_shutter_closed()
+                except:
+                    continue
+
+    def on_doubleclick_item(self, item, opt=None):
+        """
+        When item is doubleclicked, will connect to projector.
+
+        :param item: List widget item for connection.
+        :param opt: Needed by PyQt4
+        """
+        projector = item.data(QtCore.Qt.UserRole)
+        if projector.link.state() != projector.link.ConnectedState:
+            try:
+                projector.link.connect_to_host()
+            except:
+                pass
+        return
+
+    def on_connect_projector(self, opt=None):
+        """
+        Calls projector thread to connect to projector
+
+        :param opt: Needed by PyQt4
+        """
+        try:
+            ip = opt.link.ip
+            projector = opt
+            projector.link.connect_to_host()
+        except AttributeError:
+            for list_item in self.projector_list_widget.selectedItems():
+                if list_item is None:
+                    return
+                projector = list_item.data(QtCore.Qt.UserRole)
+                try:
+                    projector.link.connect_to_host()
+                except:
+                    continue
+
+    def on_delete_projector(self, opt=None):
+        """
+        Deletes a projector from the list and the database
+
+        :param opt: Needed by PyQt4
+        """
+        list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow())
+        if list_item is None:
+            return
+        projector = list_item.data(QtCore.Qt.UserRole)
+        msg = QtGui.QMessageBox()
+        msg.setText('Delete projector (%s) %s?' % (projector.link.ip, projector.link.name))
+        msg.setInformativeText('Are you sure you want to delete this projector?')
+        msg.setStandardButtons(msg.Cancel | msg.Ok)
+        msg.setDefaultButton(msg.Cancel)
+        ans = msg.exec_()
+        if ans == msg.Cancel:
+            return
+        try:
+            projector.link.projectorNetwork.disconnect(self.update_status)
+        except (AttributeError, TypeError):
+            pass
+        try:
+            projector.link.changeStatus.disconnect(self.update_status)
+        except (AttributeError, TypeError):
+            pass
+        try:
+            projector.link.authentication_error.disconnect(self.authentication_error)
+        except (AttributeError, TypeError):
+            pass
+        try:
+            projector.link.no_authentication_error.disconnect(self.no_authentication_error)
+        except (AttributeError, TypeError):
+            pass
+        try:
+            projector.link.projectorUpdateIcons.disconnect(self.update_icons)
+        except (AttributeError, TypeError):
+            pass
+        try:
+            projector.timer.stop()
+            projector.timer.timeout.disconnect(projector.link.poll_loop)
+        except (AttributeError, TypeError):
+            pass
+        try:
+            projector.socket_timer.stop()
+            projector.socket_timer.timeout.disconnect(projector.link.socket_abort)
+        except (AttributeError, TypeError):
+            pass
+        projector.thread.quit()
+        new_list = []
+        for item in self.projector_list:
+            if item.link.dbid == projector.link.dbid:
+                continue
+            new_list.append(item)
+        self.projector_list = new_list
+        list_item = self.projector_list_widget.takeItem(self.projector_list_widget.currentRow())
+        list_item = None
+        deleted = self.projectordb.delete_projector(projector.db_item)
+        for item in self.projector_list:
+            log.debug('New projector list - item: %s %s' % (item.link.ip, item.link.name))
+
+    def on_disconnect_projector(self, opt=None):
+        """
+        Calls projector thread to disconnect from projector
+
+        :param opt: Needed by PyQt4
+        """
+        try:
+            ip = opt.link.ip
+            projector = opt
+            projector.link.disconnect_from_host()
+        except AttributeError:
+            for list_item in self.projector_list_widget.selectedItems():
+                if list_item is None:
+                    return
+                projector = list_item.data(QtCore.Qt.UserRole)
+                try:
+                    projector.link.disconnect_from_host()
+                except:
+                    continue
+
+    def on_edit_projector(self, opt=None):
+        """
+        Calls edit dialog with selected projector to edit information
+
+        :param opt: Needed by PyQt4
+        """
+        list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow())
+        projector = list_item.data(QtCore.Qt.UserRole)
+        if projector is None:
+            return
+        self.old_projector = projector
+        projector.link.disconnect_from_host()
+        record = self.projectordb.get_projector_by_ip(projector.link.ip)
+        self.projector_form.exec_(record)
+        new_record = self.projectordb.get_projector_by_id(record.id)
+
+    def on_poweroff_projector(self, opt=None):
+        """
+        Calls projector link to send Power Off command
+
+        :param opt: Needed by PyQt4
+        """
+        try:
+            ip = opt.link.ip
+            projector = opt
+            projector.link.set_power_off()
+        except AttributeError:
+            for list_item in self.projector_list_widget.selectedItems():
+                if list_item is None:
+                    return
+                projector = list_item.data(QtCore.Qt.UserRole)
+                try:
+                    projector.link.set_power_off()
+                except:
+                    continue
+
+    def on_poweron_projector(self, opt=None):
+        """
+        Calls projector link to send Power On command
+
+        :param opt: Needed by PyQt4
+        """
+        try:
+            ip = opt.link.ip
+            projector = opt
+            projector.link.set_power_on()
+        except AttributeError:
+            for list_item in self.projector_list_widget.selectedItems():
+                if list_item is None:
+                    return
+                projector = list_item.data(QtCore.Qt.UserRole)
+                try:
+                    projector.link.set_power_on()
+                except:
+                    continue
+
+    def on_show_projector(self, opt=None):
+        """
+        Calls projector thread to send open shutter command
+
+        :param opt: Needed by PyQt4
+        """
+        try:
+            ip = opt.link.ip
+            projector = opt
+            projector.link.set_shutter_open()
+        except AttributeError:
+            for list_item in self.projector_list_widget.selectedItems():
+                if list_item is None:
+                    return
+                projector = list_item.data(QtCore.Qt.UserRole)
+                try:
+                    projector.link.set_shutter_open()
+                except:
+                    continue
+
+    def on_status_projector(self, opt=None):
+        """
+        Builds message box with projector status information
+
+        :param opt: Needed by PyQt4
+        """
+        lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow())
+        projector = lwi.data(QtCore.Qt.UserRole)
+        message = '<b>%s</b>: %s<BR />' % (translate('OpenLP.ProjectorManager', 'Name'),
+                                           projector.link.name)
+        message = '%s<b>%s</b>: %s<br />' % (message, translate('OpenLP.ProjectorManager', 'IP'),
+                                             projector.link.ip)
+        message = '%s<b>%s</b>: %s<br />' % (message, translate('OpenLP.ProjectorManager', 'Port'),
+                                             projector.link.port)
+        message = '%s<b>%s</b>: %s<br />' % (message, translate('OpenLP.ProjectorManager', 'Notes'),
+                                             projector.link.notes)
+        message = '%s<hr /><br >' % message
+        if projector.link.manufacturer is None:
+            message = '%s%s' % (message, translate('OpenLP.ProjectorManager',
+                                                   'Projector information not available at this time.'))
+        else:
+            message = '%s<b>%s</b>: %s<BR />' % (message, translate('OpenLP.ProjectorManager', 'Projector Name'),
+                                                 projector.link.pjlink_name)
+            message = '%s<b>%s</b>: %s<br />' % (message, translate('OpenLP.ProjectorManager', 'Manufacturer'),
+                                                 projector.link.manufacturer)
+            message = '%s<b>%s</b>: %s<br />' % (message, translate('OpenLP.ProjectorManager', 'Model'),
+                                                 projector.link.model)
+            message = '%s<b>%s</b>: %s<br /><br />' % (message, translate('OpenLP.ProjectorManager', 'Other info'),
+                                                       projector.link.other_info)
+            message = '%s<b>%s</b>: %s<br />' % (message, translate('OpenLP.ProjectorManager', 'Power status'),
+                                                 ERROR_MSG[projector.link.power])
+            message = '%s<b>%s</b>: %s<br />' % (message, translate('OpenLP.ProjectorManager', 'Shutter is'),
+                                                 translate('OpenLP.ProjectorManager', 'Closed')
+                                                 if projector.link.shutter else translate('OpenLP', 'Open'))
+            message = '%s<b>%s</b>: %s<br />' % (message,
+                                                 translate('OpenLP.ProjectorManager', 'Current source input is'),
+                                                 projector.link.source)
+            count = 1
+            for item in projector.link.lamp:
+                message = '%s <b>%s %s</b> (%s) %s: %s<br />' % (message,
+                                                                 translate('OpenLP.ProjectorManager', 'Lamp'),
+                                                                 count,
+                                                                 translate('OpenLP.ProjectorManager', 'On')
+                                                                 if item['On']
+                                                                 else translate('OpenLP.ProjectorManager', 'Off'),
+                                                                 translate('OpenLP.ProjectorManager', 'Hours'),
+                                                                 item['Hours'])
+                count = count + 1
+            message = '%s<hr /><br />' % message
+            if projector.link.projector_errors is None:
+                message = '%s%s' % (message, translate('OpenLP.ProjectorManager', 'No current errors or warnings'))
+            else:
+                message = '%s<b>%s</b>' % (message, translate('OpenLP.ProjectorManager', 'Current errors/warnings'))
+                for (key, val) in projector.link.projector_errors.items():
+                    message = '%s<b>%s</b>: %s<br />' % (message, key, ERROR_MSG[val])
+        QtGui.QMessageBox.information(self, translate('OpenLP.ProjectorManager', 'Projector Information'), message)
+
+    def _add_projector(self, projector):
+        """
+        Helper app to build a projector instance
+
+        :param projector: Dict of projector database information
+        :returns: PJLink1() instance
+        """
+        log.debug('_add_projector()')
+        return PJLink1(dbid=projector.id,
+                       ip=projector.ip,
+                       port=int(projector.port),
+                       name=projector.name,
+                       location=projector.location,
+                       notes=projector.notes,
+                       pin=None if projector.pin == '' else projector.pin,
+                       poll_time=self.poll_time,
+                       socket_timeout=self.socket_timeout
+                       )
+
+    def add_projector(self, projector, start=False):
+        """
+        Builds manager list item, projector thread, and timer for projector instance.
+
+
+        :param projector: Projector instance to add
+        :param start: Start projector if True
+        """
+        item = ProjectorItem(link=self._add_projector(projector))
+        item.db_item = projector
+        icon = QtGui.QIcon(QtGui.QPixmap(STATUS_ICONS[S_NOT_CONNECTED]))
+        item.icon = icon
+        widget = QtGui.QListWidgetItem(icon,
+                                       item.link.name,
+                                       self.projector_list_widget
+                                       )
+        widget.setData(QtCore.Qt.UserRole, item)
+        item.link.db_item = item.db_item
+        item.widget = widget
+        thread = QThread(parent=self)
+        thread.my_parent = self
+        item.moveToThread(thread)
+        thread.started.connect(item.link.thread_started)
+        thread.finished.connect(item.link.thread_stopped)
+        thread.finished.connect(thread.deleteLater)
+        item.link.projectorNetwork.connect(self.update_status)
+        item.link.changeStatus.connect(self.update_status)
+        item.link.projectorAuthentication.connect(self.authentication_error)
+        item.link.projectorNoAuthentication.connect(self.no_authentication_error)
+        item.link.projectorUpdateIcons.connect(self.update_icons)
+        timer = QtCore.QTimer(self)
+        timer.setInterval(self.poll_time)
+        timer.timeout.connect(item.link.poll_loop)
+        item.timer = timer
+        # Timeout in case of brain-dead projectors or cable disconnected
+        socket_timer = QtCore.QTimer(self)
+        socket_timer.setInterval(11000)
+        socket_timer.timeout.connect(item.link.socket_abort)
+        item.socket_timer = socket_timer
+        thread.start()
+        item.thread = thread
+        item.link.timer = timer
+        item.link.socket_timer = socket_timer
+        item.link.widget = item.widget
+        self.projector_list.append(item)
+        if start:
+            item.link.connect_to_host()
+        for item in self.projector_list:
+            log.debug('New projector list - item: (%s) %s' % (item.link.ip, item.link.name))
+
+    @pyqtSlot(str)
+    def add_projector_from_wizard(self, ip, opts=None):
+        """
+        Add a projector from the edit dialog
+
+        :param ip: IP address of new record item to find
+        :param opts: Needed by PyQt4
+        """
+        log.debug('add_projector_from_wizard(ip=%s)' % ip)
+        item = self.projectordb.get_projector_by_ip(ip)
+        self.add_projector(item)
+
+    @pyqtSlot(object)
+    def edit_projector_from_wizard(self, projector):
+        """
+        Update projector from the wizard edit page
+
+        :param projector: Projector() instance of projector with updated information
+        """
+        log.debug('edit_projector_from_wizard(ip=%s)' % projector.ip)
+        self.old_projector.link.name = projector.name
+        self.old_projector.link.ip = projector.ip
+        self.old_projector.link.pin = None if projector.pin == '' else projector.pin
+        self.old_projector.link.port = projector.port
+        self.old_projector.link.location = projector.location
+        self.old_projector.link.notes = projector.notes
+        self.old_projector.widget.setText(projector.name)
+
+    def _load_projectors(self):
+        """'
+        Load projectors - only call when initializing
+        """
+        log.debug('_load_projectors()')
+        self.projector_list_widget.clear()
+        for item in self.projectordb.get_projector_all():
+            self.add_projector(projector=item, start=self.autostart)
+
+    def get_projector_list(self):
+        """
+        Return the list of active projectors
+
+        :returns: list
+        """
+        return self.projector_list
+
+    @pyqtSlot(str, int, str)
+    def update_status(self, ip, status=None, msg=None):
+        """
+        Update the status information/icon for selected list item
+
+        :param ip: IP address of projector
+        :param status: Optional status code
+        :param msg: Optional status message
+        """
+        if status is None:
+            return
+        item = None
+        for list_item in self.projector_list:
+            if ip == list_item.link.ip:
+                item = list_item
+                break
+        message = translate('OpenLP.ProjectorManager', 'No message') if msg is None else msg
+        if status in STATUS_STRING:
+            status_code = STATUS_STRING[status]
+            message = ERROR_MSG[status] if msg is None else msg
+        elif status in ERROR_STRING:
+            status_code = ERROR_STRING[status]
+            message = ERROR_MSG[status] if msg is None else msg
+        else:
+            status_code = status
+            message = ERROR_MSG[status] if msg is None else msg
+        log.debug('(%s) updateStatus(status=%s) message: "%s"' % (item.link.name, status_code, message))
+        if status in STATUS_ICONS:
+            if item.status == status:
+                return
+            item.status = status
+            item.icon = QtGui.QIcon(QtGui.QPixmap(STATUS_ICONS[status]))
+            if status in ERROR_STRING:
+                status_code = ERROR_STRING[status]
+            elif status in STATUS_STRING:
+                status_code = STATUS_STRING[status]
+            log.debug('(%s) Updating icon with %s' % (item.link.name, status_code))
+            item.widget.setIcon(item.icon)
+            self.update_icons()
+
+    def get_toolbar_item(self, name, enabled=False, hidden=False):
+        item = self.one_toolbar.findChild(QtGui.QAction, name)
+        if item == 0:
+            log.debug('No item found with name "%s"' % name)
+            return
+        item.setVisible(False if hidden else True)
+        item.setEnabled(True if enabled else False)
+
+    @pyqtSlot()
+    def update_icons(self):
+        """
+        Update the icons when the selected projectors change
+        """
+        count = len(self.projector_list_widget.selectedItems())
+        projector = None
+        if count == 0:
+            self.get_toolbar_item('edit_projector')
+            self.get_toolbar_item('delete_projector')
+            self.get_toolbar_item('view_projector', hidden=True)
+            self.get_toolbar_item('source_view_projector', hidden=True)
+            self.get_toolbar_item('connect_projector')
+            self.get_toolbar_item('disconnect_projector')
+            self.get_toolbar_item('poweron_projector')
+            self.get_toolbar_item('poweroff_projector')
+            self.get_toolbar_item('blank_projector')
+            self.get_toolbar_item('show_projector')
+            self.get_toolbar_item('connect_projector_multiple', hidden=True)
+            self.get_toolbar_item('disconnect_projector_multiple', hidden=True)
+            self.get_toolbar_item('poweron_projector_multiple', hidden=True)
+            self.get_toolbar_item('poweroff_projector_multiple', hidden=True)
+            self.get_toolbar_item('blank_projector_multiple', hidden=True)
+            self.get_toolbar_item('show_projector_multiple', hidden=True)
+        elif count == 1:
+            projector = self.projector_list_widget.selectedItems()[0].data(QtCore.Qt.UserRole)
+            connected = projector.link.state() == projector.link.ConnectedState
+            power = projector.link.power == S_ON
+            self.get_toolbar_item('connect_projector_multiple', hidden=True)
+            self.get_toolbar_item('disconnect_projector_multiple', hidden=True)
+            self.get_toolbar_item('poweron_projector_multiple', hidden=True)
+            self.get_toolbar_item('poweroff_projector_multiple', hidden=True)
+            self.get_toolbar_item('blank_projector_multiple', hidden=True)
+            self.get_toolbar_item('show_projector_multiple', hidden=True)
+            if connected:
+                self.get_toolbar_item('view_projector', enabled=True)
+                self.get_toolbar_item('source_view_projector',
+                                      enabled=connected and power and projector.link.source_available is not None)
+                self.get_toolbar_item('edit_projector', hidden=True)
+                self.get_toolbar_item('delete_projector', hidden=True)
+            else:
+                self.get_toolbar_item('view_projector', hidden=True)
+                self.get_toolbar_item('source_view_projector', hidden=True)
+                self.get_toolbar_item('edit_projector', enabled=True)
+                self.get_toolbar_item('delete_projector', enabled=True)
+            self.get_toolbar_item('connect_projector', enabled=not connected)
+            self.get_toolbar_item('disconnect_projector', enabled=connected)
+            self.get_toolbar_item('poweron_projector', enabled=connected and (projector.link.power == S_STANDBY))
+            self.get_toolbar_item('poweroff_projector', enabled=connected and (projector.link.power == S_ON))
+            if projector.link.shutter is not None:
+                self.get_toolbar_item('blank_projector', enabled=(connected and power and not projector.link.shutter))
+                self.get_toolbar_item('show_projector', enabled=(connected and power and projector.link.shutter))
+            else:
+                self.get_toolbar_item('blank_projector', enabled=False)
+                self.get_toolbar_item('show_projector', enabled=False)
+        else:
+            self.get_toolbar_item('edit_projector', enabled=False)
+            self.get_toolbar_item('delete_projector', enabled=False)
+            self.get_toolbar_item('view_projector', hidden=True)
+            self.get_toolbar_item('source_view_projector', hidden=True)
+            self.get_toolbar_item('connect_projector', hidden=True)
+            self.get_toolbar_item('disconnect_projector', hidden=True)
+            self.get_toolbar_item('poweron_projector', hidden=True)
+            self.get_toolbar_item('poweroff_projector', hidden=True)
+            self.get_toolbar_item('blank_projector', hidden=True)
+            self.get_toolbar_item('show_projector', hidden=True)
+            self.get_toolbar_item('connect_projector_multiple', hidden=False, enabled=True)
+            self.get_toolbar_item('disconnect_projector_multiple', hidden=False, enabled=True)
+            self.get_toolbar_item('poweron_projector_multiple', hidden=False, enabled=True)
+            self.get_toolbar_item('poweroff_projector_multiple', hidden=False, enabled=True)
+            self.get_toolbar_item('blank_projector_multiple', hidden=False, enabled=True)
+            self.get_toolbar_item('show_projector_multiple', hidden=False, enabled=True)
+
+    @pyqtSlot(str)
+    def authentication_error(self, name):
+        """
+        Display warning dialog when attempting to connect with invalid pin
+
+        :param name: Name from QListWidgetItem
+        """
+        QtGui.QMessageBox.warning(self, translate('OpenLP.ProjectorManager',
+                                                  '"%s" Authentication Error' % name),
+                                  '<br />There was an authentication error while trying to connect.'
+                                  '<br /><br />Please verify your PIN setting '
+                                  'for projector item "%s"' % name)
+
+    @pyqtSlot(str)
+    def no_authentication_error(self, name):
+        """
+        Display warning dialog when pin saved for item but projector does not
+        require pin.
+
+        :param name: Name from QListWidgetItem
+        """
+        QtGui.QMessageBox.warning(self, translate('OpenLP.ProjectorManager',
+                                                  '"%s" No Authentication Error' % name),
+                                  '<br />PIN is set and projector does not require authentication.'
+                                  '<br /><br />Please verify your PIN setting '
+                                  'for projector item "%s"' % name)
+
+
+class ProjectorItem(QObject):
+    """
+    Class for the projector list widget item.
+    NOTE: Actual PJLink class instance should be saved as self.link
+    """
+    def __init__(self, link=None):
+        """
+        Initialization for ProjectorItem instance
+
+        :param link: PJLink1 instance for QListWidgetItem
+        """
+        self.link = link
+        self.thread = None
+        self.icon = None
+        self.widget = None
+        self.my_parent = None
+        self.timer = None
+        self.projectordb_item = None
+        self.poll_time = None
+        self.socket_timeout = None
+        self.status = S_NOT_CONNECTED
+        super(ProjectorItem, self).__init__()
+
+
+def not_implemented(function):
+    """
+    Temporary function to build an information message box indicating function not implemented yet
+
+    :param func: Function name
+    """
+    QtGui.QMessageBox.information(None,
+                                  translate('OpenLP.ProjectorManager', 'Not Implemented Yet'),
+                                  translate('OpenLP.ProjectorManager',
+                                            'Function "%s"<br />has not been implemented yet.'
+                                            '<br />Please check back again later.' % function))

=== added file 'openlp/core/ui/projector/sourceselectform.py'
--- openlp/core/ui/projector/sourceselectform.py	1970-01-01 00:00:00 +0000
+++ openlp/core/ui/projector/sourceselectform.py	2014-10-28 19:17:32 +0000
@@ -0,0 +1,500 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
+# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder,               #
+# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble,             #
+# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann           #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+    :mod: `openlp.core.ui.projector.sourceselectform` module
+
+    Provides the dialog window for selecting video source for projector.
+"""
+import logging
+log = logging.getLogger(__name__)
+log.debug('editform loaded')
+
+from PyQt4 import QtCore, QtGui
+from PyQt4.QtCore import pyqtSlot, QSize
+from PyQt4.QtGui import QDialog, QButtonGroup, QDialogButtonBox, QFormLayout, QLineEdit, QRadioButton, \
+    QStyle, QStylePainter, QStyleOptionTab, QTabBar, QTabWidget, QVBoxLayout, QWidget
+
+from openlp.core.common import translate, is_macosx
+from openlp.core.lib import build_icon
+from openlp.core.lib.projector.db import ProjectorSource
+from openlp.core.lib.projector.constants import PJLINK_DEFAULT_SOURCES, PJLINK_DEFAULT_CODES
+
+
+def source_group(inputs, source_text):
+    """
+    Return a dictionary where key is source[0] and values are inputs
+    grouped by source[0].
+
+    source_text = dict{"key1": "key1-text",
+                       "key2": "key2-text",
+                       ...}
+    return:
+        dict{ key1[0]: { "key11": "key11-text",
+                         "key12": "key12-text",
+                         "key13": "key13-text",
+                         ... }
+              key2[0]: {"key21": "key21-text",
+                        "key22": "key22-text",
+                        ... }
+
+    :param inputs: List of inputs
+    :param source_text: Dictionary of {code: text} values to display
+    :returns: dict
+    """
+    groupdict = {}
+    keydict = {}
+    checklist = inputs
+    key = checklist[0][0]
+    for item in checklist:
+        if item[0] == key:
+            groupdict[item] = source_text[item]
+            continue
+        else:
+            keydict[key] = groupdict
+            key = item[0]
+            groupdict = {item: source_text[item]}
+    keydict[key] = groupdict
+    return keydict
+
+
+def Build_Tab(group, source_key, default, projector, projectordb, edit=False):
+    """
+    Create the radio button page for a tab.
+    Dictionary will be a 1-key entry where key=tab to setup, val=list of inputs.
+
+    source_key: {"groupkey1": {"key11": "key11-text",
+                               "key12": "key12-text",
+                               ...
+                              },
+                 "groupkey2": {"key21": "key21-text",
+                               "key22": "key22-text",
+                               ....
+                              },
+                 ...
+                }
+
+    :param group: Button group widget to add buttons to
+    :param source_key: Dictionary of sources for radio buttons
+    :param default: Default radio button to check
+    :param projector: Projector instance
+    :param projectordb: ProjectorDB instance for session
+    :param edit: If we're editing the source text
+    """
+    buttonchecked = False
+    widget = QWidget()
+    layout = QFormLayout() if edit else QVBoxLayout()
+    layout.setSpacing(10)
+    widget.setLayout(layout)
+    tempkey = list(source_key.keys())[0]  # Should only be 1 key
+    sourcelist = list(source_key[tempkey])
+    sourcelist.sort()
+    button_count = len(sourcelist)
+    if edit:
+        for key in sourcelist:
+            item = QLineEdit()
+            item.setObjectName('source_key_%s' % key)
+            source_item = projectordb.get_source_by_code(code=key, projector_id=projector.db_item.id)
+            if source_item is None:
+                item.setText(PJLINK_DEFAULT_CODES[key])
+            else:
+                item.setText(source_item.text)
+            layout.addRow(PJLINK_DEFAULT_CODES[key], item)
+            group.append(item)
+    else:
+        for key in sourcelist:
+            source_item = projectordb.get_source_by_code(code=key, projector_id=projector.db_item.id)
+            if source_item is None:
+                text = source_key[tempkey][key]
+            else:
+                text = source_item.text
+            itemwidget = QRadioButton(text)
+            itemwidget.setAutoExclusive(True)
+            if default == key:
+                itemwidget.setChecked(True)
+                buttonchecked = itemwidget.isChecked() or buttonchecked
+            group.addButton(itemwidget, int(key))
+            layout.addWidget(itemwidget)
+        layout.addStretch()
+    return widget, button_count, buttonchecked
+
+
+def set_button_tooltip(bar):
+    """
+    Set the toolip for the standard buttons used
+
+    :param bar: QDialogButtonBar instance to update
+    """
+    for button in bar.buttons():
+        if bar.standardButton(button) == QDialogButtonBox.Cancel:
+            tip = "Ignoring current changes and return to OpenLP"
+        elif bar.standardButton(button) == QDialogButtonBox.Reset:
+            tip = "Delete all user-defined text and revert to PJLink default text"
+        elif bar.standardButton(button) == QDialogButtonBox.Discard:
+            tip = "Discard changes and reset to previous user-defined text"
+        elif bar.standardButton(button) == QDialogButtonBox.Ok:
+            tip = "Save changes and return to OpenLP"
+        else:
+            tip = ""
+        button.setToolTip(tip)
+
+
+class FingerTabBarWidget(QTabBar):
+    """
+    Realign west -orientation tabs to left-right text rather than south-north text
+    Borrowed from
+    http://www.kidstrythisathome.com/2013/03/fingertabs-horizontal-tabs-with-horizontal-text-in-pyqt/
+    """
+    def __init__(self, parent=None, *args, **kwargs):
+        """
+        Reset tab text orientation on initialization
+
+        :param width: Remove default width parameter in kwargs
+        :param height: Remove default height parameter in kwargs
+        """
+        self.tabSize = QSize(kwargs.pop('width', 100), kwargs.pop('height', 25))
+        QTabBar.__init__(self, parent, *args, **kwargs)
+
+    def paintEvent(self, event):
+        """
+        Repaint tab in left-right text orientation.
+
+        :param event: Repaint event signal
+        """
+        painter = QStylePainter(self)
+        option = QStyleOptionTab()
+
+        for index in range(self.count()):
+            self.initStyleOption(option, index)
+            tabRect = self.tabRect(index)
+            tabRect.moveLeft(10)
+            painter.drawControl(QStyle.CE_TabBarTabShape, option)
+            painter.drawText(tabRect, QtCore.Qt.AlignVCenter |
+                             QtCore.Qt.TextDontClip,
+                             self.tabText(index))
+        painter.end()
+
+    def tabSizeHint(self, index):
+        """
+        Return tabsize
+
+        :param index: Tab index to fetch tabsize from
+        :returns: instance tabSize
+        """
+        return self.tabSize
+
+
+class FingerTabWidget(QTabWidget):
+    """
+    A QTabWidget equivalent which uses our FingerTabBarWidget
+
+    Based on thread discussion
+    http://www.riverbankcomputing.com/pipermail/pyqt/2005-December/011724.html
+    """
+    def __init__(self, parent, *args):
+        """
+        Initialize FingerTabWidget instance
+        """
+        QTabWidget.__init__(self, parent, *args)
+        self.setTabBar(FingerTabBarWidget(self))
+
+
+class SourceSelectTabs(QDialog):
+    """
+    Class for handling selecting the source for the projector to use.
+    Uses tabbed interface.
+    """
+    def __init__(self, parent, projectordb, edit=False):
+        """
+        Build the source select dialog using tabbed interface.
+
+        :param projectordb: ProjectorDB session to use
+        """
+        log.debug('Initializing SourceSelectTabs()')
+        super(SourceSelectTabs, self).__init__(parent)
+        self.projectordb = projectordb
+        self.edit = edit
+        if self.edit:
+            title = translate('OpenLP.SourceSelectForm', 'Select Projector Source')
+        else:
+            title = translate('OpenLP.SourceSelectForm', 'Edit Projector Source Text')
+        self.setWindowTitle(title)
+        self.setObjectName('source_select_tabs')
+        self.setWindowIcon(build_icon(':/icon/openlp-log-32x32.png'))
+        self.setModal(True)
+        self.layout = QVBoxLayout()
+        self.layout.setObjectName('source_select_tabs_layout')
+        if is_macosx():
+            self.tabwidget = QTabWidget(self)
+        else:
+            self.tabwidget = FingerTabWidget(self)
+        self.tabwidget.setObjectName('source_select_tabs_tabwidget')
+        self.tabwidget.setUsesScrollButtons(False)
+        if is_macosx():
+            self.tabwidget.setTabPosition(QTabWidget.North)
+        else:
+            self.tabwidget.setTabPosition(QTabWidget.West)
+        self.layout.addWidget(self.tabwidget)
+        self.setLayout(self.layout)
+
+    def exec_(self, projector):
+        """
+        Override initial method so we can build the tabs.
+
+        :param projector: Projector instance to build source list from
+        """
+        self.projector = projector
+        self.source_text = self.projectordb.get_source_list(projector=projector)
+        self.source_group = source_group(projector.source_available, self.source_text)
+        # self.source_group = {'4': {'41': 'Storage 1'}, '5': {"51": 'Network 1'}}
+        self.button_group = [] if self.edit else QButtonGroup()
+        keys = list(self.source_group.keys())
+        keys.sort()
+        if self.edit:
+            for key in keys:
+                (tab, button_count, buttonchecked) = Build_Tab(group=self.button_group,
+                                                               source_key={key: self.source_group[key]},
+                                                               default=self.projector.source,
+                                                               projector=self.projector,
+                                                               projectordb=self.projectordb,
+                                                               edit=self.edit)
+                thistab = self.tabwidget.addTab(tab, PJLINK_DEFAULT_SOURCES[key])
+                if buttonchecked:
+                    self.tabwidget.setCurrentIndex(thistab)
+        else:
+            for key in keys:
+                (tab, button_count, buttonchecked) = Build_Tab(group=self.button_group,
+                                                               source_key={key: self.source_group[key]},
+                                                               default=self.projector.source,
+                                                               projector=self.projector,
+                                                               projectordb=self.projectordb,
+                                                               edit=self.edit)
+                thistab = self.tabwidget.addTab(tab, PJLINK_DEFAULT_SOURCES[key])
+                if buttonchecked:
+                    self.tabwidget.setCurrentIndex(thistab)
+        self.button_box = QDialogButtonBox(QtGui.QDialogButtonBox.Reset |
+                                           QtGui.QDialogButtonBox.Discard |
+                                           QtGui.QDialogButtonBox.Ok |
+                                           QtGui.QDialogButtonBox.Cancel)
+        self.button_box.clicked.connect(self.button_clicked)
+        self.layout.addWidget(self.button_box)
+        set_button_tooltip(self.button_box)
+        selected = super(SourceSelectTabs, self).exec_()
+        return selected
+
+    @pyqtSlot(object)
+    def button_clicked(self, button):
+        """
+        Checks which button was clicked
+
+        :param button: Button that was clicked
+        :returns: Ok:      Saves current edits
+                  Delete:  Resets text to last-saved text
+                  Reset:   Reset all text to PJLink default text
+                  Cancel:  Cancel text edit
+        """
+        if self.button_box.standardButton(button) == self.button_box.Cancel:
+            self.done(0)
+        elif self.button_box.standardButton(button) == self.button_box.Reset:
+            self.delete_sources()
+        elif self.button_box.standardButton(button) == self.button_box.Discard:
+            self.done(100)
+        elif self.button_box.standardButton(button) == self.button_box.Ok:
+            return self.accept_me()
+        else:
+            return 100
+
+    def delete_sources(self):
+        msg = QtGui.QMessageBox()
+        msg.setText('Delete entries for this projector')
+        msg.setInformativeText('Are you sure you want to delete ALL user-defined '
+                               'source input text for this projector?')
+        msg.setStandardButtons(msg.Cancel | msg.Ok)
+        msg.setDefaultButton(msg.Cancel)
+        ans = msg.exec_()
+        if ans == msg.Cancel:
+            return
+        self.projectordb.delete_all_objects(ProjectorSource, ProjectorSource.projector_id == self.projector.db_item.id)
+        self.done(100)
+
+    def accept_me(self):
+        """
+        Slot to accept 'OK' button
+        """
+        projector = self.projector.db_item
+        if self.edit:
+            for key in self.button_group:
+                code = key.objectName().split("_")[-1]
+                text = key.text().strip()
+                if key.text().strip().lower() == PJLINK_DEFAULT_CODES[code].strip().lower():
+                    continue
+                item = self.projectordb.get_source_by_code(code=code, projector_id=projector.id)
+                if item is None:
+                    log.debug("(%s) Adding new source text %s: %s" % (projector.ip, code, text))
+                    item = ProjectorSource(projector_id=projector.id, code=code, text=text)
+                else:
+                    item.text = text
+                    log.debug('(%s) Updating source code %s with text="%s"' % (projector.ip, item.code, item.text))
+                self.projectordb.add_source(item)
+            selected = 0
+        else:
+            selected = self.button_group.checkedId()
+            log.debug('SourceSelectTabs().accepted() Setting source to %s' % selected)
+        self.done(selected)
+
+
+class SourceSelectSingle(QDialog):
+    """
+    Class for handling selecting the source for the projector to use.
+    Uses single dialog interface.
+    """
+    def __init__(self, parent, projectordb, edit=False):
+        """
+        Build the source select dialog.
+
+        :param projectordb: ProjectorDB session to use
+        """
+        log.debug('Initializing SourceSelectSingle()')
+        self.projectordb = projectordb
+        super(SourceSelectSingle, self).__init__(parent)
+        self.setWindowTitle(translate('OpenLP.SourceSelectSingle', 'Select Projector Source'))
+        self.setObjectName('source_select_single')
+        self.setWindowIcon(build_icon(':/icon/openlp-log-32x32.png'))
+        self.setModal(True)
+        self.edit = edit
+
+    def exec_(self, projector, edit=False):
+        """
+        Override initial method so we can build the tabs.
+
+        :param projector: Projector instance to build source list from
+        """
+        self.projector = projector
+        self.layout = QFormLayout() if self.edit else QVBoxLayout()
+        self.layout.setObjectName('source_select_tabs_layout')
+        self.layout.setSpacing(10)
+        self.setLayout(self.layout)
+        self.setMinimumWidth(350)
+        self.button_group = [] if self.edit else QButtonGroup()
+        self.source_text = self.projectordb.get_source_list(projector=projector)
+        keys = list(self.source_text.keys())
+        keys.sort()
+        key_count = len(keys)
+        button_list = []
+        if self.edit:
+            for key in keys:
+                item = QLineEdit()
+                item.setObjectName('source_key_%s' % key)
+                source_item = self.projectordb.get_source_by_code(code=key, projector_id=self.projector.db_item.id)
+                if source_item is None:
+                    item.setText(PJLINK_DEFAULT_CODES[key])
+                else:
+                    item.old_text = item.text()
+                    item.setText(source_item.text)
+                self.layout.addRow(PJLINK_DEFAULT_CODES[key], item)
+                self.button_group.append(item)
+        else:
+            for key in keys:
+                source_text = self.projectordb.get_source_by_code(code=key, projector_id=self.projector.db_item.id)
+                text = self.source_text[key] if source_text is None else source_text.text
+                button = QtGui.QRadioButton(text)
+                button.setChecked(True if key == projector.source else False)
+                self.layout.addWidget(button)
+                self.button_group.addButton(button, int(key))
+                button_list.append(key)
+        self.button_box = QDialogButtonBox(QtGui.QDialogButtonBox.Reset |
+                                           QtGui.QDialogButtonBox.Discard |
+                                           QtGui.QDialogButtonBox.Ok |
+                                           QtGui.QDialogButtonBox.Cancel)
+        self.button_box.clicked.connect(self.button_clicked)
+        self.layout.addWidget(self.button_box)
+        self.setMinimumHeight(key_count*25)
+        set_button_tooltip(self.button_box)
+        selected = super(SourceSelectSingle, self).exec_()
+        return selected
+
+    @pyqtSlot(object)
+    def button_clicked(self, button):
+        """
+        Checks which button was clicked
+
+        :param button: Button that was clicked
+        :returns: Ok:      Saves current edits
+                  Delete:  Resets text to last-saved text
+                  Reset:   Reset all text to PJLink default text
+                  Cancel:  Cancel text edit
+        """
+        if self.button_box.standardButton(button) == self.button_box.Cancel:
+            self.done(0)
+        elif self.button_box.standardButton(button) == self.button_box.Reset:
+            self.delete_sources()
+        elif self.button_box.standardButton(button) == self.button_box.Discard:
+            self.done(100)
+        elif self.button_box.standardButton(button) == self.button_box.Ok:
+            return self.accept_me()
+        else:
+            return 100
+
+    def delete_sources(self):
+        msg = QtGui.QMessageBox()
+        msg.setText('Delete entries for this projector')
+        msg.setInformativeText('Are you sure you want to delete ALL user-defined '
+                               'source input text for this projector?')
+        msg.setStandardButtons(msg.Cancel | msg.Ok)
+        msg.setDefaultButton(msg.Cancel)
+        ans = msg.exec_()
+        if ans == msg.Cancel:
+            return
+        self.projectordb.delete_all_objects(ProjectorSource, ProjectorSource.projector_id == self.projector.db_item.id)
+        self.done(100)
+
+    @pyqtSlot()
+    def accept_me(self):
+        """
+        Slot to accept 'OK' button
+        """
+        projector = self.projector.db_item
+        if self.edit:
+            for key in self.button_group:
+                code = key.objectName().split("_")[-1]
+                text = key.text().strip()
+                if key.text().strip().lower() == PJLINK_DEFAULT_CODES[code].strip().lower():
+                    continue
+                item = self.projectordb.get_source_by_code(code=code, projector_id=projector.id)
+                if item is None:
+                    log.debug("(%s) Adding new source text %s: %s" % (projector.ip, code, text))
+                    item = ProjectorSource(projector_id=projector.id, code=code, text=text)
+                else:
+                    item.text = text
+                    log.debug('(%s) Updating source code %s with text="%s"' % (projector.ip, item.code, item.text))
+                self.projectordb.add_source(item)
+            selected = 0
+        else:
+            selected = self.button_group.checkedId()
+            log.debug('SourceSelectDialog().accepted() Setting source to %s' % selected)
+        self.done(selected)

=== added file 'openlp/core/ui/projector/tab.py'
--- openlp/core/ui/projector/tab.py	1970-01-01 00:00:00 +0000
+++ openlp/core/ui/projector/tab.py	2014-10-28 19:17:32 +0000
@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
+# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder,               #
+# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble,             #
+# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann           #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+    :mod:`openlp.core.ui.projector.tab`
+
+    Provides the settings tab in the settings dialog.
+"""
+
+import logging
+log = logging.getLogger(__name__)
+log.debug('projectortab module loaded')
+
+from PyQt4 import QtCore, QtGui
+
+from openlp.core.common import Settings, UiStrings, translate
+from openlp.core.lib import SettingsTab
+from openlp.core.lib.projector import DialogSourceStyle
+
+
+class ProjectorTab(SettingsTab):
+    """
+    Openlp Settings -> Projector settings
+    """
+    def __init__(self, parent):
+        """
+        ProjectorTab initialization
+
+        :param parent: Parent widget
+        """
+        self.icon_path = ':/projector/projector_manager.png'
+        projector_translated = translate('OpenLP.ProjectorTab', 'Projector')
+        super(ProjectorTab, self).__init__(parent, 'Projector', projector_translated)
+
+    def setupUi(self):
+        """
+        Setup the UI
+        """
+        self.setObjectName('ProjectorTab')
+        super(ProjectorTab, self).setupUi()
+        self.connect_box = QtGui.QGroupBox(self.left_column)
+        self.connect_box.setObjectName('connect_box')
+        self.connect_box_layout = QtGui.QFormLayout(self.connect_box)
+        self.connect_box_layout.setObjectName('connect_box_layout')
+        # Start comms with projectors on startup
+        self.connect_on_startup = QtGui.QCheckBox(self.connect_box)
+        self.connect_on_startup.setObjectName('connect_on_startup')
+        self.connect_box_layout.addRow(self.connect_on_startup)
+        # Socket timeout
+        self.socket_timeout_label = QtGui.QLabel(self.connect_box)
+        self.socket_timeout_label.setObjectName('socket_timeout_label')
+        self.socket_timeout_spin_box = QtGui.QSpinBox(self.connect_box)
+        self.socket_timeout_spin_box.setObjectName('socket_timeout_spin_box')
+        self.socket_timeout_spin_box.setMinimum(2)
+        self.socket_timeout_spin_box.setMaximum(10)
+        self.connect_box_layout.addRow(self.socket_timeout_label, self.socket_timeout_spin_box)
+        # Poll interval
+        self.socket_poll_label = QtGui.QLabel(self.connect_box)
+        self.socket_poll_label.setObjectName('socket_poll_label')
+        self.socket_poll_spin_box = QtGui.QSpinBox(self.connect_box)
+        self.socket_poll_spin_box.setObjectName('socket_timeout_spin_box')
+        self.socket_poll_spin_box.setMinimum(5)
+        self.socket_poll_spin_box.setMaximum(60)
+        self.connect_box_layout.addRow(self.socket_poll_label, self.socket_poll_spin_box)
+        self.left_layout.addWidget(self.connect_box)
+        # Source input select dialog box type
+        self.dialog_type_label = QtGui.QLabel(self.connect_box)
+        self.dialog_type_label.setObjectName('dialog_type_label')
+        self.dialog_type_combo_box = QtGui.QComboBox(self.connect_box)
+        self.dialog_type_combo_box.setObjectName('dialog_type_combo_box')
+        self.dialog_type_combo_box.addItems(['', ''])
+        self.connect_box_layout.addRow(self.dialog_type_label, self.dialog_type_combo_box)
+        self.left_layout.addStretch()
+        self.dialog_type_combo_box.activated.connect(self.on_dialog_type_combo_box_changed)
+
+    def retranslateUi(self):
+        """
+        Translate the UI on the fly
+        """
+        self.tab_title_visible = UiStrings().Projectors
+        self.connect_box.setTitle(
+            translate('OpenLP.ProjectorTab', 'Communication Options'))
+        self.connect_on_startup.setText(
+            translate('OpenLP.ProjectorTab', 'Connect to projectors on startup'))
+        self.socket_timeout_label.setText(
+            translate('OpenLP.ProjectorTab', 'Socket timeout (seconds)'))
+        self.socket_poll_label.setText(
+            translate('OpenLP.ProjectorTab', 'Poll time (seconds)'))
+        self.dialog_type_label.setText(
+            translate('Openlp.ProjectorTab', 'Source select dialog interface'))
+        self.dialog_type_combo_box.setItemText(DialogSourceStyle.Tabbed,
+                                               translate('OpenLP.ProjectorTab', 'Tabbed dialog box'))
+        self.dialog_type_combo_box.setItemText(DialogSourceStyle.Single,
+                                               translate('OpenLP.ProjectorTab', 'Single dialog box'))
+
+    def load(self):
+        """
+        Load the projector settings on startup
+        """
+        settings = Settings()
+        settings.beginGroup(self.settings_section)
+        self.connect_on_startup.setChecked(settings.value('connect on start'))
+        self.socket_timeout_spin_box.setValue(settings.value('socket timeout'))
+        self.socket_poll_spin_box.setValue(settings.value('poll time'))
+        self.dialog_type_combo_box.setCurrentIndex(settings.value('source dialog type'))
+        settings.endGroup()
+
+    def save(self):
+        """
+        Save the projector settings
+        """
+        settings = Settings()
+        settings.beginGroup(self.settings_section)
+        settings.setValue('connect on start', self.connect_on_startup.isChecked())
+        settings.setValue('socket timeout', self.socket_timeout_spin_box.value())
+        settings.setValue('poll time', self.socket_poll_spin_box.value())
+        settings.setValue('source dialog type', self.dialog_type_combo_box.currentIndex())
+        settings.endGroup
+
+    def on_dialog_type_combo_box_changed(self):
+        self.dialog_type = self.dialog_type_combo_box.currentIndex()

=== modified file 'openlp/core/ui/settingsform.py'
--- openlp/core/ui/settingsform.py	2014-10-25 20:26:19 +0000
+++ openlp/core/ui/settingsform.py	2014-10-28 19:17:32 +0000
@@ -38,6 +38,7 @@
 from openlp.core.ui import AdvancedTab, GeneralTab, ThemesTab
 from openlp.core.ui.media import PlayerTab
 from .settingsdialog import Ui_SettingsDialog
+from openlp.core.ui.projector.tab import ProjectorTab
 
 log = logging.getLogger(__name__)
 
@@ -66,10 +67,19 @@
         while self.stacked_layout.count():
             # take at 0 and the rest shuffle up.
             self.stacked_layout.takeAt(0)
+<<<<<<< TREE
         self.insert_tab(self.general_tab)
         self.insert_tab(self.themes_tab)
         self.insert_tab(self.advanced_tab)
         self.insert_tab(self.player_tab)
+=======
+        self.insert_tab(self.general_tab, 0, PluginStatus.Active)
+        self.insert_tab(self.themes_tab, 1, PluginStatus.Active)
+        self.insert_tab(self.projector_tab, 2, PluginStatus.Active)
+        self.insert_tab(self.advanced_tab, 3, PluginStatus.Active)
+        self.insert_tab(self.player_tab, 4, PluginStatus.Active)
+        count = 5
+>>>>>>> MERGE-SOURCE
         for plugin in self.plugin_manager.plugins:
             if plugin.settings_tab:
                 self.insert_tab(plugin.settings_tab, plugin.is_active())
@@ -123,6 +133,8 @@
         self.general_tab = GeneralTab(self)
         # Themes tab
         self.themes_tab = ThemesTab(self)
+        # Projector Tab
+        self.projector_tab = ProjectorTab(self)
         # Advanced tab
         self.advanced_tab = AdvancedTab(self)
         # Advanced tab

=== modified file 'resources/images/openlp-2.qrc'
--- resources/images/openlp-2.qrc	2014-09-05 20:15:44 +0000
+++ resources/images/openlp-2.qrc	2014-10-28 19:17:32 +0000
@@ -106,6 +106,7 @@
     <file>wizard_firsttime.bmp</file>
     <file>wizard_createtheme.bmp</file>
     <file>wizard_duplicateremoval.bmp</file>
+    <file>wizard_createprojector.png</file>
   </qresource>
   <qresource prefix="services">
     <file>service_collapse_all.png</file>
@@ -169,6 +170,34 @@
     <file>theme_new.png</file>
     <file>theme_edit.png</file>
   </qresource>
+  <qresource prefix="projector">
+    <file>projector_blank.png</file>
+    <file>projector_blank_tiled.png</file>
+    <file>projector_connect.png</file>
+    <file>projector_connect_tiled.png</file>
+    <file>projector_hdmi.png</file>
+    <file>projector_cooldown.png</file>
+    <file>projector_disconnect.png</file>
+    <file>projector_disconnect_tiled.png</file>
+    <file>projector_edit.png</file>
+    <file>projector_error.png</file>
+    <file>projector_item_connect.png</file>
+    <file>projector_item_disconnect.png</file>
+    <file>projector_manager.png</file>
+    <file>projector_new.png</file>
+    <file>projector_not_connected_error.png</file>
+    <file>projector_off.png</file>
+    <file>projector_on.png</file>
+    <file>projector_power_off.png</file>
+    <file>projector_power_off_tiled.png</file>
+    <file>projector_power_on.png</file>
+    <file>projector_power_on_tiled.png</file>
+    <file>projector_show.png</file>
+    <file>projector_show_tiled.png</file>
+    <file>projector_spacer.png</file>
+    <file>projector_warmup.png</file>
+    <file>projector_view.png</file>
+  </qresource>
   <qresource prefix="remotes">
     <file>android_app_qr.png</file>
   </qresource>

=== added file 'resources/images/projector_blank.png'
Binary files resources/images/projector_blank.png	1970-01-01 00:00:00 +0000 and resources/images/projector_blank.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_blank_tiled.png'
Binary files resources/images/projector_blank_tiled.png	1970-01-01 00:00:00 +0000 and resources/images/projector_blank_tiled.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_connect.png'
Binary files resources/images/projector_connect.png	1970-01-01 00:00:00 +0000 and resources/images/projector_connect.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_connect_tiled.png'
Binary files resources/images/projector_connect_tiled.png	1970-01-01 00:00:00 +0000 and resources/images/projector_connect_tiled.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_connectors.png'
Binary files resources/images/projector_connectors.png	1970-01-01 00:00:00 +0000 and resources/images/projector_connectors.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_cooldown.png'
Binary files resources/images/projector_cooldown.png	1970-01-01 00:00:00 +0000 and resources/images/projector_cooldown.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_disconnect.png'
Binary files resources/images/projector_disconnect.png	1970-01-01 00:00:00 +0000 and resources/images/projector_disconnect.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_disconnect_tiled.png'
Binary files resources/images/projector_disconnect_tiled.png	1970-01-01 00:00:00 +0000 and resources/images/projector_disconnect_tiled.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_edit.png'
Binary files resources/images/projector_edit.png	1970-01-01 00:00:00 +0000 and resources/images/projector_edit.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_error.png'
Binary files resources/images/projector_error.png	1970-01-01 00:00:00 +0000 and resources/images/projector_error.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_hdmi.png'
Binary files resources/images/projector_hdmi.png	1970-01-01 00:00:00 +0000 and resources/images/projector_hdmi.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_item_connect.png'
Binary files resources/images/projector_item_connect.png	1970-01-01 00:00:00 +0000 and resources/images/projector_item_connect.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_item_disconnect.png'
Binary files resources/images/projector_item_disconnect.png	1970-01-01 00:00:00 +0000 and resources/images/projector_item_disconnect.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_manager.png'
Binary files resources/images/projector_manager.png	1970-01-01 00:00:00 +0000 and resources/images/projector_manager.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_new.png'
Binary files resources/images/projector_new.png	1970-01-01 00:00:00 +0000 and resources/images/projector_new.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_not_connected_error.png'
Binary files resources/images/projector_not_connected_error.png	1970-01-01 00:00:00 +0000 and resources/images/projector_not_connected_error.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_off.png'
Binary files resources/images/projector_off.png	1970-01-01 00:00:00 +0000 and resources/images/projector_off.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_on.png'
Binary files resources/images/projector_on.png	1970-01-01 00:00:00 +0000 and resources/images/projector_on.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_power_off.png'
Binary files resources/images/projector_power_off.png	1970-01-01 00:00:00 +0000 and resources/images/projector_power_off.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_power_off_tiled.png'
Binary files resources/images/projector_power_off_tiled.png	1970-01-01 00:00:00 +0000 and resources/images/projector_power_off_tiled.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_power_on.png'
Binary files resources/images/projector_power_on.png	1970-01-01 00:00:00 +0000 and resources/images/projector_power_on.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_power_on_tiled.png'
Binary files resources/images/projector_power_on_tiled.png	1970-01-01 00:00:00 +0000 and resources/images/projector_power_on_tiled.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_show.png'
Binary files resources/images/projector_show.png	1970-01-01 00:00:00 +0000 and resources/images/projector_show.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_show_tiled.png'
Binary files resources/images/projector_show_tiled.png	1970-01-01 00:00:00 +0000 and resources/images/projector_show_tiled.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_spacer.png'
Binary files resources/images/projector_spacer.png	1970-01-01 00:00:00 +0000 and resources/images/projector_spacer.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_view.png'
Binary files resources/images/projector_view.png	1970-01-01 00:00:00 +0000 and resources/images/projector_view.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/projector_warmup.png'
Binary files resources/images/projector_warmup.png	1970-01-01 00:00:00 +0000 and resources/images/projector_warmup.png	2014-10-28 19:17:32 +0000 differ
=== added file 'resources/images/wizard_createprojector.png'
Binary files resources/images/wizard_createprojector.png	1970-01-01 00:00:00 +0000 and resources/images/wizard_createprojector.png	2014-10-28 19:17:32 +0000 differ
=== modified file 'scripts/generate_resources.sh'
--- scripts/generate_resources.sh	2014-09-08 20:43:21 +0000
+++ scripts/generate_resources.sh	2014-10-28 19:17:32 +0000
@@ -53,5 +53,6 @@
 patch --posix -s openlp/core/resources.py scripts/resources.patch
 
 # Remove temporary file
-rm openlp/core/resources.py.new
-
+rm openlp/core/resources.py.new 2>/dev/null
+rm openlp/core/resources.py.old 2>/dev/null
+rm openlp/core/resources.py.orig 2>/dev/null

=== added file 'tests/functional/openlp_core_common/test_projector_utilities.py'
--- tests/functional/openlp_core_common/test_projector_utilities.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_core_common/test_projector_utilities.py	2014-10-28 19:17:32 +0000
@@ -0,0 +1,163 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
+# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,             #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock,              #
+# Frode Woldsund, Martin Zibricky, Patrick Zimmermann                         #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Package to test the openlp.core.ui.projector.networkutils package.
+"""
+
+from unittest import TestCase
+
+from openlp.core.common import verify_ip_address, md5_hash, qmd5_hash
+
+salt = '498e4a67'
+pin = 'JBMIAProjectorLink'
+test_hash = '5d8409bc1c3fa39749434aa3a5c38682'
+
+ip4_loopback = '127.0.0.1'
+ip4_local = '192.168.1.1'
+ip4_broadcast = '255.255.255.255'
+ip4_bad = '192.168.1.256'
+
+ip6_loopback = '::1'
+ip6_link_local = 'fe80::223:14ff:fe99:d315'
+ip6_bad = 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'
+
+
+class testProjectorUtilities(TestCase):
+    """
+    Validate functions in the projector utilities module
+    """
+    def test_ip4_loopback_valid(self):
+        """
+        Test IPv4 loopbackvalid
+        """
+        # WHEN: Test with a local loopback test
+        valid = verify_ip_address(addr=ip4_loopback)
+
+        # THEN: Verify we received True
+        self.assertTrue(valid, 'IPv4 loopback address should have been valid')
+
+    def test_ip4_local_valid(self):
+        """
+        Test IPv4 local valid
+        """
+        # WHEN: Test with a local loopback test
+        valid = verify_ip_address(addr=ip4_local)
+
+        # THEN: Verify we received True
+        self.assertTrue(valid, 'IPv4 local address should have been valid')
+
+    def test_ip4_broadcast_valid(self):
+        """
+        Test IPv4 broadcast valid
+        """
+        # WHEN: Test with a local loopback test
+        valid = verify_ip_address(addr=ip4_broadcast)
+
+        # THEN: Verify we received True
+        self.assertTrue(valid, 'IPv4 broadcast address should have been valid')
+
+    def test_ip4_address_invalid(self):
+        """
+        Test IPv4 address invalid
+        """
+        # WHEN: Test with a local loopback test
+        valid = verify_ip_address(addr=ip4_bad)
+
+        # THEN: Verify we received True
+        self.assertFalse(valid, 'Bad IPv4 address should not have been valid')
+
+    def test_ip6_loopback_valid(self):
+        """
+        Test IPv6 loopback valid
+        """
+        # WHEN: Test IPv6 loopback address
+        valid = verify_ip_address(addr=ip6_loopback)
+
+        # THEN: Validate return
+        self.assertTrue(valid, 'IPv6 loopback address should have been valid')
+
+    def test_ip6_local_valid(self):
+        """
+        Test IPv6 link-local valid
+        """
+        # WHEN: Test IPv6 link-local address
+        valid = verify_ip_address(addr=ip6_link_local)
+
+        # THEN: Validate return
+        self.assertTrue(valid, 'IPv6 link-local address should have been valid')
+
+    def test_ip6_address_invalid(self):
+        """
+        Test NetworkUtils IPv6 address invalid
+        """
+        # WHEN: Given an invalid IPv6 address
+        valid = verify_ip_address(addr=ip6_bad)
+
+        # THEN: Validate bad return
+        self.assertFalse(valid, 'IPv6 bad address should have been invalid')
+
+    def test_md5_hash(self):
+        """
+        Test MD5 hash from salt+data pass (python)
+        """
+        # WHEN: Given a known salt+data
+        hash_ = md5_hash(salt=salt, data=pin)
+
+        # THEN: Validate return has is same
+        self.assertEquals(hash_, test_hash, 'MD5 should have returned a good hash')
+
+    def test_md5_hash_bad(self):
+        """
+        Test MD5 hash from salt+data fail (python)
+        """
+        # WHEN: Given a different salt+hash
+        hash_ = md5_hash(salt=pin, data=salt)
+
+        # THEN: return data is different
+        self.assertNotEquals(hash_, test_hash, 'MD5 should have returned a bad hash')
+
+    def test_qmd5_hash(self):
+        """
+        Test MD5 hash from salt+data pass (Qt)
+        """
+        # WHEN: Given a known salt+data
+        hash_ = qmd5_hash(salt=salt, data=pin)
+
+        # THEN: Validate return has is same
+        self.assertEquals(hash_, test_hash, 'Qt-MD5 should have returned a good hash')
+
+    def test_qmd5_hash_bad(self):
+        """
+        Test MD5 hash from salt+hash fail (Qt)
+        """
+        # WHEN: Given a different salt+hash
+        hash_ = qmd5_hash(salt=pin, data=salt)
+
+        # THEN: return data is different
+        self.assertNotEquals(hash_, test_hash, 'Qt-MD5 should have returned a bad hash')

=== modified file 'tests/functional/openlp_core_lib/__init__.py'
--- tests/functional/openlp_core_lib/__init__.py	2014-04-02 18:51:21 +0000
+++ tests/functional/openlp_core_lib/__init__.py	2014-10-28 19:17:32 +0000
@@ -27,5 +27,28 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 """
-Package to test the openlp.core.lib package.
+Module-level functions for the functional test suite
 """
+
+import os
+from tests.functional import patch
+
+from openlp.core.common import is_win
+
+from .test_projectordb import tmpfile
+
+
+def setUp():
+    if not is_win():
+        # Wine creates a sharing violation during tests. Ignore.
+        try:
+            os.remove(tmpfile)
+        except:
+            pass
+
+
+def tearDown():
+    """
+    Ensure test suite has been cleaned up after tests
+    """
+    patch.stopall()

=== added file 'tests/functional/openlp_core_lib/test_projectordb.py'
--- tests/functional/openlp_core_lib/test_projectordb.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_core_lib/test_projectordb.py	2014-10-28 19:17:32 +0000
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
+# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,             #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock,              #
+# Frode Woldsund, Martin Zibricky, Patrick Zimmermann                         #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Package to test the openlp.core.ui.projectordb  find, edit, delete
+record functions.
+
+PREREQUISITE: add_record() and get_all() functions validated.
+"""
+
+from unittest import TestCase
+from tests.functional import MagicMock, patch
+
+from openlp.core.lib.projector.db import Projector, ProjectorDB
+
+from tests.resources.projector.data import TEST1_DATA, TEST2_DATA, TEST3_DATA
+
+tmpfile = '/tmp/openlp-test-projectordb.sql'
+
+
+def compare_data(one, two):
+    """
+    Verify two Projector() instances contain the same data
+    """
+    return one is not None and \
+        two is not None and \
+        one.ip == two.ip and \
+        one.port == two.port and \
+        one.name == two.name and \
+        one.location == two.location and \
+        one.notes == two.notes
+
+
+def add_records(self, test):
+    """
+    Add record if not in database
+    """
+    record_list = self.projector.get_projector_all()
+    if len(record_list) < 1:
+        added = False
+        for record in test:
+            added = self.projector.add_projector(record) or added
+        return added
+
+    for new_record in test:
+        added = None
+        for record in record_list:
+            if compare_data(record, new_record):
+                break
+            added = self.projector.add_projector(new_record)
+    return added
+
+
+class TestProjectorDB(TestCase):
+    """
+    Test case for ProjectorDB
+    """
+    def setUp(self):
+        """
+        Set up anything necessary for all tests
+        """
+        if not hasattr(self, 'projector'):
+            with patch('openlp.core.lib.projector.db.init_url') as mocked_init_url:
+                mocked_init_url.start()
+                mocked_init_url.return_value = 'sqlite:///%s' % tmpfile
+                self.projector = ProjectorDB()
+
+    def find_record_by_ip_test(self):
+        """
+        Test find record by IP
+        """
+        # GIVEN: Record entries in database
+        add_records(self, [TEST1_DATA, TEST2_DATA])
+
+        # WHEN: Search for record using IP
+        record = self.projector.get_projector_by_ip(TEST2_DATA.ip)
+
+        # THEN: Verify proper record returned
+        self.assertTrue(compare_data(TEST2_DATA, record),
+                        'Record found should have been test_2 data')
+
+    def find_record_by_name_test(self):
+        """
+        Test find record by name
+        """
+        # GIVEN: Record entries in database
+        add_records(self, [TEST1_DATA, TEST2_DATA])
+
+        # WHEN: Search for record using name
+        record = self.projector.get_projector_by_name(TEST2_DATA.name)
+
+        # THEN: Verify proper record returned
+        self.assertTrue(compare_data(TEST2_DATA, record),
+                        'Record found should have been test_2 data')
+
+    def record_delete_test(self):
+        """
+        Test record can be deleted
+        """
+        # GIVEN: Record in database
+        add_records(self, [TEST3_DATA, ])
+        record = self.projector.get_projector_by_ip(TEST3_DATA.ip)
+
+        # WHEN: Record deleted
+        self.projector.delete_projector(record)
+
+        # THEN: Verify record not retrievable
+        found = self.projector.get_projector_by_ip(TEST3_DATA.ip)
+        self.assertFalse(found, 'test_3 record should have been deleted')
+
+    def record_edit_test(self):
+        """
+        Test edited record returns the same record ID with different data
+        """
+        # GIVEN: Record entries in database
+        add_records(self, [TEST1_DATA, TEST2_DATA])
+
+        # WHEN: We retrieve a specific record
+        record = self.projector.get_projector_by_ip(TEST1_DATA.ip)
+        record_id = record.id
+
+        # WHEN: Data is changed
+        record.ip = TEST3_DATA.ip
+        record.port = TEST3_DATA.port
+        record.pin = TEST3_DATA.pin
+        record.name = TEST3_DATA.name
+        record.location = TEST3_DATA.location
+        record.notes = TEST3_DATA.notes
+        updated = self.projector.update_projector(record)
+        self.assertTrue(updated, 'Save updated record should have returned True')
+        record = self.projector.get_projector_by_ip(TEST3_DATA.ip)
+
+        # THEN: Record ID should remain the same, but data should be changed
+        self.assertEqual(record_id, record.id, 'Edited record should have the same ID')
+        self.assertTrue(compare_data(TEST3_DATA, record), 'Edited record should have new data')

=== modified file 'tests/interfaces/openlp_core_ui/__init__.py'
--- tests/interfaces/openlp_core_ui/__init__.py	2014-03-14 22:08:44 +0000
+++ tests/interfaces/openlp_core_ui/__init__.py	2014-10-28 19:17:32 +0000
@@ -26,3 +26,35 @@
 # with this program; if not, write to the Free Software Foundation, Inc., 59  #
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
+"""
+Module-level functions for the functional test suite
+"""
+
+import os
+from tests.interfaces import patch
+
+from openlp.core.common import is_win
+
+from .test_projectormanager import tmpfile
+
+
+def setUp():
+    if not is_win():
+        # Wine creates a sharing violation during tests. Ignore.
+        try:
+            os.remove(tmpfile)
+        except:
+            pass
+
+
+def tearDown():
+    """
+    Ensure test suite has been cleaned up after tests
+    """
+    patch.stopall()
+    if not is_win():
+        try:
+            # In case of changed schema, remove old test file
+            os.remove(tmpfile)
+        except FileNotFoundError:
+            pass

=== added file 'tests/interfaces/openlp_core_ui/test_projectormanager.py'
--- tests/interfaces/openlp_core_ui/test_projectormanager.py	1970-01-01 00:00:00 +0000
+++ tests/interfaces/openlp_core_ui/test_projectormanager.py	2014-10-28 19:17:32 +0000
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
+# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,             #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock,              #
+# Frode Woldsund, Martin Zibricky, Patrick Zimmermann                         #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Interface tests to test the themeManager class and related methods.
+"""
+
+import os
+from unittest import TestCase
+
+from openlp.core.common import Registry, Settings
+from tests.functional import patch, MagicMock
+from tests.helpers.testmixin import TestMixin
+
+from openlp.core.ui import ProjectorManager, ProjectorEditForm
+from openlp.core.lib.projector.db import Projector, ProjectorDB
+
+from tests.resources.projector.data import TEST1_DATA, TEST2_DATA, TEST3_DATA
+
+tmpfile = '/tmp/openlp-test-projectormanager.sql'
+
+
+class TestProjectorManager(TestCase, TestMixin):
+    """
+    Test the functions in the ProjectorManager module
+    """
+    def setUp(self):
+        """
+        Create the UI and setup necessary options
+        """
+        self.build_settings()
+        self.setup_application()
+        Registry.create()
+        if not hasattr(self, 'projector_manager'):
+            with patch('openlp.core.lib.projector.db.init_url') as mocked_init_url:
+                mocked_init_url.start()
+                mocked_init_url.return_value = 'sqlite:///%s' % tmpfile
+                self.projectordb = ProjectorDB()
+                if not hasattr(self, 'projector_manager'):
+                    self.projector_manager = ProjectorManager(projectordb=self.projectordb)
+
+    def tearDown(self):
+        """
+        Remove test database.
+        Delete all the C++ objects at the end so that we don't have a segfault.
+        """
+        self.projectordb.session.close()
+        del self.projector_manager
+        self.destroy_settings()
+
+    def bootstrap_initialise_test(self):
+        """
+        Test initialize calls correct startup functions
+        """
+        # WHEN: we call bootstrap_initialise
+        self.projector_manager.bootstrap_initialise()
+        # THEN: ProjectorDB is setup
+        self.assertEqual(type(self.projector_manager.projectordb), ProjectorDB,
+                         'Initialization should have created a ProjectorDB() instance')
+
+    def bootstrap_post_set_up_test(self):
+        """
+        Test post-initialize calls proper setups
+        """
+        # GIVEN: setup mocks
+        self.projector_manager._load_projectors = MagicMock()
+
+        # WHEN: Call to initialize is run
+        self.projector_manager.bootstrap_initialise()
+        self.projector_manager.bootstrap_post_set_up()
+
+        # THEN: verify calls to retrieve saved projectors
+        self.assertEqual(1, self.projector_manager._load_projectors.call_count,
+                         'Initialization should have called load_projectors()')
+
+        # THEN: Verify edit page is initialized
+        self.assertEqual(type(self.projector_manager.projector_form), ProjectorEditForm,
+                         'Initialization should have created a Projector Edit Form')
+        self.assertIs(self.projector_manager.projectordb,
+                      self.projector_manager.projector_form.projectordb,
+                      'ProjectorEditForm should be using same ProjectorDB() instance as ProjectorManager')

=== added directory 'tests/resources/projector'
=== added file 'tests/resources/projector/data.py'
--- tests/resources/projector/data.py	1970-01-01 00:00:00 +0000
+++ tests/resources/projector/data.py	2014-10-28 19:17:32 +0000
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
+# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder,               #
+# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble,             #
+# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann           #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`tests.resources.projector.data file contains test data
+"""
+
+from openlp.core.lib.projector.db import Projector
+
+# Test data
+TEST1_DATA = Projector(ip='111.111.111.111',
+                       port='1111',
+                       pin='1111',
+                       name='___TEST_ONE___',
+                       location='location one',
+                       notes='notes one')
+
+TEST2_DATA = Projector(ip='222.222.222.222',
+                       port='2222',
+                       pin='2222',
+                       name='___TEST_TWO___',
+                       location='location two',
+                       notes='notes two')
+
+TEST3_DATA = Projector(ip='333.333.333.333',
+                       port='3333',
+                       pin='3333',
+                       name='___TEST_THREE___',
+                       location='location three',
+                       notes='notes three')


Follow ups