← Back to team overview

openlp-core team mailing list archive

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

 

Review: Needs Fixing

Need to cleanup unused imports.
The icon used in the settings dialog needs to be resized to match the other icons in the settings dialog.
The icons used to represent the projectors power/lamp state should be resized to match the size of the projector connected/disconnected icons.

Diff comments:

> === modified file '.bzrignore'
> --- .bzrignore	2014-07-11 11:35:56 +0000
> +++ .bzrignore	2014-10-08 22:36:55 +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-08 22:36:55 +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 MD%Sum on salt, data

Typo in MD5Sum

> +    using PyQt4.QCryptograhicHash.

Typo in Cryptographic

> +
> +    :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-08 22:36:55 +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-09-08 21:08:49 +0000
> +++ openlp/core/common/settings.py	2014-10-08 22:36:55 +0000
> @@ -275,6 +275,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')],
> @@ -295,7 +296,13 @@
>          '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/query time': 20  # PJLink socket timeout is 30 seconds
>      }
>      __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-08 22:36:55 +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', 'Singluar')

Typo in Singular

> +        self.Manufacturers = translate('OpenLP.Ui', 'Manufacturers', 'Plural')
> +        self.Model = translate('OpenLP.Ui', 'Model', 'Singluar')

Typo in 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', 'Singluar')

Typo in 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-08 22:36:55 +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-08 22:36:55 +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/constants.py'
> --- openlp/core/lib/projector/constants.py	1970-01-01 00:00:00 +0000
> +++ openlp/core/lib/projector/constants.py	2014-10-08 22:36:55 +0000
> @@ -0,0 +1,317 @@
> +# -*- 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:`projector` module
> +"""
> +
> +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': ['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',  # Projuct name query

Typo in product

> +                          '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_NETWORK: translate('OpenLP.ProjectorConstants', 'Network error'),

This is defined twice here and on line 254

> +             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', 'Invald packet received'),

Typo in invalid

> +             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',

Defined twice see line 224

> +                                  '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 availble'),

Typo in 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')}
> 
> === 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-08 22:36:55 +0000
> @@ -0,0 +1,291 @@
> +# -*- 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:`projector.db` module provides the database functions for the
> +    Projector module.
> +"""
> +
> +import logging
> +log = logging.getLogger(__name__)
> +log.debug('projector.lib.db module loaded')
> +
> +from os import path
> +
> +from sqlalchemy import Column, ForeignKey, Integer, MetaData, Sequence, String, and_
> +from sqlalchemy.ext.declarative import declarative_base, declared_attr
> +from sqlalchemy.orm import backref, joinedload, relationship
> +from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
> +from sqlalchemy.sql import select
> +
> +from openlp.core.common import translate
> +from openlp.core.lib.db import BaseModel, Manager, init_db, init_url
> +from openlp.core.lib.projector.constants import PJLINK_DEFAULT_SOURCES
> +
> +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):
> +    """
> +    Manufacturer table.
> +    Model table is related.
> +    """
> +    def __repr__(self):
> +        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):
> +    """
> +    Model table.
> +    Manufacturer table links here.
> +    Source table is related.
> +    """
> +    def __repr__(self):
> +        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):
> +    """
> +    Input ource table.

Typo in source

> +    Model table links here.
> +
> +    These entries map PJLink source codes to text strings.
> +    """
> +    def __repr__(self):
> +        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.
> +
> +    No relation. This keeps track of installed projectors.
> +    """
> +    ip = Column(String(100))
> +    port = Column(String(8))
> +    pin = Column(String(6))
> +    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))
> +
> +
> +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.
> +
> +        Change to Declarative means we really don't do much here.
> +        """
> +        url = init_url('projector')
> +        session, metadata = init_db(url, base=Base)
> +        Base.metadata.create_all(checkfirst=True)
> +        return session
> +
> +    def get_projector_all(self):
> +        """
> +        Retrieve all projector entries so they can be added to the Projector
> +        Manager list pane.
> +        """
> +        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: Projetor() instance

Typo in Projector

> +        """
> +        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: Projetor() instance

Typo in Projector

> +        """
> +        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
> +
> +        NOTE: Will not add new entry if IP is the same as already in the table.
> +
> +        :param projector: Projetor() instance to add

Typo in Projector

> +        :returns: bool
> +        """
> +        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
> +        """
> +        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
> +        """
> +        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, make, model, sources):
> +        """
> +        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 make: Manufacturer name as retrieved from projector
> +        :param model: Manufacturer model as retrieved from projector
> +        :returns: dict
> +        """
> +        source_dict = {}
> +        model_list = self.get_all_objects(Model, Model.name == model)
> +        if model_list is None or len(model_list) < 1:
> +            # No entry for model, so see if there's a default entry
> +            default_list = self.get_object_filtered(Manufacturer, Manufacturer.name == make)
> +            if default_list is None or len(default_list) < 1:
> +                # No entry for manufacturer, so can't check for default text
> +                log.debug('Using default PJLink text for input select')
> +                for source in sources:
> +                    log.debug('source = "%s"' % source)
> +                    source_dict[source] = '%s %s' % (PJLINK_DEFAULT_SOURCES[source[0]], source[1])
> +            else:
> +                # We have a manufacturer entry, see if there's a default
> +                # TODO: Finish this section once edit source input is done
> +                pass
> +        else:
> +            # There's at least one model entry, see if there's more than one manufacturer
> +            # TODO: Finish this section once edit source input text is done
> +            pass
> +        return source_dict
> 
> === 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-08 22:36:55 +0000
> @@ -0,0 +1,670 @@
> +# -*- 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:`projector.pjlink1` module provides the necessary functions
> +        for connecting to a PJLink-capable projector.
> +
> +    See PJLink Specifications for Class 1 for details.
> +
> +    NOTE:
> +      Function names follow  the following syntax:
> +            def process_CCCC(...):
> +      WHERE:
> +            CCCC = PJLink command being processed.
> +
> +      See PJLINK_FUNC(...) for command returned from projector.
> +
> +"""
> +
> +import logging
> +log = logging.getLogger(__name__)
> +
> +log.debug('projectorpjlink1 loaded')
> +
> +__all__ = ['PJLink1']
> +
> +from time import sleep
> +from codecs import decode, encode
> +
> +from PyQt4 import QtCore, QtGui
> +from PyQt4.QtCore import QObject, 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.
> +    """
> +    changeStatus = pyqtSignal(str, int, str)
> +    projectorNetwork = pyqtSignal(int)  # Projector network activity
> +    projectorStatus = pyqtSignal(int)
> +
> +    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
> +        """
> +        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
> +        # Allowances for Projector Wizard option
> +        if 'dbid' in kwargs:
> +            self.dbid = kwargs['dbid']
> +        else:
> +            self.dbid = None
> +        if 'location' in kwargs:
> +            self.location = kwargs['location']
> +        else:
> +            self.location = None
> +        if 'notes' in kwargs:
> +            self.notes = kwargs['notes']
> +        else:
> +            self.notes = None
> +        if 'wizard' in kwargs:
> +            self.new_wizard = True
> +        else:
> +            self.new_wizard = False
> +        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
> +        # Account for self.readLine appending \0 and/or exraneous \r

Typo in extraneous

