diff --git a/.bzrignore b/.bzrignore index a0c3f0b4f..6b7b989a6 100644 --- a/.bzrignore +++ b/.bzrignore @@ -33,3 +33,10 @@ tests.kdev4 __pycache__ *.dll .directory +*.kate-swp +# Git files +.git +.gitignore +# Rejected diff's +*.rej +*.~\?~ diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index 0776547ae..833aa5d10 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 MD5Sum on salt, data + using PyQt4.QCryptographicHash. + + :param salt: Initial salt + :param data: Data to hash + :returns: str + """ + log.debug('qmd5_hash(salt="%s"' % salt) + hash_obj = QHash(QHash.Md5) + hash_obj.addData(salt) + hash_obj.addData(data) + hash_value = hash_obj.result().toHex() + log.debug('qmd5_hash() returning "%s"' % hash_value) + return decode(hash_value.data(), 'ascii') + + from .openlpmixin import OpenLPMixin from .registry import Registry from .registrymixin import RegistryMixin 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 dc61b10fd..abbf09ec3 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -276,6 +276,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')], @@ -296,7 +297,15 @@ 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/poll time': 20, # PJLink timeout is 30 seconds + 'projector/socket timeout': 5, # 5 second socket timeout + 'projector/source dialog type': 0 # Source select dialog box type } __file_path__ = '' __obsolete_settings__ = [ diff --git a/openlp/core/common/uistrings.py b/openlp/core/common/uistrings.py index 3fe1485ba..6b952cc39 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', 'Singular') + self.Manufacturers = translate('OpenLP.Ui', 'Manufacturers', 'Plural') + self.Model = translate('OpenLP.Ui', 'Model', 'Singular') + self.Models = translate('OpenLP.Ui', 'Models', 'Plural') self.Minutes = translate('OpenLP.Ui', 'm', 'The abbreviated unit for minutes') self.Middle = translate('OpenLP.Ui', 'Middle') self.New = translate('OpenLP.Ui', 'New') @@ -118,6 +122,8 @@ 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', 'Singular') + self.Projectors = translate('OpenLP.Ui', 'Projectors', 'Plural') self.ReplaceBG = translate('OpenLP.Ui', 'Replace Background') self.ReplaceLiveBG = translate('OpenLP.Ui', 'Replace live background.') self.ResetBG = translate('OpenLP.Ui', 'Reset Background') 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/__init__.py b/openlp/core/lib/projector/__init__.py new file mode 100644 index 000000000..88dcc4882 --- /dev/null +++ b/openlp/core/lib/projector/__init__.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" + :mod:`openlp.core.ui.projector` + + Initialization for the openlp.core.ui.projector modules. +""" + + +class DialogSourceStyle(object): + """ + An enumeration for projector dialog box type. + """ + Tabbed = 0 + Single = 1 diff --git a/openlp/core/lib/projector/constants.py b/openlp/core/lib/projector/constants.py new file mode 100644 index 000000000..d3a1dc3d5 --- /dev/null +++ b/openlp/core/lib/projector/constants.py @@ -0,0 +1,366 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" + :mod:`openlp.core.lib.projector.constants` module + + Provides the constants used for projector errors/status/defaults +""" + +import logging +log = logging.getLogger(__name__) +log.debug('projector_constants loaded') + +from openlp.core.common import translate + + +__all__ = ['S_OK', 'E_GENERAL', 'E_NOT_CONNECTED', 'E_FAN', 'E_LAMP', 'E_TEMP', + 'E_COVER', 'E_FILTER', 'E_AUTHENTICATION', 'E_NO_AUTHENTICATION', + 'E_UNDEFINED', 'E_PARAMETER', 'E_UNAVAILABLE', 'E_PROJECTOR', + 'E_INVALID_DATA', 'E_WARN', 'E_ERROR', 'E_CLASS', 'E_PREFIX', + 'E_CONNECTION_REFUSED', 'E_REMOTE_HOST_CLOSED_CONNECTION', 'E_HOST_NOT_FOUND', + 'E_SOCKET_ACCESS', 'E_SOCKET_RESOURCE', 'E_SOCKET_TIMEOUT', 'E_DATAGRAM_TOO_LARGE', + 'E_NETWORK', 'E_ADDRESS_IN_USE', 'E_SOCKET_ADDRESS_NOT_AVAILABLE', + 'E_UNSUPPORTED_SOCKET_OPERATION', 'E_PROXY_AUTHENTICATION_REQUIRED', + 'E_SLS_HANDSHAKE_FAILED', 'E_UNFINISHED_SOCKET_OPERATION', 'E_PROXY_CONNECTION_REFUSED', + 'E_PROXY_CONNECTION_CLOSED', 'E_PROXY_CONNECTION_TIMEOUT', 'E_PROXY_NOT_FOUND', + 'E_PROXY_PROTOCOL', 'E_UNKNOWN_SOCKET_ERROR', + 'S_NOT_CONNECTED', 'S_CONNECTING', 'S_CONNECTED', + 'S_STATUS', 'S_OFF', 'S_INITIALIZE', 'S_STANDBY', 'S_WARMUP', 'S_ON', 'S_COOLDOWN', + 'S_INFO', 'S_NETWORK_SENDING', 'S_NETWORK_RECEIVED', + 'ERROR_STRING', 'CR', 'LF', 'PJLINK_ERST_STATUS', 'PJLINK_POWR_STATUS', + 'PJLINK_PORT', 'PJLINK_MAX_PACKET', 'TIMEOUT', 'ERROR_MSG', 'PJLINK_ERRORS', + 'STATUS_STRING', 'PJLINK_VALID_CMD', 'CONNECTION_ERRORS'] + +# Set common constants. +CR = chr(0x0D) # \r +LF = chr(0x0A) # \n +PJLINK_PORT = 4352 +TIMEOUT = 30.0 +PJLINK_MAX_PACKET = 136 +PJLINK_VALID_CMD = {'1': ['PJLINK', # Initial connection + 'POWR', # Power option + 'INPT', # Video sources option + 'AVMT', # Shutter option + 'ERST', # Error status option + 'LAMP', # Lamp(s) query (Includes fans) + 'INST', # Input sources available query + 'NAME', # Projector name query + 'INF1', # Manufacturer name query + 'INF2', # Product name query + 'INFO', # Other information query + 'CLSS' # PJLink class support query + ]} + +# Error and status codes +S_OK = E_OK = 0 # E_OK included since I sometimes forget +# Error codes. Start at 200 so we don't duplicate system error codes. +E_GENERAL = 200 # Unknown error +E_NOT_CONNECTED = 201 +E_FAN = 202 +E_LAMP = 203 +E_TEMP = 204 +E_COVER = 205 +E_FILTER = 206 +E_NO_AUTHENTICATION = 207 # PIN set and no authentication set on projector +E_UNDEFINED = 208 # ERR1 +E_PARAMETER = 209 # ERR2 +E_UNAVAILABLE = 210 # ERR3 +E_PROJECTOR = 211 # ERR4 +E_INVALID_DATA = 212 +E_WARN = 213 +E_ERROR = 214 +E_AUTHENTICATION = 215 # ERRA +E_CLASS = 216 +E_PREFIX = 217 + +# Remap Qt socket error codes to projector error codes +E_CONNECTION_REFUSED = 230 +E_REMOTE_HOST_CLOSED_CONNECTION = 231 +E_HOST_NOT_FOUND = 232 +E_SOCKET_ACCESS = 233 +E_SOCKET_RESOURCE = 234 +E_SOCKET_TIMEOUT = 235 +E_DATAGRAM_TOO_LARGE = 236 +E_NETWORK = 237 +E_ADDRESS_IN_USE = 238 +E_SOCKET_ADDRESS_NOT_AVAILABLE = 239 +E_UNSUPPORTED_SOCKET_OPERATION = 240 +E_PROXY_AUTHENTICATION_REQUIRED = 241 +E_SLS_HANDSHAKE_FAILED = 242 +E_UNFINISHED_SOCKET_OPERATION = 243 +E_PROXY_CONNECTION_REFUSED = 244 +E_PROXY_CONNECTION_CLOSED = 245 +E_PROXY_CONNECTION_TIMEOUT = 246 +E_PROXY_NOT_FOUND = 247 +E_PROXY_PROTOCOL = 248 +E_UNKNOWN_SOCKET_ERROR = -1 + +# Status codes start at 300 +S_NOT_CONNECTED = 300 +S_CONNECTING = 301 +S_CONNECTED = 302 +S_INITIALIZE = 303 +S_STATUS = 304 +S_OFF = 305 +S_STANDBY = 306 +S_WARMUP = 307 +S_ON = 308 +S_COOLDOWN = 309 +S_INFO = 310 + +# Information that does not affect status +S_NETWORK_SENDING = 400 +S_NETWORK_RECEIVED = 401 + +CONNECTION_ERRORS = {E_NOT_CONNECTED, E_NO_AUTHENTICATION, E_AUTHENTICATION, E_CLASS, + E_PREFIX, E_CONNECTION_REFUSED, E_REMOTE_HOST_CLOSED_CONNECTION, + E_HOST_NOT_FOUND, E_SOCKET_ACCESS, E_SOCKET_RESOURCE, E_SOCKET_TIMEOUT, + E_DATAGRAM_TOO_LARGE, E_NETWORK, E_ADDRESS_IN_USE, E_SOCKET_ADDRESS_NOT_AVAILABLE, + E_UNSUPPORTED_SOCKET_OPERATION, E_PROXY_AUTHENTICATION_REQUIRED, + E_SLS_HANDSHAKE_FAILED, E_UNFINISHED_SOCKET_OPERATION, E_PROXY_CONNECTION_REFUSED, + E_PROXY_CONNECTION_CLOSED, E_PROXY_CONNECTION_TIMEOUT, E_PROXY_NOT_FOUND, + E_PROXY_PROTOCOL, E_UNKNOWN_SOCKET_ERROR + } + +PJLINK_ERRORS = {'ERRA': E_AUTHENTICATION, # Authentication error + 'ERR1': E_UNDEFINED, # Undefined command error + 'ERR2': E_PARAMETER, # Invalid parameter error + 'ERR3': E_UNAVAILABLE, # Projector busy + 'ERR4': E_PROJECTOR, # Projector or display failure + E_AUTHENTICATION: translate('OpenLP.ProjectorConstants', 'ERRA'), + E_UNDEFINED: translate('OpenLP.ProjectorConstants', 'ERR1'), + E_PARAMETER: translate('OpenLP.ProjectorConstants', 'ERR2'), + E_UNAVAILABLE: translate('OpenLP.ProjectorConstants', 'ERR3'), + E_PROJECTOR: translate('OpenLP.ProjectorConstants', 'ERR4')} + +# Map error/status codes to string +ERROR_STRING = {0: translate('OpenLP.ProjectorConstants', 'S_OK'), + E_GENERAL: translate('OpenLP.ProjectorConstants', 'E_GENERAL'), + E_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'E_NOT_CONNECTED'), + E_FAN: translate('OpenLP.ProjectorConstants', 'E_FAN'), + E_LAMP: translate('OpenLP.ProjectorConstants', 'E_LAMP'), + E_TEMP: translate('OpenLP.ProjectorConstants', 'E_TEMP'), + E_COVER: translate('OpenLP.ProjectorConstants', 'E_COVER'), + E_FILTER: translate('OpenLP.ProjectorConstants', 'E_FILTER'), + E_AUTHENTICATION: translate('OpenLP.ProjectorConstants', 'E_AUTHENTICATION'), + E_NO_AUTHENTICATION: translate('OpenLP.ProjectorConstants', 'E_NO_AUTHENTICATION'), + E_UNDEFINED: translate('OpenLP.ProjectorConstants', 'E_UNDEFINED'), + E_PARAMETER: translate('OpenLP.ProjectorConstants', 'E_PARAMETER'), + E_UNAVAILABLE: translate('OpenLP.ProjectorConstants', 'E_UNAVAILABLE'), + E_PROJECTOR: translate('OpenLP.ProjectorConstants', 'E_PROJECTOR'), + E_INVALID_DATA: translate('OpenLP.ProjectorConstants', 'E_INVALID_DATA'), + E_WARN: translate('OpenLP.ProjectorConstants', 'E_WARN'), + E_ERROR: translate('OpenLP.ProjectorConstants', 'E_ERROR'), + E_CLASS: translate('OpenLP.ProjectorConstants', 'E_CLASS'), + E_PREFIX: translate('OpenLP.ProjectorConstants', 'E_PREFIX'), # Last projector error + E_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants', + 'E_CONNECTION_REFUSED'), # First QtSocket error + E_REMOTE_HOST_CLOSED_CONNECTION: translate('OpenLP.ProjectorConstants', + 'E_REMOTE_HOST_CLOSED_CONNECTION'), + E_HOST_NOT_FOUND: translate('OpenLP.ProjectorConstants', 'E_HOST_NOT_FOUND'), + E_SOCKET_ACCESS: translate('OpenLP.ProjectorConstants', 'E_SOCKET_ACCESS'), + E_SOCKET_RESOURCE: translate('OpenLP.ProjectorConstants', 'E_SOCKET_RESOURCE'), + E_SOCKET_TIMEOUT: translate('OpenLP.ProjectorConstants', 'E_SOCKET_TIMEOUT'), + E_DATAGRAM_TOO_LARGE: translate('OpenLP.ProjectorConstants', 'E_DATAGRAM_TOO_LARGE'), + E_NETWORK: translate('OpenLP.ProjectorConstants', 'E_NETWORK'), + E_ADDRESS_IN_USE: translate('OpenLP.ProjectorConstants', 'E_ADDRESS_IN_USE'), + E_SOCKET_ADDRESS_NOT_AVAILABLE: translate('OpenLP.ProjectorConstants', + 'E_SOCKET_ADDRESS_NOT_AVAILABLE'), + E_UNSUPPORTED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants', + 'E_UNSUPPORTED_SOCKET_OPERATION'), + E_PROXY_AUTHENTICATION_REQUIRED: translate('OpenLP.ProjectorConstants', + 'E_PROXY_AUTHENTICATION_REQUIRED'), + E_SLS_HANDSHAKE_FAILED: translate('OpenLP.ProjectorConstants', 'E_SLS_HANDSHAKE_FAILED'), + E_UNFINISHED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants', + 'E_UNFINISHED_SOCKET_OPERATION'), + E_PROXY_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants', 'E_PROXY_CONNECTION_REFUSED'), + E_PROXY_CONNECTION_CLOSED: translate('OpenLP.ProjectorConstants', 'E_PROXY_CONNECTION_CLOSED'), + E_PROXY_CONNECTION_TIMEOUT: translate('OpenLP.ProjectorConstants', 'E_PROXY_CONNECTION_TIMEOUT'), + E_PROXY_NOT_FOUND: translate('OpenLP.ProjectorConstants', 'E_PROXY_NOT_FOUND'), + E_PROXY_PROTOCOL: translate('OpenLP.ProjectorConstants', 'E_PROXY_PROTOCOL'), + E_UNKNOWN_SOCKET_ERROR: translate('OpenLP.ProjectorConstants', 'E_UNKNOWN_SOCKET_ERROR')} + +STATUS_STRING = {S_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'S_NOT_CONNECTED'), + S_CONNECTING: translate('OpenLP.ProjectorConstants', 'S_CONNECTING'), + S_CONNECTED: translate('OpenLP.ProjectorConstants', 'S_CONNECTED'), + S_STATUS: translate('OpenLP.ProjectorConstants', 'S_STATUS'), + S_OFF: translate('OpenLP.ProjectorConstants', 'S_OFF'), + S_INITIALIZE: translate('OpenLP.ProjectorConstants', 'S_INITIALIZE'), + S_STANDBY: translate('OpenLP.ProjectorConstants', 'S_STANDBY'), + S_WARMUP: translate('OpenLP.ProjectorConstants', 'S_WARMUP'), + S_ON: translate('OpenLP.ProjectorConstants', 'S_ON'), + S_COOLDOWN: translate('OpenLP.ProjectorConstants', 'S_COOLDOWN'), + S_INFO: translate('OpenLP.ProjectorConstants', 'S_INFO'), + S_NETWORK_SENDING: translate('OpenLP.ProjectorConstants', 'S_NETWORK_SENDING'), + S_NETWORK_RECEIVED: translate('OpenLP.ProjectorConstants', 'S_NETWORK_RECEIVED')} + +# Map error/status codes to message strings +ERROR_MSG = {E_OK: translate('OpenLP.ProjectorConstants', 'OK'), # E_OK | S_OK + E_GENERAL: translate('OpenLP.ProjectorConstants', 'General projector error'), + E_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected error'), + E_LAMP: translate('OpenLP.ProjectorConstants', 'Lamp error'), + E_FAN: translate('OpenLP.ProjectorConstants', 'Fan error'), + E_TEMP: translate('OpenLP.ProjectorConstants', 'High temperature detected'), + E_COVER: translate('OpenLP.ProjectorConstants', 'Cover open detected'), + E_FILTER: translate('OpenLP.ProjectorConstants', 'Check filter'), + E_AUTHENTICATION: translate('OpenLP.ProjectorConstants', 'Authentication Error'), + E_UNDEFINED: translate('OpenLP.ProjectorConstants', 'Undefined Command'), + E_PARAMETER: translate('OpenLP.ProjectorConstants', 'Invalid Parameter'), + E_UNAVAILABLE: translate('OpenLP.ProjectorConstants', 'Projector Busy'), + E_PROJECTOR: translate('OpenLP.ProjectorConstants', 'Projector/Display Error'), + E_INVALID_DATA: translate('OpenLP.ProjectorConstants', 'Invalid packet received'), + E_WARN: translate('OpenLP.ProjectorConstants', 'Warning condition detected'), + E_ERROR: translate('OpenLP.ProjectorConstants', 'Error condition detected'), + E_CLASS: translate('OpenLP.ProjectorConstants', 'PJLink class not supported'), + E_PREFIX: translate('OpenLP.ProjectorConstants', 'Invalid prefix character'), + E_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants', + 'The connection was refused by the peer (or timed out)'), + E_REMOTE_HOST_CLOSED_CONNECTION: translate('OpenLP.ProjectorConstants', + 'The remote host closed the connection'), + E_HOST_NOT_FOUND: translate('OpenLP.ProjectorConstants', 'The host address was not found'), + E_SOCKET_ACCESS: translate('OpenLP.ProjectorConstants', + 'The socket operation failed because the application ' + 'lacked the required privileges'), + E_SOCKET_RESOURCE: translate('OpenLP.ProjectorConstants', + 'The local system ran out of resources (e.g., too many sockets)'), + E_SOCKET_TIMEOUT: translate('OpenLP.ProjectorConstants', + 'The socket operation timed out'), + E_DATAGRAM_TOO_LARGE: translate('OpenLP.ProjectorConstants', + 'The datagram was larger than the operating system\'s limit'), + E_NETWORK: translate('OpenLP.ProjectorConstants', + 'An error occurred with the network (Possibly someone pulled the plug?)'), + E_ADDRESS_IN_USE: translate('OpenLP.ProjectorConstants', + 'The address specified with socket.bind() ' + 'is already in use and was set to be exclusive'), + E_SOCKET_ADDRESS_NOT_AVAILABLE: translate('OpenLP.ProjectorConstants', + 'The address specified to socket.bind() ' + 'does not belong to the host'), + E_UNSUPPORTED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants', + 'The requested socket operation is not supported by the local ' + 'operating system (e.g., lack of IPv6 support)'), + E_PROXY_AUTHENTICATION_REQUIRED: translate('OpenLP.ProjectorConstants', + 'The socket is using a proxy, ' + 'and the proxy requires authentication'), + E_SLS_HANDSHAKE_FAILED: translate('OpenLP.ProjectorConstants', + 'The SSL/TLS handshake failed'), + E_UNFINISHED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants', + 'The last operation attempted has not finished yet ' + '(still in progress in the background)'), + E_PROXY_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants', + 'Could not contact the proxy server because the connection ' + 'to that server was denied'), + E_PROXY_CONNECTION_CLOSED: translate('OpenLP.ProjectorConstants', + 'The connection to the proxy server was closed unexpectedly ' + '(before the connection to the final peer was established)'), + E_PROXY_CONNECTION_TIMEOUT: translate('OpenLP.ProjectorConstants', + 'The connection to the proxy server timed out or the proxy ' + 'server stopped responding in the authentication phase.'), + E_PROXY_NOT_FOUND: translate('OpenLP.ProjectorConstants', + 'The proxy address set with setProxy() was not found'), + E_PROXY_PROTOCOL: translate('OpenLP.ProjectorConstants', + 'The connection negotiation with the proxy server because the response ' + 'from the proxy server could not be understood'), + E_UNKNOWN_SOCKET_ERROR: translate('OpenLP.ProjectorConstants', 'An unidentified error occurred'), + S_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected'), + S_CONNECTING: translate('OpenLP.ProjectorConstants', 'Connecting'), + S_CONNECTED: translate('OpenLP.ProjectorConstants', 'Connected'), + S_STATUS: translate('OpenLP.ProjectorConstants', 'Getting status'), + S_OFF: translate('OpenLP.ProjectorConstants', 'Off'), + S_INITIALIZE: translate('OpenLP.ProjectorConstants', 'Initialize in progress'), + S_STANDBY: translate('OpenLP.ProjectorConstants', 'Power in standby'), + S_WARMUP: translate('OpenLP.ProjectorConstants', 'Warmup in progress'), + S_ON: translate('OpenLP.ProjectorConstants', 'Power is on'), + S_COOLDOWN: translate('OpenLP.ProjectorConstants', 'Cooldown in progress'), + S_INFO: translate('OpenLP.ProjectorConstants', 'Projector Information available'), + S_NETWORK_SENDING: translate('OpenLP.ProjectorConstants', 'Sending data'), + S_NETWORK_RECEIVED: translate('OpenLP.ProjectorConstants', 'Received data')} + +# Map for ERST return codes to string +PJLINK_ERST_STATUS = {'0': ERROR_STRING[E_OK], + '1': ERROR_STRING[E_WARN], + '2': ERROR_STRING[E_ERROR]} + +# Map for POWR return codes to status code +PJLINK_POWR_STATUS = {'0': S_STANDBY, + '1': S_ON, + '2': S_COOLDOWN, + '3': S_WARMUP} + +PJLINK_DEFAULT_SOURCES = {'1': translate('OpenLP.DB', 'RGB'), + '2': translate('OpenLP.DB', 'Video'), + '3': translate('OpenLP.DB', 'Digital'), + '4': translate('OpenLP.DB', 'Storage'), + '5': translate('OpenLP.DB', 'Network')} + +PJLINK_DEFAULT_CODES = {'11': translate('OpenLP.DB', 'RGB 1'), + '12': translate('OpenLP.DB', 'RGB 2'), + '13': translate('OpenLP.DB', 'RGB 3'), + '14': translate('OpenLP.DB', 'RGB 4'), + '15': translate('OpenLP.DB', 'RGB 5'), + '16': translate('OpenLP.DB', 'RGB 6'), + '17': translate('OpenLP.DB', 'RGB 7'), + '18': translate('OpenLP.DB', 'RGB 8'), + '19': translate('OpenLP.DB', 'RGB 9'), + '21': translate('OpenLP.DB', 'Video 1'), + '22': translate('OpenLP.DB', 'Video 2'), + '23': translate('OpenLP.DB', 'Video 3'), + '24': translate('OpenLP.DB', 'Video 4'), + '25': translate('OpenLP.DB', 'Video 5'), + '26': translate('OpenLP.DB', 'Video 6'), + '27': translate('OpenLP.DB', 'Video 7'), + '28': translate('OpenLP.DB', 'Video 8'), + '29': translate('OpenLP.DB', 'Video 9'), + '31': translate('OpenLP.DB', 'Digital 1'), + '32': translate('OpenLP.DB', 'Digital 2'), + '33': translate('OpenLP.DB', 'Digital 3'), + '34': translate('OpenLP.DB', 'Digital 4'), + '35': translate('OpenLP.DB', 'Digital 5'), + '36': translate('OpenLP.DB', 'Digital 6'), + '37': translate('OpenLP.DB', 'Digital 7'), + '38': translate('OpenLP.DB', 'Digital 8'), + '39': translate('OpenLP.DB', 'Digital 9'), + '41': translate('OpenLP.DB', 'Storage 1'), + '42': translate('OpenLP.DB', 'Storage 2'), + '43': translate('OpenLP.DB', 'Storage 3'), + '44': translate('OpenLP.DB', 'Storage 4'), + '45': translate('OpenLP.DB', 'Storage 5'), + '46': translate('OpenLP.DB', 'Storage 6'), + '47': translate('OpenLP.DB', 'Storage 7'), + '48': translate('OpenLP.DB', 'Storage 8'), + '49': translate('OpenLP.DB', 'Storage 9'), + '51': translate('OpenLP.DB', 'Network 1'), + '52': translate('OpenLP.DB', 'Network 2'), + '53': translate('OpenLP.DB', 'Network 3'), + '54': translate('OpenLP.DB', 'Network 4'), + '55': translate('OpenLP.DB', 'Network 5'), + '56': translate('OpenLP.DB', 'Network 6'), + '57': translate('OpenLP.DB', 'Network 7'), + '58': translate('OpenLP.DB', 'Network 8'), + '59': translate('OpenLP.DB', 'Network 9') + } diff --git a/openlp/core/lib/projector/db.py b/openlp/core/lib/projector/db.py new file mode 100644 index 000000000..347e93052 --- /dev/null +++ b/openlp/core/lib/projector/db.py @@ -0,0 +1,436 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" + :mod:`openlp.core.lib.projector.db` module + + Provides the database functions for the Projector module. + + The Manufacturer, Model, Source tables keep track of the video source + strings used for display of input sources. The Source table maps + manufacturer-defined or user-defined strings from PJLink default strings + to end-user readable strings; ex: PJLink code 11 would map "RGB 1" + default string to "RGB PC (analog)" string. + (Future feature). + + The Projector table keeps track of entries for controlled projectors. +""" + +import logging +log = logging.getLogger(__name__) +log.debug('projector.lib.db module loaded') + +from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, and_ +from sqlalchemy.ext.declarative import declarative_base, declared_attr +from sqlalchemy.orm import backref, relationship + +from openlp.core.lib.db import Manager, init_db, init_url +from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES + +metadata = MetaData() +Base = declarative_base(metadata) + + +class CommonBase(object): + """ + Base class to automate table name and ID column. + """ + @declared_attr + def __tablename__(cls): + return cls.__name__.lower() + + id = Column(Integer, primary_key=True) + + +class Manufacturer(CommonBase, Base): + """ + Projector manufacturer table. + + Manufacturer: + name: Column(String(30)) + models: Relationship(Model.id) + + Model table is related. + """ + def __repr__(self): + """ + Returns a basic representation of a Manufacturer table entry. + """ + return '' % self.name + + name = Column(String(30)) + models = relationship('Model', + order_by='Model.name', + backref='manufacturer', + cascade='all, delete-orphan', + primaryjoin='Manufacturer.id==Model.manufacturer_id', + lazy='joined') + + +class Model(CommonBase, Base): + """ + Projector model table. + + Model: + name: Column(String(20)) + sources: Relationship(Source.id) + manufacturer_id: Foreign_key(Manufacturer.id) + + Manufacturer table links here. + Source table is related. + """ + def __repr__(self): + """ + Returns a basic representation of a Model table entry. + """ + return '' % self.name + + manufacturer_id = Column(Integer, ForeignKey('manufacturer.id')) + name = Column(String(20)) + sources = relationship('Source', + order_by='Source.pjlink_name', + backref='model', + cascade='all, delete-orphan', + primaryjoin='Model.id==Source.model_id', + lazy='joined') + + +class Source(CommonBase, Base): + """ + Projector video source table. + + Source: + pjlink_name: Column(String(15)) + pjlink_code: Column(String(2)) + text: Column(String(30)) + model_id: Foreign_key(Model.id) + + Model table links here. + + These entries map PJLink input video source codes to text strings. + """ + def __repr__(self): + """ + Return basic representation of Source table entry. + """ + return '' % \ + (self.pjlink_name, self.pjlink_code, self.text) + model_id = Column(Integer, ForeignKey('model.id')) + pjlink_name = Column(String(15)) + pjlink_code = Column(String(2)) + text = Column(String(30)) + + +class Projector(CommonBase, Base): + """ + Projector table. + + Projector: + ip: Column(String(100)) # Allow for IPv6 or FQDN + port: Column(String(8)) + pin: Column(String(20)) # Allow for test strings + name: Column(String(20)) + location: Column(String(30)) + notes: Column(String(200)) + pjlink_name: Column(String(128)) # From projector (future) + manufacturer: Column(String(128)) # From projector (future) + model: Column(String(128)) # From projector (future) + other: Column(String(128)) # From projector (future) + sources: Column(String(128)) # From projector (future) + + ProjectorSource relates + """ + def __repr__(self): + """ + Return basic representation of Source table entry. + """ + return '< Projector(id="%s", ip="%s", port="%s", pin="%s", name="%s", location="%s",' \ + 'notes="%s", pjlink_name="%s", manufacturer="%s", model="%s", other="%s",' \ + 'sources="%s", source_list="%s") >' % (self.id, self.ip, self.port, self.pin, self.name, self.location, + self.notes, self.pjlink_name, self.manufacturer, self.model, + self.other, self.sources, self.source_list) + ip = Column(String(100)) + port = Column(String(8)) + pin = Column(String(20)) + name = Column(String(20)) + location = Column(String(30)) + notes = Column(String(200)) + pjlink_name = Column(String(128)) + manufacturer = Column(String(128)) + model = Column(String(128)) + other = Column(String(128)) + sources = Column(String(128)) + source_list = relationship('ProjectorSource', + order_by='ProjectorSource.code', + backref='projector', + cascade='all, delete-orphan', + primaryjoin='Projector.id==ProjectorSource.projector_id', + lazy='joined') + + +class ProjectorSource(CommonBase, Base): + """ + Projector local source table + This table allows mapping specific projector source input to a local + connection; i.e., '11': 'DVD Player' + + Projector Source: + projector_id: Foreign_key(Column(Projector.id)) + code: Column(String(3)) # PJLink source code + text: Column(String(20)) # Text to display + + Projector table links here + """ + def __repr__(self): + """ + Return basic representation of Source table entry. + """ + return '' % (self.id, + self.code, + self.text, + self.projector_id) + code = Column(String(3)) + text = Column(String(20)) + projector_id = Column(Integer, ForeignKey('projector.id')) + + +class ProjectorDB(Manager): + """ + Class to access the projector database. + """ + def __init__(self, *args, **kwargs): + log.debug('ProjectorDB().__init__(args="%s", kwargs="%s")' % (args, kwargs)) + super().__init__(plugin_name='projector', + init_schema=self.init_schema) + log.debug('ProjectorDB() Initialized using db url %s' % self.db_url) + + def init_schema(*args, **kwargs): + """ + Setup the projector database and initialize the schema. + + Declarative uses table classes to define schema. + """ + url = init_url('projector') + session, metadata = init_db(url, base=Base) + Base.metadata.create_all(checkfirst=True) + return session + + def get_projector_by_id(self, dbid): + """ + Locate a DB record by record ID. + + :param dbid: DB record id + :returns: Projector() instance + """ + log.debug('get_projector_by_id(id="%s")' % dbid) + projector = self.get_object_filtered(Projector, Projector.id == dbid) + if projector is None: + # Not found + log.warn('get_projector_by_id() did not find %s' % id) + return None + log.debug('get_projectorby_id() returning 1 entry for "%s" id="%s"' % (dbid, projector.id)) + return projector + + def get_projector_all(self): + """ + Retrieve all projector entries. + + :returns: List with Projector() instances used in Manager() QListWidget. + """ + log.debug('get_all() called') + return_list = [] + new_list = self.get_all_objects(Projector) + if new_list is None or new_list.count == 0: + return return_list + for new_projector in new_list: + return_list.append(new_projector) + log.debug('get_all() returning %s item(s)' % len(return_list)) + return return_list + + def get_projector_by_ip(self, ip): + """ + Locate a projector by host IP/Name. + + :param ip: Host IP/Name + :returns: Projector() instance + """ + log.debug('get_projector_by_ip(ip="%s")' % ip) + projector = self.get_object_filtered(Projector, Projector.ip == ip) + if projector is None: + # Not found + log.warn('get_projector_by_ip() did not find %s' % ip) + return None + log.debug('get_projectorby_ip() returning 1 entry for "%s" id="%s"' % (ip, projector.id)) + return projector + + def get_projector_by_name(self, name): + """ + Locate a projector by name field + + :param name: Name of projector + :returns: Projector() instance + """ + log.debug('get_projector_by_name(name="%s")' % name) + projector = self.get_object_filtered(Projector, Projector.name == name) + if projector is None: + # Not found + log.warn('get_projector_by_name() did not find "%s"' % name) + return None + log.debug('get_projector_by_name() returning one entry for "%s" id="%s"' % (name, projector.id)) + return projector + + def add_projector(self, projector): + """ + Add a new projector entry + + :param projector: Projector() instance to add + :returns: bool + True if entry added + False if entry already in DB or db error + """ + old_projector = self.get_object_filtered(Projector, Projector.ip == projector.ip) + if old_projector is not None: + log.warn('add_new() skipping entry ip="%s" (Already saved)' % old_projector.ip) + return False + log.debug('add_new() saving new entry') + log.debug('ip="%s", name="%s", location="%s"' % (projector.ip, + projector.name, + projector.location)) + log.debug('notes="%s"' % projector.notes) + return self.save_object(projector) + + def update_projector(self, projector=None): + """ + Update projector entry + + :param projector: Projector() instance with new information + :returns: bool + True if DB record updated + False if entry not in DB or DB error + """ + if projector is None: + log.error('No Projector() instance to update - cancelled') + return False + old_projector = self.get_object_filtered(Projector, Projector.id == projector.id) + if old_projector is None: + log.error('Edit called on projector instance not in database - cancelled') + return False + log.debug('(%s) Updating projector with dbid=%s' % (projector.ip, projector.id)) + old_projector.ip = projector.ip + old_projector.name = projector.name + old_projector.location = projector.location + old_projector.pin = projector.pin + old_projector.port = projector.port + old_projector.pjlink_name = projector.pjlink_name + old_projector.manufacturer = projector.manufacturer + old_projector.model = projector.model + old_projector.other = projector.other + old_projector.sources = projector.sources + return self.save_object(old_projector) + + def delete_projector(self, projector): + """ + Delete an entry by record id + + :param projector: Projector() instance to delete + :returns: bool + True if record deleted + False if DB error + """ + deleted = self.delete_object(Projector, projector.id) + if deleted: + log.debug('delete_by_id() Removed entry id="%s"' % projector.id) + else: + log.error('delete_by_id() Entry id="%s" not deleted for some reason' % projector.id) + return deleted + + def get_source_list(self, projector): + """ + Retrieves the source inputs pjlink code-to-text if available based on + manufacturer and model. + If not available, then returns the PJLink code to default text. + + :param projector: Projector instance + :returns: dict + key: (str) PJLink code for source + value: (str) From ProjectorSource, Sources tables or PJLink default code list + """ + source_dict = {} + # Get default list first + for key in projector.source_available: + item = self.get_object_filtered(ProjectorSource, + and_(ProjectorSource.code == key, + ProjectorSource.projector_id == projector.dbid)) + if item is None: + source_dict[key] = PJLINK_DEFAULT_CODES[key] + else: + source_dict[key] = item.text + return source_dict + + def get_source_by_id(self, source): + """ + Retrieves the ProjectorSource by ProjectorSource.id + + :param source: ProjectorSource id + :returns: ProjetorSource instance or None + """ + source_entry = self.get_object_filtered(ProjetorSource, ProjectorSource.id == source) + if source_entry is None: + # Not found + log.warn('get_source_by_id() did not find "%s"' % source) + return None + log.debug('get_source_by_id() returning one entry for "%s""' % (source)) + return source_entry + + def get_source_by_code(self, code, projector_id): + """ + Retrieves the ProjectorSource by ProjectorSource.id + + :param source: PJLink ID + :param projector_id: Projector.id + :returns: ProjetorSource instance or None + """ + source_entry = self.get_object_filtered(ProjectorSource, + and_(ProjectorSource.code == code, + ProjectorSource.projector_id == projector_id)) + if source_entry is None: + # Not found + log.warn('get_source_by_id() did not find code="%s" projector_id="%s"' % (code, projector_id)) + return None + log.debug('get_source_by_id() returning one entry for code="%s" projector_id="%s"' % (code, projector_id)) + return source_entry + + def add_source(self, source): + """ + Add a new ProjectorSource record + + :param source: ProjectorSource() instance to add + """ + log.debug('Saving ProjectorSource(projector_id="%s" code="%s" text="%s")' % (source.projector_id, + source.code, source.text)) + return self.save_object(source) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py new file mode 100644 index 000000000..132de0dca --- /dev/null +++ b/openlp/core/lib/projector/pjlink1.py @@ -0,0 +1,913 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" + :mod:`openlp.core.lib.projector.pjlink1` module + Provides the necessary functions for connecting to a PJLink-capable projector. + + See PJLink Class 1 Specifications for details. + http://pjlink.jbmia.or.jp/english/dl.html + Section 5-1 PJLink Specifications + Section 5-5 Guidelines for Input Terminals + + NOTE: + Function names follow the following syntax: + def process_CCCC(...): + WHERE: + CCCC = PJLink command being processed. +""" + +import logging +log = logging.getLogger(__name__) + +log.debug('pjlink1 loaded') + +__all__ = ['PJLink1'] + +from codecs import decode + +from PyQt4.QtCore import pyqtSignal, pyqtSlot +from PyQt4.QtNetwork import QAbstractSocket, QTcpSocket + +from openlp.core.common import translate, qmd5_hash +from openlp.core.lib.projector.constants import * + +# Shortcuts +SocketError = QAbstractSocket.SocketError +SocketSTate = QAbstractSocket.SocketState + +PJLINK_PREFIX = '%' +PJLINK_CLASS = '1' +PJLINK_HEADER = '%s%s' % (PJLINK_PREFIX, PJLINK_CLASS) +PJLINK_SUFFIX = CR + + +class PJLink1(QTcpSocket): + """ + Socket service for connecting to a PJLink-capable projector. + """ + # Signals sent by this module + changeStatus = pyqtSignal(str, int, str) + projectorNetwork = pyqtSignal(int) # Projector network activity + projectorStatus = pyqtSignal(int) # Status update + projectorAuthentication = pyqtSignal(str) # Authentication error + projectorNoAuthentication = pyqtSignal(str) # PIN set and no authentication needed + projectorReceivedData = pyqtSignal() # Notify when received data finished processing + projectorUpdateIcons = pyqtSignal() # Update the status icons on toolbar + + def __init__(self, name=None, ip=None, port=PJLINK_PORT, pin=None, *args, **kwargs): + """ + Setup for instance. + + :param name: Display name + :param ip: IP address to connect to + :param port: Port to use. Default to PJLINK_PORT + :param pin: Access pin (if needed) + + Optional parameters + :param dbid: Database ID number + :param location: Location where projector is physically located + :param notes: Extra notes about the projector + :param poll_time: Time (in seconds) to poll connected projector + :param socket_timeout: Time (in seconds) to abort the connection if no response + """ + log.debug('PJlink(args="%s" kwargs="%s")' % (args, kwargs)) + self.name = name + self.ip = ip + self.port = port + self.pin = pin + super(PJLink1, self).__init__() + self.dbid = None + self.location = None + self.notes = None + self.dbid = None if 'dbid' not in kwargs else kwargs['dbid'] + self.location = None if 'location' not in kwargs else kwargs['notes'] + self.notes = None if 'notes' not in kwargs else kwargs['notes'] + # Poll time 20 seconds unless called with something else + self.poll_time = 20000 if 'poll_time' not in kwargs else kwargs['poll_time'] * 1000 + # Timeout 5 seconds unless called with something else + self.socket_timeout = 5000 if 'socket_timeout' not in kwargs else kwargs['socket_timeout'] * 1000 + # In case we're called from somewhere that only wants information + self.no_poll = 'no_poll' in kwargs + self.i_am_running = False + self.status_connect = S_NOT_CONNECTED + self.last_command = '' + self.projector_status = S_NOT_CONNECTED + self.error_status = S_OK + # Socket information + # Add enough space to input buffer for extraneous \n \r + self.maxSize = PJLINK_MAX_PACKET + 2 + self.setReadBufferSize(self.maxSize) + # PJLink information + self.pjlink_class = '1' # Default class + self.reset_information() + # Set from ProjectorManager.add_projector() + self.widget = None # QListBox entry + self.timer = None # Timer that calls the poll_loop + self.send_queue = [] + self.send_busy = False + # Socket timer for some possible brain-dead projectors or network cable pulled + self.socket_timer = None + # Map command to function + self.PJLINK1_FUNC = {'AVMT': self.process_avmt, + 'CLSS': self.process_clss, + 'ERST': self.process_erst, + 'INFO': self.process_info, + 'INF1': self.process_inf1, + 'INF2': self.process_inf2, + 'INPT': self.process_inpt, + 'INST': self.process_inst, + 'LAMP': self.process_lamp, + 'NAME': self.process_name, + 'PJLINK': self.check_login, + 'POWR': self.process_powr + } + + def reset_information(self): + """ + Reset projector-specific information to default + """ + log.debug('(%s) reset_information() connect status is %s' % (self.ip, self.state())) + self.power = S_OFF + self.pjlink_name = None + self.manufacturer = None + self.model = None + self.shutter = None + self.mute = None + self.lamp = None + self.fan = None + self.source_available = None + self.source = None + self.other_info = None + if hasattr(self, 'timer'): + self.timer.stop() + if hasattr(self, 'socket_timer'): + self.socket_timer.stop() + self.send_queue = [] + self.send_busy = False + + def thread_started(self): + """ + Connects signals to methods when thread is started. + """ + log.debug('(%s) Thread starting' % self.ip) + self.i_am_running = True + self.connected.connect(self.check_login) + self.disconnected.connect(self.disconnect_from_host) + self.error.connect(self.get_error) + + def thread_stopped(self): + """ + Cleanups when thread is stopped. + """ + log.debug('(%s) Thread stopped' % self.ip) + try: + self.connected.disconnect(self.check_login) + except TypeError: + pass + try: + self.disconnected.disconnect(self.disconnect_from_host) + except TypeError: + pass + try: + self.error.disconnect(self.get_error) + except TypeError: + pass + try: + self.projectorReceivedData.disconnect(self._send_command) + except TypeError: + pass + self.disconnect_from_host() + self.deleteLater() + self.i_am_running = False + + def socket_abort(self): + """ + Aborts connection and closes socket in case of brain-dead projectors. + Should normally be called by socket_timer(). + """ + log.debug('(%s) socket_abort() - Killing connection' % self.ip) + self.disconnect_from_host(abort=True) + + def poll_loop(self): + """ + Retrieve information from projector that changes. + Normally called by timer(). + """ + if self.state() != self.ConnectedState: + return + log.debug('(%s) Updating projector status' % self.ip) + # Reset timer in case we were called from a set command + if self.timer.interval() < self.poll_time: + # Reset timer to 5 seconds + self.timer.setInterval(self.poll_time) + # Restart timer + self.timer.start() + # These commands may change during connetion + for command in ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']: + self.send_command(command, queue=True) + # The following commands do not change, so only check them once + if self.power == S_ON and self.source_available is None: + self.send_command('INST', queue=True) + if self.other_info is None: + self.send_command('INFO', queue=True) + if self.manufacturer is None: + self.send_command('INF1', queue=True) + if self.model is None: + self.send_command('INF2', queue=True) + if self.pjlink_name is None: + self.send_command('NAME', queue=True) + if self.power == S_ON and self.source_available is None: + self.send_command('INST', queue=True) + + def _get_status(self, status): + """ + Helper to retrieve status/error codes and convert to strings. + + :param status: Status/Error code + :returns: (Status/Error code, String) + """ + if status in ERROR_STRING: + return ERROR_STRING[status], ERROR_MSG[status] + elif status in STATUS_STRING: + return STATUS_STRING[status], ERROR_MSG[status] + else: + return status, translate('OpenLP.PJLink1', 'Unknown status') + + def change_status(self, status, msg=None): + """ + Check connection/error status, set status for projector, then emit status change signal + for gui to allow changing the icons. + + :param status: Status code + :param msg: Optional message + """ + message = translate('OpenLP.PJLink1', 'No message') if msg is None else msg + (code, message) = self._get_status(status) + if msg is not None: + message = msg + if status in CONNECTION_ERRORS: + # Projector, connection state + self.projector_status = self.error_status = self.status_connect = E_NOT_CONNECTED + elif status >= S_NOT_CONNECTED and status < S_STATUS: + self.status_connect = status + self.projector_status = S_NOT_CONNECTED + elif status < S_NETWORK_SENDING: + self.status_connect = S_CONNECTED + self.projector_status = status + (status_code, status_message) = self._get_status(self.status_connect) + log.debug('(%s) status_connect: %s: %s' % (self.ip, status_code, status_message if msg is None else msg)) + (status_code, status_message) = self._get_status(self.projector_status) + log.debug('(%s) projector_status: %s: %s' % (self.ip, status_code, status_message if msg is None else msg)) + (status_code, status_message) = self._get_status(self.error_status) + log.debug('(%s) error_status: %s: %s' % (self.ip, status_code, status_message if msg is None else msg)) + self.changeStatus.emit(self.ip, status, message) + + @pyqtSlot() + def check_login(self, data=None): + """ + Processes the initial connection and authentication (if needed). + Starts poll timer if connection is established. + + :param data: Optional data if called from another routine + """ + log.debug('(%s) check_login(data="%s")' % (self.ip, data)) + if data is None: + # Reconnected setup? + if not self.waitForReadyRead(2000): + # Possible timeout issue + log.error('(%s) Socket timeout waiting for login' % self.ip) + self.change_status(E_SOCKET_TIMEOUT) + return + read = self.readLine(self.maxSize) + dontcare = self.readLine(self.maxSize) # Clean out the trailing \r\n + if read is None: + log.warn('(%s) read is None - socket error?' % self.ip) + return + elif len(read) < 8: + log.warn('(%s) Not enough data read)' % self.ip) + return + data = decode(read, 'ascii') + # Possibility of extraneous data on input when reading. + # Clean out extraneous characters in buffer. + dontcare = self.readLine(self.maxSize) + log.debug('(%s) check_login() read "%s"' % (self.ip, data.strip())) + # At this point, we should only have the initial login prompt with + # possible authentication + # PJLink initial login will be: + # 'PJLink 0' - Unauthenticated login - no extra steps required. + # 'PJLink 1 XXXXXX' Authenticated login - extra processing required. + if not data.upper().startswith('PJLINK'): + # Invalid response + return self.disconnect_from_host() + if '=' in data: + # Processing a login reply + data_check = data.strip().split('=') + else: + # Process initial connection + data_check = data.strip().split(' ') + log.debug('(%s) data_check="%s"' % (self.ip, data_check)) + # Check for projector reporting an error + if data_check[1].upper() == 'ERRA': + # Authentication error + self.disconnect_from_host() + self.change_status(E_AUTHENTICATION) + log.debug('(%s) emitting projectorAuthentication() signal' % self.name) + return + elif data_check[1] == '0' and self.pin is not None: + # Pin set and no authentication needed + self.disconnect_from_host() + self.change_status(E_AUTHENTICATION) + log.debug('(%s) emitting projectorNoAuthentication() signal' % self.name) + self.projectorNoAuthentication.emit(self.name) + return + elif data_check[1] == '1': + # Authenticated login with salt + log.debug('(%s) Setting hash with salt="%s"' % (self.ip, data_check[2])) + log.debug('(%s) pin="%s"' % (self.ip, self.pin)) + salt = qmd5_hash(salt=data_check[2], data=self.pin) + else: + salt = None + # We're connected at this point, so go ahead and do regular I/O + self.readyRead.connect(self.get_data) + self.projectorReceivedData.connect(self._send_command) + # Initial data we should know about + self.send_command(cmd='CLSS', salt=salt) + self.waitForReadyRead() + if (not self.no_poll) and (self.state() == self.ConnectedState): + log.debug('(%s) Starting timer' % self.ip) + self.timer.setInterval(2000) # Set 2 seconds for initial information + self.timer.start() + + @pyqtSlot() + def get_data(self): + """ + Socket interface to retrieve data. + """ + log.debug('(%s) get_data(): Reading data' % self.ip) + if self.state() != self.ConnectedState: + log.debug('(%s) get_data(): Not connected - returning' % self.ip) + self.send_busy = False + return + read = self.readLine(self.maxSize) + if read == -1: + # No data available + log.debug('(%s) get_data(): No data available (-1)' % self.ip) + self.send_busy = False + self.projectorReceivedData.emit() + return + self.socket_timer.stop() + self.projectorNetwork.emit(S_NETWORK_RECEIVED) + data_in = decode(read, 'ascii') + data = data_in.strip() + if len(data) < 7: + # Not enough data for a packet + log.debug('(%s) get_data(): Packet length < 7: "%s"' % (self.ip, data)) + self.send_busy = False + self.projectorReceivedData.emit() + return + log.debug('(%s) get_data(): Checking new data "%s"' % (self.ip, data)) + if data.upper().startswith('PJLINK'): + # Reconnected from remote host disconnect ? + self.check_login(data) + self.send_busy = False + self.projectorReceivedData.emit() + return + elif '=' not in data: + log.warn('(%s) get_data(): Invalid packet received' % self.ip) + self.send_busy = False + self.projectorReceivedData.emit() + return + data_split = data.split('=') + try: + (prefix, class_, cmd, data) = (data_split[0][0], data_split[0][1], data_split[0][2:], data_split[1]) + except ValueError as e: + log.warn('(%s) get_data(): Invalid packet - expected header + command + data' % self.ip) + log.warn('(%s) get_data(): Received data: "%s"' % (self.ip, read)) + self.change_status(E_INVALID_DATA) + self.send_busy = False + self.projectorReceivedData.emit() + return + + if not (self.pjlink_class in PJLINK_VALID_CMD and cmd in PJLINK_VALID_CMD[self.pjlink_class]): + log.warn('(%s) get_data(): Invalid packet - unknown command "%s"' % (self.ip, cmd)) + self.send_busy = False + self.projectorReceivedData.emit() + return + return self.process_command(cmd, data) + + @pyqtSlot(int) + def get_error(self, err): + """ + Process error from SocketError signal. + Remaps system error codes to projector error codes. + + :param err: Error code + """ + log.debug('(%s) get_error(err=%s): %s' % (self.ip, err, self.errorString())) + if err <= 18: + # QSocket errors. Redefined in projector.constants so we don't mistake + # them for system errors + check = err + E_CONNECTION_REFUSED + self.timer.stop() + else: + check = err + if check < E_GENERAL: + # Some system error? + self.change_status(err, self.errorString()) + else: + self.change_status(E_NETWORK, self.errorString()) + self.projectorUpdateIcons.emit() + if self.status_connect == E_NOT_CONNECTED: + self.abort() + self.reset_information() + return + + def send_command(self, cmd, opts='?', salt=None, queue=False): + """ + Add command to output queue if not already in queue. + + :param cmd: Command to send + :param opts: Command option (if any) - defaults to '?' (get information) + :param salt: Optional salt for md5 hash initial authentication + :param queue: Option to force add to queue rather than sending directly + """ + if self.state() != self.ConnectedState: + log.warn('(%s) send_command(): Not connected - returning' % self.ip) + self.send_queue = [] + return + self.projectorNetwork.emit(S_NETWORK_SENDING) + log.debug('(%s) send_command(): Building cmd="%s" opts="%s" %s' % (self.ip, + cmd, + opts, + '' if salt is None else 'with hash')) + if salt is None: + out = '%s%s %s%s' % (PJLINK_HEADER, cmd, opts, CR) + else: + out = '%s%s%s %s%s' % (salt, PJLINK_HEADER, cmd, opts, CR) + if out in self.send_queue: + # Already there, so don't add + log.debug('(%s) send_command(out="%s") Already in queue - skipping' % (self.ip, out.strip())) + elif not queue and len(self.send_queue) == 0: + # Nothing waiting to send, so just send it + log.debug('(%s) send_command(out="%s") Sending data' % (self.ip, out.strip())) + return self._send_command(data=out) + else: + log.debug('(%s) send_command(out="%s") adding to queue' % (self.ip, out.strip())) + self.send_queue.append(out) + self.projectorReceivedData.emit() + log.debug('(%s) send_command(): send_busy is %s' % (self.ip, self.send_busy)) + if not self.send_busy: + log.debug('(%s) send_command() calling _send_string()') + self._send_command() + + @pyqtSlot() + def _send_command(self, data=None): + """ + Socket interface to send data. If data=None, then check queue. + + :param data: Immediate data to send + """ + log.debug('(%s) _send_string()' % self.ip) + log.debug('(%s) _send_string(): Connection status: %s' % (self.ip, self.state())) + if self.state() != self.ConnectedState: + log.debug('(%s) _send_string() Not connected - abort' % self.ip) + self.send_queue = [] + self.send_busy = False + return + if self.send_busy: + # Still waiting for response from last command sent + return + if data is not None: + out = data + log.debug('(%s) _send_string(data=%s)' % (self.ip, out.strip())) + elif len(self.send_queue) != 0: + out = self.send_queue.pop(0) + log.debug('(%s) _send_string(queued data=%s)' % (self.ip, out.strip())) + else: + # No data to send + log.debug('(%s) _send_string(): No data to send' % self.ip) + self.send_busy = False + return + self.send_busy = True + log.debug('(%s) _send_string(): Sending "%s"' % (self.ip, out.strip())) + log.debug('(%s) _send_string(): Queue = %s' % (self.ip, self.send_queue)) + self.socket_timer.start() + try: + self.projectorNetwork.emit(S_NETWORK_SENDING) + sent = self.write(out) + self.waitForBytesWritten(2000) # 2 seconds should be enough + if sent == -1: + # Network error? + self.change_status(E_NETWORK, + translate('OpenLP.PJLink1', 'Error while sending data to projector')) + except SocketError as e: + self.disconnect_from_host(abort=True) + self.changeStatus(E_NETWORK, '%s : %s' % (e.error(), e.errorString())) + + def process_command(self, cmd, data): + """ + Verifies any return error code. Calls the appropriate command handler. + + :param cmd: Command to process + :param data: Data being processed + """ + log.debug('(%s) Processing command "%s"' % (self.ip, cmd)) + if data in PJLINK_ERRORS: + # Oops - projector error + if data.upper() == 'ERRA': + # Authentication error + self.disconnect_from_host() + self.change_status(E_AUTHENTICATION) + log.debug('(%s) emitting projectorAuthentication() signal' % self.ip) + self.projectorAuthentication.emit(self.name) + elif data.upper() == 'ERR1': + # Undefined command + self.change_status(E_UNDEFINED, '%s "%s"' % + (translate('OpenLP.PJLink1', 'Undefined command:'), cmd)) + elif data.upper() == 'ERR2': + # Invalid parameter + self.change_status(E_PARAMETER) + elif data.upper() == 'ERR3': + # Projector busy + self.change_status(E_UNAVAILABLE) + elif data.upper() == 'ERR4': + # Projector/display error + self.change_status(E_PROJECTOR) + self.send_busy = False + self.projectorReceivedData.emit() + return + # Command succeeded - no extra information + elif data.upper() == 'OK': + log.debug('(%s) Command returned OK' % self.ip) + # A command returned successfully, recheck data + self.send_busy = False + self.projectorReceivedData.emit() + return + + if cmd in self.PJLINK1_FUNC: + self.PJLINK1_FUNC[cmd](data) + else: + log.warn('(%s) Invalid command %s' % (self.ip, cmd)) + self.send_busy = False + self.projectorReceivedData.emit() + + def process_lamp(self, data): + """ + Lamp(s) status. See PJLink Specifications for format. + Data may have more than 1 lamp to process. + Update self.lamp dictionary with lamp status. + + :param data: Lamp(s) status. + """ + lamps = [] + data_dict = data.split() + while data_dict: + try: + fill = {'Hours': int(data_dict[0]), 'On': False if data_dict[1] == '0' else True} + except ValueError: + # In case of invalid entry + log.warn('(%s) process_lamp(): Invalid data "%s"' % (self.ip, data)) + return + lamps.append(fill) + data_dict.pop(0) # Remove lamp hours + data_dict.pop(0) # Remove lamp on/off + self.lamp = lamps + return + + def process_powr(self, data): + """ + Power status. See PJLink specification for format. + Update self.power with status. Update icons if change from previous setting. + + :param data: Power status + """ + if data in PJLINK_POWR_STATUS: + power = PJLINK_POWR_STATUS[data] + update_icons = self.power != power + self.power = power + self.change_status(PJLINK_POWR_STATUS[data]) + if update_icons: + self.projectorUpdateIcons.emit() + # Update the input sources available + if power == S_ON: + self.send_command('INST') + else: + # Log unknown status response + log.warn('Unknown power response: %s' % data) + return + + def process_avmt(self, data): + """ + Process shutter and speaker status. See PJLink specification for format. + Update self.mute (audio) and self.shutter (video shutter). + + :param data: Shutter and audio status + """ + shutter = self.shutter + mute = self.mute + if data == '11': + shutter = True + mute = False + elif data == '21': + shutter = False + mute = True + elif data == '30': + shutter = False + mute = False + elif data == '31': + shutter = True + mute = True + else: + log.warn('Unknown shutter response: %s' % data) + update_icons = shutter != self.shutter + update_icons = update_icons or mute != self.mute + self.shutter = shutter + self.mute = mute + if update_icons: + self.projectorUpdateIcons.emit() + return + + def process_inpt(self, data): + """ + Current source input selected. See PJLink specification for format. + Update self.source + + :param data: Currently selected source + """ + self.source = data + return + + def process_clss(self, data): + """ + PJLink class that this projector supports. See PJLink specification for format. + Updates self.class. + + :param data: Class that projector supports. + """ + self.pjlink_class = data + log.debug('(%s) Setting pjlink_class for this projector to "%s"' % (self.ip, self.pjlink_class)) + return + + def process_name(self, data): + """ + Projector name set in projector. + Updates self.pjlink_name + + :param data: Projector name + """ + self.pjlink_name = data + return + + def process_inf1(self, data): + """ + Manufacturer name set in projector. + Updates self.manufacturer + + :param data: Projector manufacturer + """ + self.manufacturer = data + return + + def process_inf2(self, data): + """ + Projector Model set in projector. + Updates self.model. + + :param data: Model name + """ + self.model = data + return + + def process_info(self, data): + """ + Any extra info set in projector. + Updates self.other_info. + + :param data: Projector other info + """ + self.other_info = data + return + + def process_inst(self, data): + """ + Available source inputs. See PJLink specification for format. + Updates self.source_available + + :param data: Sources list + """ + sources = [] + check = data.split() + for source in check: + sources.append(source) + sources.sort() + self.source_available = sources + self.projectorUpdateIcons.emit() + return + + def process_erst(self, data): + """ + Error status. See PJLink Specifications for format. + Updates self.projector_errors + + :param data: Error status + """ + try: + datacheck = int(data) + except ValueError: + # Bad data - ignore + return + if datacheck == 0: + self.projector_errors = None + else: + self.projector_errors = {} + # Fan + if data[0] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Fan')] = \ + PJLINK_ERST_STATUS[data[0]] + # Lamp + if data[1] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Lamp')] = \ + PJLINK_ERST_STATUS[data[1]] + # Temp + if data[2] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Temperature')] = \ + PJLINK_ERST_STATUS[data[2]] + # Cover + if data[3] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Cover')] = \ + PJLINK_ERST_STATUS[data[3]] + # Filter + if data[4] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Filter')] = \ + PJLINK_ERST_STATUS[data[4]] + # Other + if data[5] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Other')] = \ + PJLINK_ERST_STATUS[data[5]] + return + + def connect_to_host(self): + """ + Initiate connection to projector. + """ + if self.state() == self.ConnectedState: + log.warn('(%s) connect_to_host(): Already connected - returning' % self.ip) + return + self.change_status(S_CONNECTING) + self.connectToHost(self.ip, self.port if type(self.port) is int else int(self.port)) + + @pyqtSlot() + def disconnect_from_host(self, abort=False): + """ + Close socket and cleanup. + """ + if abort or self.state() != self.ConnectedState: + if abort: + log.warn('(%s) disconnect_from_host(): Aborting connection' % self.ip) + else: + log.warn('(%s) disconnect_from_host(): Not connected - returning' % self.ip) + self.reset_information() + self.disconnectFromHost() + try: + self.readyRead.disconnect(self.get_data) + except TypeError: + pass + if abort: + self.change_status(E_NOT_CONNECTED) + else: + log.debug('(%s) disconnect_from_host() Current status %s' % (self.ip, + self._get_status(self.status_connect)[0])) + if self.status_connect != E_NOT_CONNECTED: + self.change_status(S_NOT_CONNECTED) + self.reset_information() + self.projectorUpdateIcons.emit() + + def get_available_inputs(self): + """ + Send command to retrieve available source inputs. + """ + return self.send_command(cmd='INST') + + def get_error_status(self): + """ + Send command to retrieve currently known errors. + """ + return self.send_command(cmd='ERST') + + def get_input_source(self): + """ + Send command to retrieve currently selected source input. + """ + return self.send_command(cmd='INPT') + + def get_lamp_status(self): + """ + Send command to return the lap status. + """ + return self.send_command(cmd='LAMP') + + def get_manufacturer(self): + """ + Send command to retrieve manufacturer name. + """ + return self.send_command(cmd='INF1') + + def get_model(self): + """ + Send command to retrieve the model name. + """ + return self.send_command(cmd='INF2') + + def get_name(self): + """ + Send command to retrieve name as set by end-user (if set). + """ + return self.send_command(cmd='NAME') + + def get_other_info(self): + """ + Send command to retrieve extra info set by manufacturer. + """ + return self.send_command(cmd='INFO') + + def get_power_status(self): + """ + Send command to retrieve power status. + """ + return self.send_command(cmd='POWR') + + def get_shutter_status(self): + """ + Send command to retrieve shutter status. + """ + return self.send_command(cmd='AVMT') + + def set_input_source(self, src=None): + """ + Verify input source available as listed in 'INST' command, + then send the command to select the input source. + + :param src: Video source to select in projector + """ + log.debug('(%s) set_input_source(src=%s)' % (self.ip, src)) + if self.source_available is None: + return + elif src not in self.source_available: + return + log.debug('(%s) Setting input source to %s' % (self.ip, src)) + self.send_command(cmd='INPT', opts=src) + self.poll_loop() + + def set_power_on(self): + """ + Send command to turn power to on. + """ + self.send_command(cmd='POWR', opts='1') + self.poll_loop() + + def set_power_off(self): + """ + Send command to turn power to standby. + """ + self.send_command(cmd='POWR', opts='0') + self.poll_loop() + + def set_shutter_closed(self): + """ + Send command to set shutter to closed position. + """ + self.send_command(cmd='AVMT', opts='11') + self.poll_loop() + + def set_shutter_open(self): + """ + Send command to set shutter to open position. + """ + self.send_command(cmd='AVMT', opts='10') + self.poll_loop() diff --git a/openlp/core/lib/toolbar.py b/openlp/core/lib/toolbar.py index b24be89a8..b30d20a89 100644 --- a/openlp/core/lib/toolbar.py +++ b/openlp/core/lib/toolbar.py @@ -82,3 +82,16 @@ class OpenLPToolbar(QtGui.QToolBar): self.actions[handle].setVisible(visible) else: log.warning('No handle "%s" in actions list.', str(handle)) + + def set_widget_enabled(self, widgets, enabled=True): + """ + Set the enabled state for a widget or a list of widgets. + + :param widgets: A list of string with widget object names. + :param enabled: The new state as bool. + """ + for handle in widgets: + if handle in self.actions: + self.actions[handle].setEnabled(enabled) + else: + log.warning('No handle "%s" in actions list.', str(handle)) diff --git a/openlp/core/ui/__init__.py b/openlp/core/ui/__init__.py index 664074a87..8569fc100 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.tab import ProjectorTab +from .projector.editform import ProjectorEditForm __all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', 'ThemeForm', 'ThemeManager', 'MediaDockManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', 'Display', 'ServiceNoteForm', 'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay', 'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm', - 'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'SingleColumnTableWidget'] + 'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'SingleColumnTableWidget', + 'ProjectorManager', 'ProjectorTab', 'ProjectorEditForm'] diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index c5821722c..825a12889 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', 'Toggle Projector Manager')) + self.view_projector_manager_item.setStatusTip(translate('OpenLP.MainWindow', + 'Toggle the visibility of the Projector Manager')) self.view_media_manager_item.setText(translate('OpenLP.MainWindow', '&Media Manager')) self.view_media_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Toggle Media Manager')) self.view_media_manager_item.setStatusTip(translate('OpenLP.MainWindow', @@ -485,6 +514,7 @@ 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' @@ -514,6 +544,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) @@ -826,6 +857,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]) @@ -1115,6 +1147,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/editform.py b/openlp/core/ui/projector/editform.py new file mode 100644 index 000000000..3aaa1cdbd --- /dev/null +++ b/openlp/core/ui/projector/editform.py @@ -0,0 +1,269 @@ + +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" + :mod: `openlp.core.ui.projector.editform` module + + Provides the functions for adding/editing entries in the projector database. +""" + +import logging +log = logging.getLogger(__name__) +log.debug('editform loaded') + +from PyQt4 import QtCore, QtGui +from PyQt4.QtCore import pyqtSlot, pyqtSignal +from PyQt4.QtGui import QDialog, QPlainTextEdit, QLineEdit, QDialogButtonBox, QLabel, QGridLayout + +from openlp.core.common import translate, verify_ip_address +from openlp.core.lib import build_icon +from openlp.core.lib.projector.db import Projector +from openlp.core.lib.projector.constants import PJLINK_PORT + + +class Ui_ProjectorEditForm(object): + """ + The :class:`~openlp.core.lib.ui.projector.editform.Ui_ProjectorEditForm` class defines + the user interface for the ProjectorEditForm dialog. + """ + def setupUi(self, edit_projector_dialog): + """ + Create the interface layout. + """ + edit_projector_dialog.setObjectName('edit_projector_dialog') + edit_projector_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo-32x32.png')) + edit_projector_dialog.setMinimumWidth(400) + edit_projector_dialog.setModal(True) + # Define the basic layout + self.dialog_layout = QGridLayout(edit_projector_dialog) + self.dialog_layout.setObjectName('dialog_layout') + self.dialog_layout.setSpacing(8) + self.dialog_layout.setContentsMargins(8, 8, 8, 8) + # IP Address + self.ip_label = QLabel(edit_projector_dialog) + self.ip_label.setObjectName('projector_edit_ip_label') + self.ip_text = QLineEdit(edit_projector_dialog) + self.ip_text.setObjectName('projector_edit_ip_text') + self.dialog_layout.addWidget(self.ip_label, 0, 0) + self.dialog_layout.addWidget(self.ip_text, 0, 1) + # Port number + self.port_label = QLabel(edit_projector_dialog) + self.port_label.setObjectName('projector_edit_ip_label') + self.port_text = QLineEdit(edit_projector_dialog) + self.port_text.setObjectName('projector_edit_port_text') + self.dialog_layout.addWidget(self.port_label, 1, 0) + self.dialog_layout.addWidget(self.port_text, 1, 1) + # PIN + self.pin_label = QLabel(edit_projector_dialog) + self.pin_label.setObjectName('projector_edit_pin_label') + self.pin_text = QLineEdit(edit_projector_dialog) + self.pin_label.setObjectName('projector_edit_pin_text') + self.dialog_layout.addWidget(self.pin_label, 2, 0) + self.dialog_layout.addWidget(self.pin_text, 2, 1) + # Name + self.name_label = QLabel(edit_projector_dialog) + self.name_label.setObjectName('projector_edit_name_label') + self.name_text = QLineEdit(edit_projector_dialog) + self.name_text.setObjectName('projector_edit_name_text') + self.dialog_layout.addWidget(self.name_label, 3, 0) + self.dialog_layout.addWidget(self.name_text, 3, 1) + # Location + self.location_label = QLabel(edit_projector_dialog) + self.location_label.setObjectName('projector_edit_location_label') + self.location_text = QLineEdit(edit_projector_dialog) + self.location_text.setObjectName('projector_edit_location_text') + self.dialog_layout.addWidget(self.location_label, 4, 0) + self.dialog_layout.addWidget(self.location_text, 4, 1) + # Notes + self.notes_label = QLabel(edit_projector_dialog) + self.notes_label.setObjectName('projector_edit_notes_label') + self.notes_text = QPlainTextEdit(edit_projector_dialog) + self.notes_text.setObjectName('projector_edit_notes_text') + self.dialog_layout.addWidget(self.notes_label, 5, 0, alignment=QtCore.Qt.AlignTop) + self.dialog_layout.addWidget(self.notes_text, 5, 1) + # Time for the buttons + self.button_box = QDialogButtonBox(QDialogButtonBox.Help | + QDialogButtonBox.Save | + QDialogButtonBox.Cancel) + self.dialog_layout.addWidget(self.button_box, 8, 0, 1, 2) + + def retranslateUi(self, edit_projector_dialog): + if self.new_projector: + title = translate('OpenLP.ProjectorEditForm', 'Add New Projector') + self.projector.port = PJLINK_PORT + else: + title = translate('OpenLP.ProjectorEditForm', 'Edit Projector') + edit_projector_dialog.setWindowTitle(title) + self.ip_label.setText(translate('OpenLP.ProjectorEditForm', 'IP Address')) + self.ip_text.setText(self.projector.ip) + self.ip_text.setFocus() + self.port_label.setText(translate('OpenLP.ProjectorEditForm', 'Port Number')) + self.port_text.setText(str(self.projector.port)) + self.pin_label.setText(translate('OpenLP.ProjectorEditForm', 'PIN')) + self.pin_text.setText(self.projector.pin) + self.name_label.setText(translate('OpenLP.ProjectorEditForm', 'Name')) + self.name_text.setText(self.projector.name) + self.location_label.setText(translate('OpenLP.ProjectorEditForm', 'Location')) + self.location_text.setText(self.projector.location) + self.notes_label.setText(translate('OpenLP.ProjectorEditForm', 'Notes')) + self.notes_text.insertPlainText(self.projector.notes) + + +class ProjectorEditForm(QDialog, Ui_ProjectorEditForm): + """ + Class to add or edit a projector entry in the database. + + Fields that are editable: + ip = Column(String(100)) + port = Column(String(8)) + pin = Column(String(20)) + name = Column(String(20)) + location = Column(String(30)) + notes = Column(String(200)) + """ + newProjector = pyqtSignal(str) + editProjector = pyqtSignal(object) + + def __init__(self, parent=None, projectordb=None): + super(ProjectorEditForm, self).__init__(parent=parent) + self.projectordb = projectordb + self.setupUi(self) + self.button_box.accepted.connect(self.accept_me) + self.button_box.helpRequested.connect(self.help_me) + self.button_box.rejected.connect(self.cancel_me) + + def exec_(self, projector=None): + if projector is None: + self.projector = Projector() + self.new_projector = True + else: + self.projector = projector + self.new_projector = False + self.retranslateUi(self) + reply = QDialog.exec_(self) + self.projector = None + return reply + + @pyqtSlot() + def accept_me(self): + """ + Validate input before accepting input. + """ + log.debug('accept_me() signal received') + if len(self.name_text.text().strip()) < 1: + QtGui.QMessageBox.warning(self, + translate('OpenLP.ProjectorEdit', 'Name Not Set'), + translate('OpenLP.ProjectorEdit', + 'You must enter a name for this entry.
' + 'Please enter a new name for this entry.')) + valid = False + return + name = self.name_text.text().strip() + record = self.projectordb.get_projector_by_name(name) + if record is not None and record.id != self.projector.id: + QtGui.QMessageBox.warning(self, + translate('OpenLP.ProjectorEdit', 'Duplicate Name'), + translate('OpenLP.ProjectorEdit', + 'There is already an entry with name "%s" in ' + 'the database as ID "%s".
' + 'Please enter a different name.' % (name, record.id))) + valid = False + return + adx = self.ip_text.text() + valid = verify_ip_address(adx) + if valid: + ip = self.projectordb.get_projector_by_ip(adx) + if ip is None: + valid = True + self.new_projector = True + elif ip.id != self.projector.id: + QtGui.QMessageBox.warning(self, + translate('OpenLP.ProjectorWizard', 'Duplicate IP Address'), + translate('OpenLP.ProjectorWizard', + 'IP address "%s"
is already in the database as ID %s.' + '

