diff --git a/.bzrignore b/.bzrignore index a0c3f0b4f..390fde6af 100644 --- a/.bzrignore +++ b/.bzrignore @@ -33,3 +33,4 @@ tests.kdev4 __pycache__ *.dll .directory +*.kate-swp diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index 0776547ae..47023a588 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -30,13 +30,17 @@ The :mod:`common` module contains most of the components and libraries that make OpenLP work. """ +import hashlib import re import os import logging import sys import traceback +from ipaddress import IPv4Address, IPv6Address, AddressValueError +from codecs import decode, encode from PyQt4 import QtCore +from PyQt4.QtCore import QCryptographicHash as QHash log = logging.getLogger(__name__ + '.__init__') @@ -154,6 +158,81 @@ def is_linux(): """ 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 diff --git a/openlp/core/common/registryproperties.py b/openlp/core/common/registryproperties.py index e2cfffa09..145f86b0a 100644 --- a/openlp/core/common/registryproperties.py +++ b/openlp/core/common/registryproperties.py @@ -148,3 +148,12 @@ class RegistryProperties(object): 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 diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index f7202b590..db925c77f 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -275,6 +275,7 @@ class Settings(QtCore.QSettings): '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 @@ class Settings(QtCore.QSettings): '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__ = [ diff --git a/openlp/core/common/uistrings.py b/openlp/core/common/uistrings.py index 3fe1485ba..c1e7ebb8b 100644 --- a/openlp/core/common/uistrings.py +++ b/openlp/core/common/uistrings.py @@ -99,6 +99,10 @@ class UiStrings(object): 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 @@ class UiStrings(object): 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') diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index b85070445..dd9eb8978 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -334,3 +334,6 @@ from .dockwidget import OpenLPDockWidget from .imagemanager import ImageManager from .renderer import Renderer from .mediamanageritem import MediaManagerItem +from .projector.db import ProjectorDB, Projector +from .projector.pjlink1 import PJLink1 +from .projector.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING diff --git a/openlp/core/lib/db.py b/openlp/core/lib/db.py index 8e9380241..c458e0f6e 100644 --- a/openlp/core/lib/db.py +++ b/openlp/core/lib/db.py @@ -48,20 +48,53 @@ from openlp.core.utils import delete_file log = logging.getLogger(__name__) -def init_db(url, auto_flush=True, auto_commit=False): +def init_db(url, auto_flush=True, auto_commit=False, base=None): """ Initialise and return the session and metadata for a database :param url: The database to initialise connection with :param auto_flush: Sets the flushing behaviour of the session :param auto_commit: Sets the commit behaviour of the session + :param base: If using declarative, the base class to bind with """ engine = create_engine(url, poolclass=NullPool) - metadata = MetaData(bind=engine) + if base is None: + metadata = MetaData(bind=engine) + else: + base.metadata.bind = engine + metadata = None session = scoped_session(sessionmaker(autoflush=auto_flush, autocommit=auto_commit, bind=engine)) return session, metadata +def init_url(plugin_name, db_file_name=None): + """ + Return the database URL. + + :param plugin_name: The name of the plugin for the database creation. + :param db_file_name: The database file name. Defaults to None resulting in the plugin_name being used. + """ + settings = Settings() + settings.beginGroup(plugin_name) + db_url = '' + db_type = settings.value('db type') + if db_type == 'sqlite': + if db_file_name is None: + db_url = 'sqlite:///%s/%s.sqlite' % (AppLocation.get_section_data_path(plugin_name), plugin_name) + else: + db_url = 'sqlite:///%s/%s' % (AppLocation.get_section_data_path(plugin_name), db_file_name) + else: + db_url = '%s://%s:%s@%s/%s' % (db_type, urlquote(settings.value('db username')), + urlquote(settings.value('db password')), + urlquote(settings.value('db hostname')), + urlquote(settings.value('db database'))) + if db_type == 'mysql': + db_encoding = settings.value('db encoding') + db_url += '?charset=%s' % urlquote(db_encoding) + settings.endGroup() + return db_url + + def get_upgrade_op(session): """ Create a migration context and an operations object for performing upgrades. @@ -159,7 +192,7 @@ class Manager(object): """ 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 @@ class Manager(object): :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) diff --git a/openlp/core/lib/projector/constants.py b/openlp/core/lib/projector/constants.py new file mode 100644 index 000000000..eeb969987 --- /dev/null +++ b/openlp/core/lib/projector/constants.py @@ -0,0 +1,317 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`projector` module +""" + +import logging +log = logging.getLogger(__name__) +log.debug('projector_constants loaded') + +from openlp.core.common import translate + + +__all__ = ['S_OK', 'E_GENERAL', 'E_NOT_CONNECTED', 'E_FAN', 'E_LAMP', 'E_TEMP', + 'E_COVER', 'E_FILTER', 'E_AUTHENTICATION', 'E_NO_AUTHENTICATION', + 'E_UNDEFINED', 'E_PARAMETER', 'E_UNAVAILABLE', 'E_PROJECTOR', + 'E_INVALID_DATA', 'E_WARN', 'E_ERROR', 'E_CLASS', 'E_PREFIX', + 'E_CONNECTION_REFUSED', 'E_REMOTE_HOST_CLOSED_CONNECTION', 'E_HOST_NOT_FOUND', + 'E_SOCKET_ACCESS', 'E_SOCKET_RESOURCE', 'E_SOCKET_TIMEOUT', 'E_DATAGRAM_TOO_LARGE', + 'E_NETWORK', 'E_ADDRESS_IN_USE', 'E_SOCKET_ADDRESS_NOT_AVAILABLE', + 'E_UNSUPPORTED_SOCKET_OPERATION', 'E_PROXY_AUTHENTICATION_REQUIRED', + 'E_SLS_HANDSHAKE_FAILED', 'E_UNFINISHED_SOCKET_OPERATION', 'E_PROXY_CONNECTION_REFUSED', + 'E_PROXY_CONNECTION_CLOSED', 'E_PROXY_CONNECTION_TIMEOUT', 'E_PROXY_NOT_FOUND', + 'E_PROXY_PROTOCOL', 'E_UNKNOWN_SOCKET_ERROR', + 'S_NOT_CONNECTED', 'S_CONNECTING', 'S_CONNECTED', + 'S_STATUS', 'S_OFF', 'S_INITIALIZE', 'S_STANDBY', 'S_WARMUP', 'S_ON', 'S_COOLDOWN', + 'S_INFO', 'S_NETWORK_SENDING', 'S_NETWORK_RECEIVED', + 'ERROR_STRING', 'CR', 'LF', 'PJLINK_ERST_STATUS', 'PJLINK_POWR_STATUS', + 'PJLINK_PORT', 'PJLINK_MAX_PACKET', 'TIMEOUT', 'ERROR_MSG', 'PJLINK_ERRORS', + 'STATUS_STRING', 'PJLINK_VALID_CMD', 'CONNECTION_ERRORS'] + +# Set common constants. +CR = chr(0x0D) # \r +LF = chr(0x0A) # \n +PJLINK_PORT = 4352 +TIMEOUT = 30.0 +PJLINK_MAX_PACKET = 136 +PJLINK_VALID_CMD = {'1': ['POWR', # Power option + 'INPT', # Video sources option + 'AVMT', # Shutter option + 'ERST', # Error status option + 'LAMP', # Lamp(s) query (Includes fans) + 'INST', # Input sources available query + 'NAME', # Projector name query + 'INF1', # Manufacturer name query + 'INF2', # Projuct name query + '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} + +PJLINK_DEFAULT_SOURCES = {'1': translate('OpenLP.DB', 'RGB'), + '2': translate('OpenLP.DB', 'Video'), + '3': translate('OpenLP.DB', 'Digital'), + '4': translate('OpenLP.DB', 'Storage'), + '5': translate('OpenLP.DB', 'Network')} diff --git a/openlp/core/lib/projector/db.py b/openlp/core/lib/projector/db.py new file mode 100644 index 000000000..d7abf6fe0 --- /dev/null +++ b/openlp/core/lib/projector/db.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`projector.db` module provides the database functions for the + Projector module. +""" + +import logging +log = logging.getLogger(__name__) +log.debug('projector.lib.db module loaded') + +from os import path + +from sqlalchemy import Column, ForeignKey, Integer, MetaData, Sequence, String, and_ +from sqlalchemy.ext.declarative import declarative_base, declared_attr +from sqlalchemy.orm import backref, joinedload, relationship +from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound +from sqlalchemy.sql import select + +from openlp.core.common import translate +from openlp.core.lib.db import BaseModel, Manager, init_db, init_url +from openlp.core.lib.projector.constants import PJLINK_DEFAULT_SOURCES + +metadata = MetaData() +Base = declarative_base(metadata) + + +class CommonBase(object): + """ + Base class to automate table name and ID column. + """ + @declared_attr + def __tablename__(cls): + return cls.__name__.lower() + id = Column(Integer, primary_key=True) + + +class Manufacturer(CommonBase, Base): + """ + Manufacturer table. + Model table is related. + """ + def __repr__(self): + return '' % 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 '' % 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 '' % \ + (self.pjlink_name, self.pjlink_code, self.text) + model_id = Column(Integer, ForeignKey('model.id')) + pjlink_name = Column(String(15)) + pjlink_code = Column(String(2)) + text = Column(String(30)) + + +class Projector(CommonBase, Base): + """ + Projector table. + + No relation. This keeps track of installed projectors. + """ + ip = Column(String(100)) + port = Column(String(8)) + pin = Column(String(6)) + name = Column(String(20)) + location = Column(String(30)) + notes = Column(String(200)) + pjlink_name = Column(String(128)) + manufacturer = Column(String(128)) + model = Column(String(128)) + other = Column(String(128)) + sources = Column(String(128)) + + +class ProjectorDB(Manager): + """ + Class to access the projector database. + """ + def __init__(self, *args, **kwargs): + log.debug('ProjectorDB().__init__(args="%s", kwargs="%s")' % (args, kwargs)) + super().__init__(plugin_name='projector', + init_schema=self.init_schema) + log.debug('ProjectorDB() Initialized using db url %s' % self.db_url) + + def init_schema(*args, **kwargs): + """ + Setup the projector database and initialize the schema. + + Change to Declarative means we really don't do much here. + """ + url = init_url('projector') + session, metadata = init_db(url, base=Base) + Base.metadata.create_all(checkfirst=True) + return session + + def get_projector_all(self): + """ + Retrieve all projector entries so they can be added to the Projector + Manager list pane. + """ + log.debug('get_all() called') + return_list = [] + new_list = self.get_all_objects(Projector) + if new_list is None or new_list.count == 0: + return return_list + for new_projector in new_list: + return_list.append(new_projector) + log.debug('get_all() returning %s item(s)' % len(return_list)) + return return_list + + def get_projector_by_ip(self, ip): + """ + Locate a projector by host IP/Name. + + :param ip: Host IP/Name + :returns: Projetor() instance + """ + 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') + for source in sources: + log.debug('source = "%s"' % source) + source_dict[source] = '%s %s' % (PJLINK_DEFAULT_SOURCES[source[0]], source[1]) + else: + # We have a manufacturer entry, see if there's a default + # TODO: Finish this section once edit source input is done + pass + else: + # There's at least one model entry, see if there's more than one manufacturer + # TODO: Finish this section once edit source input text is done + pass + return source_dict diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py new file mode 100644 index 000000000..f5866dd8e --- /dev/null +++ b/openlp/core/lib/projector/pjlink1.py @@ -0,0 +1,668 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`projector.pjlink1` module provides the necessary functions + for connecting to a PJLink-capable projector. + + See PJLink Specifications for Class 1 for details. + + NOTE: + Function names follow the following syntax: + def process_CCCC(...): + WHERE: + CCCC = PJLink command being processed. + + See PJLINK_FUNC(...) for command returned from projector. + +""" + +import logging +log = logging.getLogger(__name__) + +log.debug('projectorpjlink1 loaded') + +__all__ = ['PJLink1'] + +from time import sleep +from codecs import decode, encode + +from PyQt4 import QtCore, QtGui +from PyQt4.QtCore import QObject, pyqtSignal, pyqtSlot +from PyQt4.QtNetwork import QAbstractSocket, QTcpSocket + +from openlp.core.common import translate, qmd5_hash +from openlp.core.lib.projector.constants import * + +# Shortcuts +SocketError = QAbstractSocket.SocketError +SocketSTate = QAbstractSocket.SocketState + +PJLINK_PREFIX = '%' +PJLINK_CLASS = '1' +PJLINK_HEADER = '%s%s' % (PJLINK_PREFIX, PJLINK_CLASS) +PJLINK_SUFFIX = CR + + +class PJLink1(QTcpSocket): + """ + Socket service for connecting to a PJLink-capable projector. + """ + changeStatus = pyqtSignal(str, int, str) + projectorNetwork = pyqtSignal(int) # Projector network activity + projectorStatus = pyqtSignal(int) + + def __init__(self, name=None, ip=None, port=PJLINK_PORT, pin=None, *args, **kwargs): + """ + Setup for instance. + + :param name: Display name + :param ip: IP address to connect to + :param port: Port to use. Default to PJLINK_PORT + :param pin: Access pin (if needed) + + Optional parameters + :param dbid: Database ID number + :param location: Location where projector is physically located + :param notes: Extra notes about the projector + """ + log.debug('PJlink(args="%s" kwargs="%s")' % (args, kwargs)) + self.name = name + self.ip = ip + self.port = port + self.pin = pin + super(PJLink1, self).__init__() + self.dbid = None + self.location = None + self.notes = None + # Allowances for Projector Wizard option + if 'dbid' in kwargs: + self.dbid = kwargs['dbid'] + else: + self.dbid = None + if 'location' in kwargs: + self.location = kwargs['location'] + else: + self.location = None + if 'notes' in kwargs: + self.notes = kwargs['notes'] + else: + self.notes = None + if 'wizard' in kwargs: + self.new_wizard = True + else: + self.new_wizard = False + self.i_am_running = False + self.status_connect = S_NOT_CONNECTED + self.last_command = '' + self.projector_status = S_NOT_CONNECTED + self.error_status = S_OK + # Socket information + # Account for self.readLine appending \0 and/or exraneous \r + self.maxSize = PJLINK_MAX_PACKET + 2 + self.setReadBufferSize(self.maxSize) + # PJLink projector information + self.pjlink_class = '1' # Default class + self.power = S_OFF + self.pjlink_name = None + self.manufacturer = None + self.model = None + self.shutter = None + self.mute = None + self.lamp = None + self.fan = None + self.source_available = None + self.source = None + self.projector_errors = None + # Set from ProjectorManager.add_projector() + self.widget = None # QListBox entry + self.timer = None # Timer that calls the poll_loop + # Map command returned to function + self.PJLINK1_FUNC = {'AVMT': self.process_avmt, + 'CLSS': self.process_clss, + 'ERST': self.process_erst, + 'INFO': self.process_info, + 'INF1': self.process_inf1, + 'INF2': self.process_inf2, + 'INPT': self.process_inpt, + 'INST': self.process_inst, + 'LAMP': self.process_lamp, + 'NAME': self.process_name, + 'POWR': self.process_powr + } + + def thread_started(self): + """ + Connects signals to methods when thread is started. + """ + log.debug('(%s) Thread starting' % self.ip) + self.i_am_running = True + self.connected.connect(self.check_login) + self.disconnected.connect(self.disconnect_from_host) + self.error.connect(self.get_error) + + def thread_stopped(self): + """ + Cleanups when thread is stopped. + """ + log.debug('(%s) Thread stopped' % self.ip) + self.connected.disconnect(self.check_login) + self.disconnected.disconnect(self.disconnect_from_host) + self.error.disconnect(self.get_error) + self.disconnect_from_host() + self.deleteLater() + self.i_am_running = False + + def poll_loop(self): + """ + Called by QTimer in ProjectorManager.ProjectorItem. + Retrieves status information. + """ + if self.state() != self.ConnectedState: + return + log.debug('(%s) Updating projector status' % self.ip) + # Reset timer in case we were called from a set command + self.timer.start() + for i in ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']: + self.send_command(i) + self.waitForReadyRead() + + def _get_status(self, status): + """ + Helper to retrieve status/error codes and convert to strings. + """ + # Return the status code as a string + if status in ERROR_STRING: + return (ERROR_STRING[status], ERROR_MSG[status]) + elif status in STATUS_STRING: + return (STATUS_STRING[status], ERROR_MSG[status]) + else: + return (status, 'Unknown status') + + def change_status(self, status, msg=None): + """ + Check connection/error status, set status for projector, then emit status change signal + for gui to allow changing the icons. + """ + message = 'No message' if msg is None else msg + (code, message) = self._get_status(status) + if msg is not None: + message = msg + if status in CONNECTION_ERRORS: + # Projector, connection state + self.projector_status = self.error_status = self.status_connect = E_NOT_CONNECTED + elif status >= S_NOT_CONNECTED and status < S_STATUS: + self.status_connect = status + self.projector_status = S_NOT_CONNECTED + elif status < S_NETWORK_SENDING: + self.status_connect = S_CONNECTED + self.projector_status = status + (status_code, status_message) = self._get_status(self.status_connect) + log.debug('(%s) status_connect: %s: %s' % (self.ip, status_code, status_message if msg is None else msg)) + (status_code, status_message) = self._get_status(self.projector_status) + log.debug('(%s) projector_status: %s: %s' % (self.ip, status_code, status_message if msg is None else msg)) + (status_code, status_message) = self._get_status(self.error_status) + log.debug('(%s) error_status: %s: %s' % (self.ip, status_code, status_message if msg is None else msg)) + self.changeStatus.emit(self.ip, status, message) + + def check_command(self, cmd): + """ + Verifies command is valid based on PJLink class. + """ + return self.pjlink_class in PJLINK_VALID_CMD and \ + cmd in PJLINK_VALID_CMD[self.pjlink_class] + + def check_login(self): + """ + Processes the initial connection and authentication (if needed). + """ + self.waitForReadyRead(5000) # 5 seconds should be more than enough + read = self.readLine(self.maxSize) + dontcare = self.readLine(self.maxSize) # Clean out the trailing \r\n + if len(read) < 8: + log.warn('(%s) Not enough data read)' % self.ip) + return + data = decode(read, 'ascii') + # Possibility of extraneous data on input when reading. + # Clean out extraneous characters in buffer. + dontcare = self.readLine(self.maxSize) + log.debug('(%s) check_login() read "%s"' % (self.ip, data)) + # At this point, we should only have the initial login prompt with + # possible authentication + if not data.upper().startswith('PJLINK'): + # Invalid response + return self.disconnect_from_host() + data_check = data.strip().split(' ') + log.debug('(%s) data_check="%s"' % (self.ip, data_check)) + salt = None + # PJLink initial login will be: + # 'PJLink 0' - Unauthenticated login - no extra steps required. + # 'PJLink 1 XXXXXX' Authenticated login - extra processing required. + if data_check[1] == '1': + # Authenticated login with salt + salt = qmd5_hash(salt=data_check[2], data=self.pin) + # We're connected at this point, so go ahead and do regular I/O + self.readyRead.connect(self.get_data) + # Initial data we should know about + self.send_command(cmd='CLSS', salt=salt) + self.waitForReadyRead() + # These should never change once we get this information + if self.manufacturer is None: + for i in ['INF1', 'INF2', 'INFO', 'NAME', 'INST']: + self.send_command(cmd=i) + self.waitForReadyRead() + self.change_status(S_CONNECTED) + if not self.new_wizard: + self.timer.start() + self.poll_loop() + + def get_data(self): + """ + Socket interface to retrieve data. + """ + log.debug('(%s) Reading data' % self.ip) + if self.state() != self.ConnectedState: + log.debug('(%s) get_data(): Not connected - returning' % self.ip) + return + read = self.readLine(self.maxSize) + if read == -1: + # No data available + log.debug('(%s) get_data(): No data available (-1)' % self.ip) + return + self.projectorNetwork.emit(S_NETWORK_RECEIVED) + data_in = decode(read, 'ascii') + data = data_in.strip() + if len(data) < 8: + # Not enough data for a packet + log.debug('(%s) get_data(): Packet length < 8: "%s"' % (self.ip, data)) + return + log.debug('(%s) Checking new data "%s"' % (self.ip, data)) + if '=' in data: + pass + else: + log.warn('(%s) Invalid packet received') + return + data_split = data.split('=') + try: + (prefix, class_, cmd, data) = (data_split[0][0], data_split[0][1], data_split[0][2:], data_split[1]) + except ValueError as e: + log.warn('(%s) Invalid packet - expected header + command + data' % self.ip) + log.warn('(%s) Received data: "%s"' % (self.ip, read)) + self.change_status(E_INVALID_DATA) + return + + if not self.check_command(cmd): + log.warn('(%s) Invalid packet - unknown command "%s"' % self.ip, cmd) + return + return self.process_command(cmd, data) + + @pyqtSlot(int) + def get_error(self, err): + """ + Process error from SocketError signal + """ + log.debug('(%s) get_error(err=%s): %s' % (self.ip, err, self.errorString())) + if err <= 18: + # QSocket errors. Redefined in projectorconstants so we don't mistake + # them for system errors + check = err + E_CONNECTION_REFUSED + self.timer.stop() + else: + check = err + if check < E_GENERAL: + # Some system error? + self.change_status(err, self.errorString()) + else: + self.change_status(E_NETWORK, self.errorString()) + return + + def send_command(self, cmd, opts='?', salt=None): + """ + Socket interface to send commands to projector. + """ + if self.state() != self.ConnectedState: + log.warn('(%s) send_command(): Not connected - returning' % self.ip) + return + self.projectorNetwork.emit(S_NETWORK_SENDING) + log.debug('(%s) Sending cmd="%s" opts="%s" %s' % (self.ip, + cmd, + opts, + '' if salt is None else 'with hash')) + if salt is None: + out = '%s%s %s%s' % (PJLINK_HEADER, cmd, opts, CR) + else: + out = '%s%s %s%s' % (salt, cmd, opts, CR) + sent = self.write(out) + self.waitForBytesWritten(5000) # 5 seconds should be enough + if sent == -1: + # Network error? + self.projectorNetwork.emit(S_NETWORK_RECEIVED) + self.change_status(E_NETWORK, 'Error while sending data to projector') + + def process_command(self, cmd, data): + """ + Verifies any return error code. Calls the appropriate command handler. + """ + log.debug('(%s) Processing command "%s"' % (self.ip, cmd)) + if data in PJLINK_ERRORS: + # Oops - projector error + if data.upper() == 'ERRA': + # Authentication error + self.change_status(E_AUTHENTICATION) + return + elif data.upper() == 'ERR1': + # Undefined command + self.change_status(E_UNDEFINED, 'Undefined command: "%s"' % cmd) + return + elif data.upper() == 'ERR2': + # Invalid parameter + self.change_status(E_PARAMETER) + return + elif data.upper() == 'ERR3': + # Projector busy + self.change_status(E_UNAVAILABLE) + return + elif data.upper() == 'ERR4': + # Projector/display error + self.change_status(E_PROJECTOR) + return + + # Command succeeded - no extra information + if data.upper() == 'OK': + log.debug('(%s) Command returned OK' % self.ip) + return + + if cmd in self.PJLINK1_FUNC: + return self.PJLINK1_FUNC[cmd](data) + else: + log.warn('(%s) Invalid command %s' % (self.ip, cmd)) + + def process_lamp(self, data): + """ + Lamp(s) status. See PJLink Specifications for format. + """ + lamps = [] + data_dict = data.split() + while data_dict: + fill = {'Hours': int(data_dict[0]), 'On': False if data_dict[1] == '0' else True} + lamps.append(fill) + data_dict.pop(0) # Remove lamp hours + data_dict.pop(0) # Remove lamp on/off + self.lamp = lamps + return + + def process_powr(self, data): + """ + Power status. See PJLink specification for format. + """ + if data in PJLINK_POWR_STATUS: + self.power = PJLINK_POWR_STATUS[data] + self.change_status(PJLINK_POWR_STATUS[data]) + else: + # Log unknown status response + log.warn('Unknown power response: %s' % data) + return + + def process_avmt(self, data): + """ + Shutter open/closed. See PJLink specification for format. + """ + if data == '11': + self.shutter = True + self.mute = False + elif data == '21': + self.shutter = False + self.mute = True + elif data == '30': + self.shutter = False + self.mute = False + elif data == '31': + self.shutter = True + self.mute = True + else: + log.warn('Unknown shutter response: %s' % data) + return + + def process_inpt(self, data): + """ + Current source input selected. See PJLink specification for format. + """ + self.source = data + return + + def process_clss(self, data): + """ + PJLink class that this projector supports. See PJLink specification for format. + """ + self.pjlink_class = data + log.debug('(%s) Setting pjlink_class for this projector to "%s"' % (self.ip, self.pjlink_class)) + return + + def process_name(self, data): + """ + Projector name set by customer. + """ + self.pjlink_name = data + return + + def process_inf1(self, data): + """ + Manufacturer name set by manufacturer. + """ + self.manufacturer = data + return + + def process_inf2(self, data): + """ + Projector Model set by manufacturer. + """ + self.model = data + return + + def process_info(self, data): + """ + Any extra info set by manufacturer. + """ + self.other_info = data + return + + def process_inst(self, data): + """ + Available source inputs. See PJLink specification for format. + """ + sources = [] + check = data.split() + for source in check: + sources.append(source) + self.source_available = sources + return + + def process_erst(self, data): + """ + Error status. See PJLink Specifications for format. + """ + if int(data) == 0: + self.projector_errors = None + else: + self.projector_errors = {} + # Fan + if data[0] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Fan')] = \ + PJLINK_ERST_STATUS[data[0]] + # Lamp + if data[1] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Lamp')] = \ + PJLINK_ERST_STATUS[data[1]] + # Temp + if data[2] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Temperature')] = \ + PJLINK_ERST_STATUS[data[2]] + # Cover + if data[3] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Cover')] = \ + PJLINK_ERST_STATUS[data[3]] + # Filter + if data[4] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Filter')] = \ + PJLINK_ERST_STATUS[data[4]] + # Other + if data[5] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Other')] = \ + PJLINK_ERST_STATUS[data[5]] + return + + def connect_to_host(self): + """ + Initiate connection. + """ + if self.state() == self.ConnectedState: + log.warn('(%s) connect_to_host(): Already connected - returning' % self.ip) + return + self.change_status(S_CONNECTING) + self.connectToHost(self.ip, self.port if type(self.port) is int else int(self.port)) + + @pyqtSlot() + def disconnect_from_host(self): + """ + Close socket and cleanup. + """ + if self.state() != self.ConnectedState: + log.warn('(%s) disconnect_from_host(): Not connected - returning' % self.ip) + return + self.disconnectFromHost() + try: + self.readyRead.disconnect(self.get_data) + except TypeError: + pass + self.change_status(S_NOT_CONNECTED) + self.timer.stop() + + def get_available_inputs(self): + """ + Send command to retrieve available source inputs. + """ + return self.send_command(cmd='INST') + + def get_error_status(self): + """ + Send command to retrieve currently known errors. + """ + return self.send_command(cmd='ERST') + + def get_input_source(self): + """ + Send command to retrieve currently selected source input. + """ + return self.send_command(cmd='INPT') + + def get_lamp_status(self): + """ + Send command to return the lap status. + """ + return self.send_command(cmd='LAMP') + + def get_manufacturer(self): + """ + Send command to retrieve manufacturer name. + """ + return self.send_command(cmd='INF1') + + def get_model(self): + """ + Send command to retrieve the model name. + """ + return self.send_command(cmd='INF2') + + def get_name(self): + """ + Send command to retrieve name as set by end-user (if set). + """ + return self.send_command(cmd='NAME') + + def get_other_info(self): + """ + Send command to retrieve extra info set by manufacturer. + """ + return self.send_command(cmd='INFO') + + def get_power_status(self): + """ + Send command to retrieve power status. + """ + return self.send_command(cmd='POWR') + + def get_shutter_status(self): + """ + Send command to retrive shutter status. + """ + return self.send_command(cmd='AVMT') + + def set_input_source(self, src=None): + """ + Verify input source available as listed in 'INST' command, + then send the command to select the input source. + """ + if self.source_available is None: + return + elif src not in self.source_available: + return + self.send_command(cmd='INPT', opts=src) + self.waitForReadyRead() + self.poll_loop() + + def set_power_on(self): + """ + Send command to turn power to on. + """ + self.send_command(cmd='POWR', opts='1') + self.waitForReadyRead() + self.poll_loop() + + def set_power_off(self): + """ + Send command to turn power to standby. + """ + self.send_command(cmd='POWR', opts='0') + self.waitForReadyRead() + self.poll_loop() + + def set_shutter_closed(self): + """ + Send command to set shutter to closed position. + """ + self.send_command(cmd='AVMT', opts='11') + self.waitForReadyRead() + self.poll_loop() + + def set_shutter_open(self): + """ + Send command to set shutter to open position. + """ + self.send_command(cmd='AVMT', opts='10') + self.waitForReadyRead() + self.poll_loop() diff --git a/openlp/core/ui/__init__.py b/openlp/core/ui/__init__.py index 664074a87..9bece8dd9 100644 --- a/openlp/core/ui/__init__.py +++ b/openlp/core/ui/__init__.py @@ -124,9 +124,13 @@ from .shortcutlistform import ShortcutListForm from .mediadockmanager import MediaDockManager from .servicemanager import ServiceManager from .thememanager import ThemeManager +from .projector.manager import ProjectorManager +from .projector.wizard import ProjectorWizard +from .projector.tab import ProjectorTab __all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', 'ThemeForm', 'ThemeManager', 'MediaDockManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', 'Display', 'ServiceNoteForm', 'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay', 'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm', - 'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'SingleColumnTableWidget'] + 'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'SingleColumnTableWidget', + 'ProjectorManager', 'ProjectorTab', 'ProjectorWizard'] diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 77a903c5f..d2b3f5b60 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -53,6 +53,7 @@ from openlp.core.ui.media import MediaController from openlp.core.utils import LanguageManager, add_actions, get_application_version from openlp.core.utils.actions import ActionList, CategoryOrder from openlp.core.ui.firsttimeform import FirstTimeForm +from openlp.core.ui.projector.manager import ProjectorManager log = logging.getLogger(__name__) @@ -178,6 +179,14 @@ class Ui_MainWindow(object): 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 @@ class Ui_MainWindow(object): 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 @@ class Ui_MainWindow(object): '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 @@ class Ui_MainWindow(object): 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 @@ class Ui_MainWindow(object): 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 @@ class Ui_MainWindow(object): 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 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): 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 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): 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 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): 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 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): """ 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 diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py new file mode 100644 index 000000000..cb25f5ee3 --- /dev/null +++ b/openlp/core/ui/projector/manager.py @@ -0,0 +1,836 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod: projectormanager` module provides the functions for + the display/control of Projectors. +""" + +import logging +log = logging.getLogger(__name__) +log.debug('projectormanager loaded') + +from PyQt4 import QtCore, QtGui +from PyQt4.QtCore import QObject, QThread, pyqtSlot + +from openlp.core.common import Registry, RegistryProperties, Settings, OpenLPMixin, \ + RegistryMixin, translate +from openlp.core.lib import OpenLPToolbar, ImageSource, get_text_file_string, build_icon,\ + check_item_selected, create_thumb +from openlp.core.lib.ui import critical_error_message_box, create_widget_action +from openlp.core.utils import get_locale_key, get_filesystem_encoding + +from openlp.core.lib.projector.db import ProjectorDB +from openlp.core.lib.projector.pjlink1 import PJLink1 +from openlp.core.ui.projector.wizard import ProjectorWizard +from openlp.core.lib.projector.constants import * + +# Dict for matching projector status to display icon +STATUS_ICONS = {S_NOT_CONNECTED: ':/projector/projector_disconnect.png', + S_CONNECTING: ':/projector/projector_connect.png', + S_CONNECTED: ':/projector/projector_off.png', + S_OFF: ':/projector/projector_off.png', + S_INITIALIZE: ':/projector/projector_off.png', + S_STANDBY: ':/projector/projector_off.png', + S_WARMUP: ':/projector/projector_warmup.png', + S_ON: ':/projector/projector_on.png', + S_COOLDOWN: ':/projector/projector_cooldown.png', + E_ERROR: ':/projector/projector_error.png', + E_NETWORK: ':/projector/projector_not_connected.png', + E_AUTHENTICATION: ':/projector/projector_not_connected.png', + E_UNKNOWN_SOCKET_ERROR: ':/icons/openlp-logo-64x64.png' + } + + +class Ui_ProjectorManager(object): + """ + UI part of the Projector Manager + """ + def setup_ui(self, widget): + """ + Define the UI + :param widget: The screen object the dialog is to be attached to. + """ + log.debug('setup_ui()') + # Create ProjectorManager box + self.layout = QtGui.QVBoxLayout(widget) + self.layout.setSpacing(0) + self.layout.setMargin(0) + self.layout.setObjectName('layout') + # Add toolbar + self.toolbar = OpenLPToolbar(widget) + self.toolbar.add_toolbar_action('newProjector', + text=translate('OpenLP.Projector', 'Add Projector'), + icon=':/projector/projector_new.png', + tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'), + triggers=self.on_add_projector) + self.toolbar.addSeparator() + self.toolbar.add_toolbar_action('connect_all_projectors', + text=translate('OpenLP.ProjectorManager', 'Connect to all projectors'), + icon=':/projector/projector_connect.png', + tootip=translate('OpenLP.ProjectorManager', 'Connect to all projectors'), + triggers=self.on_connect_all_projectors) + self.toolbar.add_toolbar_action('disconnect_all_projectors', + text=translate('OpenLP.ProjectorManager', 'Disconnect from all projectors'), + icon=':/projector/projector_disconnect.png', + tooltip=translate('OpenLP.ProjectorManager', 'Disconnect from all projectors'), + triggers=self.on_disconnect_all_projectors) + self.toolbar.addSeparator() + self.toolbar.add_toolbar_action('poweron_all_projectors', + text=translate('OpenLP.ProjectorManager', 'Power On All Projectors'), + icon=':/projector/projector_power_on.png', + tooltip=translate('OpenLP.ProjectorManager', 'Power on all projectors'), + triggers=self.on_poweron_all_projectors) + self.toolbar.add_toolbar_action('poweroff_all_projectors', + text=translate('OpenLP.ProjectorManager', 'Standby All Projector'), + icon=':/projector/projector_power_off.png', + tooltip=translate('OpenLP.ProjectorManager', 'Put all projectors in standby'), + triggers=self.on_poweroff_all_projectors) + self.toolbar.addSeparator() + self.toolbar.add_toolbar_action('blank_projector', + text=translate('OpenLP.ProjectorManager', 'Blank All Projector Screens'), + icon=':/projector/projector_blank.png', + tooltip=translate('OpenLP.ProjectorManager', 'Blank all projector screens'), + triggers=self.on_blank_all_projectors) + self.toolbar.add_toolbar_action('show_all_projector', + text=translate('OpenLP.ProjectorManager', 'Show All Projector Screens'), + icon=':/projector/projector_show.png', + tooltip=translate('OpenLP.ProjectorManager', 'Show all projector screens'), + triggers=self.on_show_all_projectors) + self.layout.addWidget(self.toolbar) + # Add the projector list box + self.projector_widget = QtGui.QWidgetAction(self.toolbar) + self.projector_widget.setObjectName('projector_widget') + # Create projector manager list + self.projector_list_widget = QtGui.QListWidget(widget) + self.projector_list_widget.setAlternatingRowColors(True) + self.projector_list_widget.setIconSize(QtCore.QSize(90, 50)) + self.projector_list_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.projector_list_widget.setObjectName('projector_list_widget') + self.layout.addWidget(self.projector_list_widget) + self.projector_list_widget.customContextMenuRequested.connect(self.context_menu) + # Build the context menu + self.menu = QtGui.QMenu() + self.view_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&View Projector Information'), + icon=':/projector/projector_view.png', + triggers=self.on_view_projector) + self.status_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + 'View &Projector Status'), + icon=':/projector/projector_status.png', + triggers=self.on_status_projector) + self.edit_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&Edit Projector'), + icon=':/projector/projector_edit.png', + triggers=self.on_edit_projector) + self.menu.addSeparator() + self.connect_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&Connect Projector'), + icon=':/projector/projector_connect.png', + triggers=self.on_connect_projector) + self.disconnect_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + 'D&isconnect Projector'), + icon=':/projector/projector_disconnect.png', + triggers=self.on_disconnect_projector) + self.menu.addSeparator() + self.poweron_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + 'Power &On Projector'), + icon=':/projector/projector_power_on.png', + triggers=self.on_poweron_projector) + self.poweroff_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + 'Power O&ff Projector'), + icon=':/projector/projector_power_off.png', + triggers=self.on_poweroff_projector) + self.menu.addSeparator() + self.select_input_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + 'Select &Input'), + icon=':/projector/projector_connectors.png', + triggers=self.on_select_input) + self.blank_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&Blank Projector Screen'), + icon=':/projector/projector_blank.png', + triggers=self.on_blank_projector) + self.show_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&Show Projector Screen'), + icon=':/projector/projector_show.png', + triggers=self.on_show_projector) + self.menu.addSeparator() + self.delete_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&Delete Projector'), + icon=':/general/general_delete.png', + triggers=self.on_delete_projector) + + +class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorManager, RegistryProperties): + """ + Manage the projectors. + """ + def __init__(self, parent=None, projectordb=None): + log.debug('__init__()') + super().__init__(parent) + self.settings_section = 'projector' + self.projectordb = projectordb + self.projector_list = [] + + def bootstrap_initialise(self): + self.setup_ui(self) + if self.projectordb is None: + # Work around for testing creating a ~/.openlp.data.projector.projector.sql file + log.debug('Creating new ProjectorDB() instance') + self.projectordb = ProjectorDB() + else: + log.debug('Using existing ProjectorDB() instance') + settings = Settings() + settings.beginGroup(self.settings_section) + self.autostart = settings.value('connect on start') + settings.endGroup() + del(settings) + + def bootstrap_post_set_up(self): + self.load_projectors() + self.projector_form = ProjectorWizard(self, projectordb=self.projectordb) + self.projector_form.edit_page.newProjector.connect(self.add_projector_from_wizard) + self.projector_form.edit_page.editProjector.connect(self.edit_projector_from_wizard) + + def context_menu(self, point): + """ + Build the Right Click Context menu and set state. + + :param point: The position of the mouse so the correct item can be found. + """ + # QListWidgetItem + item = self.projector_list_widget.itemAt(point) + if item is None: + return + real_projector = item.data(QtCore.Qt.UserRole) + projector_name = str(item.text()) + visible = real_projector.link.status_connect >= S_CONNECTED + log.debug('(%s) Building menu - visible = %s' % (projector_name, visible)) + self.delete_action.setVisible(True) + self.edit_action.setVisible(True) + self.view_action.setVisible(True) + self.connect_action.setVisible(not visible) + self.disconnect_action.setVisible(visible) + self.status_action.setVisible(visible) + if visible: + self.select_input_action.setVisible(real_projector.link.power == S_ON) + self.poweron_action.setVisible(real_projector.link.power == S_STANDBY) + self.poweroff_action.setVisible(real_projector.link.power == S_ON) + self.blank_action.setVisible(real_projector.link.power == S_ON and + not real_projector.link.shutter) + self.show_action.setVisible(real_projector.link.power == S_ON and + real_projector.link.shutter) + else: + self.select_input_action.setVisible(False) + self.poweron_action.setVisible(False) + self.poweroff_action.setVisible(False) + self.blank_action.setVisible(False) + self.show_action.setVisible(False) + self.menu.projector = real_projector + self.menu.exec_(self.projector_list_widget.mapToGlobal(point)) + + def _select_input_widget(self, parent, selected, code, text): + """ + Build the radio button widget for selecting source input menu + + :param parent: parent widget + :param selected: Selected widget text + :param code: PJLink code for this widget + :param text: Text to display + :returns: radio button widget + """ + widget = QtGui.QRadioButton(text, parent=parent) + widget.setChecked(True if code == selected else False) + widget.button_role = code + widget.clicked.connect(self._select_input_radio) + self.radio_buttons.append(widget) + return widget + + def _select_input_radio(self, opt1=None, opt2=None): + """ + Returns the currently selected radio button + + :param opt1: Needed by PyQt4 + :param op2: future + :returns: Selected button role + """ + for i in self.radio_buttons: + if i.isChecked(): + self.radio_button_selected = i.button_role + break + return + + def on_select_input(self, opt=None): + """ + Builds menu for 'Select Input' option, then calls the selected projector + item to change input source. + + :param opt: Needed by PyQt4 + :returns: None + """ + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + projector = list_item.data(QtCore.Qt.UserRole) + layout = QtGui.QVBoxLayout() + box = QtGui.QDialog(parent=self) + box.setModal(True) + title = QtGui.QLabel(translate('OpenLP.ProjectorManager', 'Select the input source below')) + layout.addWidget(title) + self.radio_button_selected = None + self.radio_buttons = [] + source_list = self.projectordb.get_source_list(make=projector.link.manufacturer, + model=projector.link.model, + sources=projector.link.source_available + ) + if source_list is None: + return + sort = [] + for i in source_list.keys(): + sort.append(i) + sort.sort() + for i in sort: + button = self._select_input_widget(parent=self, + selected=projector.link.source, + code=i, + text=source_list[i]) + layout.addWidget(button) + button_box = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | + QtGui.QDialogButtonBox.Cancel) + button_box.accepted.connect(box.accept) + button_box.rejected.connect(box.reject) + layout.addWidget(button_box) + box.setLayout(layout) + check = box.exec_() + if check == 0: + # Cancel button clicked or window closed - don't set source + return + selected = self.radio_button_selected + projector.link.set_input_source(self.radio_button_selected) + self.radio_button_selected = None + + def on_add_projector(self, opt=None): + """ + Calls wizard to add a new projector to the database + + :param opt: Needed by PyQt4 + :returns: None + """ + self.projector_form.exec_() + + def on_blank_all_projectors(self, opt=None): + """ + Cycles through projector list to send blank screen command + + :param opt: Needed by PyQt4 + :returns: None + """ + for item in self.projector_list: + self.on_blank_projector(item) + + def on_blank_projector(self, opt=None): + """ + Calls projector thread to send blank screen command + + :param opt: Needed by PyQt4 + :returns: None + """ + try: + ip = opt.link.ip + projector = opt + except AttributeError: + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + return projector.link.set_shutter_closed() + + def on_connect_projector(self, opt=None): + """ + Calls projector thread to connect to projector + + :param opt: Needed by PyQt4 + :returns: None + """ + try: + ip = opt.link.ip + projector = opt + except AttributeError: + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + return projector.link.connect_to_host() + + def on_connect_all_projectors(self, opt=None): + """ + Cycles through projector list to tell threads to connect to projectors + + :param opt: Needed by PyQt4 + :returns: None + """ + for item in self.projector_list: + self.on_connect_projector(item) + + def on_delete_projector(self, opt=None): + """ + Deletes a projector from the list and the database + + :param opt: Needed by PyQt4 + :returns: None + """ + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + msg = QtGui.QMessageBox() + msg.setText('Delete projector (%s) %s?' % (projector.link.ip, projector.link.name)) + msg.setInformativeText('Are you sure you want to delete this projector?') + msg.setStandardButtons(msg.Cancel | msg.Ok) + msg.setDefaultButton(msg.Cancel) + ans = msg.exec_() + if ans == msg.Cancel: + return + try: + projector.link.projectorNetwork.disconnect(self.update_status) + except TypeError: + pass + try: + projector.link.changeStatus.disconnect(self.update_status) + except TypeError: + pass + + try: + projector.timer.stop() + projector.timer.timeout.disconnect(projector.link.poll_loop) + except TypeError: + pass + projector.thread.quit() + new_list = [] + for item in self.projector_list: + if item.link.dbid == projector.link.dbid: + continue + new_list.append(item) + self.projector_list = new_list + list_item = self.projector_list_widget.takeItem(self.projector_list_widget.currentRow()) + list_item = None + deleted = self.projectordb.delete_projector(projector.db_item) + for item in self.projector_list: + log.debug('New projector list - item: %s %s' % (item.link.ip, item.link.name)) + + def on_disconnect_projector(self, opt=None): + """ + Calls projector thread to disconnect from projector + + :param opt: Needed by PyQt4 + :returns: None + """ + try: + ip = opt.link.ip + projector = opt + except AttributeError: + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + return projector.link.disconnect_from_host() + + def on_disconnect_all_projectors(self, opt=None): + """ + Cycles through projector list to have projector threads disconnect + + :param opt: Needed by PyQt4 + :returns: None + """ + for item in self.projector_list: + self.on_disconnect_projector(item) + + def on_edit_projector(self, opt=None): + """ + Calls wizard with selected projector to edit information + + :param opt: Needed by PyQt4 + :returns: None + """ + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + projector = list_item.data(QtCore.Qt.UserRole) + if projector is None: + return + self.old_projector = projector + projector.link.disconnect_from_host() + record = self.projectordb.get_projector_by_ip(projector.link.ip) + self.projector_form.exec_(record) + + def on_poweroff_all_projectors(self, opt=None): + """ + Cycles through projector list to send Power Off command + + :param opt: Needed by PyQt4 + :returns: None + """ + for item in self.projector_list: + self.on_poweroff_projector(item) + + def on_poweroff_projector(self, opt=None): + """ + Calls projector link to send Power Off command + + :param opt: Needed by PyQt4 + :returns: None + """ + try: + ip = opt.link.ip + projector = opt + except AttributeError: + # Must have been called by a mouse-click on item + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + return projector.link.set_power_off() + + def on_poweron_all_projectors(self, opt=None): + """ + Cycles through projector list to send Power On command + + :param opt: Needed by PyQt4 + :returns: None + """ + for item in self.projector_list: + self.on_poweron_projector(item) + + def on_poweron_projector(self, opt=None): + """ + Calls projector link to send Power On command + + :param opt: Needed by PyQt4 + :returns: None + """ + try: + ip = opt.link.ip + projector = opt + except AttributeError: + lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + if lwi is None: + return + projector = lwi.data(QtCore.Qt.UserRole) + return projector.link.set_power_on() + + def on_show_all_projectors(self, opt=None): + """ + Cycles through projector list to send open shutter command + + :param opt: Needed by PyQt4 + :returns: None + """ + for i in self.projector_list: + self.on_show_projector(i.link) + + def on_show_projector(self, opt=None): + """ + Calls projector thread to send open shutter command + + :param opt: Needed by PyQt4 + :returns: None + """ + try: + ip = opt.link.ip + projector = opt + except AttributeError: + lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + if lwi is None: + return + projector = lwi.data(QtCore.Qt.UserRole) + return projector.link.set_shutter_open() + + def on_status_projector(self, opt=None): + """ + Builds message box with projector status information + + :param opt: Needed by PyQt4 + :returns: None + """ + lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + projector = lwi.data(QtCore.Qt.UserRole) + s = '%s: %s
' % (translate('OpenLP.ProjectorManager', 'Name'), projector.link.name) + s = '%s%s: %s
' % (s, translate('OpenLP.ProjectorManager', 'IP'), projector.link.ip) + s = '%s%s: %s
' % (s, translate('OpenLP.ProjectorManager', 'Port'), projector.link.port) + s = '%s

' % s + if projector.link.manufacturer is None: + s = '%s%s' % (s, translate('OpenLP.ProjectorManager', + 'Projector information not available at this time.')) + else: + s = '%s%s: %s
' % (s, translate('OpenLP.ProjectorManager', 'Manufacturer'), + projector.link.manufacturer) + s = '%s%s: %s

' % (s, translate('OpenLP.ProjectorManager', 'Model'), + projector.link.model) + s = '%s%s: %s
' % (s, translate('OpenLP.ProjectorManager', 'Power status'), + ERROR_MSG[projector.link.power]) + s = '%s%s: %s
' % (s, translate('OpenLP.ProjectorManager', 'Shutter is'), + 'Closed' if projector.link.shutter else 'Open') + s = '%s%s: %s
' % (s, translate('OpenLP.ProjectorManager', 'Current source input is'), + projector.link.source) + s = '%s

' % s + if projector.link.projector_errors is None: + s = '%s%s' % (s, translate('OpenLP.ProjectorManager', 'No current errors or warnings')) + else: + s = '%s%s' % (s, translate('OpenLP.ProjectorManager', 'Current errors/warnings')) + for (key, val) in projector.link.projector_errors.items(): + s = '%s%s: %s
' % (s, key, ERROR_MSG[val]) + s = '%s

' % s + s = '%s%s
' % (s, translate('OpenLP.ProjectorManager', 'Lamp status')) + c = 1 + for i in projector.link.lamp: + s = '%s %s %s (%s) %s: %s
' % (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

%s: %s

%s: %s

' + '%s: %s

%s: %s

' + '%s:
%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"
has not been implemented yet.' + '
Please check back again later.' % function)) diff --git a/openlp/core/ui/projector/tab.py b/openlp/core/ui/projector/tab.py new file mode 100644 index 000000000..1d0ed90f2 --- /dev/null +++ b/openlp/core/ui/projector/tab.py @@ -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 diff --git a/openlp/core/ui/projector/wizard.py b/openlp/core/ui/projector/wizard.py new file mode 100644 index 000000000..82168aed0 --- /dev/null +++ b/openlp/core/ui/projector/wizard.py @@ -0,0 +1,551 @@ +# -*- 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.projector.db import ProjectorDB, Projector +from openlp.core.lib.projector.pjlink1 import PJLink1 +from openlp.core.lib.projector.constants import * + +PAGE_COUNT = 4 +(ConnectWelcome, + ConnectHost, + ConnectEdit, + ConnectFinish) = range(PAGE_COUNT) + +PAGE_NEXT = {ConnectWelcome: ConnectHost, + ConnectHost: ConnectEdit, + ConnectEdit: ConnectFinish, + ConnectFinish: -1} + + +class ProjectorWizard(QtGui.QWizard, RegistryProperties): + """ + Wizard for adding/editing projector entries. + """ + def __init__(self, parent, projectordb): + log.debug('__init__()') + super().__init__(parent) + self.db = projectordb + self.projector = None + self.setObjectName('projector_wizard') + self.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + self.setModal(True) + self.setWizardStyle(QtGui.QWizard.ModernStyle) + self.setMinimumSize(650, 550) + self.setOption(QtGui.QWizard.NoBackButtonOnStartPage) + self.spacer = QtGui.QSpacerItem(10, 0, + QtGui.QSizePolicy.Fixed, + QtGui.QSizePolicy.Minimum) + self.setOption(self.HaveHelpButton, True) + self.welcome_page = ConnectWelcomePage(self, ConnectWelcome) + self.host_page = ConnectHostPage(self, ConnectHost) + self.edit_page = ConnectEditPage(self, ConnectEdit) + self.finish_page = ConnectFinishPage(self, ConnectFinish) + self.setPage(self.welcome_page.pageId, self.welcome_page) + self.setPage(self.host_page.pageId, self.host_page) + self.setPage(self.edit_page.pageId, self.edit_page) + self.setPage(self.finish_page.pageId, self.finish_page) + self.registerFields() + self.retranslateUi() + # Connect signals + self.button(QtGui.QWizard.HelpButton).clicked.connect(self.showHelp) + log.debug('ProjectorWizard() started') + + def exec_(self, projector=None): + """ + Override function to determine whether we are called to add a new + projector or edit an old projector. + + :param projector: Projector instance + :returns: None + """ + self.projector = projector + if self.projector is None: + log.debug('ProjectorWizard() Adding new projector') + self.setWindowTitle(translate('OpenLP.ProjectorWizard', + 'New Projector Wizard')) + self.setStartId(ConnectWelcome) + self.restart() + else: + log.debug('ProjectorWizard() Editing existing projector') + self.setWindowTitle(translate('OpenLP.ProjectorWizard', + 'Edit Projector Wizard')) + self.setStartId(ConnectEdit) + self.restart() + saved = QtGui.QWizard.exec_(self) + self.projector = None + return saved + + def registerFields(self): + """ + Register selected fields for use by all pages. + """ + self.host_page.registerField('ip_number*', self.host_page.ip_number_text) + self.edit_page.registerField('pjlink_port', self.host_page.pjlink_port_text) + self.edit_page.registerField('pjlink_pin', self.host_page.pjlink_pin_text) + self.edit_page.registerField('projector_name*', self.edit_page.name_text) + self.edit_page.registerField('projector_location', self.edit_page.location_text) + self.edit_page.registerField('projector_notes', self.edit_page.notes_text, 'plainText') + self.edit_page.registerField('projector_make', self.host_page.manufacturer_text) + self.edit_page.registerField('projector_model', self.host_page.model_text) + + @pyqtSlot() + def showHelp(self): + """ + Show the pop-up help message. + """ + page = self.currentPage() + try: + help_page = page.help_ + except: + help_page = self.no_help + QtGui.QMessageBox.information(self, self.title_help, help_page) + + def retranslateUi(self): + """ + Fixed-text strings used for translations + """ + self.title_help = translate('OpenLP.ProjectorWizard', 'Projector Wizard Help') + self.no_help = translate('OpenLP.ProjectorWizard', + 'Sorry - no help available for this page.') + self.welcome_page.title_label.setText('%s' % + translate('OpenLP.ProjectorWizard', + 'Welcome to the
Projector Wizard')) + self.welcome_page.information_label.setText(translate('OpenLP.ProjectorWizard', 'This wizard will help you to ' + 'create and edit your Projector control.

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

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', + 'IP: The IP address of the projector to connect to.
' + 'Port: The port number. Default is 4352.
' + 'PIN: If needed, enter the PIN access code for the projector.
' + '
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:' + '

PJLink Port: The network port to use. Default is %s.' + '

PJLink PIN: The PJLink access PIN. Only required if ' + 'PJLink PIN is set in projector. 4 characters max.

Name: ' + 'A unique name you want to give to this projector entry. 20 characters max. ' + '

Location: The location of the projector. 30 characters ' + 'max.

Notes: Any notes you want to add about this ' + 'projector. 200 characters max.

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('%s' % + translate('OpenLP.ProjectorWizard', 'Projector Added')) + self.finish_page.information_label.setText(translate('OpenLP.ProjectorWizard', + '
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"
is already in the database as ID %s.' + '

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"
is not a valid IP address.' + '

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.' + '
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.' + '
Remove PIN entry before hitting "NEXT"') + ) + valid = False + new.disconnect() + del(new) + """ + return valid + + +class ConnectEditPage(ConnectBase): + """ + Full information page. + """ + newProjector = QtCore.pyqtSignal(str) + editProjector = QtCore.pyqtSignal(object) + + def __init__(self, parent, page): + super().__init__(parent, page) + self.setObjectName('edit_page') + self.editPageLayout = QtGui.QHBoxLayout(self) + self.editPageLayout.setObjectName('layout') + # Projector DB information + self.localAreaBox = QtGui.QGroupBox(self) + self.localAreaBox.setObjectName('edit_local_area_box') + self.localAreaForm = QtGui.QFormLayout(self.localAreaBox) + self.localAreaForm.setObjectName('edit_local_area_form') + self.ip_number_label = QtGui.QLabel(self.localAreaBox) + self.ip_number_label.setObjectName('edit_ip_number_label') + self.ip_number_text = QtGui.QLineEdit(self.localAreaBox) + self.ip_number_text.setObjectName('edit_ip_number_text') + self.localAreaForm.addRow(self.ip_number_label, self.ip_number_text) + self.pjlink_port_label = QtGui.QLabel(self.localAreaBox) + self.pjlink_port_label.setObjectName('edit_pjlink_port_label') + self.pjlink_port_text = QtGui.QLineEdit(self.localAreaBox) + self.pjlink_port_text.setMaxLength(5) + self.pjlink_port_text.setObjectName('edit_pjlink_port_text') + self.localAreaForm.addRow(self.pjlink_port_label, self.pjlink_port_text) + self.pjlink_pin_label = QtGui.QLabel(self.localAreaBox) + self.pjlink_pin_label.setObjectName('pjlink_pin_label') + self.pjlink_pin_text = QtGui.QLineEdit(self.localAreaBox) + self.pjlink_pin_text.setObjectName('pjlink_pin_text') + self.localAreaForm.addRow(self.pjlink_pin_label, self.pjlink_pin_text) + self.name_label = QtGui.QLabel(self.localAreaBox) + self.name_label.setObjectName('name_label') + self.name_text = QtGui.QLineEdit(self.localAreaBox) + self.name_text.setObjectName('name_label') + self.name_text.setMaxLength(20) + self.localAreaForm.addRow(self.name_label, self.name_text) + self.location_label = QtGui.QLabel(self.localAreaBox) + self.location_label.setObjectName('location_label') + self.location_text = QtGui.QLineEdit(self.localAreaBox) + self.location_text.setObjectName('location_text') + self.location_text.setMaxLength(30) + self.localAreaForm.addRow(self.location_label, self.location_text) + self.notes_label = QtGui.QLabel(self.localAreaBox) + self.notes_label.setObjectName('notes_label') + self.notes_text = QtGui.QPlainTextEdit(self.localAreaBox) + self.notes_text.setObjectName('notes_text') + self.localAreaForm.addRow(self.notes_label, self.notes_text) + self.editPageLayout.addWidget(self.localAreaBox) + # Projector retrieved information + self.remoteAreaBox = QtGui.QGroupBox(self) + self.remoteAreaBox.setObjectName('edit_remote_area_box') + self.remoteAreaForm = QtGui.QFormLayout(self.remoteAreaBox) + self.remoteAreaForm.setObjectName('edit_remote_area_form') + self.projector_make_label = QtGui.QLabel(self.remoteAreaBox) + self.projector_make_label.setObjectName('projector_make_label') + self.projector_make_text = QtGui.QLabel(self.remoteAreaBox) + self.projector_make_text.setObjectName('projector_make_text') + self.remoteAreaForm.addRow(self.projector_make_label, self.projector_make_text) + self.projector_model_label = QtGui.QLabel(self.remoteAreaBox) + self.projector_model_label.setObjectName('projector_model_text') + self.projector_model_text = QtGui.QLabel(self.remoteAreaBox) + self.projector_model_text.setObjectName('projector_model_text') + self.remoteAreaForm.addRow(self.projector_model_label, self.projector_model_text) + self.editPageLayout.addWidget(self.remoteAreaBox) + + def initializePage(self): + """ + Fill in the blanks for information from previous page/projector to edit. + """ + if self.wizard().projector is not None: + log.debug('ConnectEditPage.initializePage() Editing existing projector') + self.ip_number_text.setText(self.wizard().projector.ip) + self.pjlink_port_text.setText(str(self.wizard().projector.port)) + self.pjlink_pin_text.setText(self.wizard().projector.pin) + self.name_text.setText(self.wizard().projector.name) + self.location_text.setText(self.wizard().projector.location) + self.notes_text.insertPlainText(self.wizard().projector.notes) + self.myButtons = [QtGui.QWizard.HelpButton, + QtGui.QWizard.Stretch, + QtGui.QWizard.FinishButton, + QtGui.QWizard.CancelButton] + else: + log.debug('Retrieving information from host page') + self.ip_number_text.setText(self.wizard().field('ip_number')) + self.pjlink_port_text.setText(self.wizard().field('pjlink_port')) + self.pjlink_pin_text.setText(self.wizard().field('pjlink_pin')) + make = self.wizard().field('projector_make') + model = self.wizard().field('projector_model') + if make is None or make.strip() == '': + self.projector_make_text.setText('Unavailable ') + else: + self.projector_make_text.setText(make) + if model is None or model.strip() == '': + self.projector_model_text.setText('Unavailable ') + else: + self.projector_model_text.setText(model) + self.myButtons = [QtGui.QWizard.HelpButton, + QtGui.QWizard.Stretch, + QtGui.QWizard.BackButton, + QtGui.QWizard.NextButton, + QtGui.QWizard.CancelButton] + + def validatePage(self): + """ + Last verification if editiing existing entry in case of IP change. Add entry to DB. + """ + log.debug('ConnectEditPage().validatePage()') + if self.wizard().projector is not None: + ip = self.ip_number_text.text() + port = self.pjlink_port_text.text() + name = self.name_text.text() + location = self.location_text.text() + notes = self.notes_text.toPlainText() + pin = self.pjlink_pin_text.text() + log.debug('edit-page() Verifying info : ip="%s"' % ip) + valid = verify_ip_address(ip) + if not valid: + QtGui.QMessageBox.warning(self, + translate('OpenLP.ProjectorWizard', 'Invalid IP'), + translate('OpenLP.ProjectorWizard', + 'IP "%s"
is not a valid IP address.' + '

Please enter a valid IP address.' % ip)) + return False + log.debug('Saving edited projector %s' % ip) + self.wizard().projector.ip = ip + self.wizard().projector.port = port + self.wizard().projector.name = name + self.wizard().projector.location = location + self.wizard().projector.notes = notes + self.wizard().projector.pin = pin + saved = self.db.update_projector(self.wizard().projector) + if not saved: + QtGui.QMessageBox.error(self, translate('OpenLP.ProjectorWizard', 'Database Error'), + translate('OpenLP.ProjectorWizard', 'There was an error saving projector ' + 'information. See the log for the error')) + return False + self.editProjector.emit(self.wizard().projector) + else: + projector = Projector(ip=self.wizard().field('ip_number'), + port=self.wizard().field('pjlink_port'), + name=self.wizard().field('projector_name'), + location=self.wizard().field('projector_location'), + notes=self.wizard().field('projector_notes'), + pin=self.wizard().field('pjlink_pin')) + log.debug('Adding new projector %s' % projector.ip) + if self.wizard().db.get_projector_by_ip(projector.ip) is None: + saved = self.wizard().db.add_projector(projector) + if not saved: + QtGui.QMessageBox.error(self, translate('OpenLP.ProjectorWizard', 'Database Error'), + translate('OpenLP.ProjectorWizard', 'There was an error saving projector ' + 'information. See the log for the error')) + return False + self.newProjector.emit('%s' % projector.ip) + return True + + def nextId(self): + """ + Returns the next page ID if new entry or end of wizard if editing entry. + """ + if self.wizard().projector is None: + return PAGE_NEXT[self.pageId] + else: + return -1 + + +class ConnectFinishPage(ConnectBase): + """ + Buh-Bye page + """ + def __init__(self, parent, page): + super().__init__(parent, page) + self.setObjectName('connect_finish_page') + 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() diff --git a/openlp/core/ui/settingsform.py b/openlp/core/ui/settingsform.py index a1077e1f4..7045e7bd1 100644 --- a/openlp/core/ui/settingsform.py +++ b/openlp/core/ui/settingsform.py @@ -38,6 +38,7 @@ from openlp.core.lib import PluginStatus, build_icon from openlp.core.ui import AdvancedTab, GeneralTab, ThemesTab from openlp.core.ui.media import PlayerTab from .settingsdialog import Ui_SettingsDialog +from openlp.core.ui.projector.tab import ProjectorTab log = logging.getLogger(__name__) @@ -67,9 +68,10 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog, RegistryProperties): 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 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog, RegistryProperties): 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 diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index 0347cc3c7..2337a3f64 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -106,6 +106,7 @@ wizard_firsttime.bmp wizard_createtheme.bmp wizard_duplicateremoval.bmp + wizard_createprojector.png service_collapse_all.png @@ -169,6 +170,26 @@ theme_new.png theme_edit.png + + projector_blank.png + projector_connect.png + projector_connectors.png + projector_cooldown.png + projector_disconnect.png + projector_edit.png + projector_error.png + projector_manager.png + projector_new.png + projector_not_connected.png + projector_off.png + projector_on.png + projector_power_off.png + projector_power_on.png + projector_show.png + projector_status.png + projector_warmup.png + projector_view.png + android_app_qr.png diff --git a/resources/images/projector_blank.png b/resources/images/projector_blank.png new file mode 100644 index 000000000..28feae350 Binary files /dev/null and b/resources/images/projector_blank.png differ diff --git a/resources/images/projector_connect.png b/resources/images/projector_connect.png new file mode 100644 index 000000000..2e9b08c02 Binary files /dev/null and b/resources/images/projector_connect.png differ diff --git a/resources/images/projector_connectors.png b/resources/images/projector_connectors.png new file mode 100644 index 000000000..77a6a4dd7 Binary files /dev/null and b/resources/images/projector_connectors.png differ diff --git a/resources/images/projector_cooldown.png b/resources/images/projector_cooldown.png new file mode 100644 index 000000000..c4a4140f5 Binary files /dev/null and b/resources/images/projector_cooldown.png differ diff --git a/resources/images/projector_disconnect.png b/resources/images/projector_disconnect.png new file mode 100644 index 000000000..2c49e920d Binary files /dev/null and b/resources/images/projector_disconnect.png differ diff --git a/resources/images/projector_edit.png b/resources/images/projector_edit.png new file mode 100644 index 000000000..84e345d22 Binary files /dev/null and b/resources/images/projector_edit.png differ diff --git a/resources/images/projector_error.png b/resources/images/projector_error.png new file mode 100644 index 000000000..6cfa3e86a Binary files /dev/null and b/resources/images/projector_error.png differ diff --git a/resources/images/projector_manager.png b/resources/images/projector_manager.png new file mode 100644 index 000000000..69a481cb0 Binary files /dev/null and b/resources/images/projector_manager.png differ diff --git a/resources/images/projector_new.png b/resources/images/projector_new.png new file mode 100644 index 000000000..cb2be2258 Binary files /dev/null and b/resources/images/projector_new.png differ diff --git a/resources/images/projector_not_connected.png b/resources/images/projector_not_connected.png new file mode 100644 index 000000000..24ec5eccc Binary files /dev/null and b/resources/images/projector_not_connected.png differ diff --git a/resources/images/projector_off.png b/resources/images/projector_off.png new file mode 100644 index 000000000..88e1ccb0c Binary files /dev/null and b/resources/images/projector_off.png differ diff --git a/resources/images/projector_on.png b/resources/images/projector_on.png new file mode 100644 index 000000000..6555c7aac Binary files /dev/null and b/resources/images/projector_on.png differ diff --git a/resources/images/projector_power_off.png b/resources/images/projector_power_off.png new file mode 100644 index 000000000..c415f3410 Binary files /dev/null and b/resources/images/projector_power_off.png differ diff --git a/resources/images/projector_power_on.png b/resources/images/projector_power_on.png new file mode 100644 index 000000000..21458cb49 Binary files /dev/null and b/resources/images/projector_power_on.png differ diff --git a/resources/images/projector_show.png b/resources/images/projector_show.png new file mode 100644 index 000000000..6ac732d7b Binary files /dev/null and b/resources/images/projector_show.png differ diff --git a/resources/images/projector_status.png b/resources/images/projector_status.png new file mode 100644 index 000000000..c65f61630 Binary files /dev/null and b/resources/images/projector_status.png differ diff --git a/resources/images/projector_view.png b/resources/images/projector_view.png new file mode 100644 index 000000000..84763d6f1 Binary files /dev/null and b/resources/images/projector_view.png differ diff --git a/resources/images/projector_warmup.png b/resources/images/projector_warmup.png new file mode 100644 index 000000000..b692bb6fa Binary files /dev/null and b/resources/images/projector_warmup.png differ diff --git a/resources/images/wizard_createprojector.png b/resources/images/wizard_createprojector.png new file mode 100644 index 000000000..d17a01e41 Binary files /dev/null and b/resources/images/wizard_createprojector.png differ diff --git a/scripts/generate_resources.sh b/scripts/generate_resources.sh index eb2cd7c46..3aa545756 100755 --- a/scripts/generate_resources.sh +++ b/scripts/generate_resources.sh @@ -53,5 +53,6 @@ cat openlp/core/resources.py.new | sed '/# Created: /d;/# by: /d' > openlp/ 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 diff --git a/tests/functional/openlp_core_common/test_projector_utilities.py b/tests/functional/openlp_core_common/test_projector_utilities.py new file mode 100644 index 000000000..1a3063321 --- /dev/null +++ b/tests/functional/openlp_core_common/test_projector_utilities.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Package to test the openlp.core.ui.projector.networkutils package. +""" + +from unittest import TestCase + +from openlp.core.common import verify_ip_address, md5_hash, qmd5_hash + +salt = '498e4a67' +pin = 'JBMIAProjectorLink' +test_hash = '5d8409bc1c3fa39749434aa3a5c38682' + +ip4_loopback = '127.0.0.1' +ip4_local = '192.168.1.1' +ip4_broadcast = '255.255.255.255' +ip4_bad = '192.168.1.256' + +ip6_loopback = '::1' +ip6_link_local = 'fe80::223:14ff:fe99:d315' +ip6_bad = 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' + + +class testProjectorUtilities(TestCase): + """ + Validate functions in the projector utilities module + """ + def test_ip4_loopback_valid(self): + """ + Test IPv4 loopbackvalid + """ + # WHEN: Test with a local loopback test + valid = verify_ip_address(addr=ip4_loopback) + + # THEN: Verify we received True + self.assertTrue(valid, 'IPv4 loopback address should have been valid') + + def test_ip4_local_valid(self): + """ + Test IPv4 local valid + """ + # WHEN: Test with a local loopback test + valid = verify_ip_address(addr=ip4_local) + + # THEN: Verify we received True + self.assertTrue(valid, 'IPv4 local address should have been valid') + + def test_ip4_broadcast_valid(self): + """ + Test IPv4 broadcast valid + """ + # WHEN: Test with a local loopback test + valid = verify_ip_address(addr=ip4_broadcast) + + # THEN: Verify we received True + self.assertTrue(valid, 'IPv4 broadcast address should have been valid') + + def test_ip4_address_invalid(self): + """ + Test IPv4 address invalid + """ + # WHEN: Test with a local loopback test + valid = verify_ip_address(addr=ip4_bad) + + # THEN: Verify we received True + self.assertFalse(valid, 'Bad IPv4 address should not have been valid') + + def test_ip6_loopback_valid(self): + """ + Test IPv6 loopback valid + """ + # WHEN: Test IPv6 loopback address + valid = verify_ip_address(addr=ip6_loopback) + + # THEN: Validate return + self.assertTrue(valid, 'IPv6 loopback address should have been valid') + + def test_ip6_local_valid(self): + """ + Test IPv6 link-local valid + """ + # WHEN: Test IPv6 link-local address + valid = verify_ip_address(addr=ip6_link_local) + + # THEN: Validate return + self.assertTrue(valid, 'IPv6 link-local address should have been valid') + + def test_ip6_address_invalid(self): + """ + Test NetworkUtils IPv6 address invalid + """ + # WHEN: Given an invalid IPv6 address + valid = verify_ip_address(addr=ip6_bad) + + # THEN: Validate bad return + self.assertFalse(valid, 'IPv6 bad address should have been invalid') + + def test_md5_hash(self): + """ + Test MD5 hash from salt+data pass (python) + """ + # WHEN: Given a known salt+data + hash_ = md5_hash(salt=salt, data=pin) + + # THEN: Validate return has is same + self.assertEquals(hash_, test_hash, 'MD5 should have returned a good hash') + + def test_md5_hash_bad(self): + """ + Test MD5 hash from salt+data fail (python) + """ + # WHEN: Given a different salt+hash + hash_ = md5_hash(salt=pin, data=salt) + + # THEN: return data is different + self.assertNotEquals(hash_, test_hash, 'MD5 should have returned a bad hash') + + def test_qmd5_hash(self): + """ + Test MD5 hash from salt+data pass (Qt) + """ + # WHEN: Given a known salt+data + hash_ = qmd5_hash(salt=salt, data=pin) + + # THEN: Validate return has is same + self.assertEquals(hash_, test_hash, 'Qt-MD5 should have returned a good hash') + + def test_qmd5_hash_bad(self): + """ + Test MD5 hash from salt+hash fail (Qt) + """ + # WHEN: Given a different salt+hash + hash_ = qmd5_hash(salt=pin, data=salt) + + # THEN: return data is different + self.assertNotEquals(hash_, test_hash, 'Qt-MD5 should have returned a bad hash') diff --git a/tests/functional/openlp_core_lib/__init__.py b/tests/functional/openlp_core_lib/__init__.py index 0f0461449..0691295a6 100644 --- a/tests/functional/openlp_core_lib/__init__.py +++ b/tests/functional/openlp_core_lib/__init__.py @@ -27,5 +27,28 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -Package to test the openlp.core.lib package. +Module-level functions for the functional test suite """ + +import os +from tests.functional import patch + +from openlp.core.common import is_win + +from .test_projectordb import tmpfile + + +def setUp(): + if not is_win(): + # Wine creates a sharing violation during tests. Ignore. + try: + os.remove(tmpfile) + except: + pass + + +def tearDown(): + """ + Ensure test suite has been cleaned up after tests + """ + patch.stopall() diff --git a/tests/functional/openlp_core_lib/test_projectordb.py b/tests/functional/openlp_core_lib/test_projectordb.py new file mode 100644 index 000000000..8717825bb --- /dev/null +++ b/tests/functional/openlp_core_lib/test_projectordb.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Package to test the openlp.core.ui.projectordb find, edit, delete +record functions. + +PREREQUISITE: add_record() and get_all() functions validated. +""" + +from unittest import TestCase +from tests.functional import MagicMock, patch + +from openlp.core.lib.projectordb import Projector, ProjectorDB + +from tests.resources.projector.data import TEST1_DATA, TEST2_DATA, TEST3_DATA + +tmpfile = '/tmp/openlp-test-projectordb.sql' + + +def compare_data(one, two): + """ + Verify two Projector() instances contain the same data + """ + return one is not None and \ + two is not None and \ + one.ip == two.ip and \ + one.port == two.port and \ + one.name == two.name and \ + one.location == two.location and \ + one.notes == two.notes + + +def add_records(self, test): + """ + Add record if not in database + """ + record_list = self.projector.get_projector_all() + if len(record_list) < 1: + added = False + for record in test: + added = self.projector.add_projector(record) or added + return added + + for new_record in test: + added = None + for record in record_list: + if compare_data(record, new_record): + break + added = self.projector.add_projector(new_record) + return added + + +class TestProjectorDB(TestCase): + """ + Test case for ProjectorDB + """ + def setUp(self): + """ + Set up anything necessary for all tests + """ + if not hasattr(self, 'projector'): + with patch('openlp.core.lib.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') diff --git a/tests/interfaces/openlp_core_ui/__init__.py b/tests/interfaces/openlp_core_ui/__init__.py index 6b241e7fc..34420bdb8 100644 --- a/tests/interfaces/openlp_core_ui/__init__.py +++ b/tests/interfaces/openlp_core_ui/__init__.py @@ -26,3 +26,28 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +""" +Module-level functions for the functional test suite +""" + +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() diff --git a/tests/interfaces/openlp_core_ui/test_projectormanager.py b/tests/interfaces/openlp_core_ui/test_projectormanager.py new file mode 100644 index 000000000..e12f9a74a --- /dev/null +++ b/tests/interfaces/openlp_core_ui/test_projectormanager.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Interface tests to test the themeManager class and related methods. +""" +from unittest import TestCase + +from openlp.core.common import Registry, Settings +from tests.functional import patch, MagicMock +from tests.helpers.testmixin import TestMixin + +from openlp.core.ui import ProjectorManager, ProjectorWizard +from openlp.core.lib.projectordb import Projector, ProjectorDB + +from tests.resources.projector.data import TEST1_DATA, TEST2_DATA, TEST3_DATA + +tmpfile = '/tmp/openlp-test-projectormanager.sql' + + +class TestProjectorManager(TestCase, TestMixin): + """ + Test the functions in the ProjectorManager module + """ + def setUp(self): + """ + Create the UI and setup necessary options + """ + self.build_settings() + self.get_application() + Registry.create() + if not hasattr(self, 'projector_manager'): + with patch('openlp.core.lib.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') diff --git a/tests/resources/projector/data.py b/tests/resources/projector/data.py new file mode 100644 index 000000000..798e8af36 --- /dev/null +++ b/tests/resources/projector/data.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`tests.resources.projector.data file contains test data +""" + +from openlp.core.lib.projectordb import Projector + +# Test data +TEST1_DATA = Projector(ip='111.111.111.111', + port='1111', + pin='1111', + name='___TEST_ONE___', + location='location one', + notes='notes one') + +TEST2_DATA = Projector(ip='222.222.222.222', + port='2222', + pin='2222', + name='___TEST_TWO___', + location='location two', + notes='notes two') + +TEST3_DATA = Projector(ip='333.333.333.333', + port='3333', + pin='3333', + name='___TEST_THREE___', + location='location three', + notes='notes three')