> +        self.maxSize = PJLINK_MAX_PACKET + 2
> +        self.setReadBufferSize(self.maxSize)
> +        # PJLink projector information
> +        self.pjlink_class = '1'  # Default class
> +        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.projector_errors = None
> +        # Set from ProjectorManager.add_projector()
> +        self.widget = None  # QListBox entry
> +        self.timer = None  # Timer that calls the poll_loop
> +        # Map command returned 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,
> +                             'POWR': self.process_powr
> +                             }
> +
> +    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)
> +        self.connected.disconnect(self.check_login)
> +        self.disconnected.disconnect(self.disconnect_from_host)
> +        self.error.disconnect(self.get_error)
> +        self.disconnect_from_host()
> +        self.deleteLater()
> +        self.i_am_running = False
> +
> +    def poll_loop(self):
> +        """
> +        Called by QTimer in ProjectorManager.ProjectorItem.
> +        Retrieves status information.
> +        """
> +        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
> +        self.timer.start()
> +        for i in ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']:
> +            self.send_command(i)
> +            self.waitForReadyRead()
> +        if self.source_available is None:
> +            self.send_command('INST')
> +
> +    def _get_status(self, status):
> +        """
> +        Helper to retrieve status/error codes and convert to strings.
> +        """
> +        # Return the status code as a 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, '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.
> +        """
> +        message = '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)
> +
> +    def check_command(self, cmd):
> +        """
> +        Verifies command is valid based on PJLink class.
> +        """
> +        return self.pjlink_class in PJLINK_VALID_CMD and \
> +            cmd in PJLINK_VALID_CMD[self.pjlink_class]
> +
> +    def check_login(self):
> +        """
> +        Processes the initial connection and authentication (if needed).
> +        """
> +        self.waitForReadyRead(5000)  # 5 seconds should be more than enough
> +        read = self.readLine(self.maxSize)
> +        dontcare = self.readLine(self.maxSize)  # Clean out the trailing \r\n
> +        if 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))
> +        # At this point, we should only have the initial login prompt with
> +        # possible authentication
> +        if not data.upper().startswith('PJLINK'):
> +            # Invalid response
> +            return self.disconnect_from_host()
> +        data_check = data.strip().split(' ')
> +        log.debug('(%s) data_check="%s"' % (self.ip, data_check))
> +        salt = None
> +        # PJLink initial login will be:
> +        # 'PJLink 0' - Unauthenticated login - no extra steps required.
> +        # 'PJLink 1 XXXXXX' Authenticated login - extra processing required.
> +        if data_check[1] == '1':
> +            # Authenticated login with salt
> +            salt = qmd5_hash(salt=data_check[2], data=self.pin)
> +        # We're connected at this point, so go ahead and do regular I/O
> +        self.readyRead.connect(self.get_data)
> +        # Initial data we should know about
> +        self.send_command(cmd='CLSS', salt=salt)
> +        self.waitForReadyRead()
> +        # These should never change once we get this information
> +        if self.manufacturer is None:
> +            for i in ['INF1', 'INF2', 'INFO', 'NAME', 'INST']:
> +                self.send_command(cmd=i)
> +                self.waitForReadyRead()
> +        self.change_status(S_CONNECTED)
> +        if not self.new_wizard:
> +            self.timer.start()
> +            self.poll_loop()
> +
> +    def get_data(self):
> +        """
> +        Socket interface to retrieve data.
> +        """
> +        log.debug('(%s) Reading data' % self.ip)
> +        if self.state() != self.ConnectedState:
> +            log.debug('(%s) get_data(): Not connected - returning' % self.ip)
> +            return
> +        read = self.readLine(self.maxSize)
> +        if read == -1:
> +            # No data available
> +            log.debug('(%s) get_data(): No data available (-1)' % self.ip)
> +            return
> +        self.projectorNetwork.emit(S_NETWORK_RECEIVED)
> +        data_in = decode(read, 'ascii')
> +        data = data_in.strip()
> +        if len(data) < 8:
> +            # Not enough data for a packet
> +            log.debug('(%s) get_data(): Packet length < 8: "%s"' % (self.ip, data))
> +            return
> +        log.debug('(%s) Checking new data "%s"' % (self.ip, data))
> +        if '=' in data:
> +            pass
> +        else:
> +            log.warn('(%s) Invalid packet received')
> +            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) Invalid packet - expected header + command + data' % self.ip)
> +            log.warn('(%s) Received data: "%s"' % (self.ip, read))
> +            self.change_status(E_INVALID_DATA)
> +            return
> +
> +        if not self.check_command(cmd):
> +            log.warn('(%s) Invalid packet - unknown command "%s"' % self.ip, cmd)
> +            return
> +        return self.process_command(cmd, data)
> +
> +    @pyqtSlot(int)
> +    def get_error(self, err):
> +        """
> +        Process error from SocketError signal
> +        """
> +        log.debug('(%s) get_error(err=%s): %s' % (self.ip, err, self.errorString()))
> +        if err <= 18:
> +            # QSocket errors. Redefined in projectorconstants 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())
> +        return
> +
> +    def send_command(self, cmd, opts='?', salt=None):
> +        """
> +        Socket interface to send commands to projector.
> +        """
> +        if self.state() != self.ConnectedState:
> +            log.warn('(%s) send_command(): Not connected - returning' % self.ip)
> +            return
> +        self.projectorNetwork.emit(S_NETWORK_SENDING)
> +        log.debug('(%s) Sending 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' % (salt, cmd, opts, CR)
> +        sent = self.write(out)
> +        self.waitForBytesWritten(5000)  # 5 seconds should be enough
> +        if sent == -1:
> +            # Network error?
> +            self.projectorNetwork.emit(S_NETWORK_RECEIVED)
> +            self.change_status(E_NETWORK, 'Error while sending data to projector')
> +
> +    def process_command(self, cmd, data):
> +        """
> +        Verifies any return error code. Calls the appropriate command handler.
> +        """
> +        log.debug('(%s) Processing command "%s"' % (self.ip, cmd))
> +        if data in PJLINK_ERRORS:
> +            # Oops - projector error
> +            if data.upper() == 'ERRA':
> +                # Authentication error
> +                self.change_status(E_AUTHENTICATION)
> +                return
> +            elif data.upper() == 'ERR1':
> +                # Undefined command
> +                self.change_status(E_UNDEFINED, 'Undefined command: "%s"' % cmd)
> +                return
> +            elif data.upper() == 'ERR2':
> +                # Invalid parameter
> +                self.change_status(E_PARAMETER)
> +                return
> +            elif data.upper() == 'ERR3':
> +                # Projector busy
> +                self.change_status(E_UNAVAILABLE)
> +                return
> +            elif data.upper() == 'ERR4':
> +                # Projector/display error
> +                self.change_status(E_PROJECTOR)
> +                return
> +
> +        # Command succeeded - no extra information
> +        if data.upper() == 'OK':
> +            log.debug('(%s) Command returned OK' % self.ip)
> +            return
> +
> +        if cmd in self.PJLINK1_FUNC:
> +            return self.PJLINK1_FUNC[cmd](data)
> +        else:
> +            log.warn('(%s) Invalid command %s' % (self.ip, cmd))
> +
> +    def process_lamp(self, data):
> +        """
> +        Lamp(s) status. See PJLink Specifications for format.
> +        """
> +        lamps = []
> +        data_dict = data.split()
> +        while data_dict:
> +            fill = {'Hours': int(data_dict[0]), 'On': False if data_dict[1] == '0' else True}
> +            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.
> +        """
> +        if data in PJLINK_POWR_STATUS:
> +            self.power = PJLINK_POWR_STATUS[data]
> +            self.change_status(PJLINK_POWR_STATUS[data])
> +        else:
> +            # Log unknown status response
> +            log.warn('Unknown power response: %s' % data)
> +        return
> +
> +    def process_avmt(self, data):
> +        """
> +        Shutter open/closed. See PJLink specification for format.
> +        """
> +        if data == '11':
> +            self.shutter = True
> +            self.mute = False
> +        elif data == '21':
> +            self.shutter = False
> +            self.mute = True
> +        elif data == '30':
> +            self.shutter = False
> +            self.mute = False
> +        elif data == '31':
> +            self.shutter = True
> +            self.mute = True
> +        else:
> +            log.warn('Unknown shutter response: %s' % data)
> +        return
> +
> +    def process_inpt(self, data):
> +        """
> +        Current source input selected. See PJLink specification for format.
> +        """
> +        self.source = data
> +        return
> +
> +    def process_clss(self, data):
> +        """
> +        PJLink class that this projector supports. See PJLink specification for format.
> +        """
> +        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 by customer.
> +        """
> +        self.pjlink_name = data
> +        return
> +
> +    def process_inf1(self, data):
> +        """
> +        Manufacturer name set by manufacturer.
> +        """
> +        self.manufacturer = data
> +        return
> +
> +    def process_inf2(self, data):
> +        """
> +        Projector Model set by manufacturer.
> +        """
> +        self.model = data
> +        return
> +
> +    def process_info(self, data):
> +        """
> +        Any extra info set by manufacturer.
> +        """
> +        self.other_info = data
> +        return
> +
> +    def process_inst(self, data):
> +        """
> +        Available source inputs. See PJLink specification for format.
> +        """
> +        sources = []
> +        check = data.split()
> +        for source in check:
> +            sources.append(source)
> +        self.source_available = sources
> +        return
> +
> +    def process_erst(self, data):
> +        """
> +        Error status. See PJLink Specifications for format.
> +        """
> +        if int(data) == 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.
> +        """
> +        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):
> +        """
> +        Close socket and cleanup.
> +        """
> +        if self.state() != self.ConnectedState:
> +            log.warn('(%s) disconnect_from_host(): Not connected - returning' % self.ip)
> +            return
> +        self.disconnectFromHost()
> +        try:
> +            self.readyRead.disconnect(self.get_data)
> +        except TypeError:
> +            pass
> +        self.change_status(S_NOT_CONNECTED)
> +        self.timer.stop()
> +
> +    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 retrive shutter status.

Typo in retrieve