Please Enter a different IP address.' % (adx, ip.id))) + valid = False + return + else: + QtGui.QMessageBox.warning(self, + translate('OpenLP.ProjectorWizard', 'Invalid IP Address'), + translate('OpenLP.ProjectorWizard', + 'IP address "%s"
is not a valid IP address.' + '

Please enter a valid IP address.' % adx)) + valid = False + return + port = int(self.port_text.text()) + if port < 1000 or port > 32767: + QtGui.QMessageBox.warning(self, + translate('OpenLP.ProjectorWizard', 'Invalid Port Number'), + translate('OpenLP.ProjectorWizard', + 'Port numbers below 1000 are reserved for admin use only, ' + '
and port numbers above 32767 are not currently usable.' + '

Please enter a valid port number between ' + ' 1000 and 32767.' + '

Default PJLink port is %s' % PJLINK_PORT)) + valid = False + if valid: + self.projector.ip = self.ip_text.text() + self.projector.pin = self.pin_text.text() + self.projector.port = int(self.port_text.text()) + self.projector.name = self.name_text.text() + self.projector.location = self.location_text.text() + self.projector.notes = self.notes_text.toPlainText() + if self.new_projector: + saved = self.projectordb.add_projector(self.projector) + else: + saved = self.projectordb.update_projector(self.projector) + if not saved: + QtGui.QMessageBox.warning(self, + translate('OpenLP.ProjectorEditForm', 'Database Error'), + translate('OpenLP.ProjectorEditForm', + 'There was an error saving projector ' + 'information. See the log for the error')) + return saved + if self.new_projector: + self.newProjector.emit(adx) + else: + self.editProjector.emit(self.projector) + self.close() + + @pyqtSlot() + def help_me(self): + """ + Show a help message about the input fields. + """ + log.debug('help_me() signal received') + + @pyqtSlot() + def cancel_me(self): + """ + Cancel button clicked - just close. + """ + log.debug('cancel_me() signal received') + self.close() diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py new file mode 100644 index 000000000..cac5de46d --- /dev/null +++ b/openlp/core/ui/projector/manager.py @@ -0,0 +1,983 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" + :mod: openlp.core.ui.projector.manager` module + + Provides the functions for the display/control of Projectors. +""" + +import logging +log = logging.getLogger(__name__) +log.debug('projectormanager loaded') + +from PyQt4 import QtCore, QtGui +from PyQt4.QtCore import QObject, QThread, pyqtSlot +from PyQt4.QtGui import QWidget + +from openlp.core.common import RegistryProperties, Settings, OpenLPMixin, \ + RegistryMixin, translate +from openlp.core.lib import OpenLPToolbar +from openlp.core.lib.ui import create_widget_action +from openlp.core.lib.projector import DialogSourceStyle +from openlp.core.lib.projector.constants import * +from openlp.core.lib.projector.db import ProjectorDB +from openlp.core.lib.projector.pjlink1 import PJLink1 +from openlp.core.ui.projector.editform import ProjectorEditForm +from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle + +# Dict for matching projector status to display icon +STATUS_ICONS = {S_NOT_CONNECTED: ':/projector/projector_item_disconnect.png', + S_CONNECTING: ':/projector/projector_item_connect.png', + S_CONNECTED: ':/projector/projector_off.png', + S_OFF: ':/projector/projector_off.png', + S_INITIALIZE: ':/projector/projector_off.png', + S_STANDBY: ':/projector/projector_off.png', + S_WARMUP: ':/projector/projector_warmup.png', + S_ON: ':/projector/projector_on.png', + S_COOLDOWN: ':/projector/projector_cooldown.png', + E_ERROR: ':/projector/projector_error.png', + E_NETWORK: ':/projector/projector_not_connected_error.png', + E_AUTHENTICATION: ':/projector/projector_not_connected_error.png', + E_UNKNOWN_SOCKET_ERROR: ':/projector/projector_not_connected_error.png', + E_NOT_CONNECTED: ':/projector/projector_not_connected_error.png' + } + + +class Ui_ProjectorManager(object): + """ + UI part of the Projector Manager + """ + def setup_ui(self, widget): + """ + Define the UI + + :param widget: The screen object the dialog is to be attached to. + """ + log.debug('setup_ui()') + # Create ProjectorManager box + self.layout = QtGui.QVBoxLayout(widget) + self.layout.setSpacing(0) + self.layout.setMargin(0) + self.layout.setObjectName('layout') + # Add one selection toolbar + self.one_toolbar = OpenLPToolbar(widget) + self.one_toolbar.add_toolbar_action('new_projector', + text=translate('OpenLP.ProjectorManager', 'Add Projector'), + icon=':/projector/projector_new.png', + tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'), + triggers=self.on_add_projector) + # Show edit/delete when projector not connected + self.one_toolbar.add_toolbar_action('edit_projector', + text=translate('OpenLP.ProjectorManager', 'Edit Projector'), + icon=':/general/general_edit.png', + tooltip=translate('OpenLP.ProjectorManager', 'Edit selected projector'), + triggers=self.on_edit_projector) + self.one_toolbar.add_toolbar_action('delete_projector', + text=translate('OpenLP.ProjectorManager', 'Delete Projector'), + icon=':/general/general_delete.png', + tooltip=translate('OpenLP.ProjectorManager', 'Delete selected projector'), + triggers=self.on_delete_projector) + # Show source/view when projector connected + self.one_toolbar.add_toolbar_action('source_view_projector', + text=translate('OpenLP.ProjectorManager', 'Select Input Source'), + icon=':/projector/projector_hdmi.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Choose input source on selected projector'), + triggers=self.on_select_input) + self.one_toolbar.add_toolbar_action('view_projector', + text=translate('OpenLP.ProjectorManager', 'View Projector'), + icon=':/system/system_about.png', + tooltip=translate('OpenLP.ProjectorManager', + 'View selected projector information'), + triggers=self.on_status_projector) + self.one_toolbar.addSeparator() + self.one_toolbar.add_toolbar_action('connect_projector', + text=translate('OpenLP.ProjectorManager', + 'Connect to selected projector'), + icon=':/projector/projector_connect.png', + tootip=translate('OpenLP.ProjectorManager', + 'Connect to selected projector'), + triggers=self.on_connect_projector) + self.one_toolbar.add_toolbar_action('connect_projector_multiple', + text=translate('OpenLP.ProjectorManager', + 'Connect to selected projectors'), + icon=':/projector/projector_connect_tiled.png', + tootip=translate('OpenLP.ProjectorManager', + 'Connect to selected projector'), + triggers=self.on_connect_projector) + self.one_toolbar.add_toolbar_action('disconnect_projector', + text=translate('OpenLP.ProjectorManager', + 'Disconnect from selected projectors'), + icon=':/projector/projector_disconnect.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Disconnect from selected projector'), + triggers=self.on_disconnect_projector) + self.one_toolbar.add_toolbar_action('disconnect_projector_multiple', + text=translate('OpenLP.ProjectorManager', + 'Disconnect from selected projector'), + icon=':/projector/projector_disconnect_tiled.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Disconnect from selected projector'), + triggers=self.on_disconnect_projector) + self.one_toolbar.addSeparator() + self.one_toolbar.add_toolbar_action('poweron_projector', + text=translate('OpenLP.ProjectorManager', + 'Power on selected projector'), + icon=':/projector/projector_power_on.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Power on selected projector'), + triggers=self.on_poweron_projector) + self.one_toolbar.add_toolbar_action('poweron_projector_multiple', + text=translate('OpenLP.ProjectorManager', + 'Power on selected projector'), + icon=':/projector/projector_power_on_tiled.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Power on selected projector'), + triggers=self.on_poweron_projector) + self.one_toolbar.add_toolbar_action('poweroff_projector', + text=translate('OpenLP.ProjectorManager', 'Standby selected projector'), + icon=':/projector/projector_power_off.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Put selected projector in standby'), + triggers=self.on_poweroff_projector) + self.one_toolbar.add_toolbar_action('poweroff_projector_multiple', + text=translate('OpenLP.ProjectorManager', 'Standby selected projector'), + icon=':/projector/projector_power_off_tiled.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Put selected projector in standby'), + triggers=self.on_poweroff_projector) + self.one_toolbar.addSeparator() + self.one_toolbar.add_toolbar_action('blank_projector', + text=translate('OpenLP.ProjectorManager', + 'Blank selected projector screen'), + icon=':/projector/projector_blank.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Blank selected projector screen'), + triggers=self.on_blank_projector) + self.one_toolbar.add_toolbar_action('blank_projector_multiple', + text=translate('OpenLP.ProjectorManager', + 'Blank selected projector screen'), + icon=':/projector/projector_blank_tiled.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Blank selected projector screen'), + triggers=self.on_blank_projector) + self.one_toolbar.add_toolbar_action('show_projector', + ext=translate('OpenLP.ProjectorManager', + 'Show selected projector screen'), + icon=':/projector/projector_show.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Show selected projector screen'), + triggers=self.on_show_projector) + self.one_toolbar.add_toolbar_action('show_projector_multiple', + ext=translate('OpenLP.ProjectorManager', + 'Show selected projector screen'), + icon=':/projector/projector_show_tiled.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Show selected projector screen'), + triggers=self.on_show_projector) + self.layout.addWidget(self.one_toolbar) + self.projector_one_widget = QtGui.QWidgetAction(self.one_toolbar) + self.projector_one_widget.setObjectName('projector_one_toolbar_widget') + # Create projector manager list + self.projector_list_widget = QtGui.QListWidget(widget) + self.projector_list_widget.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) + self.projector_list_widget.setAlternatingRowColors(True) + self.projector_list_widget.setIconSize(QtCore.QSize(90, 50)) + self.projector_list_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.projector_list_widget.setObjectName('projector_list_widget') + self.layout.addWidget(self.projector_list_widget) + self.projector_list_widget.customContextMenuRequested.connect(self.context_menu) + self.projector_list_widget.itemDoubleClicked.connect(self.on_doubleclick_item) + # Build the context menu + self.menu = QtGui.QMenu() + self.status_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&View Projector Information'), + icon=':/system/system_about.png', + triggers=self.on_status_projector) + self.edit_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&Edit Projector'), + icon=':/projector/projector_edit.png', + triggers=self.on_edit_projector) + self.menu.addSeparator() + self.connect_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&Connect Projector'), + icon=':/projector/projector_connect.png', + triggers=self.on_connect_projector) + self.disconnect_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + 'D&isconnect Projector'), + icon=':/projector/projector_disconnect.png', + triggers=self.on_disconnect_projector) + self.menu.addSeparator() + self.poweron_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + 'Power &On Projector'), + icon=':/projector/projector_power_on.png', + triggers=self.on_poweron_projector) + self.poweroff_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + 'Power O&ff Projector'), + icon=':/projector/projector_power_off.png', + triggers=self.on_poweroff_projector) + self.menu.addSeparator() + self.select_input_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + 'Select &Input'), + icon=':/projector/projector_hdmi.png', + triggers=self.on_select_input) + self.edit_input_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + 'Edit Input Source'), + icon=':/general/general_edit.png', + triggers=self.on_edit_input) + self.blank_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&Blank Projector Screen'), + icon=':/projector/projector_blank.png', + triggers=self.on_blank_projector) + self.show_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&Show Projector Screen'), + icon=':/projector/projector_show.png', + triggers=self.on_show_projector) + self.menu.addSeparator() + self.delete_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&Delete Projector'), + icon=':/general/general_delete.png', + triggers=self.on_delete_projector) + self.update_icons() + + +class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, RegistryProperties): + """ + Manage the projectors. + """ + def __init__(self, parent=None, projectordb=None): + """ + Basic initialization. + + :param parent: Who I belong to. + :param projectordb: Database session inherited from superclass. + """ + log.debug('__init__()') + super().__init__(parent) + self.settings_section = 'projector' + self.projectordb = projectordb + self.projector_list = [] + self.source_select_form = None + + def bootstrap_initialise(self): + """ + Pre-initialize setups. + """ + self.setup_ui(self) + if self.projectordb is None: + # Work around for testing creating a ~/.openlp.data.projector.projector.sql file + log.debug('Creating new ProjectorDB() instance') + self.projectordb = ProjectorDB() + else: + log.debug('Using existing ProjectorDB() instance') + self.get_settings() + + def bootstrap_post_set_up(self): + """ + Post-initialize setups. + """ + # Set 1.5 second delay before loading all projectors + if self.autostart: + log.debug('Delaying 1.5 seconds before loading all projectors') + QtCore.QTimer().singleShot(1500, self._load_projectors) + else: + log.debug('Loading all projectors') + self._load_projectors() + self.projector_form = ProjectorEditForm(self, projectordb=self.projectordb) + self.projector_form.newProjector.connect(self.add_projector_from_wizard) + self.projector_form.editProjector.connect(self.edit_projector_from_wizard) + self.projector_list_widget.itemSelectionChanged.connect(self.update_icons) + + def get_settings(self): + """ + Retrieve the saved settings + """ + settings = Settings() + settings.beginGroup(self.settings_section) + self.autostart = settings.value('connect on start') + self.poll_time = settings.value('poll time') + self.socket_timeout = settings.value('socket timeout') + self.source_select_dialog_type = settings.value('source dialog type') + settings.endGroup() + del settings + + def context_menu(self, point): + """ + Build the Right Click Context menu and set state. + + :param point: The position of the mouse so the correct item can be found. + """ + # QListWidgetItem to build menu for. + item = self.projector_list_widget.itemAt(point) + if item is None: + return + real_projector = item.data(QtCore.Qt.UserRole) + projector_name = str(item.text()) + visible = real_projector.link.status_connect >= S_CONNECTED + log.debug('(%s) Building menu - visible = %s' % (projector_name, visible)) + self.delete_action.setVisible(True) + self.edit_action.setVisible(True) + self.connect_action.setVisible(not visible) + self.disconnect_action.setVisible(visible) + self.status_action.setVisible(visible) + if visible: + self.select_input_action.setVisible(real_projector.link.power == S_ON) + self.edit_input_action.setVisible(real_projector.link.power == S_ON) + self.poweron_action.setVisible(real_projector.link.power == S_STANDBY) + self.poweroff_action.setVisible(real_projector.link.power == S_ON) + self.blank_action.setVisible(real_projector.link.power == S_ON and + not real_projector.link.shutter) + self.show_action.setVisible(real_projector.link.power == S_ON and + real_projector.link.shutter) + else: + self.select_input_action.setVisible(False) + self.edit_input_action.setVisible(False) + self.poweron_action.setVisible(False) + self.poweroff_action.setVisible(False) + self.blank_action.setVisible(False) + self.show_action.setVisible(False) + self.menu.projector = real_projector + self.menu.exec_(self.projector_list_widget.mapToGlobal(point)) + + def on_edit_input(self, opt=None): + self.on_select_input(opt=opt, edit=True) + + def on_select_input(self, opt=None, edit=False): + """ + Builds menu for 'Select Input' option, then calls the selected projector + item to change input source. + + :param opt: Needed by PyQt4 + """ + self.get_settings() # In case the dialog interface setting was changed + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + projector = list_item.data(QtCore.Qt.UserRole) + # QTabwidget for source select + source = 100 + while source > 99: + if self.source_select_dialog_type == DialogSourceStyle.Tabbed: + source_select_form = SourceSelectTabs(parent=self, + projectordb=self.projectordb, + edit=edit) + else: + source_select_form = SourceSelectSingle(parent=self, + projectordb=self.projectordb, + edit=edit) + source = source_select_form.exec_(projector.link) + log.debug('(%s) source_select_form() returned %s' % (projector.link.ip, source)) + if source is not None and source > 0: + projector.link.set_input_source(str(source)) + return + + def on_add_projector(self, opt=None): + """ + Calls edit dialog to add a new projector to the database + + :param opt: Needed by PyQt4 + """ + self.projector_form.exec_() + + def on_blank_projector(self, opt=None): + """ + Calls projector thread to send blank screen command + + :param opt: Needed by PyQt4 + """ + try: + ip = opt.link.ip + projector = opt + projector.link.set_shutter_closed() + except AttributeError: + for list_item in self.projector_list_widget.selectedItems(): + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + try: + projector.link.set_shutter_closed() + except: + continue + + def on_doubleclick_item(self, item, opt=None): + """ + When item is doubleclicked, will connect to projector. + + :param item: List widget item for connection. + :param opt: Needed by PyQt4 + """ + projector = item.data(QtCore.Qt.UserRole) + if projector.link.state() != projector.link.ConnectedState: + try: + projector.link.connect_to_host() + except: + pass + return + + def on_connect_projector(self, opt=None): + """ + Calls projector thread to connect to projector + + :param opt: Needed by PyQt4 + """ + try: + ip = opt.link.ip + projector = opt + projector.link.connect_to_host() + except AttributeError: + for list_item in self.projector_list_widget.selectedItems(): + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + try: + projector.link.connect_to_host() + except: + continue + + def on_delete_projector(self, opt=None): + """ + Deletes a projector from the list and the database + + :param opt: Needed by PyQt4 + """ + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + msg = QtGui.QMessageBox() + msg.setText('Delete projector (%s) %s?' % (projector.link.ip, projector.link.name)) + msg.setInformativeText('Are you sure you want to delete this projector?') + msg.setStandardButtons(msg.Cancel | msg.Ok) + msg.setDefaultButton(msg.Cancel) + ans = msg.exec_() + if ans == msg.Cancel: + return + try: + projector.link.projectorNetwork.disconnect(self.update_status) + except (AttributeError, TypeError): + pass + try: + projector.link.changeStatus.disconnect(self.update_status) + except (AttributeError, TypeError): + pass + try: + projector.link.authentication_error.disconnect(self.authentication_error) + except (AttributeError, TypeError): + pass + try: + projector.link.no_authentication_error.disconnect(self.no_authentication_error) + except (AttributeError, TypeError): + pass + try: + projector.link.projectorUpdateIcons.disconnect(self.update_icons) + except (AttributeError, TypeError): + pass + try: + projector.timer.stop() + projector.timer.timeout.disconnect(projector.link.poll_loop) + except (AttributeError, TypeError): + pass + try: + projector.socket_timer.stop() + projector.socket_timer.timeout.disconnect(projector.link.socket_abort) + except (AttributeError, TypeError): + pass + projector.thread.quit() + new_list = [] + for item in self.projector_list: + if item.link.dbid == projector.link.dbid: + continue + new_list.append(item) + self.projector_list = new_list + list_item = self.projector_list_widget.takeItem(self.projector_list_widget.currentRow()) + list_item = None + deleted = self.projectordb.delete_projector(projector.db_item) + for item in self.projector_list: + log.debug('New projector list - item: %s %s' % (item.link.ip, item.link.name)) + + def on_disconnect_projector(self, opt=None): + """ + Calls projector thread to disconnect from projector + + :param opt: Needed by PyQt4 + """ + try: + ip = opt.link.ip + projector = opt + projector.link.disconnect_from_host() + except AttributeError: + for list_item in self.projector_list_widget.selectedItems(): + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + try: + projector.link.disconnect_from_host() + except: + continue + + def on_edit_projector(self, opt=None): + """ + Calls edit dialog with selected projector to edit information + + :param opt: Needed by PyQt4 + """ + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + projector = list_item.data(QtCore.Qt.UserRole) + if projector is None: + return + self.old_projector = projector + projector.link.disconnect_from_host() + record = self.projectordb.get_projector_by_ip(projector.link.ip) + self.projector_form.exec_(record) + new_record = self.projectordb.get_projector_by_id(record.id) + + def on_poweroff_projector(self, opt=None): + """ + Calls projector link to send Power Off command + + :param opt: Needed by PyQt4 + """ + try: + ip = opt.link.ip + projector = opt + projector.link.set_power_off() + except AttributeError: + for list_item in self.projector_list_widget.selectedItems(): + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + try: + projector.link.set_power_off() + except: + continue + + def on_poweron_projector(self, opt=None): + """ + Calls projector link to send Power On command + + :param opt: Needed by PyQt4 + """ + try: + ip = opt.link.ip + projector = opt + projector.link.set_power_on() + except AttributeError: + for list_item in self.projector_list_widget.selectedItems(): + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + try: + projector.link.set_power_on() + except: + continue + + def on_show_projector(self, opt=None): + """ + Calls projector thread to send open shutter command + + :param opt: Needed by PyQt4 + """ + try: + ip = opt.link.ip + projector = opt + projector.link.set_shutter_open() + except AttributeError: + for list_item in self.projector_list_widget.selectedItems(): + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + try: + projector.link.set_shutter_open() + except: + continue + + def on_status_projector(self, opt=None): + """ + Builds message box with projector status information + + :param opt: Needed by PyQt4 + """ + lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + projector = lwi.data(QtCore.Qt.UserRole) + message = '%s: %s
' % (translate('OpenLP.ProjectorManager', 'Name'), + projector.link.name) + message = '%s%s: %s
' % (message, translate('OpenLP.ProjectorManager', 'IP'), + projector.link.ip) + message = '%s%s: %s
' % (message, translate('OpenLP.ProjectorManager', 'Port'), + projector.link.port) + message = '%s%s: %s
' % (message, translate('OpenLP.ProjectorManager', 'Notes'), + projector.link.notes) + message = '%s

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

