← Back to team overview

openlp-core team mailing list archive

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

 

Review: Needs Information

Comment on line 216

Diff comments:

> === modified file '.bzrignore'
> --- .bzrignore	2014-07-11 11:35:56 +0000
> +++ .bzrignore	2014-10-03 17:39:47 +0000
> @@ -33,3 +33,4 @@
>  __pycache__
>  *.dll
>  .directory
> +*.kate-swp
> 
> === 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-03 17:39:47 +0000
> @@ -30,13 +30,18 @@
>  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 +159,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
> +    using PyQt4.QCryptograhicHash.
> +
> +    :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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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')
> +        self.Manufacturers = translate('OpenLP.Ui', 'Manufacturers', 'Plural')
> +        self.Model = translate('OpenLP.Ui', 'Model', 'Singluar')
> +        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')
> +        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-03 17:39:47 +0000
> @@ -334,3 +334,6 @@
>  from .imagemanager import ImageManager
>  from .renderer import Renderer
>  from .mediamanageritem import MediaManagerItem
> +from .projectordb import ProjectorDB, Projector
> +from .projectorpjlink1 import PJLink1
> +from .projectorconstants 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-03 17:39:47 +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.create_all(engine, checkfirst=True)

Why are you doing a "create_all" here? The normal route doesn't do that here. Also, shouldn't use be binding the engine to the metadata?

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 file 'openlp/core/lib/projectorconstants.py'
> --- openlp/core/lib/projectorconstants.py	1970-01-01 00:00:00 +0000
> +++ openlp/core/lib/projectorconstants.py	2014-10-03 17:39:47 +0000
> @@ -0,0 +1,334 @@
> +# -*- 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
> +from openlp.core.lib.projectordb import Projector
> +
> +
> +__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
> +                          '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"),
> +             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"),
> +             E_WARN: translate('OpenLP.ProjectorConstants', "Warning condition detected"),
> +             E_ERROR: translate('OpenLP.ProjectorConstants', "Error condition detected"),
> +             E_CLASS: translate('OpenLP.ProjectorConstants', "PJLink class not supported"),
> +             E_PREFIX: translate('OpenLP.ProjectorConstants', "Invalid prefix character"),
> +             E_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants',
> +                                             "The connection was refused by the peer (or timed out)"),
> +             E_REMOTE_HOST_CLOSED_CONNECTION: translate('OpenLP.ProjectorConstants',
> +                                                        "The remote host closed the connection"),
> +             E_HOST_NOT_FOUND: translate('OpenLP.ProjectorConstants', "The host address was not found"),
> +             E_SOCKET_ACCESS: translate('OpenLP.ProjectorConstants',
> +                                        "The socket operation failed because the application "
> +                                        "lacked the required privileges"),
> +             E_SOCKET_RESOURCE: translate('OpenLP.ProjectorConstants',
> +                                          "The local system ran out of resources (e.g., too many sockets)"),
> +             E_SOCKET_TIMEOUT: translate('OpenLP.ProjectorConstants',
> +                                         "The socket operation timed out"),
> +             E_DATAGRAM_TOO_LARGE: translate('OpenLP.ProjectorConstants',
> +                                             "The datagram was larger than the operating system's limit"),
> +             E_NETWORK: translate('OpenLP.ProjectorConstants',
> +                                  "An error occurred with the network (Possibly someone pulled the plug?)"),
> +             E_ADDRESS_IN_USE: translate('OpenLP.ProjectorConstants',
> +                                         "The address specified with socket.bind() "
> +                                         "is already in use and was set to be exclusive"),
> +             E_SOCKET_ADDRESS_NOT_AVAILABLE: translate('OpenLP.ProjectorConstants',
> +                                                       "The address specified to socket.bind() "
> +                                                       "does not belong to the host"),
> +             E_UNSUPPORTED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants',
> +                                                       "The requested socket operation is not supported by the local "
> +                                                       "operating system (e.g., lack of IPv6 support)"),
> +             E_PROXY_AUTHENTICATION_REQUIRED: translate('OpenLP.ProjectorConstants',
> +                                                        "The socket is using a proxy, "
> +                                                        "and the proxy requires authentication"),
> +             E_SLS_HANDSHAKE_FAILED: translate('OpenLP.ProjectorConstants',
> +                                               "The SSL/TLS handshake failed"),
> +             E_UNFINISHED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants',
> +                                                      "The last operation attempted has not finished yet "
> +                                                      "(still in progress in the background)"),
> +             E_PROXY_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants',
> +                                                   "Could not contact the proxy server because the connection "
> +                                                   "to that server was denied"),
> +             E_PROXY_CONNECTION_CLOSED: translate('OpenLP.ProjectorConstants',
> +                                                  "The connection to the proxy server was closed unexpectedly "
> +                                                  "(before the connection to the final peer was established)"),
> +             E_PROXY_CONNECTION_TIMEOUT: translate('OpenLP.ProjectorConstants',
> +                                                   "The connection to the proxy server timed out or the proxy "
> +                                                   "server stopped responding in the authentication phase."),
> +             E_PROXY_NOT_FOUND: translate('OpenLP.ProjectorConstants',
> +                                          "The proxy address set with setProxy() was not found"),
> +             E_PROXY_PROTOCOL: translate('OpenLP.ProjectorConstants',
> +                                         "The connection negotiation with the proxy server because the response "
> +                                         "from the proxy server could not be understood"),
> +             E_UNKNOWN_SOCKET_ERROR: translate('OpenLP.ProjectorConstants', "An unidentified error occurred"),
> +             S_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', "Not connected"),
> +             S_CONNECTING: translate('OpenLP.ProjectorConstants', "Connecting"),
> +             S_CONNECTED: translate('OpenLP.ProjectorConstants', "Connected"),
> +             S_STATUS: translate('OpenLP.ProjectorConstants', "Getting status"),
> +             S_OFF: translate('OpenLP.ProjectorConstants', "Off"),
> +             S_INITIALIZE: translate('OpenLP.ProjectorConstants', "Initialize in progress"),
> +             S_STANDBY: translate('OpenLP.ProjectorConstants', "Power in standby"),
> +             S_WARMUP: translate('OpenLP.ProjectorConstants', "Warmup in progress"),
> +             S_ON: translate('OpenLP.ProjectorConstants', "Power is on"),
> +             S_COOLDOWN: translate('OpenLP.ProjectorConstants', "Cooldown in progress"),
> +             S_INFO: translate('OpenLP.ProjectorConstants', "Projector Information availble"),
> +             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}
> +
> +# 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')
> 
> === added file 'openlp/core/lib/projectordb.py'
> --- openlp/core/lib/projectordb.py	1970-01-01 00:00:00 +0000
> +++ openlp/core/lib/projectordb.py	2014-10-03 17:39:47 +0000
> @@ -0,0 +1,293 @@
> +# -*- 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
> +
> +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.
> +    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)
> +        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
> +        """
> +        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
> +        '''
> +        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
> +        :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')
> +                pjlink_default = {'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')}
> +                for source in sources:
> +                    source_dict[source] = '%s %s' % (pjlink_default[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/projectorpjlink1.py'
> --- openlp/core/lib/projectorpjlink1.py	1970-01-01 00:00:00 +0000
> +++ openlp/core/lib/projectorpjlink1.py	2014-10-03 17:39:47 +0000
> @@ -0,0 +1,695 @@
> +# -*- 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 specifics.
> +"""
> +
> +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.projectorconstants 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
> +        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
> +                             }
> +    ###################################################################
> +    #                      Initialization routines                    #
> +    ###################################################################
> +
> +    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()
> +
> +    ###################################################################
> +    #                      Routines for the Gui                       #
> +    ###################################################################
> +
> +    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)
> +
> +    ###################################################################
> +    #                 Projector communications routines               #
> +    ###################################################################
> +
> +    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')
> +
> +    ###################################################################
> +    #                 Process projector return codes                  #
> +    #                                                                 #
> +    # NOTE:                                                           #
> +    # Function names follow  the following syntax:                    #
> +    #       def process_CCCC(...):                                    #
> +    # WHERE:                                                          #
> +    #       CCCC = PJLink command being processed.                    #
> +    #                                                                 #
> +    # See PJLINK_FUNC(...) above for command returned from projector  #
> +    # and PJLink Specification for what command does.                 #
> +    #                                                                 #
> +    ###################################################################
> +
> +    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
> +
> +    ###################################################################
> +    #                Routines called by the GUI                       #
> +    ###################################################################
> +
> +    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()
> +
> +    ###################################################################
> +    #                      Get commands                               #
> +    ###################################################################
> +
> +    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.
> +        '''
> +        return self.send_command(cmd='AVMT')
> +
> +    ###################################################################
> +    #                      Set commands                               #
> +    ###################################################################
> +
> +    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-03 17:39:47 +0000
> @@ -124,9 +124,13 @@
>  from .mediadockmanager import MediaDockManager
>  from .servicemanager import ServiceManager
>  from .thememanager import ThemeManager
> +from .projectormanager import ProjectorManager
> +from .projectorwizard import ProjectorWizard
> +from .projectortab 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-03 17:39:47 +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.projectormanager 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'))
> +        self.view_projector_manager_item.setStatusTip(translate('OpenLP.MainWindow',
> +                                                                'Toggle the visibiilty of the Projector Manager'))
>          self.view_media_manager_item.setText(translate('OpenLP.MainWindow', '&Media Manager'))
>          self.view_media_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Toggle Media Manager'))
>          self.view_media_manager_item.setStatusTip(translate('OpenLP.MainWindow',
> @@ -485,6 +514,7 @@
>          self.service_manager_settings_section = 'servicemanager'
>          self.songs_settings_section = 'songs'
>          self.themes_settings_section = 'themes'
> +        self.projector_settings_section = 'projector'
>          self.players_settings_section = 'players'
>          self.display_tags_section = 'displayTags'
>          self.header_section = 'SettingsImport'
> @@ -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 file 'openlp/core/ui/projectormanager.py'
> --- openlp/core/ui/projectormanager.py	1970-01-01 00:00:00 +0000
> +++ openlp/core/ui/projectormanager.py	2014-10-03 17:39:47 +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.projectordb import ProjectorDB
> +from openlp.core.lib.projectorpjlink1 import PJLink1
> +from openlp.core.ui.projectorwizard import ProjectorWizard
> +from openlp.core.lib.projectorconstants 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
> +        :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
> +        :returs: 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
> +    :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/projectortab.py'
> --- openlp/core/ui/projectortab.py	1970-01-01 00:00:00 +0000
> +++ openlp/core/ui/projectortab.py	2014-10-03 17:39:47 +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
> 
> === added file 'openlp/core/ui/projectorwizard.py'
> --- openlp/core/ui/projectorwizard.py	1970-01-01 00:00:00 +0000
> +++ openlp/core/ui/projectorwizard.py	2014-10-03 17:39:47 +0000
> @@ -0,0 +1,549 @@
> +# -*- 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.
> +"""
> +
> +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
> +from openlp.core.lib import build_icon
> +
> +from openlp.core.common import verify_ip_address
> +from openlp.core.lib.projectordb import ProjectorDB, Projector
> +from openlp.core.lib.projectorpjlink1 import PJLink1
> +from openlp.core.lib.projectorconstants 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, projector=None):
> +        log.debug('__init__()')
> +        super().__init__(parent)
> +        self.db = projectordb
> +        self.projector = projector
> +        self.setObjectName('projector_wizard')
> +        self.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
> +        self.setModal(True)
> +        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
> +        '''
> +        if projector is None:
> +            log.debug('ProjectorWizard() Adding new projetor')
> +            self.setWindowTitle(translate('OpenLP.ProjectorWizard',
> +                                          'New Projector Wizard'))
> +            self.restart()
> +        else:
> +            log.debug('ProjectorWizard() Editing existing projetor')
> +            self.setWindowTitle(translate('OpenLP.ProjectorWizard',
> +                                          'Edit Projector Wizard'))
> +            self.edit_page.projector = projector
> +            self.edit_page.db = self.db
> +            self.setStartId(ConnectEdit)
> +        return QtGui.QWizard.exec_(self)
> +
> +    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.'))
> +        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. '
> +                                             "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 "
> +                                         '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)
> +        self.setPixmap(QtGui.QWizard.WatermarkPixmap,
> +                       QtGui.QPixmap(":/wizards/wizard_createprojector.png"))
> +        self.setObjectName('welcome_page')
> +        self.myButtons = [QtGui.QWizard.Stretch,
> +                          QtGui.QWizard.NextButton]
> +        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.projector = None
> +        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.projector is not None:
> +            log.debug('ConnectEditPage.initializePage()  Editing existing projector')
> +            self.ip_number_text.setText(self.projector.ip)
> +            self.pjlink_port_text.setText(str(self.projector.port))
> +            self.pjlink_pin_text.setText(self.projector.pin)
> +            self.name_text.setText(self.projector.name)
> +            self.location_text.setText(self.projector.location)
> +            self.notes_text.insertPlainText(self.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.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.projector.ip = ip
> +            self.projector.port = port
> +            self.projector.name = name
> +            self.projector.location = location
> +            self.projector.notes = notes
> +            self.projector.pin = pin
> +            saved = self.db.update_projector(self.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.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.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')
> +        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-03 17:39:47 +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.projectortab 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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +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-03 17:39:47 +0000
> @@ -0,0 +1,162 @@
> +# -*- 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-03 17:39:47 +0000
> @@ -27,5 +27,26 @@
>  # 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-03 17:39:47 +0000
> @@ -0,0 +1,159 @@
> +# -*- 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.projectordb import Projector, ProjectorDB
> +from openlp.core.lib.projectorconstants 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.projectordb.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-03 17:39:47 +0000
> @@ -26,3 +26,26 @@
>  # 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
> +"""
> +
> +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-03 17:39:47 +0000
> @@ -0,0 +1,100 @@
> +# -*- 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.projectordb import Projector, ProjectorDB
> +from openlp.core.lib.projectorconstants 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.projectordb.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')
> 


-- 
https://code.launchpad.net/~alisonken1/openlp/projector-2.1-merge/+merge/237112
Your team OpenLP Core is subscribed to branch lp:openlp.


Follow ups

References