> +        """
> +        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.
> +        """
> +        if self.source_available is None:
> +            return
> +        elif src not in self.source_available:
> +            return
> +        self.send_command(cmd='INPT', opts=src)
> +        self.waitForReadyRead()
> +        self.poll_loop()
> +
> +    def set_power_on(self):
> +        """
> +        Send command to turn power to on.
> +        """
> +        self.send_command(cmd='POWR', opts='1')
> +        self.waitForReadyRead()
> +        self.poll_loop()
> +
> +    def set_power_off(self):
> +        """
> +        Send command to turn power to standby.
> +        """
> +        self.send_command(cmd='POWR', opts='0')
> +        self.waitForReadyRead()
> +        self.poll_loop()
> +
> +    def set_shutter_closed(self):
> +        """
> +        Send command to set shutter to closed position.
> +        """
> +        self.send_command(cmd='AVMT', opts='11')
> +        self.waitForReadyRead()
> +        self.poll_loop()
> +
> +    def set_shutter_open(self):
> +        """
> +        Send command to set shutter to open position.
> +        """
> +        self.send_command(cmd='AVMT', opts='10')
> +        self.waitForReadyRead()
> +        self.poll_loop()
> 
> === 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-08 22:36:55 +0000
> @@ -124,9 +124,13 @@
>  from .mediadockmanager import MediaDockManager
>  from .servicemanager import ServiceManager
>  from .thememanager import ThemeManager
> +from .projector.manager import ProjectorManager
> +from .projector.wizard import ProjectorWizard
> +from .projector.tab import ProjectorTab
>  
>  __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', 'ProjectorWizard']
> 
> === modified file 'openlp/core/ui/mainwindow.py'
> --- openlp/core/ui/mainwindow.py	2014-09-04 21:38:39 +0000
> +++ openlp/core/ui/mainwindow.py	2014-10-08 22:36:55 +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', 'Toogle Projector Manager'))

Typo in Toggle

> +        self.view_projector_manager_item.setStatusTip(translate('OpenLP.MainWindow',
> +                                                                'Toggle the visibiilty of the Projector Manager'))

Typo in visibility