' % (message, translate('OpenLP.ProjectorManager', 'Other info'), + projector.link.other_info) + message = '%s%s: %s
' % (message, translate('OpenLP.ProjectorManager', 'Power status'), + ERROR_MSG[projector.link.power]) + message = '%s%s: %s
' % (message, translate('OpenLP.ProjectorManager', 'Shutter is'), + translate('OpenLP.ProjectorManager', 'Closed') + if projector.link.shutter else translate('OpenLP', 'Open')) + message = '%s%s: %s
' % (message, + translate('OpenLP.ProjectorManager', 'Current source input is'), + projector.link.source) + count = 1 + for item in projector.link.lamp: + message = '%s %s %s (%s) %s: %s
' % (message, + translate('OpenLP.ProjectorManager', 'Lamp'), + count, + translate('OpenLP.ProjectorManager', 'On') + if item['On'] + else translate('OpenLP.ProjectorManager', 'Off'), + translate('OpenLP.ProjectorManager', 'Hours'), + item['Hours']) + count = count + 1 + message = '%s

' % message + if projector.link.projector_errors is None: + message = '%s%s' % (message, translate('OpenLP.ProjectorManager', 'No current errors or warnings')) + else: + message = '%s%s' % (message, translate('OpenLP.ProjectorManager', 'Current errors/warnings')) + for (key, val) in projector.link.projector_errors.items(): + message = '%s%s: %s
' % (message, key, ERROR_MSG[val]) + QtGui.QMessageBox.information(self, translate('OpenLP.ProjectorManager', 'Projector Information'), message) + + def _add_projector(self, projector): + """ + Helper app to build a projector instance + + :param projector: Dict of projector database information + :returns: PJLink1() instance + """ + log.debug('_add_projector()') + return PJLink1(dbid=projector.id, + ip=projector.ip, + port=int(projector.port), + name=projector.name, + location=projector.location, + notes=projector.notes, + pin=None if projector.pin == '' else projector.pin, + poll_time=self.poll_time, + socket_timeout=self.socket_timeout + ) + + def add_projector(self, projector, start=False): + """ + Builds manager list item, projector thread, and timer for projector instance. + + + :param projector: Projector instance to add + :param start: Start projector if True + """ + item = ProjectorItem(link=self._add_projector(projector)) + item.db_item = projector + icon = QtGui.QIcon(QtGui.QPixmap(STATUS_ICONS[S_NOT_CONNECTED])) + item.icon = icon + widget = QtGui.QListWidgetItem(icon, + item.link.name, + self.projector_list_widget + ) + widget.setData(QtCore.Qt.UserRole, item) + item.link.db_item = item.db_item + item.widget = widget + thread = QThread(parent=self) + thread.my_parent = self + item.moveToThread(thread) + thread.started.connect(item.link.thread_started) + thread.finished.connect(item.link.thread_stopped) + thread.finished.connect(thread.deleteLater) + item.link.projectorNetwork.connect(self.update_status) + item.link.changeStatus.connect(self.update_status) + item.link.projectorAuthentication.connect(self.authentication_error) + item.link.projectorNoAuthentication.connect(self.no_authentication_error) + item.link.projectorUpdateIcons.connect(self.update_icons) + timer = QtCore.QTimer(self) + timer.setInterval(self.poll_time) + timer.timeout.connect(item.link.poll_loop) + item.timer = timer + # Timeout in case of brain-dead projectors or cable disconnected + socket_timer = QtCore.QTimer(self) + socket_timer.setInterval(11000) + socket_timer.timeout.connect(item.link.socket_abort) + item.socket_timer = socket_timer + thread.start() + item.thread = thread + item.link.timer = timer + item.link.socket_timer = socket_timer + item.link.widget = item.widget + self.projector_list.append(item) + if start: + item.link.connect_to_host() + for item in self.projector_list: + log.debug('New projector list - item: (%s) %s' % (item.link.ip, item.link.name)) + + @pyqtSlot(str) + def add_projector_from_wizard(self, ip, opts=None): + """ + Add a projector from the edit dialog + + :param ip: IP address of new record item to find + :param opts: Needed by PyQt4 + """ + log.debug('add_projector_from_wizard(ip=%s)' % ip) + item = self.projectordb.get_projector_by_ip(ip) + self.add_projector(item) + + @pyqtSlot(object) + def edit_projector_from_wizard(self, projector): + """ + Update projector from the wizard edit page + + :param projector: Projector() instance of projector with updated information + """ + log.debug('edit_projector_from_wizard(ip=%s)' % projector.ip) + self.old_projector.link.name = projector.name + self.old_projector.link.ip = projector.ip + self.old_projector.link.pin = None if projector.pin == '' else projector.pin + self.old_projector.link.port = projector.port + self.old_projector.link.location = projector.location + self.old_projector.link.notes = projector.notes + self.old_projector.widget.setText(projector.name) + + def _load_projectors(self): + """' + Load projectors - only call when initializing + """ + log.debug('_load_projectors()') + self.projector_list_widget.clear() + for item in self.projectordb.get_projector_all(): + self.add_projector(projector=item, start=self.autostart) + + def get_projector_list(self): + """ + Return the list of active projectors + + :returns: list + """ + return self.projector_list + + @pyqtSlot(str, int, str) + def update_status(self, ip, status=None, msg=None): + """ + Update the status information/icon for selected list item + + :param ip: IP address of projector + :param status: Optional status code + :param msg: Optional status message + """ + if status is None: + return + item = None + for list_item in self.projector_list: + if ip == list_item.link.ip: + item = list_item + break + message = translate('OpenLP.ProjectorManager', 'No message') if msg is None else msg + if status in STATUS_STRING: + status_code = STATUS_STRING[status] + message = ERROR_MSG[status] if msg is None else msg + elif status in ERROR_STRING: + status_code = ERROR_STRING[status] + message = ERROR_MSG[status] if msg is None else msg + else: + status_code = status + message = ERROR_MSG[status] if msg is None else msg + log.debug('(%s) updateStatus(status=%s) message: "%s"' % (item.link.name, status_code, message)) + if status in STATUS_ICONS: + if item.status == status: + return + item.status = status + item.icon = QtGui.QIcon(QtGui.QPixmap(STATUS_ICONS[status])) + if status in ERROR_STRING: + status_code = ERROR_STRING[status] + elif status in STATUS_STRING: + status_code = STATUS_STRING[status] + log.debug('(%s) Updating icon with %s' % (item.link.name, status_code)) + item.widget.setIcon(item.icon) + self.update_icons() + + def get_toolbar_item(self, name, enabled=False, hidden=False): + item = self.one_toolbar.findChild(QtGui.QAction, name) + if item == 0: + log.debug('No item found with name "%s"' % name) + return + item.setVisible(False if hidden else True) + item.setEnabled(True if enabled else False) + + @pyqtSlot() + def update_icons(self): + """ + Update the icons when the selected projectors change + """ + count = len(self.projector_list_widget.selectedItems()) + projector = None + if count == 0: + self.get_toolbar_item('edit_projector') + self.get_toolbar_item('delete_projector') + self.get_toolbar_item('view_projector', hidden=True) + self.get_toolbar_item('source_view_projector', hidden=True) + self.get_toolbar_item('connect_projector') + self.get_toolbar_item('disconnect_projector') + self.get_toolbar_item('poweron_projector') + self.get_toolbar_item('poweroff_projector') + self.get_toolbar_item('blank_projector') + self.get_toolbar_item('show_projector') + self.get_toolbar_item('connect_projector_multiple', hidden=True) + self.get_toolbar_item('disconnect_projector_multiple', hidden=True) + self.get_toolbar_item('poweron_projector_multiple', hidden=True) + self.get_toolbar_item('poweroff_projector_multiple', hidden=True) + self.get_toolbar_item('blank_projector_multiple', hidden=True) + self.get_toolbar_item('show_projector_multiple', hidden=True) + elif count == 1: + projector = self.projector_list_widget.selectedItems()[0].data(QtCore.Qt.UserRole) + connected = projector.link.state() == projector.link.ConnectedState + power = projector.link.power == S_ON + self.get_toolbar_item('connect_projector_multiple', hidden=True) + self.get_toolbar_item('disconnect_projector_multiple', hidden=True) + self.get_toolbar_item('poweron_projector_multiple', hidden=True) + self.get_toolbar_item('poweroff_projector_multiple', hidden=True) + self.get_toolbar_item('blank_projector_multiple', hidden=True) + self.get_toolbar_item('show_projector_multiple', hidden=True) + if connected: + self.get_toolbar_item('view_projector', enabled=True) + self.get_toolbar_item('source_view_projector', + enabled=connected and power and projector.link.source_available is not None) + self.get_toolbar_item('edit_projector', hidden=True) + self.get_toolbar_item('delete_projector', hidden=True) + else: + self.get_toolbar_item('view_projector', hidden=True) + self.get_toolbar_item('source_view_projector', hidden=True) + self.get_toolbar_item('edit_projector', enabled=True) + self.get_toolbar_item('delete_projector', enabled=True) + self.get_toolbar_item('connect_projector', enabled=not connected) + self.get_toolbar_item('disconnect_projector', enabled=connected) + self.get_toolbar_item('poweron_projector', enabled=connected and (projector.link.power == S_STANDBY)) + self.get_toolbar_item('poweroff_projector', enabled=connected and (projector.link.power == S_ON)) + if projector.link.shutter is not None: + self.get_toolbar_item('blank_projector', enabled=(connected and power and not projector.link.shutter)) + self.get_toolbar_item('show_projector', enabled=(connected and power and projector.link.shutter)) + else: + self.get_toolbar_item('blank_projector', enabled=False) + self.get_toolbar_item('show_projector', enabled=False) + else: + self.get_toolbar_item('edit_projector', enabled=False) + self.get_toolbar_item('delete_projector', enabled=False) + self.get_toolbar_item('view_projector', hidden=True) + self.get_toolbar_item('source_view_projector', hidden=True) + self.get_toolbar_item('connect_projector', hidden=True) + self.get_toolbar_item('disconnect_projector', hidden=True) + self.get_toolbar_item('poweron_projector', hidden=True) + self.get_toolbar_item('poweroff_projector', hidden=True) + self.get_toolbar_item('blank_projector', hidden=True) + self.get_toolbar_item('show_projector', hidden=True) + self.get_toolbar_item('connect_projector_multiple', hidden=False, enabled=True) + self.get_toolbar_item('disconnect_projector_multiple', hidden=False, enabled=True) + self.get_toolbar_item('poweron_projector_multiple', hidden=False, enabled=True) + self.get_toolbar_item('poweroff_projector_multiple', hidden=False, enabled=True) + self.get_toolbar_item('blank_projector_multiple', hidden=False, enabled=True) + self.get_toolbar_item('show_projector_multiple', hidden=False, enabled=True) + + @pyqtSlot(str) + def authentication_error(self, name): + """ + Display warning dialog when attempting to connect with invalid pin + + :param name: Name from QListWidgetItem + """ + QtGui.QMessageBox.warning(self, translate('OpenLP.ProjectorManager', + '"%s" Authentication Error' % name), + '
There was an authentication error while trying to connect.' + '

Please verify your PIN setting ' + 'for projector item "%s"' % name) + + @pyqtSlot(str) + def no_authentication_error(self, name): + """ + Display warning dialog when pin saved for item but projector does not + require pin. + + :param name: Name from QListWidgetItem + """ + QtGui.QMessageBox.warning(self, translate('OpenLP.ProjectorManager', + '"%s" No Authentication Error' % name), + '
PIN is set and projector does not require authentication.' + '

Please verify your PIN setting ' + 'for projector item "%s"' % name) + + +class ProjectorItem(QObject): + """ + Class for the projector list widget item. + NOTE: Actual PJLink class instance should be saved as self.link + """ + def __init__(self, link=None): + """ + Initialization for ProjectorItem instance + + :param link: PJLink1 instance for QListWidgetItem + """ + self.link = link + self.thread = None + self.icon = None + self.widget = None + self.my_parent = None + self.timer = None + self.projectordb_item = None + self.poll_time = None + self.socket_timeout = None + self.status = S_NOT_CONNECTED + super(ProjectorItem, self).__init__() + + +def not_implemented(function): + """ + Temporary function to build an information message box indicating function not implemented yet + + :param func: Function name + """ + QtGui.QMessageBox.information(None, + translate('OpenLP.ProjectorManager', 'Not Implemented Yet'), + translate('OpenLP.ProjectorManager', + 'Function "%s"
has not been implemented yet.' + '
Please check back again later.' % function)) diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py new file mode 100644 index 000000000..6e7ab7f76 --- /dev/null +++ b/openlp/core/ui/projector/sourceselectform.py @@ -0,0 +1,500 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" + :mod: `openlp.core.ui.projector.sourceselectform` module + + Provides the dialog window for selecting video source for projector. +""" +import logging +log = logging.getLogger(__name__) +log.debug('editform loaded') + +from PyQt4 import QtCore, QtGui +from PyQt4.QtCore import pyqtSlot, QSize +from PyQt4.QtGui import QDialog, QButtonGroup, QDialogButtonBox, QFormLayout, QLineEdit, QRadioButton, \ + QStyle, QStylePainter, QStyleOptionTab, QTabBar, QTabWidget, QVBoxLayout, QWidget + +from openlp.core.common import translate, is_macosx +from openlp.core.lib import build_icon +from openlp.core.lib.projector.db import ProjectorSource +from openlp.core.lib.projector.constants import PJLINK_DEFAULT_SOURCES, PJLINK_DEFAULT_CODES + + +def source_group(inputs, source_text): + """ + Return a dictionary where key is source[0] and values are inputs + grouped by source[0]. + + source_text = dict{"key1": "key1-text", + "key2": "key2-text", + ...} + return: + dict{ key1[0]: { "key11": "key11-text", + "key12": "key12-text", + "key13": "key13-text", + ... } + key2[0]: {"key21": "key21-text", + "key22": "key22-text", + ... } + + :param inputs: List of inputs + :param source_text: Dictionary of {code: text} values to display + :returns: dict + """ + groupdict = {} + keydict = {} + checklist = inputs + key = checklist[0][0] + for item in checklist: + if item[0] == key: + groupdict[item] = source_text[item] + continue + else: + keydict[key] = groupdict + key = item[0] + groupdict = {item: source_text[item]} + keydict[key] = groupdict + return keydict + + +def Build_Tab(group, source_key, default, projector, projectordb, edit=False): + """ + Create the radio button page for a tab. + Dictionary will be a 1-key entry where key=tab to setup, val=list of inputs. + + source_key: {"groupkey1": {"key11": "key11-text", + "key12": "key12-text", + ... + }, + "groupkey2": {"key21": "key21-text", + "key22": "key22-text", + .... + }, + ... + } + + :param group: Button group widget to add buttons to + :param source_key: Dictionary of sources for radio buttons + :param default: Default radio button to check + :param projector: Projector instance + :param projectordb: ProjectorDB instance for session + :param edit: If we're editing the source text + """ + buttonchecked = False + widget = QWidget() + layout = QFormLayout() if edit else QVBoxLayout() + layout.setSpacing(10) + widget.setLayout(layout) + tempkey = list(source_key.keys())[0] # Should only be 1 key + sourcelist = list(source_key[tempkey]) + sourcelist.sort() + button_count = len(sourcelist) + if edit: + for key in sourcelist: + item = QLineEdit() + item.setObjectName('source_key_%s' % key) + source_item = projectordb.get_source_by_code(code=key, projector_id=projector.db_item.id) + if source_item is None: + item.setText(PJLINK_DEFAULT_CODES[key]) + else: + item.setText(source_item.text) + layout.addRow(PJLINK_DEFAULT_CODES[key], item) + group.append(item) + else: + for key in sourcelist: + source_item = projectordb.get_source_by_code(code=key, projector_id=projector.db_item.id) + if source_item is None: + text = source_key[tempkey][key] + else: + text = source_item.text + itemwidget = QRadioButton(text) + itemwidget.setAutoExclusive(True) + if default == key: + itemwidget.setChecked(True) + buttonchecked = itemwidget.isChecked() or buttonchecked + group.addButton(itemwidget, int(key)) + layout.addWidget(itemwidget) + layout.addStretch() + return widget, button_count, buttonchecked + + +def set_button_tooltip(bar): + """ + Set the toolip for the standard buttons used + + :param bar: QDialogButtonBar instance to update + """ + for button in bar.buttons(): + if bar.standardButton(button) == QDialogButtonBox.Cancel: + tip = "Ignoring current changes and return to OpenLP" + elif bar.standardButton(button) == QDialogButtonBox.Reset: + tip = "Delete all user-defined text and revert to PJLink default text" + elif bar.standardButton(button) == QDialogButtonBox.Discard: + tip = "Discard changes and reset to previous user-defined text" + elif bar.standardButton(button) == QDialogButtonBox.Ok: + tip = "Save changes and return to OpenLP" + else: + tip = "" + button.setToolTip(tip) + + +class FingerTabBarWidget(QTabBar): + """ + Realign west -orientation tabs to left-right text rather than south-north text + Borrowed from + http://www.kidstrythisathome.com/2013/03/fingertabs-horizontal-tabs-with-horizontal-text-in-pyqt/ + """ + def __init__(self, parent=None, *args, **kwargs): + """ + Reset tab text orientation on initialization + + :param width: Remove default width parameter in kwargs + :param height: Remove default height parameter in kwargs + """ + self.tabSize = QSize(kwargs.pop('width', 100), kwargs.pop('height', 25)) + QTabBar.__init__(self, parent, *args, **kwargs) + + def paintEvent(self, event): + """ + Repaint tab in left-right text orientation. + + :param event: Repaint event signal + """ + painter = QStylePainter(self) + option = QStyleOptionTab() + + for index in range(self.count()): + self.initStyleOption(option, index) + tabRect = self.tabRect(index) + tabRect.moveLeft(10) + painter.drawControl(QStyle.CE_TabBarTabShape, option) + painter.drawText(tabRect, QtCore.Qt.AlignVCenter | + QtCore.Qt.TextDontClip, + self.tabText(index)) + painter.end() + + def tabSizeHint(self, index): + """ + Return tabsize + + :param index: Tab index to fetch tabsize from + :returns: instance tabSize + """ + return self.tabSize + + +class FingerTabWidget(QTabWidget): + """ + A QTabWidget equivalent which uses our FingerTabBarWidget + + Based on thread discussion + http://www.riverbankcomputing.com/pipermail/pyqt/2005-December/011724.html + """ + def __init__(self, parent, *args): + """ + Initialize FingerTabWidget instance + """ + QTabWidget.__init__(self, parent, *args) + self.setTabBar(FingerTabBarWidget(self)) + + +class SourceSelectTabs(QDialog): + """ + Class for handling selecting the source for the projector to use. + Uses tabbed interface. + """ + def __init__(self, parent, projectordb, edit=False): + """ + Build the source select dialog using tabbed interface. + + :param projectordb: ProjectorDB session to use + """ + log.debug('Initializing SourceSelectTabs()') + super(SourceSelectTabs, self).__init__(parent) + self.projectordb = projectordb + self.edit = edit + if self.edit: + title = translate('OpenLP.SourceSelectForm', 'Select Projector Source') + else: + title = translate('OpenLP.SourceSelectForm', 'Edit Projector Source Text') + self.setWindowTitle(title) + self.setObjectName('source_select_tabs') + self.setWindowIcon(build_icon(':/icon/openlp-log-32x32.png')) + self.setModal(True) + self.layout = QVBoxLayout() + self.layout.setObjectName('source_select_tabs_layout') + if is_macosx(): + self.tabwidget = QTabWidget(self) + else: + self.tabwidget = FingerTabWidget(self) + self.tabwidget.setObjectName('source_select_tabs_tabwidget') + self.tabwidget.setUsesScrollButtons(False) + if is_macosx(): + self.tabwidget.setTabPosition(QTabWidget.North) + else: + self.tabwidget.setTabPosition(QTabWidget.West) + self.layout.addWidget(self.tabwidget) + self.setLayout(self.layout) + + def exec_(self, projector): + """ + Override initial method so we can build the tabs. + + :param projector: Projector instance to build source list from + """ + self.projector = projector + self.source_text = self.projectordb.get_source_list(projector=projector) + self.source_group = source_group(projector.source_available, self.source_text) + # self.source_group = {'4': {'41': 'Storage 1'}, '5': {"51": 'Network 1'}} + self.button_group = [] if self.edit else QButtonGroup() + keys = list(self.source_group.keys()) + keys.sort() + if self.edit: + for key in keys: + (tab, button_count, buttonchecked) = Build_Tab(group=self.button_group, + source_key={key: self.source_group[key]}, + default=self.projector.source, + projector=self.projector, + projectordb=self.projectordb, + edit=self.edit) + thistab = self.tabwidget.addTab(tab, PJLINK_DEFAULT_SOURCES[key]) + if buttonchecked: + self.tabwidget.setCurrentIndex(thistab) + else: + for key in keys: + (tab, button_count, buttonchecked) = Build_Tab(group=self.button_group, + source_key={key: self.source_group[key]}, + default=self.projector.source, + projector=self.projector, + projectordb=self.projectordb, + edit=self.edit) + thistab = self.tabwidget.addTab(tab, PJLINK_DEFAULT_SOURCES[key]) + if buttonchecked: + self.tabwidget.setCurrentIndex(thistab) + self.button_box = QDialogButtonBox(QtGui.QDialogButtonBox.Reset | + QtGui.QDialogButtonBox.Discard | + QtGui.QDialogButtonBox.Ok | + QtGui.QDialogButtonBox.Cancel) + self.button_box.clicked.connect(self.button_clicked) + self.layout.addWidget(self.button_box) + set_button_tooltip(self.button_box) + selected = super(SourceSelectTabs, self).exec_() + return selected + + @pyqtSlot(object) + def button_clicked(self, button): + """ + Checks which button was clicked + + :param button: Button that was clicked + :returns: Ok: Saves current edits + Delete: Resets text to last-saved text + Reset: Reset all text to PJLink default text + Cancel: Cancel text edit + """ + if self.button_box.standardButton(button) == self.button_box.Cancel: + self.done(0) + elif self.button_box.standardButton(button) == self.button_box.Reset: + self.delete_sources() + elif self.button_box.standardButton(button) == self.button_box.Discard: + self.done(100) + elif self.button_box.standardButton(button) == self.button_box.Ok: + return self.accept_me() + else: + return 100 + + def delete_sources(self): + msg = QtGui.QMessageBox() + msg.setText('Delete entries for this projector') + msg.setInformativeText('Are you sure you want to delete ALL user-defined ' + 'source input text for this projector?') + msg.setStandardButtons(msg.Cancel | msg.Ok) + msg.setDefaultButton(msg.Cancel) + ans = msg.exec_() + if ans == msg.Cancel: + return + self.projectordb.delete_all_objects(ProjectorSource, ProjectorSource.projector_id == self.projector.db_item.id) + self.done(100) + + def accept_me(self): + """ + Slot to accept 'OK' button + """ + projector = self.projector.db_item + if self.edit: + for key in self.button_group: + code = key.objectName().split("_")[-1] + text = key.text().strip() + if key.text().strip().lower() == PJLINK_DEFAULT_CODES[code].strip().lower(): + continue + item = self.projectordb.get_source_by_code(code=code, projector_id=projector.id) + if item is None: + log.debug("(%s) Adding new source text %s: %s" % (projector.ip, code, text)) + item = ProjectorSource(projector_id=projector.id, code=code, text=text) + else: + item.text = text + log.debug('(%s) Updating source code %s with text="%s"' % (projector.ip, item.code, item.text)) + self.projectordb.add_source(item) + selected = 0 + else: + selected = self.button_group.checkedId() + log.debug('SourceSelectTabs().accepted() Setting source to %s' % selected) + self.done(selected) + + +class SourceSelectSingle(QDialog): + """ + Class for handling selecting the source for the projector to use. + Uses single dialog interface. + """ + def __init__(self, parent, projectordb, edit=False): + """ + Build the source select dialog. + + :param projectordb: ProjectorDB session to use + """ + log.debug('Initializing SourceSelectSingle()') + self.projectordb = projectordb + super(SourceSelectSingle, self).__init__(parent) + self.setWindowTitle(translate('OpenLP.SourceSelectSingle', 'Select Projector Source')) + self.setObjectName('source_select_single') + self.setWindowIcon(build_icon(':/icon/openlp-log-32x32.png')) + self.setModal(True) + self.edit = edit + + def exec_(self, projector, edit=False): + """ + Override initial method so we can build the tabs. + + :param projector: Projector instance to build source list from + """ + self.projector = projector + self.layout = QFormLayout() if self.edit else QVBoxLayout() + self.layout.setObjectName('source_select_tabs_layout') + self.layout.setSpacing(10) + self.setLayout(self.layout) + self.setMinimumWidth(350) + self.button_group = [] if self.edit else QButtonGroup() + self.source_text = self.projectordb.get_source_list(projector=projector) + keys = list(self.source_text.keys()) + keys.sort() + key_count = len(keys) + button_list = [] + if self.edit: + for key in keys: + item = QLineEdit() + item.setObjectName('source_key_%s' % key) + source_item = self.projectordb.get_source_by_code(code=key, projector_id=self.projector.db_item.id) + if source_item is None: + item.setText(PJLINK_DEFAULT_CODES[key]) + else: + item.old_text = item.text() + item.setText(source_item.text) + self.layout.addRow(PJLINK_DEFAULT_CODES[key], item) + self.button_group.append(item) + else: + for key in keys: + source_text = self.projectordb.get_source_by_code(code=key, projector_id=self.projector.db_item.id) + text = self.source_text[key] if source_text is None else source_text.text + button = QtGui.QRadioButton(text) + button.setChecked(True if key == projector.source else False) + self.layout.addWidget(button) + self.button_group.addButton(button, int(key)) + button_list.append(key) + self.button_box = QDialogButtonBox(QtGui.QDialogButtonBox.Reset | + QtGui.QDialogButtonBox.Discard | + QtGui.QDialogButtonBox.Ok | + QtGui.QDialogButtonBox.Cancel) + self.button_box.clicked.connect(self.button_clicked) + self.layout.addWidget(self.button_box) + self.setMinimumHeight(key_count*25) + set_button_tooltip(self.button_box) + selected = super(SourceSelectSingle, self).exec_() + return selected + + @pyqtSlot(object) + def button_clicked(self, button): + """ + Checks which button was clicked + + :param button: Button that was clicked + :returns: Ok: Saves current edits + Delete: Resets text to last-saved text + Reset: Reset all text to PJLink default text + Cancel: Cancel text edit + """ + if self.button_box.standardButton(button) == self.button_box.Cancel: + self.done(0) + elif self.button_box.standardButton(button) == self.button_box.Reset: + self.delete_sources() + elif self.button_box.standardButton(button) == self.button_box.Discard: + self.done(100) + elif self.button_box.standardButton(button) == self.button_box.Ok: + return self.accept_me() + else: + return 100 + + def delete_sources(self): + msg = QtGui.QMessageBox() + msg.setText('Delete entries for this projector') + msg.setInformativeText('Are you sure you want to delete ALL user-defined ' + 'source input text for this projector?') + msg.setStandardButtons(msg.Cancel | msg.Ok) + msg.setDefaultButton(msg.Cancel) + ans = msg.exec_() + if ans == msg.Cancel: + return + self.projectordb.delete_all_objects(ProjectorSource, ProjectorSource.projector_id == self.projector.db_item.id) + self.done(100) + + @pyqtSlot() + def accept_me(self): + """ + Slot to accept 'OK' button + """ + projector = self.projector.db_item + if self.edit: + for key in self.button_group: + code = key.objectName().split("_")[-1] + text = key.text().strip() + if key.text().strip().lower() == PJLINK_DEFAULT_CODES[code].strip().lower(): + continue + item = self.projectordb.get_source_by_code(code=code, projector_id=projector.id) + if item is None: + log.debug("(%s) Adding new source text %s: %s" % (projector.ip, code, text)) + item = ProjectorSource(projector_id=projector.id, code=code, text=text) + else: + item.text = text + log.debug('(%s) Updating source code %s with text="%s"' % (projector.ip, item.code, item.text)) + self.projectordb.add_source(item) + selected = 0 + else: + selected = self.button_group.checkedId() + log.debug('SourceSelectDialog().accepted() Setting source to %s' % selected) + self.done(selected) diff --git a/openlp/core/ui/projector/tab.py b/openlp/core/ui/projector/tab.py new file mode 100644 index 000000000..f569ae073 --- /dev/null +++ b/openlp/core/ui/projector/tab.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" + :mod:`openlp.core.ui.projector.tab` + + Provides the settings tab in the settings dialog. +""" + +import logging +log = logging.getLogger(__name__) +log.debug('projectortab module loaded') + +from PyQt4 import QtCore, QtGui + +from openlp.core.common import Settings, UiStrings, translate +from openlp.core.lib import SettingsTab +from openlp.core.lib.projector import DialogSourceStyle + + +class ProjectorTab(SettingsTab): + """ + Openlp Settings -> Projector settings + """ + def __init__(self, parent): + """ + ProjectorTab initialization + + :param parent: Parent widget + """ + self.icon_path = ':/projector/projector_manager.png' + projector_translated = translate('OpenLP.ProjectorTab', 'Projector') + super(ProjectorTab, self).__init__(parent, 'Projector', projector_translated) + + def setupUi(self): + """ + Setup the UI + """ + self.setObjectName('ProjectorTab') + super(ProjectorTab, self).setupUi() + self.connect_box = QtGui.QGroupBox(self.left_column) + self.connect_box.setObjectName('connect_box') + self.connect_box_layout = QtGui.QFormLayout(self.connect_box) + self.connect_box_layout.setObjectName('connect_box_layout') + # Start comms with projectors on startup + self.connect_on_startup = QtGui.QCheckBox(self.connect_box) + self.connect_on_startup.setObjectName('connect_on_startup') + self.connect_box_layout.addRow(self.connect_on_startup) + # Socket timeout + self.socket_timeout_label = QtGui.QLabel(self.connect_box) + self.socket_timeout_label.setObjectName('socket_timeout_label') + self.socket_timeout_spin_box = QtGui.QSpinBox(self.connect_box) + self.socket_timeout_spin_box.setObjectName('socket_timeout_spin_box') + self.socket_timeout_spin_box.setMinimum(2) + self.socket_timeout_spin_box.setMaximum(10) + self.connect_box_layout.addRow(self.socket_timeout_label, self.socket_timeout_spin_box) + # Poll interval + self.socket_poll_label = QtGui.QLabel(self.connect_box) + self.socket_poll_label.setObjectName('socket_poll_label') + self.socket_poll_spin_box = QtGui.QSpinBox(self.connect_box) + self.socket_poll_spin_box.setObjectName('socket_timeout_spin_box') + self.socket_poll_spin_box.setMinimum(5) + self.socket_poll_spin_box.setMaximum(60) + self.connect_box_layout.addRow(self.socket_poll_label, self.socket_poll_spin_box) + self.left_layout.addWidget(self.connect_box) + # Source input select dialog box type + self.dialog_type_label = QtGui.QLabel(self.connect_box) + self.dialog_type_label.setObjectName('dialog_type_label') + self.dialog_type_combo_box = QtGui.QComboBox(self.connect_box) + self.dialog_type_combo_box.setObjectName('dialog_type_combo_box') + self.dialog_type_combo_box.addItems(['', '']) + self.connect_box_layout.addRow(self.dialog_type_label, self.dialog_type_combo_box) + self.left_layout.addStretch() + self.dialog_type_combo_box.activated.connect(self.on_dialog_type_combo_box_changed) + + def retranslateUi(self): + """ + Translate the UI on the fly + """ + self.tab_title_visible = UiStrings().Projectors + self.connect_box.setTitle( + translate('OpenLP.ProjectorTab', 'Communication Options')) + self.connect_on_startup.setText( + translate('OpenLP.ProjectorTab', 'Connect to projectors on startup')) + self.socket_timeout_label.setText( + translate('OpenLP.ProjectorTab', 'Socket timeout (seconds)')) + self.socket_poll_label.setText( + translate('OpenLP.ProjectorTab', 'Poll time (seconds)')) + self.dialog_type_label.setText( + translate('Openlp.ProjectorTab', 'Source select dialog interface')) + self.dialog_type_combo_box.setItemText(DialogSourceStyle.Tabbed, + translate('OpenLP.ProjectorTab', 'Tabbed dialog box')) + self.dialog_type_combo_box.setItemText(DialogSourceStyle.Single, + translate('OpenLP.ProjectorTab', 'Single dialog box')) + + def load(self): + """ + Load the projector settings on startup + """ + settings = Settings() + settings.beginGroup(self.settings_section) + self.connect_on_startup.setChecked(settings.value('connect on start')) + self.socket_timeout_spin_box.setValue(settings.value('socket timeout')) + self.socket_poll_spin_box.setValue(settings.value('poll time')) + self.dialog_type_combo_box.setCurrentIndex(settings.value('source dialog type')) + settings.endGroup() + + def save(self): + """ + Save the projector settings + """ + settings = Settings() + settings.beginGroup(self.settings_section) + settings.setValue('connect on start', self.connect_on_startup.isChecked()) + settings.setValue('socket timeout', self.socket_timeout_spin_box.value()) + settings.setValue('poll time', self.socket_poll_spin_box.value()) + settings.setValue('source dialog type', self.dialog_type_combo_box.currentIndex()) + settings.endGroup + + def on_dialog_type_combo_box_changed(self): + self.dialog_type = self.dialog_type_combo_box.currentIndex() diff --git a/openlp/core/ui/settingsform.py b/openlp/core/ui/settingsform.py index 1499eab89..581f5c18c 100644 --- a/openlp/core/ui/settingsform.py +++ b/openlp/core/ui/settingsform.py @@ -38,6 +38,7 @@ from openlp.core.lib import 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__) @@ -70,6 +71,7 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog, RegistryProperties): self.insert_tab(self.themes_tab) self.insert_tab(self.advanced_tab) self.insert_tab(self.player_tab) + self.insert_tab(self.projector_tab) for plugin in self.plugin_manager.plugins: if plugin.settings_tab: self.insert_tab(plugin.settings_tab, plugin.is_active()) @@ -135,6 +137,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..ba0f10e96 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,34 @@ theme_new.png theme_edit.png + + projector_blank.png + projector_blank_tiled.png + projector_connect.png + projector_connect_tiled.png + projector_hdmi.png + projector_cooldown.png + projector_disconnect.png + projector_disconnect_tiled.png + projector_edit.png + projector_error.png + projector_item_connect.png + projector_item_disconnect.png + projector_manager.png + projector_new.png + projector_not_connected_error.png + projector_off.png + projector_on.png + projector_power_off.png + projector_power_off_tiled.png + projector_power_on.png + projector_power_on_tiled.png + projector_show.png + projector_show_tiled.png + projector_spacer.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_blank_tiled.png b/resources/images/projector_blank_tiled.png new file mode 100644 index 000000000..f70fd8074 Binary files /dev/null and b/resources/images/projector_blank_tiled.png differ diff --git a/resources/images/projector_connect.png b/resources/images/projector_connect.png new file mode 100644 index 000000000..cc3540629 Binary files /dev/null and b/resources/images/projector_connect.png differ diff --git a/resources/images/projector_connect_tiled.png b/resources/images/projector_connect_tiled.png new file mode 100644 index 000000000..0030f6da9 Binary files /dev/null and b/resources/images/projector_connect_tiled.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..3ccf5d570 Binary files /dev/null and b/resources/images/projector_disconnect.png differ diff --git a/resources/images/projector_disconnect_tiled.png b/resources/images/projector_disconnect_tiled.png new file mode 100644 index 000000000..714d1039b Binary files /dev/null and b/resources/images/projector_disconnect_tiled.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_hdmi.png b/resources/images/projector_hdmi.png new file mode 100644 index 000000000..b4a64cb28 Binary files /dev/null and b/resources/images/projector_hdmi.png differ diff --git a/resources/images/projector_item_connect.png b/resources/images/projector_item_connect.png new file mode 100644 index 000000000..6d5b27b5b Binary files /dev/null and b/resources/images/projector_item_connect.png differ diff --git a/resources/images/projector_item_disconnect.png b/resources/images/projector_item_disconnect.png new file mode 100644 index 000000000..2981c5b9e Binary files /dev/null and b/resources/images/projector_item_disconnect.png differ diff --git a/resources/images/projector_manager.png b/resources/images/projector_manager.png new file mode 100644 index 000000000..770fa572e 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..ef83f38d5 Binary files /dev/null and b/resources/images/projector_new.png differ diff --git a/resources/images/projector_not_connected_error.png b/resources/images/projector_not_connected_error.png new file mode 100644 index 000000000..0a170e2d9 Binary files /dev/null and b/resources/images/projector_not_connected_error.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..c2fc02ad1 Binary files /dev/null and b/resources/images/projector_power_off.png differ diff --git a/resources/images/projector_power_off_tiled.png b/resources/images/projector_power_off_tiled.png new file mode 100644 index 000000000..69e4ae934 Binary files /dev/null and b/resources/images/projector_power_off_tiled.png differ diff --git a/resources/images/projector_power_on.png b/resources/images/projector_power_on.png new file mode 100644 index 000000000..cdbfb96b7 Binary files /dev/null and b/resources/images/projector_power_on.png differ diff --git a/resources/images/projector_power_on_tiled.png b/resources/images/projector_power_on_tiled.png new file mode 100644 index 000000000..b38970c60 Binary files /dev/null and b/resources/images/projector_power_on_tiled.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_show_tiled.png b/resources/images/projector_show_tiled.png new file mode 100644 index 000000000..9d7ff8d08 Binary files /dev/null and b/resources/images/projector_show_tiled.png differ diff --git a/resources/images/projector_spacer.png b/resources/images/projector_spacer.png new file mode 100644 index 000000000..ca5a54a04 Binary files /dev/null and b/resources/images/projector_spacer.png differ diff --git a/resources/images/projector_view.png b/resources/images/projector_view.png new file mode 100644 index 000000000..e4d7e971b 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..ba79b59c3 --- /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.projector.db import Projector, ProjectorDB + +from tests.resources.projector.data import TEST1_DATA, TEST2_DATA, TEST3_DATA + +tmpfile = '/tmp/openlp-test-projectordb.sql' + + +def compare_data(one, two): + """ + Verify two Projector() instances contain the same data + """ + return one is not None and \ + two is not None and \ + one.ip == two.ip and \ + one.port == two.port and \ + one.name == two.name and \ + one.location == two.location and \ + one.notes == two.notes + + +def add_records(self, test): + """ + Add record if not in database + """ + record_list = self.projector.get_projector_all() + if len(record_list) < 1: + added = False + for record in test: + added = self.projector.add_projector(record) or added + return added + + for new_record in test: + added = None + for record in record_list: + if compare_data(record, new_record): + break + added = self.projector.add_projector(new_record) + return added + + +class TestProjectorDB(TestCase): + """ + Test case for ProjectorDB + """ + def setUp(self): + """ + Set up anything necessary for all tests + """ + if not hasattr(self, 'projector'): + with patch('openlp.core.lib.projector.db.init_url') as mocked_init_url: + mocked_init_url.start() + mocked_init_url.return_value = 'sqlite:///%s' % tmpfile + self.projector = ProjectorDB() + + def find_record_by_ip_test(self): + """ + Test find record by IP + """ + # GIVEN: Record entries in database + add_records(self, [TEST1_DATA, TEST2_DATA]) + + # WHEN: Search for record using IP + record = self.projector.get_projector_by_ip(TEST2_DATA.ip) + + # THEN: Verify proper record returned + self.assertTrue(compare_data(TEST2_DATA, record), + 'Record found should have been test_2 data') + + def find_record_by_name_test(self): + """ + Test find record by name + """ + # GIVEN: Record entries in database + add_records(self, [TEST1_DATA, TEST2_DATA]) + + # WHEN: Search for record using name + record = self.projector.get_projector_by_name(TEST2_DATA.name) + + # THEN: Verify proper record returned + self.assertTrue(compare_data(TEST2_DATA, record), + 'Record found should have been test_2 data') + + def record_delete_test(self): + """ + Test record can be deleted + """ + # GIVEN: Record in database + add_records(self, [TEST3_DATA, ]) + record = self.projector.get_projector_by_ip(TEST3_DATA.ip) + + # WHEN: Record deleted + self.projector.delete_projector(record) + + # THEN: Verify record not retrievable + found = self.projector.get_projector_by_ip(TEST3_DATA.ip) + self.assertFalse(found, 'test_3 record should have been deleted') + + def record_edit_test(self): + """ + Test edited record returns the same record ID with different data + """ + # GIVEN: Record entries in database + add_records(self, [TEST1_DATA, TEST2_DATA]) + + # WHEN: We retrieve a specific record + record = self.projector.get_projector_by_ip(TEST1_DATA.ip) + record_id = record.id + + # WHEN: Data is changed + record.ip = TEST3_DATA.ip + record.port = TEST3_DATA.port + record.pin = TEST3_DATA.pin + record.name = TEST3_DATA.name + record.location = TEST3_DATA.location + record.notes = TEST3_DATA.notes + updated = self.projector.update_projector(record) + self.assertTrue(updated, 'Save updated record should have returned True') + record = self.projector.get_projector_by_ip(TEST3_DATA.ip) + + # THEN: Record ID should remain the same, but data should be changed + self.assertEqual(record_id, record.id, 'Edited record should have the same ID') + self.assertTrue(compare_data(TEST3_DATA, record), 'Edited record should have new data') diff --git a/tests/interfaces/openlp_core_ui/__init__.py b/tests/interfaces/openlp_core_ui/__init__.py index 6b241e7fc..747685c24 100644 --- a/tests/interfaces/openlp_core_ui/__init__.py +++ b/tests/interfaces/openlp_core_ui/__init__.py @@ -26,3 +26,35 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +""" +Module-level functions for the functional test suite +""" + +import os +from tests.interfaces import patch + +from openlp.core.common import is_win + +from .test_projectormanager import tmpfile + + +def setUp(): + if not is_win(): + # Wine creates a sharing violation during tests. Ignore. + try: + os.remove(tmpfile) + except: + pass + + +def tearDown(): + """ + Ensure test suite has been cleaned up after tests + """ + patch.stopall() + if not is_win(): + try: + # In case of changed schema, remove old test file + os.remove(tmpfile) + except FileNotFoundError: + pass 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..a46b0b93c --- /dev/null +++ b/tests/interfaces/openlp_core_ui/test_projectormanager.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Interface tests to test the themeManager class and related methods. +""" + +import os +from unittest import TestCase + +from openlp.core.common import Registry, Settings +from tests.functional import patch, MagicMock +from tests.helpers.testmixin import TestMixin + +from openlp.core.ui import ProjectorManager, ProjectorEditForm +from openlp.core.lib.projector.db import Projector, ProjectorDB + +from tests.resources.projector.data import TEST1_DATA, TEST2_DATA, TEST3_DATA + +tmpfile = '/tmp/openlp-test-projectormanager.sql' + + +class TestProjectorManager(TestCase, TestMixin): + """ + Test the functions in the ProjectorManager module + """ + def setUp(self): + """ + Create the UI and setup necessary options + """ + self.build_settings() + self.setup_application() + Registry.create() + if not hasattr(self, 'projector_manager'): + with patch('openlp.core.lib.projector.db.init_url') as mocked_init_url: + mocked_init_url.start() + mocked_init_url.return_value = 'sqlite:///%s' % tmpfile + self.projectordb = ProjectorDB() + if not hasattr(self, 'projector_manager'): + self.projector_manager = ProjectorManager(projectordb=self.projectordb) + + def tearDown(self): + """ + Remove test database. + Delete all the C++ objects at the end so that we don't have a segfault. + """ + self.projectordb.session.close() + del self.projector_manager + self.destroy_settings() + + def bootstrap_initialise_test(self): + """ + Test initialize calls correct startup functions + """ + # WHEN: we call bootstrap_initialise + self.projector_manager.bootstrap_initialise() + # THEN: ProjectorDB is setup + self.assertEqual(type(self.projector_manager.projectordb), ProjectorDB, + 'Initialization should have created a ProjectorDB() instance') + + def bootstrap_post_set_up_test(self): + """ + Test post-initialize calls proper setups + """ + # GIVEN: setup mocks + self.projector_manager._load_projectors = MagicMock() + + # WHEN: Call to initialize is run + self.projector_manager.bootstrap_initialise() + self.projector_manager.bootstrap_post_set_up() + + # THEN: verify calls to retrieve saved projectors + self.assertEqual(1, self.projector_manager._load_projectors.call_count, + 'Initialization should have called load_projectors()') + + # THEN: Verify edit page is initialized + self.assertEqual(type(self.projector_manager.projector_form), ProjectorEditForm, + 'Initialization should have created a Projector Edit Form') + self.assertIs(self.projector_manager.projectordb, + self.projector_manager.projector_form.projectordb, + 'ProjectorEditForm should be using same ProjectorDB() instance as ProjectorManager') diff --git a/tests/resources/projector/data.py b/tests/resources/projector/data.py new file mode 100644 index 000000000..37923d5b7 --- /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.projector.db import Projector + +# Test data +TEST1_DATA = Projector(ip='111.111.111.111', + port='1111', + pin='1111', + name='___TEST_ONE___', + location='location one', + notes='notes one') + +TEST2_DATA = Projector(ip='222.222.222.222', + port='2222', + pin='2222', + name='___TEST_TWO___', + location='location two', + notes='notes two') + +TEST3_DATA = Projector(ip='333.333.333.333', + port='3333', + pin='3333', + name='___TEST_THREE___', + location='location three', + notes='notes three')