>          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'
> @@ -515,6 +545,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)
> @@ -825,6 +856,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])
> @@ -1114,6 +1146,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/manager.py'
> --- openlp/core/ui/projector/manager.py	1970-01-01 00:00:00 +0000
> +++ openlp/core/ui/projector/manager.py	2014-10-08 22:36:55 +0000
> @@ -0,0 +1,836 @@
> +# -*- 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: projectormanager` 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 openlp.core.common import Registry, RegistryProperties, Settings, OpenLPMixin, \
> +    RegistryMixin, translate
> +from openlp.core.lib import OpenLPToolbar, ImageSource, get_text_file_string, build_icon,\
> +    check_item_selected, create_thumb
> +from openlp.core.lib.ui import critical_error_message_box, create_widget_action
> +from openlp.core.utils import get_locale_key, get_filesystem_encoding
> +
> +from openlp.core.lib.projector.db import ProjectorDB
> +from openlp.core.lib.projector.pjlink1 import PJLink1
> +from openlp.core.ui.projector.wizard import ProjectorWizard
> +from openlp.core.lib.projector.constants import *
> +
> +# Dict for matching projector status to display icon
> +STATUS_ICONS = {S_NOT_CONNECTED:  ':/projector/projector_disconnect.png',
> +                S_CONNECTING:  ':/projector/projector_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.png',
> +                E_AUTHENTICATION:  ':/projector/projector_not_connected.png',
> +                E_UNKNOWN_SOCKET_ERROR: ':/icons/openlp-logo-64x64.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 toolbar
> +        self.toolbar = OpenLPToolbar(widget)
> +        self.toolbar.add_toolbar_action('newProjector',
> +                                        text=translate('OpenLP.Projector', 'Add Projector'),
> +                                        icon=':/projector/projector_new.png',
> +                                        tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'),
> +                                        triggers=self.on_add_projector)
> +        self.toolbar.addSeparator()
> +        self.toolbar.add_toolbar_action('connect_all_projectors',
> +                                        text=translate('OpenLP.ProjectorManager', 'Connect to all projectors'),
> +                                        icon=':/projector/projector_connect.png',
> +                                        tootip=translate('OpenLP.ProjectorManager', 'Connect to all projectors'),
> +                                        triggers=self.on_connect_all_projectors)
> +        self.toolbar.add_toolbar_action('disconnect_all_projectors',
> +                                        text=translate('OpenLP.ProjectorManager', 'Disconnect from all projectors'),
> +                                        icon=':/projector/projector_disconnect.png',
> +                                        tooltip=translate('OpenLP.ProjectorManager', 'Disconnect from all projectors'),
> +                                        triggers=self.on_disconnect_all_projectors)
> +        self.toolbar.addSeparator()
> +        self.toolbar.add_toolbar_action('poweron_all_projectors',
> +                                        text=translate('OpenLP.ProjectorManager', 'Power On All Projectors'),
> +                                        icon=':/projector/projector_power_on.png',
> +                                        tooltip=translate('OpenLP.ProjectorManager', 'Power on all projectors'),
> +                                        triggers=self.on_poweron_all_projectors)
> +        self.toolbar.add_toolbar_action('poweroff_all_projectors',
> +                                        text=translate('OpenLP.ProjectorManager', 'Standby All Projector'),
> +                                        icon=':/projector/projector_power_off.png',
> +                                        tooltip=translate('OpenLP.ProjectorManager', 'Put all projectors in standby'),
> +                                        triggers=self.on_poweroff_all_projectors)
> +        self.toolbar.addSeparator()
> +        self.toolbar.add_toolbar_action('blank_projector',
> +                                        text=translate('OpenLP.ProjectorManager', 'Blank All Projector Screens'),
> +                                        icon=':/projector/projector_blank.png',
> +                                        tooltip=translate('OpenLP.ProjectorManager', 'Blank all projector screens'),
> +                                        triggers=self.on_blank_all_projectors)
> +        self.toolbar.add_toolbar_action('show_all_projector',
> +                                        text=translate('OpenLP.ProjectorManager', 'Show All Projector Screens'),
> +                                        icon=':/projector/projector_show.png',
> +                                        tooltip=translate('OpenLP.ProjectorManager', 'Show all projector screens'),
> +                                        triggers=self.on_show_all_projectors)
> +        self.layout.addWidget(self.toolbar)
> +        # Add the projector list box
> +        self.projector_widget = QtGui.QWidgetAction(self.toolbar)
> +        self.projector_widget.setObjectName('projector_widget')
> +        # Create projector manager list
> +        self.projector_list_widget = QtGui.QListWidget(widget)
> +        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)
> +        # Build the context menu
> +        self.menu = QtGui.QMenu()
> +        self.view_action = create_widget_action(self.menu,
> +                                                text=translate('OpenLP.ProjectorManager',
> +                                                               '&View Projector Information'),
> +                                                icon=':/projector/projector_view.png',
> +                                                triggers=self.on_view_projector)
> +        self.status_action = create_widget_action(self.menu,
> +                                                  text=translate('OpenLP.ProjectorManager',
> +                                                                 'View &Projector Status'),
> +                                                  icon=':/projector/projector_status.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_connectors.png',
> +                                                        triggers=self.on_select_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)
> +
> +
> +class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorManager, RegistryProperties):
> +    """
> +    Manage the projectors.
> +    """
> +    def __init__(self, parent=None, projectordb=None):
> +        log.debug('__init__()')
> +        super().__init__(parent)
> +        self.settings_section = 'projector'
> +        self.projectordb = projectordb
> +        self.projector_list = []
> +
> +    def bootstrap_initialise(self):
> +        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')
> +        settings = Settings()
> +        settings.beginGroup(self.settings_section)
> +        self.autostart = settings.value('connect on start')
> +        settings.endGroup()
> +        del(settings)
> +
> +    def bootstrap_post_set_up(self):
> +        self.load_projectors()
> +        self.projector_form = ProjectorWizard(self, projectordb=self.projectordb)
> +        self.projector_form.edit_page.newProjector.connect(self.add_projector_from_wizard)
> +        self.projector_form.edit_page.editProjector.connect(self.edit_projector_from_wizard)
> +
> +    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
> +        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.view_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.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.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 _select_input_widget(self, parent, selected, code, text):
> +        """
> +        Build the radio button widget for selecting source input menu
> +
> +        :param parent: parent widget
> +        :param selected: Selected widget text
> +        :param code: PJLink code for this widget
> +        :param text: Text to display
> +        :returns: radio button widget
> +        """
> +        widget = QtGui.QRadioButton(text, parent=parent)
> +        widget.setChecked(True if code == selected else False)
> +        widget.button_role = code
> +        widget.clicked.connect(self._select_input_radio)
> +        self.radio_buttons.append(widget)
> +        return widget
> +
> +    def _select_input_radio(self, opt1=None, opt2=None):
> +        """
> +        Returns the currently selected radio button
> +
> +        :param opt1: Needed by PyQt4
> +        :param op2: future

Typo in opt2

> +        :returns: Selected button role
> +        """
> +        for i in self.radio_buttons:
> +            if i.isChecked():
> +                self.radio_button_selected = i.button_role
> +                break
> +        return
> +
> +    def on_select_input(self, opt=None):
> +        """
> +        Builds menu for 'Select Input' option, then calls the selected projector
> +        item to change input source.
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow())
> +        projector = list_item.data(QtCore.Qt.UserRole)
> +        layout = QtGui.QVBoxLayout()
> +        box = QtGui.QDialog(parent=self)
> +        box.setModal(True)
> +        title = QtGui.QLabel(translate('OpenLP.ProjectorManager', 'Select the input source below'))
> +        layout.addWidget(title)
> +        self.radio_button_selected = None
> +        self.radio_buttons = []
> +        source_list = self.projectordb.get_source_list(make=projector.link.manufacturer,
> +                                                       model=projector.link.model,
> +                                                       sources=projector.link.source_available
> +                                                       )
> +        if source_list is None:
> +            return
> +        sort = []
> +        for i in source_list.keys():
> +            sort.append(i)
> +        sort.sort()
> +        for i in sort:
> +            button = self._select_input_widget(parent=self,
> +                                               selected=projector.link.source,
> +                                               code=i,
> +                                               text=source_list[i])
> +            layout.addWidget(button)
> +        button_box = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok |
> +                                            QtGui.QDialogButtonBox.Cancel)
> +        button_box.accepted.connect(box.accept)
> +        button_box.rejected.connect(box.reject)
> +        layout.addWidget(button_box)
> +        box.setLayout(layout)
> +        check = box.exec_()
> +        if check == 0:
> +            # Cancel button clicked or window closed - don't set source
> +            return
> +        selected = self.radio_button_selected
> +        projector.link.set_input_source(self.radio_button_selected)
> +        self.radio_button_selected = None
> +
> +    def on_add_projector(self, opt=None):
> +        """
> +        Calls wizard to add a new projector to the database
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        self.projector_form.exec_()
> +
> +    def on_blank_all_projectors(self, opt=None):
> +        """
> +        Cycles through projector list to send blank screen command
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        for item in self.projector_list:
> +            self.on_blank_projector(item)
> +
> +    def on_blank_projector(self, opt=None):
> +        """
> +        Calls projector thread to send blank screen command
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        try:
> +            ip = opt.link.ip
> +            projector = opt
> +        except AttributeError:
> +            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)
> +        return projector.link.set_shutter_closed()
> +
> +    def on_connect_projector(self, opt=None):
> +        """
> +        Calls projector thread to connect to projector
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        try:
> +            ip = opt.link.ip
> +            projector = opt
> +        except AttributeError:
> +            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)
> +        return projector.link.connect_to_host()
> +
> +    def on_connect_all_projectors(self, opt=None):
> +        """
> +        Cycles through projector list to tell threads to connect to projectors
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        for item in self.projector_list:
> +            self.on_connect_projector(item)
> +
> +    def on_delete_projector(self, opt=None):
> +        """
> +        Deletes a projector from the list and the database
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        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 TypeError:
> +            pass
> +        try:
> +            projector.link.changeStatus.disconnect(self.update_status)
> +        except TypeError:
> +            pass
> +
> +        try:
> +            projector.timer.stop()
> +            projector.timer.timeout.disconnect(projector.link.poll_loop)
> +        except 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
> +        :returns: None
> +        """
> +        try:
> +            ip = opt.link.ip
> +            projector = opt
> +        except AttributeError:
> +            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)
> +        return projector.link.disconnect_from_host()
> +
> +    def on_disconnect_all_projectors(self, opt=None):
> +        """
> +        Cycles through projector list to have projector threads disconnect
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        for item in self.projector_list:
> +            self.on_disconnect_projector(item)
> +
> +    def on_edit_projector(self, opt=None):
> +        """
> +        Calls wizard with selected projector to edit information
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        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)
> +
> +    def on_poweroff_all_projectors(self, opt=None):
> +        """
> +        Cycles through projector list to send Power Off command
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        for item in self.projector_list:
> +            self.on_poweroff_projector(item)
> +
> +    def on_poweroff_projector(self, opt=None):
> +        """
> +        Calls projector link to send Power Off command
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        try:
> +            ip = opt.link.ip
> +            projector = opt
> +        except AttributeError:
> +            # Must have been called by a mouse-click on item
> +            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)
> +        return projector.link.set_power_off()
> +
> +    def on_poweron_all_projectors(self, opt=None):
> +        """
> +        Cycles through projector list to send Power On command
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        for item in self.projector_list:
> +            self.on_poweron_projector(item)
> +
> +    def on_poweron_projector(self, opt=None):
> +        """
> +        Calls projector link to send Power On command
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        try:
> +            ip = opt.link.ip
> +            projector = opt
> +        except AttributeError:
> +            lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow())
> +            if lwi is None:
> +                return
> +            projector = lwi.data(QtCore.Qt.UserRole)
> +        return projector.link.set_power_on()
> +
> +    def on_show_all_projectors(self, opt=None):
> +        """
> +        Cycles through projector list to send open shutter command
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        for i in self.projector_list:
> +            self.on_show_projector(i.link)
> +
> +    def on_show_projector(self, opt=None):
> +        """
> +        Calls projector thread to send open shutter command
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        try:
> +            ip = opt.link.ip
> +            projector = opt
> +        except AttributeError:
> +            lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow())
> +            if lwi is None:
> +                return
> +            projector = lwi.data(QtCore.Qt.UserRole)
> +        return projector.link.set_shutter_open()
> +
> +    def on_status_projector(self, opt=None):
> +        """
> +        Builds message box with projector status information
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow())
> +        projector = lwi.data(QtCore.Qt.UserRole)
> +        s = '<b>%s</b>: %s<BR />' % (translate('OpenLP.ProjectorManager', 'Name'), projector.link.name)
> +        s = '%s<b>%s</b>: %s<br />' % (s, translate('OpenLP.ProjectorManager', 'IP'), projector.link.ip)
> +        s = '%s<b>%s</b>: %s<br />' % (s, translate('OpenLP.ProjectorManager', 'Port'), projector.link.port)
> +        s = '%s<hr /><br >' % s
> +        if projector.link.manufacturer is None:
> +            s = '%s%s' % (s, translate('OpenLP.ProjectorManager',
> +                                       'Projector information not available at this time.'))
> +        else:
> +            s = '%s<b>%s</b>: %s<br />' % (s, translate('OpenLP.ProjectorManager', 'Manufacturer'),
> +                                           projector.link.manufacturer)
> +            s = '%s<b>%s</b>: %s<br /><br />' % (s, translate('OpenLP.ProjectorManager', 'Model'),
> +                                                 projector.link.model)
> +            s = '%s<b>%s</b>: %s<br />' % (s, translate('OpenLP.ProjectorManager', 'Power status'),
> +                                           ERROR_MSG[projector.link.power])
> +            s = '%s<b>%s</b>: %s<br />' % (s, translate('OpenLP.ProjectorManager', 'Shutter is'),
> +                                           'Closed' if projector.link.shutter else 'Open')
> +            s = '%s<b>%s</b>: %s<br />' % (s, translate('OpenLP.ProjectorManager', 'Current source input is'),
> +                                           projector.link.source)
> +            s = '%s<hr /><br />' % s
> +            if projector.link.projector_errors is None:
> +                s = '%s%s' % (s, translate('OpenLP.ProjectorManager', 'No current errors or warnings'))
> +            else:
> +                s = '%s<b>%s</b>' % (s, translate('OpenLP.ProjectorManager', 'Current errors/warnings'))
> +                for (key, val) in projector.link.projector_errors.items():
> +                    s = '%s<b>%s</b>: %s<br />' % (s, key, ERROR_MSG[val])
> +            s = '%s<hr /><br />' % s
> +            s = '%s<b>%s</b><br />' % (s, translate('OpenLP.ProjectorManager', 'Lamp status'))
> +            c = 1
> +            for i in projector.link.lamp:
> +                s = '%s <b>%s %s</b> (%s) %s: %s<br />' % (s,
> +                                                           translate('OpenLP.ProjectorManager', 'Lamp'),
> +                                                           c,
> +                                                           translate('OpenLP.ProjectorManager', 'On') if i['On'] else
> +                                                           translate('OpenLP.ProjectorManager', 'Off'),
> +                                                           translate('OpenLP.ProjectorManager', 'Hours'),
> +                                                           i['Hours'])
> +                c = c + 1
> +        QtGui.QMessageBox.information(self, translate('OpenLP.ProjectorManager', 'Projector Information'), s)
> +
> +    def on_view_projector(self, opt=None):
> +        """
> +        Builds message box with projector information stored in database
> +
> +        :param opt: Needed by PyQt4
> +        :returns: None
> +        """
> +        lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow())
> +        projector = lwi.data(QtCore.Qt.UserRole)
> +        dbid = translate('OpenLP.ProjectorManager', 'DB Entry')
> +        ip = translate('OpenLP.ProjectorManager', 'IP')
> +        port = translate('OpenLP.ProjectorManager', 'Port')
> +        name = translate('OpenLP.ProjectorManager', 'Name')
> +        location = translate('OpenLP.ProjectorManager', 'Location')
> +        notes = translate('OpenLP.ProjectorManager', 'Notes')
> +        QtGui.QMessageBox.information(self, translate('OpenLP.ProjectorManager',
> +                                      'Projector %s Information' % projector.link.name),
> +                                      '%s: %s<br /><br />%s: %s<br /><br />%s: %s<br /><br />'
> +                                      '%s: %s<br /><br />%s: %s<br /><br />'
> +                                      '%s:<br />%s' % (dbid, projector.link.dbid,
> +                                                       ip, projector.link.ip,
> +                                                       port, projector.link.port,
> +                                                       name, projector.link.name,
> +                                                       location, projector.link.location,
> +                                                       notes, projector.link.notes))
> +
> +    def _add_projector(self, projector):
> +        """
> +        Helper app to build a projector instance
> +
> +        :param p: Dict of projector database information
> +        :returns: PJLink() 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=projector.pin
> +                       )
> +
> +    def add_projector(self, opt1, opt2=None):
> +        """
> +        Builds manager list item, projector thread, and timer for projector instance.
> +
> +        If called by add projector wizard:
> +            opt1 = wizard instance
> +            opt2 = item
> +        Otherwise:
> +            opt1 = item
> +            opt2 = None
> +
> +        We are not concerned with the wizard instance,
> +        just the projector item
> +
> +        :param opt1: See docstring
> +        :param opt2: See docstring
> +        :returns: None
> +        """
> +        if opt1 is None:
> +            return
> +        if opt2 is None:
> +            projector = opt1
> +        else:
> +            projector = opt2
> +        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.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)
> +        timer = QtCore.QTimer(self)
> +        timer.setInterval(20000)  # 20 second poll interval
> +        timer.timeout.connect(item.link.poll_loop)
> +        item.timer = timer
> +        thread.start()
> +        item.thread = thread
> +        item.link.timer = timer
> +        item.link.widget = item.widget
> +        self.projector_list.append(item)
> +        if self.autostart:
> +            item.link.connect_to_host()
> +        for i in self.projector_list:
> +            log.debug('New projector list - item: (%s) %s' % (i.link.ip, i.link.name))
> +
> +    @pyqtSlot(str)
> +    def add_projector_from_wizard(self, ip, opts=None):
> +        """
> +        Add a projector from the wizard
> +
> +        :param ip: IP address of new record item
> +        :param opts: Needed by PyQt4
> +        :returns: None
> +        """
> +        log.debug('load_projector(ip=%s)' % ip)
> +        item = self.projectordb.get_projector_by_ip(ip)
> +        self.add_projector(item)
> +
> +    @pyqtSlot(object)
> +    def edit_projector_from_wizard(self, projector, opts=None):
> +        """
> +        Update projector from the wizard edit page
> +
> +        :param projector: Projector() instance of projector with updated information
> +        :param opts: Needed by PyQt4
> +        :returns: None
> +        """
> +
> +        self.old_projector.link.name = projector.name
> +        self.old_projector.link.ip = projector.ip
> +        self.old_projector.link.pin = 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 i in self.projectordb.get_projector_all():
> +            self.add_projector(i)
> +
> +    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
> +        :returns: None
> +        """
> +        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 = '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:
> +            item.icon = QtGui.QIcon(QtGui.QPixmap(STATUS_ICONS[status]))
> +            log.debug('(%s) Updating icon' % item.link.name)
> +            item.widget.setIcon(item.icon)
> +
> +
> +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):
> +        self.link = link
> +        self.thread = None
> +        self.icon = None
> +        self.widget = None
> +        self.my_parent = None
> +        self.timer = None
> +        self.projectordb_item = None
> +        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

Param should be function not func

> +    :returns: None
> +    """
> +    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/tab.py'
> --- openlp/core/ui/projector/tab.py	1970-01-01 00:00:00 +0000
> +++ openlp/core/ui/projector/tab.py	2014-10-08 22:36:55 +0000
> @@ -0,0 +1,96 @@
> +# -*- 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:`projector.ui.projectortab` module 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 Registry, Settings, UiStrings, translate
> +from openlp.core.lib import SettingsTab
> +from openlp.core.lib.ui import find_and_set_in_combo_box
> +
> +
> +class ProjectorTab(SettingsTab):
> +    """
> +    Openlp Settings -> Projector settings
> +    """
> +    def __init__(self, parent):
> +        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.setTitle('Communication Options')
> +        self.connect_box.setObjectName('connect_box')
> +        self.connect_box_layout = QtGui.QVBoxLayout(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.addWidget(self.connect_on_startup)
> +        self.left_layout.addWidget(self.connect_box)
> +        self.left_layout.addStretch()
> +
> +    def retranslateUi(self):
> +        """
> +        Translate the UI on the fly
> +        """
> +        self.tab_title_visible = UiStrings().Projectors
> +        self.connect_on_startup.setText(
> +            translate('OpenLP.ProjectorTab', 'Connect to projectors on startup'))
> +
> +    def load(self):
> +        """
> +        Load the projetor settings on startup
> +        """
> +        settings = Settings()
> +        settings.beginGroup(self.settings_section)
> +        self.connect_on_startup.setChecked(settings.value('connect on start'))
> +        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.endGroup

Needs to have parentheses to make it a function call

> 
> === added file 'openlp/core/ui/projector/wizard.py'
> --- openlp/core/ui/projector/wizard.py	1970-01-01 00:00:00 +0000
> +++ openlp/core/ui/projector/wizard.py	2014-10-08 22:36:55 +0000
> @@ -0,0 +1,560 @@
> +# -*- 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:`projector.projectorwizard` module handles the GUI Wizard for adding
> +    new projetor entries.

Typo in projector

> +"""
> +
> +import logging
> +log = logging.getLogger(__name__)
> +log.debug('projectorwizard loaded')
> +
> +from ipaddress import IPv4Address, IPv6Address, AddressValueError
> +
> +from PyQt4 import QtCore, QtGui
> +from PyQt4.QtCore import pyqtSlot, pyqtSignal
> +
> +from openlp.core.common import Registry, RegistryProperties, translate, is_macosx
> +from openlp.core.lib import build_icon
> +
> +from openlp.core.common import verify_ip_address
> +from openlp.core.lib.projector.db import ProjectorDB, Projector
> +from openlp.core.lib.projector.pjlink1 import PJLink1
> +from openlp.core.lib.projector.constants import *
> +
> +PAGE_COUNT = 4
> +(ConnectWelcome,
> + ConnectHost,
> + ConnectEdit,
> + ConnectFinish) = range(PAGE_COUNT)
> +
> +PAGE_NEXT = {ConnectWelcome: ConnectHost,
> +             ConnectHost: ConnectEdit,
> +             ConnectEdit: ConnectFinish,
> +             ConnectFinish: -1}
> +
> +
> +class ProjectorWizard(QtGui.QWizard, RegistryProperties):
> +    """
> +    Wizard for adding/editing projector entries.
> +    """
> +    def __init__(self, parent, projectordb):
> +        log.debug('__init__()')
> +        super().__init__(parent)
> +        self.db = projectordb
> +        self.projector = None
> +        self.setObjectName('projector_wizard')
> +        self.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
> +        self.setModal(True)
> +        if is_macosx():
> +            self.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png'))
> +        else:
> +            self.setWizardStyle(QtGui.QWizard.ModernStyle)
> +        self.setMinimumSize(650, 550)
> +        self.setOption(QtGui.QWizard.NoBackButtonOnStartPage)
> +        self.spacer = QtGui.QSpacerItem(10, 0,
> +                                        QtGui.QSizePolicy.Fixed,
> +                                        QtGui.QSizePolicy.Minimum)
> +        self.setOption(self.HaveHelpButton, True)
> +        self.welcome_page = ConnectWelcomePage(self, ConnectWelcome)
> +        self.host_page = ConnectHostPage(self, ConnectHost)
> +        self.edit_page = ConnectEditPage(self, ConnectEdit)
> +        self.finish_page = ConnectFinishPage(self, ConnectFinish)
> +        self.setPage(self.welcome_page.pageId, self.welcome_page)
> +        self.setPage(self.host_page.pageId, self.host_page)
> +        self.setPage(self.edit_page.pageId, self.edit_page)
> +        self.setPage(self.finish_page.pageId, self.finish_page)
> +        self.registerFields()
> +        self.retranslateUi()
> +        # Connect signals
> +        self.button(QtGui.QWizard.HelpButton).clicked.connect(self.showHelp)
> +        log.debug('ProjectorWizard() started')
> +
> +    def exec_(self, projector=None):
> +        """
> +        Override function to determine whether we are called to add a new
> +        projector or edit an old projector.
> +
> +        :param projector: Projector instance
> +        :returns: None
> +        """
> +        self.projector = projector
> +        if self.projector is None:
> +            log.debug('ProjectorWizard() Adding new projector')
> +            self.setWindowTitle(translate('OpenLP.ProjectorWizard',
> +                                          'New Projector Wizard'))
> +            self.setStartId(ConnectWelcome)
> +            self.restart()
> +        else:
> +            log.debug('ProjectorWizard() Editing existing projector')
> +            self.setWindowTitle(translate('OpenLP.ProjectorWizard',
> +                                          'Edit Projector Wizard'))
> +            self.setStartId(ConnectEdit)
> +            self.restart()
> +        saved = QtGui.QWizard.exec_(self)
> +        self.projector = None
> +        return saved
> +
> +    def registerFields(self):
> +        """
> +        Register selected fields for use by all pages.
> +        """
> +        self.host_page.registerField('ip_number*', self.host_page.ip_number_text)
> +        self.edit_page.registerField('pjlink_port', self.host_page.pjlink_port_text)
> +        self.edit_page.registerField('pjlink_pin', self.host_page.pjlink_pin_text)
> +        self.edit_page.registerField('projector_name*', self.edit_page.name_text)
> +        self.edit_page.registerField('projector_location', self.edit_page.location_text)
> +        self.edit_page.registerField('projector_notes', self.edit_page.notes_text, 'plainText')
> +        self.edit_page.registerField('projector_make', self.host_page.manufacturer_text)
> +        self.edit_page.registerField('projector_model', self.host_page.model_text)
> +
> +    @pyqtSlot()
> +    def showHelp(self):
> +        """
> +        Show the pop-up help message.
> +        """
> +        page = self.currentPage()
> +        try:
> +            help_page = page.help_
> +        except:
> +            help_page = self.no_help
> +        QtGui.QMessageBox.information(self, self.title_help, help_page)
> +
> +    def retranslateUi(self):
> +        """
> +        Fixed-text strings used for translations
> +        """
> +        self.title_help = translate('OpenLP.ProjectorWizard', 'Projector Wizard Help')
> +        self.no_help = translate('OpenLP.ProjectorWizard',
> +                                 'Sorry - no help available for this page.')
> +        self.welcome_page.title_label.setText('<span style=\'font-size:14pt; font-weight:600;\'>%s</span>' %
> +                                              translate('OpenLP.ProjectorWizard',
> +                                                        'Welcome to the<br />Projector Wizard'))
> +        self.welcome_page.information_label.setText(translate('OpenLP.ProjectorWizard', 'This wizard will help you to '
> +                                                              'create and edit your Projector control. <br /><br />'
> +                                                              'Press "Next" button below to continue.'))

Needs to be changed to 'Press the %s button below to continue.') % self.buttonText(QtGui.QWizard.NextButton))

> +        self.host_page.setTitle(translate('OpenLP.ProjectorWizard', 'Host IP Number'))
> +        self.host_page.setSubTitle(translate('OpenLP.ProjectorWizard',
> +                                             'Enter the IP address, port, and PIN used to conenct to the projector. '

Typo in connect

> +                                             'The port should only be changed if you know what you\'re doing, and '
> +                                             'the pin should only be entered if it\'s required.'
> +                                             '<br /><br />Once the IP address is checked and is '
> +                                             'not in the database, you can continue to the next page'))
> +        self.host_page.help_ = translate('OpenLP.ProjectorWizard',
> +                                         '<b>IP</b>: The IP address of the projector to connect to.<br />'
> +                                         '<b>Port</b>: The port number. Default is 4352.<br />'
> +                                         '<b>PIN</b>: If needed, enter the PIN access code for the projector.<br />'
> +                                         '<br />Once I verify the address is a valid IP and not in the database, you '

Need to reword this

> +                                         'can then add the rest of the information on the next page.')
> +        self.host_page.ip_number_label.setText(translate('OpenLP.ProjectorWizard', 'IP Number: '))
> +        self.host_page.pjlink_port_label.setText(translate('OpenLP.ProjectorWizard', 'Port: '))
> +        self.host_page.pjlink_pin_label.setText(translate('OpenLP.ProjectorWizard', 'PIN: '))
> +        self.edit_page.setTitle(translate('OpenLP.ProjectorWizard', 'Add/Edit Projector Information'))
> +        self.edit_page.setSubTitle(translate('OpenLP.ProjectorWizard',
> +                                             'Enter the information below in the left panel for the projector.'))
> +        self.edit_page.help_ = translate('OpenLP.ProjectorWizard',
> +                                         'Please enter the following information:'
> +                                         '<br /><br /><b>PJLink Port</b>: The network port to use. Default is %s.'
> +                                         '<br /><br /><b>PJLink PIN</b>: The PJLink access PIN. Only required if '
> +                                         'PJLink PIN is set in projector. 4 characters max. <br /><br /><b>Name</b>: '
> +                                         'A unique name you want to give to this projector entry. 20 characters max. '
> +                                         '<br /><br /><b>Location</b>: The location of the projector. 30 characters '
> +                                         'max.<br /><br /><b>Notes</b>: Any notes you want to add about this '
> +                                         'projector. 200 characters max.<br /><br />The "Manufacturer" and "Model" '
> +                                         'information will only be available if the projector is connected to the '
> +                                         'network and can be accessed while running this wizard. '
> +                                         '(Currently not implemented)' % PJLINK_PORT)
> +        self.edit_page.ip_number_label.setText(translate('OpenLP.ProjectorWizard', 'IP Number: '))
> +        self.edit_page.pjlink_port_label.setText(translate('OpenLP.ProjectorWizard', 'PJLink port: '))
> +        self.edit_page.pjlink_pin_label.setText(translate('OpenLP.ProjectorWizard', 'PJLink PIN: '))
> +        self.edit_page.name_label.setText(translate('OpenLP.ProjectorWizard', 'Name: '))
> +        self.edit_page.location_label.setText(translate('OpenLP.ProjectorWizard', 'Location: '))
> +        self.edit_page.notes_label.setText(translate('OpenLP.ProjectorWizard', 'Notes: '))
> +        self.edit_page.projector_make_label.setText(translate('OpenLP.ProjectorWizard', 'Manufacturer: '))
> +        self.edit_page.projector_model_label.setText(translate('OpenLP.ProjectorWizard', 'Model: '))
> +        self.finish_page.title_label.setText('<span style=\'font-size:14pt; font-weight:600;\'>%s</span>' %
> +                                             translate('OpenLP.ProjectorWizard', 'Projector Added'))
> +        self.finish_page.information_label.setText(translate('OpenLP.ProjectorWizard',
> +                                                   '<br />Have fun with your new projector.'))
> +
> +
> +class ConnectBase(QtGui.QWizardPage):
> +    """
> +    Base class for the projector wizard pages.
> +    """
> +    def __init__(self, parent=None, page=None):
> +        super().__init__(parent)
> +        self.pageId = page
> +
> +    def nextId(self):
> +        """
> +        Returns next page to show.
> +        """
> +        return PAGE_NEXT[self.pageId]
> +
> +    def setVisible(self, visible):
> +        """
> +        Set buttons for bottom of page.
> +        """
> +        QtGui.QWizardPage.setVisible(self, visible)
> +        if visible:
> +            try:
> +                self.myCustomButton()
> +            except:
> +                try:
> +                    self.wizard().setButtonLayout(self.myButtons)
> +                except:
> +                    self.wizard().setButtonLayout([QtGui.QWizard.Stretch,
> +                                                   QtGui.QWizard.BackButton,
> +                                                   QtGui.QWizard.NextButton,
> +                                                   QtGui.QWizard.CancelButton])
> +
> +
> +class ConnectWelcomePage(ConnectBase):
> +    """
> +    Splash screen
> +    """
> +    def __init__(self, parent, page):
> +        super().__init__(parent, page)
> +        if is_macosx():
> +            self.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png'))
> +        else:
> +            self.setPixmap(QtGui.QWizard.WatermarkPixmap,
> +                           QtGui.QPixmap(':/wizards/wizard_createprojector.png'))
> +        self.setObjectName('welcome_page')
> +        self.myButtons = [QtGui.QWizard.Stretch,
> +                          QtGui.QWizard.NextButton]

Need a cancel button here for consistency

> +        self.layout = QtGui.QVBoxLayout(self)
> +        self.layout.setObjectName('layout')
> +        self.title_label = QtGui.QLabel(self)
> +        self.title_label.setObjectName('title_label')
> +        self.layout.addWidget(self.title_label)
> +        self.layout.addSpacing(40)
> +        self.information_label = QtGui.QLabel(self)
> +        self.information_label.setWordWrap(True)
> +        self.information_label.setObjectName('information_label')
> +        self.layout.addWidget(self.information_label)
> +        self.layout.addStretch()
> +
> +
> +class ConnectHostPage(ConnectBase):
> +    """
> +    Get initial information.
> +    """
> +    def __init__(self, parent, page):
> +        super().__init__(parent, page)
> +        self.setObjectName('host_page')
> +        self.myButtons = [QtGui.QWizard.HelpButton,
> +                          QtGui.QWizard.Stretch,
> +                          QtGui.QWizard.BackButton,
> +                          QtGui.QWizard.NextButton,
> +                          QtGui.QWizard.CancelButton]
> +        self.hostPageLayout = QtGui.QHBoxLayout(self)
> +        self.hostPageLayout.setObjectName('layout')
> +        # Projector DB information
> +        self.localAreaBox = QtGui.QGroupBox(self)
> +        self.localAreaBox.setObjectName('host_local_area_box')
> +        self.localAreaForm = QtGui.QFormLayout(self.localAreaBox)
> +        self.localAreaForm.setObjectName('host_local_area_form')
> +        self.ip_number_label = QtGui.QLabel(self.localAreaBox)
> +        self.ip_number_label.setObjectName('host_ip_number_label')
> +        self.ip_number_text = QtGui.QLineEdit(self.localAreaBox)
> +        self.ip_number_text.setObjectName('host_ip_number_text')
> +        self.localAreaForm.addRow(self.ip_number_label, self.ip_number_text)
> +        self.pjlink_port_label = QtGui.QLabel(self.localAreaBox)
> +        self.pjlink_port_label.setObjectName('host_pjlink_port_label')
> +        self.pjlink_port_text = QtGui.QLineEdit(self.localAreaBox)
> +        self.pjlink_port_text.setMaxLength(5)
> +        self.pjlink_port_text.setText(str(PJLINK_PORT))
> +        self.pjlink_port_text.setObjectName('host_pjlink_port_text')
> +        self.localAreaForm.addRow(self.pjlink_port_label, self.pjlink_port_text)
> +        self.pjlink_pin_label = QtGui.QLabel(self.localAreaBox)
> +        self.pjlink_pin_label.setObjectName('host_pjlink_pin_label')
> +        self.pjlink_pin_text = QtGui.QLineEdit(self.localAreaBox)
> +        self.pjlink_pin_text.setObjectName('host_pjlink_pin_text')
> +        self.localAreaForm.addRow(self.pjlink_pin_label, self.pjlink_pin_text)
> +        self.hostPageLayout.addWidget(self.localAreaBox)
> +        self.manufacturer_text = QtGui.QLineEdit(self)
> +        self.manufacturer_text.setVisible(False)
> +        self.model_text = QtGui.QLineEdit(self)
> +        self.model_text.setVisible(False)
> +
> +    def validatePage(self):
> +        """
> +        Validate IP number/FQDN before continuing to next page.
> +        """
> +        adx = self.wizard().field('ip_number')
> +        port = self.wizard().field('pjlink_port')
> +        pin = self.wizard().field('pjlink_pin')
> +        log.debug('ip="%s" port="%s" pin="%s"' % (adx, port, pin))
> +        valid = verify_ip_address(adx)
> +        if valid:
> +            ip = self.wizard().db.get_projector_by_ip(adx)
> +            if ip is None:
> +                valid = True
> +            else:
> +                QtGui.QMessageBox.warning(self,
> +                                          translate('OpenLP.ProjectorWizard', 'Already Saved'),
> +                                          translate('OpenLP.ProjectorWizard',
> +                                                    'IP "%s"<br />is already in the database as ID %s.'
> +                                                    '<br /><br />Please Enter a different IP.' % (adx, ip.id)))
> +                valid = False
> +        else:
> +            QtGui.QMessageBox.warning(self,
> +                                      translate('OpenLP.ProjectorWizard', 'Invalid IP'),
> +                                      translate('OpenLP.ProjectorWizard',
> +                                                'IP "%s"<br>is not a valid IP address.'
> +                                                '<br /><br />Please enter a valid IP address.' % adx))
> +            valid = False
> +        """
> +        FIXME - Future plan to retrieve manufacture/model input source information. Not implemented yet.
> +        new = PJLink(host=adx, port=port, pin=pin if pin.strip() != '' else None)
> +        if new.connect():
> +            mfg = new.get_manufacturer()
> +            log.debug('Setting manufacturer_text to %s' % mfg)
> +            self.manufacturer_text.setText(mfg)
> +            model = new.get_model()
> +            log.debug('Setting model_text to %s' % model)
> +            self.model_text.setText(model)
> +        else:
> +            if new.status_error == E_AUTHENTICATION:
> +                QtGui.QMessageBox.warning(self,
> +                                          translate('OpenLP.ProjectorWizard', 'Requires Authorization'),
> +                                          translate('OpenLP.ProjectorWizard',
> +                                                    'Projector requires authorization and either PIN not set '
> +                                                    'or invalid PIN set.'
> +                                                    '<br />Enter a valid PIN before hitting "NEXT"')
> +                                          )
> +            elif new.status_error == E_NO_AUTHENTICATION:
> +                QtGui.QMessageBox.warning(self,
> +                                          translate('OpenLP.ProjectorWizard', 'No Authorization Required'),
> +                                          translate('OpenLP.ProjectorWizard',
> +                                                    'Projector does not require authorization and PIN set.'
> +                                                    '<br />Remove PIN entry before hitting "NEXT"')
> +                                          )
> +            valid = False
> +        new.disconnect()
> +        del(new)
> +        """
> +        return valid
> +
> +
> +class ConnectEditPage(ConnectBase):
> +    """
> +    Full information page.
> +    """
> +    newProjector = QtCore.pyqtSignal(str)
> +    editProjector = QtCore.pyqtSignal(object)
> +
> +    def __init__(self, parent, page):
> +        super().__init__(parent, page)
> +        self.setObjectName('edit_page')
> +        self.editPageLayout = QtGui.QHBoxLayout(self)
> +        self.editPageLayout.setObjectName('layout')
> +        # Projector DB information
> +        self.localAreaBox = QtGui.QGroupBox(self)
> +        self.localAreaBox.setObjectName('edit_local_area_box')
> +        self.localAreaForm = QtGui.QFormLayout(self.localAreaBox)
> +        self.localAreaForm.setObjectName('edit_local_area_form')
> +        self.ip_number_label = QtGui.QLabel(self.localAreaBox)
> +        self.ip_number_label.setObjectName('edit_ip_number_label')
> +        self.ip_number_text = QtGui.QLineEdit(self.localAreaBox)
> +        self.ip_number_text.setObjectName('edit_ip_number_text')
> +        self.localAreaForm.addRow(self.ip_number_label, self.ip_number_text)
> +        self.pjlink_port_label = QtGui.QLabel(self.localAreaBox)
> +        self.pjlink_port_label.setObjectName('edit_pjlink_port_label')
> +        self.pjlink_port_text = QtGui.QLineEdit(self.localAreaBox)
> +        self.pjlink_port_text.setMaxLength(5)
> +        self.pjlink_port_text.setObjectName('edit_pjlink_port_text')
> +        self.localAreaForm.addRow(self.pjlink_port_label, self.pjlink_port_text)
> +        self.pjlink_pin_label = QtGui.QLabel(self.localAreaBox)
> +        self.pjlink_pin_label.setObjectName('pjlink_pin_label')
> +        self.pjlink_pin_text = QtGui.QLineEdit(self.localAreaBox)
> +        self.pjlink_pin_text.setObjectName('pjlink_pin_text')
> +        self.localAreaForm.addRow(self.pjlink_pin_label, self.pjlink_pin_text)
> +        self.name_label = QtGui.QLabel(self.localAreaBox)
> +        self.name_label.setObjectName('name_label')
> +        self.name_text = QtGui.QLineEdit(self.localAreaBox)
> +        self.name_text.setObjectName('name_label')
> +        self.name_text.setMaxLength(20)
> +        self.localAreaForm.addRow(self.name_label, self.name_text)
> +        self.location_label = QtGui.QLabel(self.localAreaBox)
> +        self.location_label.setObjectName('location_label')
> +        self.location_text = QtGui.QLineEdit(self.localAreaBox)
> +        self.location_text.setObjectName('location_text')
> +        self.location_text.setMaxLength(30)
> +        self.localAreaForm.addRow(self.location_label, self.location_text)
> +        self.notes_label = QtGui.QLabel(self.localAreaBox)
> +        self.notes_label.setObjectName('notes_label')
> +        self.notes_text = QtGui.QPlainTextEdit(self.localAreaBox)
> +        self.notes_text.setObjectName('notes_text')
> +        self.localAreaForm.addRow(self.notes_label, self.notes_text)
> +        self.editPageLayout.addWidget(self.localAreaBox)
> +        # Projector retrieved information
> +        self.remoteAreaBox = QtGui.QGroupBox(self)
> +        self.remoteAreaBox.setObjectName('edit_remote_area_box')
> +        self.remoteAreaForm = QtGui.QFormLayout(self.remoteAreaBox)
> +        self.remoteAreaForm.setObjectName('edit_remote_area_form')
> +        self.projector_make_label = QtGui.QLabel(self.remoteAreaBox)
> +        self.projector_make_label.setObjectName('projector_make_label')
> +        self.projector_make_text = QtGui.QLabel(self.remoteAreaBox)
> +        self.projector_make_text.setObjectName('projector_make_text')
> +        self.remoteAreaForm.addRow(self.projector_make_label, self.projector_make_text)
> +        self.projector_model_label = QtGui.QLabel(self.remoteAreaBox)
> +        self.projector_model_label.setObjectName('projector_model_text')
> +        self.projector_model_text = QtGui.QLabel(self.remoteAreaBox)
> +        self.projector_model_text.setObjectName('projector_model_text')
> +        self.remoteAreaForm.addRow(self.projector_model_label, self.projector_model_text)
> +        self.editPageLayout.addWidget(self.remoteAreaBox)
> +
> +    def initializePage(self):
> +        """
> +        Fill in the blanks for information from previous page/projector to edit.
> +        """
> +        if self.wizard().projector is not None:
> +            log.debug('ConnectEditPage.initializePage()  Editing existing projector')
> +            self.ip_number_text.setText(self.wizard().projector.ip)
> +            self.pjlink_port_text.setText(str(self.wizard().projector.port))
> +            self.pjlink_pin_text.setText(self.wizard().projector.pin)
> +            self.name_text.setText(self.wizard().projector.name)
> +            self.location_text.setText(self.wizard().projector.location)
> +            self.notes_text.insertPlainText(self.wizard().projector.notes)
> +            self.myButtons = [QtGui.QWizard.HelpButton,
> +                              QtGui.QWizard.Stretch,
> +                              QtGui.QWizard.FinishButton,
> +                              QtGui.QWizard.CancelButton]
> +        else:
> +            log.debug('Retrieving information from host page')
> +            self.ip_number_text.setText(self.wizard().field('ip_number'))
> +            self.pjlink_port_text.setText(self.wizard().field('pjlink_port'))
> +            self.pjlink_pin_text.setText(self.wizard().field('pjlink_pin'))
> +            make = self.wizard().field('projector_make')
> +            model = self.wizard().field('projector_model')
> +            if make is None or make.strip() == '':
> +                self.projector_make_text.setText('Unavailable           ')
> +            else:
> +                self.projector_make_text.setText(make)
> +            if model is None or model.strip() == '':
> +                self.projector_model_text.setText('Unavailable           ')
> +            else:
> +                self.projector_model_text.setText(model)
> +            self.myButtons = [QtGui.QWizard.HelpButton,
> +                              QtGui.QWizard.Stretch,
> +                              QtGui.QWizard.BackButton,
> +                              QtGui.QWizard.NextButton,
> +                              QtGui.QWizard.CancelButton]
> +
> +    def validatePage(self):
> +        """
> +        Last verification if editiing existing entry in case of IP change. Add entry to DB.
> +        """
> +        log.debug('ConnectEditPage().validatePage()')
> +        if self.wizard().projector is not None:
> +            ip = self.ip_number_text.text()
> +            port = self.pjlink_port_text.text()
> +            name = self.name_text.text()
> +            location = self.location_text.text()
> +            notes = self.notes_text.toPlainText()
> +            pin = self.pjlink_pin_text.text()
> +            log.debug('edit-page() Verifying info : ip="%s"' % ip)
> +            valid = verify_ip_address(ip)
> +            if not valid:
> +                QtGui.QMessageBox.warning(self,
> +                                          translate('OpenLP.ProjectorWizard', 'Invalid IP'),
> +                                          translate('OpenLP.ProjectorWizard',
> +                                                    'IP "%s"<br>is not a valid IP address.'
> +                                                    '<br /><br />Please enter a valid IP address.' % ip))
> +                return False
> +            log.debug('Saving edited projector %s' % ip)
> +            self.wizard().projector.ip = ip
> +            self.wizard().projector.port = port
> +            self.wizard().projector.name = name
> +            self.wizard().projector.location = location
> +            self.wizard().projector.notes = notes
> +            self.wizard().projector.pin = pin
> +            saved = self.wizard().db.update_projector(self.wizard().projector)
> +            if not saved:
> +                QtGui.QMessageBox.error(self, translate('OpenLP.ProjectorWizard', 'Database Error'),
> +                                        translate('OpenLP.ProjectorWizard', 'There was an error saving projector '
> +                                                  'information. See the log for the error'))
> +                return False
> +            self.editProjector.emit(self.wizard().projector)
> +        else:
> +            projector = Projector(ip=self.wizard().field('ip_number'),
> +                                  port=self.wizard().field('pjlink_port'),
> +                                  name=self.wizard().field('projector_name'),
> +                                  location=self.wizard().field('projector_location'),
> +                                  notes=self.wizard().field('projector_notes'),
> +                                  pin=self.wizard().field('pjlink_pin'))
> +            log.debug('Adding new projector %s' % projector.ip)
> +            if self.wizard().db.get_projector_by_ip(projector.ip) is None:
> +                saved = self.wizard().db.add_projector(projector)
> +                if not saved:
> +                    QtGui.QMessageBox.error(self, translate('OpenLP.ProjectorWizard', 'Database Error'),
> +                                            translate('OpenLP.ProjectorWizard', 'There was an error saving projector '
> +                                                      'information. See the log for the error'))
> +                    return False
> +                self.newProjector.emit('%s' % projector.ip)
> +        return True
> +
> +    def nextId(self):
> +        """
> +        Returns the next page ID if new entry or end of wizard if editing entry.
> +        """
> +        if self.wizard().projector is None:
> +            return PAGE_NEXT[self.pageId]
> +        else:
> +            return -1
> +
> +
> +class ConnectFinishPage(ConnectBase):
> +    """
> +    Buh-Bye page
> +    """
> +    def __init__(self, parent, page):
> +        super().__init__(parent, page)
> +        self.setObjectName('connect_finish_page')
> +        if is_macosx():
> +            self.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png'))
> +        else:
> +            self.setPixmap(QtGui.QWizard.WatermarkPixmap, QtGui.QPixmap(':/wizards/wizard_createprojector.png'))
> +        self.myButtons = [QtGui.QWizard.Stretch,
> +                          QtGui.QWizard.FinishButton]
> +        self.isFinalPage()
> +        self.layout = QtGui.QVBoxLayout(self)
> +        self.layout.setObjectName('layout')
> +        self.title_label = QtGui.QLabel(self)
> +        self.title_label.setObjectName('title_label')
> +        self.layout.addWidget(self.title_label)
> +        self.layout.addSpacing(40)
> +        self.information_label = QtGui.QLabel(self)
> +        self.information_label.setWordWrap(True)
> +        self.information_label.setObjectName('information_label')
> +        self.layout.addWidget(self.information_label)
> +        self.layout.addStretch()
> 
> === modified file 'openlp/core/ui/settingsform.py'
> --- openlp/core/ui/settingsform.py	2014-04-12 20:19:22 +0000
> +++ openlp/core/ui/settingsform.py	2014-10-08 22:36:55 +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__)
>  
> @@ -67,9 +68,10 @@
>              self.stacked_layout.takeAt(0)
>          self.insert_tab(self.general_tab, 0, PluginStatus.Active)
>          self.insert_tab(self.themes_tab, 1, PluginStatus.Active)
> -        self.insert_tab(self.advanced_tab, 2, PluginStatus.Active)
> -        self.insert_tab(self.player_tab, 3, PluginStatus.Active)
> -        count = 4
> +        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
>          for plugin in self.plugin_manager.plugins:
>              if plugin.settings_tab:
>                  self.insert_tab(plugin.settings_tab, count, plugin.status)
> @@ -125,6 +127,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-08 22:36:55 +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,26 @@
>      <file>theme_new.png</file>
>      <file>theme_edit.png</file>
>    </qresource>
> +  <qresource prefix="projector">
> +    <file>projector_blank.png</file>
> +    <file>projector_connect.png</file>
> +    <file>projector_connectors.png</file>
> +    <file>projector_cooldown.png</file>
> +    <file>projector_disconnect.png</file>
> +    <file>projector_edit.png</file>
> +    <file>projector_error.png</file>
> +    <file>projector_manager.png</file>
> +    <file>projector_new.png</file>
> +    <file>projector_not_connected.png</file>
> +    <file>projector_off.png</file>
> +    <file>projector_on.png</file>
> +    <file>projector_power_off.png</file>
> +    <file>projector_power_on.png</file>
> +    <file>projector_show.png</file>
> +    <file>projector_status.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-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +0000 differ
> === added file 'resources/images/projector_not_connected.png'
> Binary files resources/images/projector_not_connected.png	1970-01-01 00:00:00 +0000 and resources/images/projector_not_connected.png	2014-10-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +0000 differ
> === added file 'resources/images/projector_status.png'
> Binary files resources/images/projector_status.png	1970-01-01 00:00:00 +0000 and resources/images/projector_status.png	2014-10-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +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-08 22:36:55 +0000
> @@ -26,3 +26,28 @@
>  # 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
> +"""
> +

missing os import

> +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()
> 
> === 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-08 22:36:55 +0000
> @@ -0,0 +1,101 @@
> +# -*- 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.
> +"""
> +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, ProjectorWizard
> +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.get_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):
> +        """
> +        Delete all the C++ objects at the end so that we don't have a segfault
> +        """
> +        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 wizard page is initialized
> +        self.assertEqual(type(self.projector_manager.projector_form), ProjectorWizard,
> +                         'Initialization should have created a Wizard')
> +        self.assertIs(self.projector_manager.projectordb,
> +                      self.projector_manager.projector_form.db,
> +                      'Wizard should be using same ProjectorDB() instance')
> 
> === 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-08 22:36:55 +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')
> 


-- 
https://code.launchpad.net/~alisonken1/openlp/projector-2.1-merge/+merge/237692
Your team OpenLP Core is requested to review the proposed merge of lp:~alisonken1/openlp/projector-2.1-merge into lp:openlp.


References