From 0227e0a2bd1d0df53a2c08fc7270bf0d97e425fc Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 6 Oct 2014 12:10:03 -0700 Subject: [PATCH 001/115] Initial projector code --- .bzrignore | 1 + openlp/core/common/__init__.py | 79 ++ openlp/core/common/registryproperties.py | 9 + openlp/core/common/settings.py | 9 +- openlp/core/common/uistrings.py | 6 + openlp/core/lib/__init__.py | 3 + openlp/core/lib/db.py | 62 +- openlp/core/lib/projector/constants.py | 317 +++++++ openlp/core/lib/projector/db.py | 291 ++++++ openlp/core/lib/projector/pjlink1.py | 668 ++++++++++++++ openlp/core/ui/__init__.py | 6 +- openlp/core/ui/mainwindow.py | 42 +- openlp/core/ui/projector/manager.py | 836 ++++++++++++++++++ openlp/core/ui/projector/tab.py | 96 ++ openlp/core/ui/projector/wizard.py | 551 ++++++++++++ openlp/core/ui/settingsform.py | 10 +- resources/images/openlp-2.qrc | 21 + resources/images/projector_blank.png | Bin 0 -> 385 bytes resources/images/projector_connect.png | Bin 0 -> 1436 bytes resources/images/projector_connectors.png | Bin 0 -> 8853 bytes resources/images/projector_cooldown.png | Bin 0 -> 4487 bytes resources/images/projector_disconnect.png | Bin 0 -> 1267 bytes resources/images/projector_edit.png | Bin 0 -> 726 bytes resources/images/projector_error.png | Bin 0 -> 4310 bytes resources/images/projector_manager.png | Bin 0 -> 7512 bytes resources/images/projector_new.png | Bin 0 -> 720 bytes resources/images/projector_not_connected.png | Bin 0 -> 1160 bytes resources/images/projector_off.png | Bin 0 -> 3749 bytes resources/images/projector_on.png | Bin 0 -> 4266 bytes resources/images/projector_power_off.png | Bin 0 -> 1219 bytes resources/images/projector_power_on.png | Bin 0 -> 1258 bytes resources/images/projector_show.png | Bin 0 -> 744 bytes resources/images/projector_status.png | Bin 0 -> 4255 bytes resources/images/projector_view.png | Bin 0 -> 5930 bytes resources/images/projector_warmup.png | Bin 0 -> 4554 bytes resources/images/wizard_createprojector.png | Bin 0 -> 149097 bytes scripts/generate_resources.sh | 5 +- .../test_projector_utilities.py | 163 ++++ tests/functional/openlp_core_lib/__init__.py | 25 +- .../openlp_core_lib/test_projectordb.py | 160 ++++ tests/interfaces/openlp_core_ui/__init__.py | 25 + .../openlp_core_ui/test_projectormanager.py | 101 +++ tests/resources/projector/data.py | 55 ++ 43 files changed, 3511 insertions(+), 30 deletions(-) create mode 100644 openlp/core/lib/projector/constants.py create mode 100644 openlp/core/lib/projector/db.py create mode 100644 openlp/core/lib/projector/pjlink1.py create mode 100644 openlp/core/ui/projector/manager.py create mode 100644 openlp/core/ui/projector/tab.py create mode 100644 openlp/core/ui/projector/wizard.py create mode 100644 resources/images/projector_blank.png create mode 100644 resources/images/projector_connect.png create mode 100644 resources/images/projector_connectors.png create mode 100644 resources/images/projector_cooldown.png create mode 100644 resources/images/projector_disconnect.png create mode 100644 resources/images/projector_edit.png create mode 100644 resources/images/projector_error.png create mode 100644 resources/images/projector_manager.png create mode 100644 resources/images/projector_new.png create mode 100644 resources/images/projector_not_connected.png create mode 100644 resources/images/projector_off.png create mode 100644 resources/images/projector_on.png create mode 100644 resources/images/projector_power_off.png create mode 100644 resources/images/projector_power_on.png create mode 100644 resources/images/projector_show.png create mode 100644 resources/images/projector_status.png create mode 100644 resources/images/projector_view.png create mode 100644 resources/images/projector_warmup.png create mode 100644 resources/images/wizard_createprojector.png create mode 100644 tests/functional/openlp_core_common/test_projector_utilities.py create mode 100644 tests/functional/openlp_core_lib/test_projectordb.py create mode 100644 tests/interfaces/openlp_core_ui/test_projectormanager.py create mode 100644 tests/resources/projector/data.py diff --git a/.bzrignore b/.bzrignore index a0c3f0b4f..390fde6af 100644 --- a/.bzrignore +++ b/.bzrignore @@ -33,3 +33,4 @@ tests.kdev4 __pycache__ *.dll .directory +*.kate-swp diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index 0776547ae..47023a588 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -30,13 +30,17 @@ The :mod:`common` module contains most of the components and libraries that make OpenLP work. """ +import hashlib import re import os import logging import sys import traceback +from ipaddress import IPv4Address, IPv6Address, AddressValueError +from codecs import decode, encode from PyQt4 import QtCore +from PyQt4.QtCore import QCryptographicHash as QHash log = logging.getLogger(__name__ + '.__init__') @@ -154,6 +158,81 @@ def is_linux(): """ return sys.platform.startswith('linux') + +def verify_ipv4(addr): + """ + Validate an IPv4 address + + :param addr: Address to validate + :returns: bool + """ + try: + valid = IPv4Address(addr) + return True + except AddressValueError: + return False + + +def verify_ipv6(addr): + """ + Validate an IPv6 address + + :param addr: Address to validate + :returns: bool + """ + try: + valid = IPv6Address(addr) + return True + except AddressValueError: + return False + + +def verify_ip_address(addr): + """ + Validate an IP address as either IPv4 or IPv6 + + :param addr: Address to validate + :returns: bool + """ + return True if verify_ipv4(addr) else verify_ipv6(addr) + + +def md5_hash(salt, data): + """ + Returns the hashed output of md5sum on salt,data + using Python3 hashlib + + :param salt: Initial salt + :param data: Data to hash + :returns: str + """ + log.debug('md5_hash(salt="%s")' % salt) + hash_obj = hashlib.new('md5') + hash_obj.update(salt.encode('ascii')) + hash_obj.update(data.encode('ascii')) + hash_value = hash_obj.hexdigest() + log.debug('md5_hash() returning "%s"' % hash_value) + return hash_value + + +def qmd5_hash(salt, data): + """ + Returns the hashed output of MD%Sum on salt, data + using PyQt4.QCryptograhicHash. + + :param salt: Initial salt + :param data: Data to hash + :returns: str + """ + log.debug('qmd5_hash(salt="%s"' % salt) + hash_obj = QHash(QHash.Md5) + hash_obj.addData(salt) + hash_obj.addData(data) + hash_value = hash_obj.result().toHex() + log.debug('qmd5_hash() returning "%s"' % hash_value) + return decode(hash_value.data(), 'ascii') + + from .openlpmixin import OpenLPMixin from .registry import Registry from .registrymixin import RegistryMixin diff --git a/openlp/core/common/registryproperties.py b/openlp/core/common/registryproperties.py index e2cfffa09..145f86b0a 100644 --- a/openlp/core/common/registryproperties.py +++ b/openlp/core/common/registryproperties.py @@ -148,3 +148,12 @@ class RegistryProperties(object): if not hasattr(self, '_alerts_manager') or not self._alerts_manager: self._alerts_manager = Registry().get('alerts_manager') return self._alerts_manager + + @property + def projector_manager(self): + """ + Adds the projector manager to the class dynamically + """ + if not hasattr(self, '_projector_manager') or not self._projector_manager: + self._projector_manager = Registry().get('projector_manager') + return self._projector_manager diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index f7202b590..db925c77f 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -275,6 +275,7 @@ class Settings(QtCore.QSettings): 'shortcuts/toolsAddToolItem': [], 'shortcuts/updateThemeImages': [], 'shortcuts/up': [QtGui.QKeySequence(QtCore.Qt.Key_Up)], + 'shortcuts/viewProjectorManagerItem': [QtGui.QKeySequence('F6')], 'shortcuts/viewThemeManagerItem': [QtGui.QKeySequence('F10')], 'shortcuts/viewMediaManagerItem': [QtGui.QKeySequence('F8')], 'shortcuts/viewPreviewPanel': [QtGui.QKeySequence('F11')], @@ -295,7 +296,13 @@ class Settings(QtCore.QSettings): 'user interface/main window splitter geometry': QtCore.QByteArray(), 'user interface/main window state': QtCore.QByteArray(), 'user interface/preview panel': True, - 'user interface/preview splitter geometry': QtCore.QByteArray() + 'user interface/preview splitter geometry': QtCore.QByteArray(), + 'projector/db type': 'sqlite', + 'projector/enable': True, + 'projector/connect on start': False, + 'projector/last directory import': '', + 'projector/last directory export': '', + 'projector/query time': 20 # PJLink socket timeout is 30 seconds } __file_path__ = '' __obsolete_settings__ = [ diff --git a/openlp/core/common/uistrings.py b/openlp/core/common/uistrings.py index 3fe1485ba..c1e7ebb8b 100644 --- a/openlp/core/common/uistrings.py +++ b/openlp/core/common/uistrings.py @@ -99,6 +99,10 @@ class UiStrings(object): self.LiveBGError = translate('OpenLP.Ui', 'Live Background Error') self.LiveToolbar = translate('OpenLP.Ui', 'Live Toolbar') self.Load = translate('OpenLP.Ui', 'Load') + self.Manufacturer = translate('OpenLP.Ui', 'Manufacturer', 'Singluar') + self.Manufacturers = translate('OpenLP.Ui', 'Manufacturers', 'Plural') + self.Model = translate('OpenLP.Ui', 'Model', 'Singluar') + self.Models = translate('OpenLP.Ui', 'Models', 'Plural') self.Minutes = translate('OpenLP.Ui', 'm', 'The abbreviated unit for minutes') self.Middle = translate('OpenLP.Ui', 'Middle') self.New = translate('OpenLP.Ui', 'New') @@ -118,6 +122,8 @@ class UiStrings(object): self.PlaySlidesToEnd = translate('OpenLP.Ui', 'Play Slides to End') self.Preview = translate('OpenLP.Ui', 'Preview') self.PrintService = translate('OpenLP.Ui', 'Print Service') + self.Projector = translate('OpenLP.Ui', 'Projector', 'Singluar') + self.Projectors = translate('OpenLP.Ui', 'Projectors', 'Plural') self.ReplaceBG = translate('OpenLP.Ui', 'Replace Background') self.ReplaceLiveBG = translate('OpenLP.Ui', 'Replace live background.') self.ResetBG = translate('OpenLP.Ui', 'Reset Background') diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index b85070445..dd9eb8978 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -334,3 +334,6 @@ from .dockwidget import OpenLPDockWidget from .imagemanager import ImageManager from .renderer import Renderer from .mediamanageritem import MediaManagerItem +from .projector.db import ProjectorDB, Projector +from .projector.pjlink1 import PJLink1 +from .projector.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING diff --git a/openlp/core/lib/db.py b/openlp/core/lib/db.py index 8e9380241..c458e0f6e 100644 --- a/openlp/core/lib/db.py +++ b/openlp/core/lib/db.py @@ -48,20 +48,53 @@ from openlp.core.utils import delete_file log = logging.getLogger(__name__) -def init_db(url, auto_flush=True, auto_commit=False): +def init_db(url, auto_flush=True, auto_commit=False, base=None): """ Initialise and return the session and metadata for a database :param url: The database to initialise connection with :param auto_flush: Sets the flushing behaviour of the session :param auto_commit: Sets the commit behaviour of the session + :param base: If using declarative, the base class to bind with """ engine = create_engine(url, poolclass=NullPool) - metadata = MetaData(bind=engine) + if base is None: + metadata = MetaData(bind=engine) + else: + base.metadata.bind = engine + metadata = None session = scoped_session(sessionmaker(autoflush=auto_flush, autocommit=auto_commit, bind=engine)) return session, metadata +def init_url(plugin_name, db_file_name=None): + """ + Return the database URL. + + :param plugin_name: The name of the plugin for the database creation. + :param db_file_name: The database file name. Defaults to None resulting in the plugin_name being used. + """ + settings = Settings() + settings.beginGroup(plugin_name) + db_url = '' + db_type = settings.value('db type') + if db_type == 'sqlite': + if db_file_name is None: + db_url = 'sqlite:///%s/%s.sqlite' % (AppLocation.get_section_data_path(plugin_name), plugin_name) + else: + db_url = 'sqlite:///%s/%s' % (AppLocation.get_section_data_path(plugin_name), db_file_name) + else: + db_url = '%s://%s:%s@%s/%s' % (db_type, urlquote(settings.value('db username')), + urlquote(settings.value('db password')), + urlquote(settings.value('db hostname')), + urlquote(settings.value('db database'))) + if db_type == 'mysql': + db_encoding = settings.value('db encoding') + db_url += '?charset=%s' % urlquote(db_encoding) + settings.endGroup() + return db_url + + def get_upgrade_op(session): """ Create a migration context and an operations object for performing upgrades. @@ -159,7 +192,7 @@ class Manager(object): """ Provide generic object persistence management """ - def __init__(self, plugin_name, init_schema, db_file_name=None, upgrade_mod=None): + def __init__(self, plugin_name, init_schema, db_file_name=None, upgrade_mod=None, session=None): """ Runs the initialisation process that includes creating the connection to the database and the tables if they do not exist. @@ -170,26 +203,15 @@ class Manager(object): :param upgrade_mod: The file name to use for this database. Defaults to None resulting in the plugin_name being used. """ - settings = Settings() - settings.beginGroup(plugin_name) - self.db_url = '' self.is_dirty = False self.session = None - db_type = settings.value('db type') - if db_type == 'sqlite': - if db_file_name: - self.db_url = 'sqlite:///%s/%s' % (AppLocation.get_section_data_path(plugin_name), db_file_name) - else: - self.db_url = 'sqlite:///%s/%s.sqlite' % (AppLocation.get_section_data_path(plugin_name), plugin_name) + # See if we're using declarative_base with a pre-existing session. + log.debug('Manager: Testing for pre-existing session') + if session is not None: + log.debug('Manager: Using existing session') else: - self.db_url = '%s://%s:%s@%s/%s' % (db_type, urlquote(settings.value('db username')), - urlquote(settings.value('db password')), - urlquote(settings.value('db hostname')), - urlquote(settings.value('db database'))) - if db_type == 'mysql': - db_encoding = settings.value('db encoding') - self.db_url += '?charset=%s' % urlquote(db_encoding) - settings.endGroup() + log.debug('Manager: Creating new session') + self.db_url = init_url(plugin_name, db_file_name) if upgrade_mod: try: db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod) diff --git a/openlp/core/lib/projector/constants.py b/openlp/core/lib/projector/constants.py new file mode 100644 index 000000000..eeb969987 --- /dev/null +++ b/openlp/core/lib/projector/constants.py @@ -0,0 +1,317 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`projector` module +""" + +import logging +log = logging.getLogger(__name__) +log.debug('projector_constants loaded') + +from openlp.core.common import translate + + +__all__ = ['S_OK', 'E_GENERAL', 'E_NOT_CONNECTED', 'E_FAN', 'E_LAMP', 'E_TEMP', + 'E_COVER', 'E_FILTER', 'E_AUTHENTICATION', 'E_NO_AUTHENTICATION', + 'E_UNDEFINED', 'E_PARAMETER', 'E_UNAVAILABLE', 'E_PROJECTOR', + 'E_INVALID_DATA', 'E_WARN', 'E_ERROR', 'E_CLASS', 'E_PREFIX', + 'E_CONNECTION_REFUSED', 'E_REMOTE_HOST_CLOSED_CONNECTION', 'E_HOST_NOT_FOUND', + 'E_SOCKET_ACCESS', 'E_SOCKET_RESOURCE', 'E_SOCKET_TIMEOUT', 'E_DATAGRAM_TOO_LARGE', + 'E_NETWORK', 'E_ADDRESS_IN_USE', 'E_SOCKET_ADDRESS_NOT_AVAILABLE', + 'E_UNSUPPORTED_SOCKET_OPERATION', 'E_PROXY_AUTHENTICATION_REQUIRED', + 'E_SLS_HANDSHAKE_FAILED', 'E_UNFINISHED_SOCKET_OPERATION', 'E_PROXY_CONNECTION_REFUSED', + 'E_PROXY_CONNECTION_CLOSED', 'E_PROXY_CONNECTION_TIMEOUT', 'E_PROXY_NOT_FOUND', + 'E_PROXY_PROTOCOL', 'E_UNKNOWN_SOCKET_ERROR', + 'S_NOT_CONNECTED', 'S_CONNECTING', 'S_CONNECTED', + 'S_STATUS', 'S_OFF', 'S_INITIALIZE', 'S_STANDBY', 'S_WARMUP', 'S_ON', 'S_COOLDOWN', + 'S_INFO', 'S_NETWORK_SENDING', 'S_NETWORK_RECEIVED', + 'ERROR_STRING', 'CR', 'LF', 'PJLINK_ERST_STATUS', 'PJLINK_POWR_STATUS', + 'PJLINK_PORT', 'PJLINK_MAX_PACKET', 'TIMEOUT', 'ERROR_MSG', 'PJLINK_ERRORS', + 'STATUS_STRING', 'PJLINK_VALID_CMD', 'CONNECTION_ERRORS'] + +# Set common constants. +CR = chr(0x0D) # \r +LF = chr(0x0A) # \n +PJLINK_PORT = 4352 +TIMEOUT = 30.0 +PJLINK_MAX_PACKET = 136 +PJLINK_VALID_CMD = {'1': ['POWR', # Power option + 'INPT', # Video sources option + 'AVMT', # Shutter option + 'ERST', # Error status option + 'LAMP', # Lamp(s) query (Includes fans) + 'INST', # Input sources available query + 'NAME', # Projector name query + 'INF1', # Manufacturer name query + 'INF2', # Projuct name query + 'INFO', # Other information query + 'CLSS' # PJLink class support query + ]} + +# Error and status codes +S_OK = E_OK = 0 # E_OK included since I sometimes forget +# Error codes. Start at 200 so we don't duplicate system error codes. +E_GENERAL = 200 # Unknown error +E_NOT_CONNECTED = 201 +E_FAN = 202 +E_LAMP = 203 +E_TEMP = 204 +E_COVER = 205 +E_FILTER = 206 +E_NO_AUTHENTICATION = 207 # PIN set and no authentication set on projector +E_UNDEFINED = 208 # ERR1 +E_PARAMETER = 209 # ERR2 +E_UNAVAILABLE = 210 # ERR3 +E_PROJECTOR = 211 # ERR4 +E_INVALID_DATA = 212 +E_WARN = 213 +E_ERROR = 214 +E_AUTHENTICATION = 215 # ERRA +E_CLASS = 216 +E_PREFIX = 217 + +# Remap Qt socket error codes to projector error codes +E_CONNECTION_REFUSED = 230 +E_REMOTE_HOST_CLOSED_CONNECTION = 231 +E_HOST_NOT_FOUND = 232 +E_SOCKET_ACCESS = 233 +E_SOCKET_RESOURCE = 234 +E_SOCKET_TIMEOUT = 235 +E_DATAGRAM_TOO_LARGE = 236 +E_NETWORK = 237 +E_ADDRESS_IN_USE = 238 +E_SOCKET_ADDRESS_NOT_AVAILABLE = 239 +E_UNSUPPORTED_SOCKET_OPERATION = 240 +E_PROXY_AUTHENTICATION_REQUIRED = 241 +E_SLS_HANDSHAKE_FAILED = 242 +E_UNFINISHED_SOCKET_OPERATION = 243 +E_PROXY_CONNECTION_REFUSED = 244 +E_PROXY_CONNECTION_CLOSED = 245 +E_PROXY_CONNECTION_TIMEOUT = 246 +E_PROXY_NOT_FOUND = 247 +E_PROXY_PROTOCOL = 248 +E_UNKNOWN_SOCKET_ERROR = -1 + +# Status codes start at 300 +S_NOT_CONNECTED = 300 +S_CONNECTING = 301 +S_CONNECTED = 302 +S_INITIALIZE = 303 +S_STATUS = 304 +S_OFF = 305 +S_STANDBY = 306 +S_WARMUP = 307 +S_ON = 308 +S_COOLDOWN = 309 +S_INFO = 310 + +# Information that does not affect status +S_NETWORK_SENDING = 400 +S_NETWORK_RECEIVED = 401 + +CONNECTION_ERRORS = {E_NOT_CONNECTED, E_NO_AUTHENTICATION, E_AUTHENTICATION, E_CLASS, + E_PREFIX, E_CONNECTION_REFUSED, E_REMOTE_HOST_CLOSED_CONNECTION, + E_HOST_NOT_FOUND, E_SOCKET_ACCESS, E_SOCKET_RESOURCE, E_SOCKET_TIMEOUT, + E_DATAGRAM_TOO_LARGE, E_NETWORK, E_ADDRESS_IN_USE, E_SOCKET_ADDRESS_NOT_AVAILABLE, + E_UNSUPPORTED_SOCKET_OPERATION, E_PROXY_AUTHENTICATION_REQUIRED, + E_SLS_HANDSHAKE_FAILED, E_UNFINISHED_SOCKET_OPERATION, E_PROXY_CONNECTION_REFUSED, + E_PROXY_CONNECTION_CLOSED, E_PROXY_CONNECTION_TIMEOUT, E_PROXY_NOT_FOUND, + E_PROXY_PROTOCOL, E_UNKNOWN_SOCKET_ERROR + } + +PJLINK_ERRORS = {'ERRA': E_AUTHENTICATION, # Authentication error + 'ERR1': E_UNDEFINED, # Undefined command error + 'ERR2': E_PARAMETER, # Invalid parameter error + 'ERR3': E_UNAVAILABLE, # Projector busy + 'ERR4': E_PROJECTOR, # Projector or display failure + E_AUTHENTICATION: translate('OpenLP.ProjectorConstants', 'ERRA'), + E_UNDEFINED: translate('OpenLP.ProjectorConstants', 'ERR1'), + E_PARAMETER: translate('OpenLP.ProjectorConstants', 'ERR2'), + E_UNAVAILABLE: translate('OpenLP.ProjectorConstants', 'ERR3'), + E_PROJECTOR: translate('OpenLP.ProjectorConstants', 'ERR4')} + +# Map error/status codes to string +ERROR_STRING = {0: translate('OpenLP.ProjectorConstants', 'S_OK'), + E_GENERAL: translate('OpenLP.ProjectorConstants', 'E_GENERAL'), + E_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'E_NOT_CONNECTED'), + E_FAN: translate('OpenLP.ProjectorConstants', 'E_FAN'), + E_LAMP: translate('OpenLP.ProjectorConstants', 'E_LAMP'), + E_TEMP: translate('OpenLP.ProjectorConstants', 'E_TEMP'), + E_COVER: translate('OpenLP.ProjectorConstants', 'E_COVER'), + E_FILTER: translate('OpenLP.ProjectorConstants', 'E_FILTER'), + E_AUTHENTICATION: translate('OpenLP.ProjectorConstants', 'E_AUTHENTICATION'), + E_NO_AUTHENTICATION: translate('OpenLP.ProjectorConstants', 'E_NO_AUTHENTICATION'), + E_UNDEFINED: translate('OpenLP.ProjectorConstants', 'E_UNDEFINED'), + E_PARAMETER: translate('OpenLP.ProjectorConstants', 'E_PARAMETER'), + E_UNAVAILABLE: translate('OpenLP.ProjectorConstants', 'E_UNAVAILABLE'), + E_PROJECTOR: translate('OpenLP.ProjectorConstants', 'E_PROJECTOR'), + E_INVALID_DATA: translate('OpenLP.ProjectorConstants', 'E_INVALID_DATA'), + E_WARN: translate('OpenLP.ProjectorConstants', 'E_WARN'), + E_ERROR: translate('OpenLP.ProjectorConstants', 'E_ERROR'), + E_CLASS: translate('OpenLP.ProjectorConstants', 'E_CLASS'), + E_PREFIX: translate('OpenLP.ProjectorConstants', 'E_PREFIX'), # Last projector error + E_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants', + 'E_CONNECTION_REFUSED'), # First QtSocket error + E_REMOTE_HOST_CLOSED_CONNECTION: translate('OpenLP.ProjectorConstants', + 'E_REMOTE_HOST_CLOSED_CONNECTION'), + E_HOST_NOT_FOUND: translate('OpenLP.ProjectorConstants', 'E_HOST_NOT_FOUND'), + E_SOCKET_ACCESS: translate('OpenLP.ProjectorConstants', 'E_SOCKET_ACCESS'), + E_SOCKET_RESOURCE: translate('OpenLP.ProjectorConstants', 'E_SOCKET_RESOURCE'), + E_SOCKET_TIMEOUT: translate('OpenLP.ProjectorConstants', 'E_SOCKET_TIMEOUT'), + E_DATAGRAM_TOO_LARGE: translate('OpenLP.ProjectorConstants', 'E_DATAGRAM_TOO_LARGE'), + E_NETWORK: translate('OpenLP.ProjectorConstants', 'E_NETWORK'), + E_ADDRESS_IN_USE: translate('OpenLP.ProjectorConstants', 'E_ADDRESS_IN_USE'), + E_SOCKET_ADDRESS_NOT_AVAILABLE: translate('OpenLP.ProjectorConstants', + 'E_SOCKET_ADDRESS_NOT_AVAILABLE'), + E_UNSUPPORTED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants', + 'E_UNSUPPORTED_SOCKET_OPERATION'), + E_PROXY_AUTHENTICATION_REQUIRED: translate('OpenLP.ProjectorConstants', + 'E_PROXY_AUTHENTICATION_REQUIRED'), + E_SLS_HANDSHAKE_FAILED: translate('OpenLP.ProjectorConstants', 'E_SLS_HANDSHAKE_FAILED'), + E_UNFINISHED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants', + 'E_UNFINISHED_SOCKET_OPERATION'), + E_PROXY_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants', 'E_PROXY_CONNECTION_REFUSED'), + E_PROXY_CONNECTION_CLOSED: translate('OpenLP.ProjectorConstants', 'E_PROXY_CONNECTION_CLOSED'), + E_PROXY_CONNECTION_TIMEOUT: translate('OpenLP.ProjectorConstants', 'E_PROXY_CONNECTION_TIMEOUT'), + E_PROXY_NOT_FOUND: translate('OpenLP.ProjectorConstants', 'E_PROXY_NOT_FOUND'), + E_PROXY_PROTOCOL: translate('OpenLP.ProjectorConstants', 'E_PROXY_PROTOCOL'), + E_UNKNOWN_SOCKET_ERROR: translate('OpenLP.ProjectorConstants', 'E_UNKNOWN_SOCKET_ERROR')} + +STATUS_STRING = {S_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'S_NOT_CONNECTED'), + S_CONNECTING: translate('OpenLP.ProjectorConstants', 'S_CONNECTING'), + S_CONNECTED: translate('OpenLP.ProjectorConstants', 'S_CONNECTED'), + S_STATUS: translate('OpenLP.ProjectorConstants', 'S_STATUS'), + S_OFF: translate('OpenLP.ProjectorConstants', 'S_OFF'), + S_INITIALIZE: translate('OpenLP.ProjectorConstants', 'S_INITIALIZE'), + S_STANDBY: translate('OpenLP.ProjectorConstants', 'S_STANDBY'), + S_WARMUP: translate('OpenLP.ProjectorConstants', 'S_WARMUP'), + S_ON: translate('OpenLP.ProjectorConstants', 'S_ON'), + S_COOLDOWN: translate('OpenLP.ProjectorConstants', 'S_COOLDOWN'), + S_INFO: translate('OpenLP.ProjectorConstants', 'S_INFO'), + S_NETWORK_SENDING: translate('OpenLP.ProjectorConstants', 'S_NETWORK_SENDING'), + S_NETWORK_RECEIVED: translate('OpenLP.ProjectorConstants', 'S_NETWORK_RECEIVED')} + +# Map error/status codes to message strings +ERROR_MSG = {E_OK: translate('OpenLP.ProjectorConstants', 'OK'), # E_OK | S_OK + E_GENERAL: translate('OpenLP.ProjectorConstants', 'General projector error'), + E_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected error'), + E_NETWORK: translate('OpenLP.ProjectorConstants', 'Network error'), + E_LAMP: translate('OpenLP.ProjectorConstants', 'Lamp error'), + E_FAN: translate('OpenLP.ProjectorConstants', 'Fan error'), + E_TEMP: translate('OpenLP.ProjectorConstants', 'High temperature detected'), + E_COVER: translate('OpenLP.ProjectorConstants', 'Cover open detected'), + E_FILTER: translate('OpenLP.ProjectorConstants', 'Check filter'), + E_AUTHENTICATION: translate('OpenLP.ProjectorConstants', 'Authentication Error'), + E_UNDEFINED: translate('OpenLP.ProjectorConstants', 'Undefined Command'), + E_PARAMETER: translate('OpenLP.ProjectorConstants', 'Invalid Parameter'), + E_UNAVAILABLE: translate('OpenLP.ProjectorConstants', 'Projector Busy'), + E_PROJECTOR: translate('OpenLP.ProjectorConstants', 'Projector/Display Error'), + E_INVALID_DATA: translate('OpenLP.ProjectorConstants', 'Invald packet received'), + E_WARN: translate('OpenLP.ProjectorConstants', 'Warning condition detected'), + E_ERROR: translate('OpenLP.ProjectorConstants', 'Error condition detected'), + E_CLASS: translate('OpenLP.ProjectorConstants', 'PJLink class not supported'), + E_PREFIX: translate('OpenLP.ProjectorConstants', 'Invalid prefix character'), + E_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants', + 'The connection was refused by the peer (or timed out)'), + E_REMOTE_HOST_CLOSED_CONNECTION: translate('OpenLP.ProjectorConstants', + 'The remote host closed the connection'), + E_HOST_NOT_FOUND: translate('OpenLP.ProjectorConstants', 'The host address was not found'), + E_SOCKET_ACCESS: translate('OpenLP.ProjectorConstants', + 'The socket operation failed because the application ' + 'lacked the required privileges'), + E_SOCKET_RESOURCE: translate('OpenLP.ProjectorConstants', + 'The local system ran out of resources (e.g., too many sockets)'), + E_SOCKET_TIMEOUT: translate('OpenLP.ProjectorConstants', + 'The socket operation timed out'), + E_DATAGRAM_TOO_LARGE: translate('OpenLP.ProjectorConstants', + 'The datagram was larger than the operating system\'s limit'), + E_NETWORK: translate('OpenLP.ProjectorConstants', + 'An error occurred with the network (Possibly someone pulled the plug?)'), + E_ADDRESS_IN_USE: translate('OpenLP.ProjectorConstants', + 'The address specified with socket.bind() ' + 'is already in use and was set to be exclusive'), + E_SOCKET_ADDRESS_NOT_AVAILABLE: translate('OpenLP.ProjectorConstants', + 'The address specified to socket.bind() ' + 'does not belong to the host'), + E_UNSUPPORTED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants', + 'The requested socket operation is not supported by the local ' + 'operating system (e.g., lack of IPv6 support)'), + E_PROXY_AUTHENTICATION_REQUIRED: translate('OpenLP.ProjectorConstants', + 'The socket is using a proxy, ' + 'and the proxy requires authentication'), + E_SLS_HANDSHAKE_FAILED: translate('OpenLP.ProjectorConstants', + 'The SSL/TLS handshake failed'), + E_UNFINISHED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants', + 'The last operation attempted has not finished yet ' + '(still in progress in the background)'), + E_PROXY_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants', + 'Could not contact the proxy server because the connection ' + 'to that server was denied'), + E_PROXY_CONNECTION_CLOSED: translate('OpenLP.ProjectorConstants', + 'The connection to the proxy server was closed unexpectedly ' + '(before the connection to the final peer was established)'), + E_PROXY_CONNECTION_TIMEOUT: translate('OpenLP.ProjectorConstants', + 'The connection to the proxy server timed out or the proxy ' + 'server stopped responding in the authentication phase.'), + E_PROXY_NOT_FOUND: translate('OpenLP.ProjectorConstants', + 'The proxy address set with setProxy() was not found'), + E_PROXY_PROTOCOL: translate('OpenLP.ProjectorConstants', + 'The connection negotiation with the proxy server because the response ' + 'from the proxy server could not be understood'), + E_UNKNOWN_SOCKET_ERROR: translate('OpenLP.ProjectorConstants', 'An unidentified error occurred'), + S_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected'), + S_CONNECTING: translate('OpenLP.ProjectorConstants', 'Connecting'), + S_CONNECTED: translate('OpenLP.ProjectorConstants', 'Connected'), + S_STATUS: translate('OpenLP.ProjectorConstants', 'Getting status'), + S_OFF: translate('OpenLP.ProjectorConstants', 'Off'), + S_INITIALIZE: translate('OpenLP.ProjectorConstants', 'Initialize in progress'), + S_STANDBY: translate('OpenLP.ProjectorConstants', 'Power in standby'), + S_WARMUP: translate('OpenLP.ProjectorConstants', 'Warmup in progress'), + S_ON: translate('OpenLP.ProjectorConstants', 'Power is on'), + S_COOLDOWN: translate('OpenLP.ProjectorConstants', 'Cooldown in progress'), + S_INFO: translate('OpenLP.ProjectorConstants', 'Projector Information availble'), + S_NETWORK_SENDING: translate('OpenLP.ProjectorConstants', 'Sending data'), + S_NETWORK_RECEIVED: translate('OpenLP.ProjectorConstants', 'Received data')} + +# Map for ERST return codes to string +PJLINK_ERST_STATUS = {'0': ERROR_STRING[E_OK], + '1': ERROR_STRING[E_WARN], + '2': ERROR_STRING[E_ERROR]} + +# Map for POWR return codes to status code +PJLINK_POWR_STATUS = {'0': S_STANDBY, + '1': S_ON, + '2': S_COOLDOWN, + '3': S_WARMUP} + +PJLINK_DEFAULT_SOURCES = {'1': translate('OpenLP.DB', 'RGB'), + '2': translate('OpenLP.DB', 'Video'), + '3': translate('OpenLP.DB', 'Digital'), + '4': translate('OpenLP.DB', 'Storage'), + '5': translate('OpenLP.DB', 'Network')} diff --git a/openlp/core/lib/projector/db.py b/openlp/core/lib/projector/db.py new file mode 100644 index 000000000..d7abf6fe0 --- /dev/null +++ b/openlp/core/lib/projector/db.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`projector.db` module provides the database functions for the + Projector module. +""" + +import logging +log = logging.getLogger(__name__) +log.debug('projector.lib.db module loaded') + +from os import path + +from sqlalchemy import Column, ForeignKey, Integer, MetaData, Sequence, String, and_ +from sqlalchemy.ext.declarative import declarative_base, declared_attr +from sqlalchemy.orm import backref, joinedload, relationship +from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound +from sqlalchemy.sql import select + +from openlp.core.common import translate +from openlp.core.lib.db import BaseModel, Manager, init_db, init_url +from openlp.core.lib.projector.constants import PJLINK_DEFAULT_SOURCES + +metadata = MetaData() +Base = declarative_base(metadata) + + +class CommonBase(object): + """ + Base class to automate table name and ID column. + """ + @declared_attr + def __tablename__(cls): + return cls.__name__.lower() + id = Column(Integer, primary_key=True) + + +class Manufacturer(CommonBase, Base): + """ + Manufacturer table. + Model table is related. + """ + def __repr__(self): + return '' % self.name + name = Column(String(30)) + models = relationship('Model', + order_by='Model.name', + backref='manufacturer', + cascade='all, delete-orphan', + primaryjoin='Manufacturer.id==Model.manufacturer_id', + lazy='joined') + + +class Model(CommonBase, Base): + """ + Model table. + Manufacturer table links here. + Source table is related. + """ + def __repr__(self): + return '' % self.name + manufacturer_id = Column(Integer, ForeignKey('manufacturer.id')) + name = Column(String(20)) + sources = relationship('Source', + order_by='Source.pjlink_name', + backref='model', + cascade='all, delete-orphan', + primaryjoin='Model.id==Source.model_id', + lazy='joined') + + +class Source(CommonBase, Base): + """ + Input ource table. + Model table links here. + + These entries map PJLink source codes to text strings. + """ + def __repr__(self): + return '' % \ + (self.pjlink_name, self.pjlink_code, self.text) + model_id = Column(Integer, ForeignKey('model.id')) + pjlink_name = Column(String(15)) + pjlink_code = Column(String(2)) + text = Column(String(30)) + + +class Projector(CommonBase, Base): + """ + Projector table. + + No relation. This keeps track of installed projectors. + """ + ip = Column(String(100)) + port = Column(String(8)) + pin = Column(String(6)) + name = Column(String(20)) + location = Column(String(30)) + notes = Column(String(200)) + pjlink_name = Column(String(128)) + manufacturer = Column(String(128)) + model = Column(String(128)) + other = Column(String(128)) + sources = Column(String(128)) + + +class ProjectorDB(Manager): + """ + Class to access the projector database. + """ + def __init__(self, *args, **kwargs): + log.debug('ProjectorDB().__init__(args="%s", kwargs="%s")' % (args, kwargs)) + super().__init__(plugin_name='projector', + init_schema=self.init_schema) + log.debug('ProjectorDB() Initialized using db url %s' % self.db_url) + + def init_schema(*args, **kwargs): + """ + Setup the projector database and initialize the schema. + + Change to Declarative means we really don't do much here. + """ + url = init_url('projector') + session, metadata = init_db(url, base=Base) + Base.metadata.create_all(checkfirst=True) + return session + + def get_projector_all(self): + """ + Retrieve all projector entries so they can be added to the Projector + Manager list pane. + """ + log.debug('get_all() called') + return_list = [] + new_list = self.get_all_objects(Projector) + if new_list is None or new_list.count == 0: + return return_list + for new_projector in new_list: + return_list.append(new_projector) + log.debug('get_all() returning %s item(s)' % len(return_list)) + return return_list + + def get_projector_by_ip(self, ip): + """ + Locate a projector by host IP/Name. + + :param ip: Host IP/Name + :returns: Projetor() instance + """ + log.debug('get_projector_by_ip(ip="%s")' % ip) + projector = self.get_object_filtered(Projector, Projector.ip == ip) + if projector is None: + # Not found + log.warn('get_projector_by_ip() did not find %s' % ip) + return None + log.debug('get_projectorby_ip() returning 1 entry for "%s" id="%s"' % (ip, projector.id)) + return projector + + def get_projector_by_name(self, name): + """ + Locate a projector by name field + + :param name: Name of projector + :returns: Projetor() instance + """ + log.debug('get_projector_by_name(name="%s")' % name) + projector = self.get_object_filtered(Projector, Projector.name == name) + if projector is None: + # Not found + log.warn('get_projector_by_name() did not find "%s"' % name) + return None + log.debug('get_projector_by_name() returning one entry for "%s" id="%s"' % (name, projector.id)) + return projector + + def add_projector(self, projector): + """ + Add a new projector entry + + NOTE: Will not add new entry if IP is the same as already in the table. + + :param projector: Projetor() instance to add + :returns: bool + """ + old_projector = self.get_object_filtered(Projector, Projector.ip == projector.ip) + if old_projector is not None: + log.warn('add_new() skipping entry ip="%s" (Already saved)' % old_projector.ip) + return False + log.debug('add_new() saving new entry') + log.debug('ip="%s", name="%s", location="%s"' % (projector.ip, + projector.name, + projector.location)) + log.debug('notes="%s"' % projector.notes) + return self.save_object(projector) + + def update_projector(self, projector=None): + """ + Update projector entry + + :param projector: Projector() instance with new information + :returns: bool + """ + if projector is None: + log.error('No Projector() instance to update - cancelled') + return False + old_projector = self.get_object_filtered(Projector, Projector.id == projector.id) + if old_projector is None: + log.error('Edit called on projector instance not in database - cancelled') + return False + log.debug('(%s) Updating projector with dbid=%s' % (projector.ip, projector.id)) + old_projector.ip = projector.ip + old_projector.name = projector.name + old_projector.location = projector.location + old_projector.pin = projector.pin + old_projector.port = projector.port + old_projector.pjlink_name = projector.pjlink_name + old_projector.manufacturer = projector.manufacturer + old_projector.model = projector.model + old_projector.other = projector.other + old_projector.sources = projector.sources + return self.save_object(old_projector) + + def delete_projector(self, projector): + """ + Delete an entry by record id + + :param projector: Projector() instance to delete + :returns: bool + """ + deleted = self.delete_object(Projector, projector.id) + if deleted: + log.debug('delete_by_id() Removed entry id="%s"' % projector.id) + else: + log.error('delete_by_id() Entry id="%s" not deleted for some reason' % projector.id) + return deleted + + def get_source_list(self, make, model, sources): + """ + Retrieves the source inputs pjlink code-to-text if available based on + manufacturer and model. + If not available, then returns the PJLink code to default text. + + :param make: Manufacturer name as retrieved from projector + :param model: Manufacturer model as retrieved from projector + :returns: dict + """ + source_dict = {} + model_list = self.get_all_objects(Model, Model.name == model) + if model_list is None or len(model_list) < 1: + # No entry for model, so see if there's a default entry + default_list = self.get_object_filtered(Manufacturer, Manufacturer.name == make) + if default_list is None or len(default_list) < 1: + # No entry for manufacturer, so can't check for default text + log.debug('Using default PJLink text for input select') + for source in sources: + log.debug('source = "%s"' % source) + source_dict[source] = '%s %s' % (PJLINK_DEFAULT_SOURCES[source[0]], source[1]) + else: + # We have a manufacturer entry, see if there's a default + # TODO: Finish this section once edit source input is done + pass + else: + # There's at least one model entry, see if there's more than one manufacturer + # TODO: Finish this section once edit source input text is done + pass + return source_dict diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py new file mode 100644 index 000000000..f5866dd8e --- /dev/null +++ b/openlp/core/lib/projector/pjlink1.py @@ -0,0 +1,668 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`projector.pjlink1` module provides the necessary functions + for connecting to a PJLink-capable projector. + + See PJLink Specifications for Class 1 for details. + + NOTE: + Function names follow the following syntax: + def process_CCCC(...): + WHERE: + CCCC = PJLink command being processed. + + See PJLINK_FUNC(...) for command returned from projector. + +""" + +import logging +log = logging.getLogger(__name__) + +log.debug('projectorpjlink1 loaded') + +__all__ = ['PJLink1'] + +from time import sleep +from codecs import decode, encode + +from PyQt4 import QtCore, QtGui +from PyQt4.QtCore import QObject, pyqtSignal, pyqtSlot +from PyQt4.QtNetwork import QAbstractSocket, QTcpSocket + +from openlp.core.common import translate, qmd5_hash +from openlp.core.lib.projector.constants import * + +# Shortcuts +SocketError = QAbstractSocket.SocketError +SocketSTate = QAbstractSocket.SocketState + +PJLINK_PREFIX = '%' +PJLINK_CLASS = '1' +PJLINK_HEADER = '%s%s' % (PJLINK_PREFIX, PJLINK_CLASS) +PJLINK_SUFFIX = CR + + +class PJLink1(QTcpSocket): + """ + Socket service for connecting to a PJLink-capable projector. + """ + changeStatus = pyqtSignal(str, int, str) + projectorNetwork = pyqtSignal(int) # Projector network activity + projectorStatus = pyqtSignal(int) + + def __init__(self, name=None, ip=None, port=PJLINK_PORT, pin=None, *args, **kwargs): + """ + Setup for instance. + + :param name: Display name + :param ip: IP address to connect to + :param port: Port to use. Default to PJLINK_PORT + :param pin: Access pin (if needed) + + Optional parameters + :param dbid: Database ID number + :param location: Location where projector is physically located + :param notes: Extra notes about the projector + """ + log.debug('PJlink(args="%s" kwargs="%s")' % (args, kwargs)) + self.name = name + self.ip = ip + self.port = port + self.pin = pin + super(PJLink1, self).__init__() + self.dbid = None + self.location = None + self.notes = None + # Allowances for Projector Wizard option + if 'dbid' in kwargs: + self.dbid = kwargs['dbid'] + else: + self.dbid = None + if 'location' in kwargs: + self.location = kwargs['location'] + else: + self.location = None + if 'notes' in kwargs: + self.notes = kwargs['notes'] + else: + self.notes = None + if 'wizard' in kwargs: + self.new_wizard = True + else: + self.new_wizard = False + self.i_am_running = False + self.status_connect = S_NOT_CONNECTED + self.last_command = '' + self.projector_status = S_NOT_CONNECTED + self.error_status = S_OK + # Socket information + # Account for self.readLine appending \0 and/or exraneous \r + self.maxSize = PJLINK_MAX_PACKET + 2 + self.setReadBufferSize(self.maxSize) + # PJLink projector information + self.pjlink_class = '1' # Default class + self.power = S_OFF + self.pjlink_name = None + self.manufacturer = None + self.model = None + self.shutter = None + self.mute = None + self.lamp = None + self.fan = None + self.source_available = None + self.source = None + self.projector_errors = None + # Set from ProjectorManager.add_projector() + self.widget = None # QListBox entry + self.timer = None # Timer that calls the poll_loop + # Map command returned to function + self.PJLINK1_FUNC = {'AVMT': self.process_avmt, + 'CLSS': self.process_clss, + 'ERST': self.process_erst, + 'INFO': self.process_info, + 'INF1': self.process_inf1, + 'INF2': self.process_inf2, + 'INPT': self.process_inpt, + 'INST': self.process_inst, + 'LAMP': self.process_lamp, + 'NAME': self.process_name, + 'POWR': self.process_powr + } + + def thread_started(self): + """ + Connects signals to methods when thread is started. + """ + log.debug('(%s) Thread starting' % self.ip) + self.i_am_running = True + self.connected.connect(self.check_login) + self.disconnected.connect(self.disconnect_from_host) + self.error.connect(self.get_error) + + def thread_stopped(self): + """ + Cleanups when thread is stopped. + """ + log.debug('(%s) Thread stopped' % self.ip) + self.connected.disconnect(self.check_login) + self.disconnected.disconnect(self.disconnect_from_host) + self.error.disconnect(self.get_error) + self.disconnect_from_host() + self.deleteLater() + self.i_am_running = False + + def poll_loop(self): + """ + Called by QTimer in ProjectorManager.ProjectorItem. + Retrieves status information. + """ + if self.state() != self.ConnectedState: + return + log.debug('(%s) Updating projector status' % self.ip) + # Reset timer in case we were called from a set command + self.timer.start() + for i in ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']: + self.send_command(i) + self.waitForReadyRead() + + def _get_status(self, status): + """ + Helper to retrieve status/error codes and convert to strings. + """ + # Return the status code as a string + if status in ERROR_STRING: + return (ERROR_STRING[status], ERROR_MSG[status]) + elif status in STATUS_STRING: + return (STATUS_STRING[status], ERROR_MSG[status]) + else: + return (status, 'Unknown status') + + def change_status(self, status, msg=None): + """ + Check connection/error status, set status for projector, then emit status change signal + for gui to allow changing the icons. + """ + message = 'No message' if msg is None else msg + (code, message) = self._get_status(status) + if msg is not None: + message = msg + if status in CONNECTION_ERRORS: + # Projector, connection state + self.projector_status = self.error_status = self.status_connect = E_NOT_CONNECTED + elif status >= S_NOT_CONNECTED and status < S_STATUS: + self.status_connect = status + self.projector_status = S_NOT_CONNECTED + elif status < S_NETWORK_SENDING: + self.status_connect = S_CONNECTED + self.projector_status = status + (status_code, status_message) = self._get_status(self.status_connect) + log.debug('(%s) status_connect: %s: %s' % (self.ip, status_code, status_message if msg is None else msg)) + (status_code, status_message) = self._get_status(self.projector_status) + log.debug('(%s) projector_status: %s: %s' % (self.ip, status_code, status_message if msg is None else msg)) + (status_code, status_message) = self._get_status(self.error_status) + log.debug('(%s) error_status: %s: %s' % (self.ip, status_code, status_message if msg is None else msg)) + self.changeStatus.emit(self.ip, status, message) + + def check_command(self, cmd): + """ + Verifies command is valid based on PJLink class. + """ + return self.pjlink_class in PJLINK_VALID_CMD and \ + cmd in PJLINK_VALID_CMD[self.pjlink_class] + + def check_login(self): + """ + Processes the initial connection and authentication (if needed). + """ + self.waitForReadyRead(5000) # 5 seconds should be more than enough + read = self.readLine(self.maxSize) + dontcare = self.readLine(self.maxSize) # Clean out the trailing \r\n + if len(read) < 8: + log.warn('(%s) Not enough data read)' % self.ip) + return + data = decode(read, 'ascii') + # Possibility of extraneous data on input when reading. + # Clean out extraneous characters in buffer. + dontcare = self.readLine(self.maxSize) + log.debug('(%s) check_login() read "%s"' % (self.ip, data)) + # At this point, we should only have the initial login prompt with + # possible authentication + if not data.upper().startswith('PJLINK'): + # Invalid response + return self.disconnect_from_host() + data_check = data.strip().split(' ') + log.debug('(%s) data_check="%s"' % (self.ip, data_check)) + salt = None + # PJLink initial login will be: + # 'PJLink 0' - Unauthenticated login - no extra steps required. + # 'PJLink 1 XXXXXX' Authenticated login - extra processing required. + if data_check[1] == '1': + # Authenticated login with salt + salt = qmd5_hash(salt=data_check[2], data=self.pin) + # We're connected at this point, so go ahead and do regular I/O + self.readyRead.connect(self.get_data) + # Initial data we should know about + self.send_command(cmd='CLSS', salt=salt) + self.waitForReadyRead() + # These should never change once we get this information + if self.manufacturer is None: + for i in ['INF1', 'INF2', 'INFO', 'NAME', 'INST']: + self.send_command(cmd=i) + self.waitForReadyRead() + self.change_status(S_CONNECTED) + if not self.new_wizard: + self.timer.start() + self.poll_loop() + + def get_data(self): + """ + Socket interface to retrieve data. + """ + log.debug('(%s) Reading data' % self.ip) + if self.state() != self.ConnectedState: + log.debug('(%s) get_data(): Not connected - returning' % self.ip) + return + read = self.readLine(self.maxSize) + if read == -1: + # No data available + log.debug('(%s) get_data(): No data available (-1)' % self.ip) + return + self.projectorNetwork.emit(S_NETWORK_RECEIVED) + data_in = decode(read, 'ascii') + data = data_in.strip() + if len(data) < 8: + # Not enough data for a packet + log.debug('(%s) get_data(): Packet length < 8: "%s"' % (self.ip, data)) + return + log.debug('(%s) Checking new data "%s"' % (self.ip, data)) + if '=' in data: + pass + else: + log.warn('(%s) Invalid packet received') + return + data_split = data.split('=') + try: + (prefix, class_, cmd, data) = (data_split[0][0], data_split[0][1], data_split[0][2:], data_split[1]) + except ValueError as e: + log.warn('(%s) Invalid packet - expected header + command + data' % self.ip) + log.warn('(%s) Received data: "%s"' % (self.ip, read)) + self.change_status(E_INVALID_DATA) + return + + if not self.check_command(cmd): + log.warn('(%s) Invalid packet - unknown command "%s"' % self.ip, cmd) + return + return self.process_command(cmd, data) + + @pyqtSlot(int) + def get_error(self, err): + """ + Process error from SocketError signal + """ + log.debug('(%s) get_error(err=%s): %s' % (self.ip, err, self.errorString())) + if err <= 18: + # QSocket errors. Redefined in projectorconstants so we don't mistake + # them for system errors + check = err + E_CONNECTION_REFUSED + self.timer.stop() + else: + check = err + if check < E_GENERAL: + # Some system error? + self.change_status(err, self.errorString()) + else: + self.change_status(E_NETWORK, self.errorString()) + return + + def send_command(self, cmd, opts='?', salt=None): + """ + Socket interface to send commands to projector. + """ + if self.state() != self.ConnectedState: + log.warn('(%s) send_command(): Not connected - returning' % self.ip) + return + self.projectorNetwork.emit(S_NETWORK_SENDING) + log.debug('(%s) Sending cmd="%s" opts="%s" %s' % (self.ip, + cmd, + opts, + '' if salt is None else 'with hash')) + if salt is None: + out = '%s%s %s%s' % (PJLINK_HEADER, cmd, opts, CR) + else: + out = '%s%s %s%s' % (salt, cmd, opts, CR) + sent = self.write(out) + self.waitForBytesWritten(5000) # 5 seconds should be enough + if sent == -1: + # Network error? + self.projectorNetwork.emit(S_NETWORK_RECEIVED) + self.change_status(E_NETWORK, 'Error while sending data to projector') + + def process_command(self, cmd, data): + """ + Verifies any return error code. Calls the appropriate command handler. + """ + log.debug('(%s) Processing command "%s"' % (self.ip, cmd)) + if data in PJLINK_ERRORS: + # Oops - projector error + if data.upper() == 'ERRA': + # Authentication error + self.change_status(E_AUTHENTICATION) + return + elif data.upper() == 'ERR1': + # Undefined command + self.change_status(E_UNDEFINED, 'Undefined command: "%s"' % cmd) + return + elif data.upper() == 'ERR2': + # Invalid parameter + self.change_status(E_PARAMETER) + return + elif data.upper() == 'ERR3': + # Projector busy + self.change_status(E_UNAVAILABLE) + return + elif data.upper() == 'ERR4': + # Projector/display error + self.change_status(E_PROJECTOR) + return + + # Command succeeded - no extra information + if data.upper() == 'OK': + log.debug('(%s) Command returned OK' % self.ip) + return + + if cmd in self.PJLINK1_FUNC: + return self.PJLINK1_FUNC[cmd](data) + else: + log.warn('(%s) Invalid command %s' % (self.ip, cmd)) + + def process_lamp(self, data): + """ + Lamp(s) status. See PJLink Specifications for format. + """ + lamps = [] + data_dict = data.split() + while data_dict: + fill = {'Hours': int(data_dict[0]), 'On': False if data_dict[1] == '0' else True} + lamps.append(fill) + data_dict.pop(0) # Remove lamp hours + data_dict.pop(0) # Remove lamp on/off + self.lamp = lamps + return + + def process_powr(self, data): + """ + Power status. See PJLink specification for format. + """ + if data in PJLINK_POWR_STATUS: + self.power = PJLINK_POWR_STATUS[data] + self.change_status(PJLINK_POWR_STATUS[data]) + else: + # Log unknown status response + log.warn('Unknown power response: %s' % data) + return + + def process_avmt(self, data): + """ + Shutter open/closed. See PJLink specification for format. + """ + if data == '11': + self.shutter = True + self.mute = False + elif data == '21': + self.shutter = False + self.mute = True + elif data == '30': + self.shutter = False + self.mute = False + elif data == '31': + self.shutter = True + self.mute = True + else: + log.warn('Unknown shutter response: %s' % data) + return + + def process_inpt(self, data): + """ + Current source input selected. See PJLink specification for format. + """ + self.source = data + return + + def process_clss(self, data): + """ + PJLink class that this projector supports. See PJLink specification for format. + """ + self.pjlink_class = data + log.debug('(%s) Setting pjlink_class for this projector to "%s"' % (self.ip, self.pjlink_class)) + return + + def process_name(self, data): + """ + Projector name set by customer. + """ + self.pjlink_name = data + return + + def process_inf1(self, data): + """ + Manufacturer name set by manufacturer. + """ + self.manufacturer = data + return + + def process_inf2(self, data): + """ + Projector Model set by manufacturer. + """ + self.model = data + return + + def process_info(self, data): + """ + Any extra info set by manufacturer. + """ + self.other_info = data + return + + def process_inst(self, data): + """ + Available source inputs. See PJLink specification for format. + """ + sources = [] + check = data.split() + for source in check: + sources.append(source) + self.source_available = sources + return + + def process_erst(self, data): + """ + Error status. See PJLink Specifications for format. + """ + if int(data) == 0: + self.projector_errors = None + else: + self.projector_errors = {} + # Fan + if data[0] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Fan')] = \ + PJLINK_ERST_STATUS[data[0]] + # Lamp + if data[1] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Lamp')] = \ + PJLINK_ERST_STATUS[data[1]] + # Temp + if data[2] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Temperature')] = \ + PJLINK_ERST_STATUS[data[2]] + # Cover + if data[3] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Cover')] = \ + PJLINK_ERST_STATUS[data[3]] + # Filter + if data[4] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Filter')] = \ + PJLINK_ERST_STATUS[data[4]] + # Other + if data[5] != '0': + self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Other')] = \ + PJLINK_ERST_STATUS[data[5]] + return + + def connect_to_host(self): + """ + Initiate connection. + """ + if self.state() == self.ConnectedState: + log.warn('(%s) connect_to_host(): Already connected - returning' % self.ip) + return + self.change_status(S_CONNECTING) + self.connectToHost(self.ip, self.port if type(self.port) is int else int(self.port)) + + @pyqtSlot() + def disconnect_from_host(self): + """ + Close socket and cleanup. + """ + if self.state() != self.ConnectedState: + log.warn('(%s) disconnect_from_host(): Not connected - returning' % self.ip) + return + self.disconnectFromHost() + try: + self.readyRead.disconnect(self.get_data) + except TypeError: + pass + self.change_status(S_NOT_CONNECTED) + self.timer.stop() + + def get_available_inputs(self): + """ + Send command to retrieve available source inputs. + """ + return self.send_command(cmd='INST') + + def get_error_status(self): + """ + Send command to retrieve currently known errors. + """ + return self.send_command(cmd='ERST') + + def get_input_source(self): + """ + Send command to retrieve currently selected source input. + """ + return self.send_command(cmd='INPT') + + def get_lamp_status(self): + """ + Send command to return the lap status. + """ + return self.send_command(cmd='LAMP') + + def get_manufacturer(self): + """ + Send command to retrieve manufacturer name. + """ + return self.send_command(cmd='INF1') + + def get_model(self): + """ + Send command to retrieve the model name. + """ + return self.send_command(cmd='INF2') + + def get_name(self): + """ + Send command to retrieve name as set by end-user (if set). + """ + return self.send_command(cmd='NAME') + + def get_other_info(self): + """ + Send command to retrieve extra info set by manufacturer. + """ + return self.send_command(cmd='INFO') + + def get_power_status(self): + """ + Send command to retrieve power status. + """ + return self.send_command(cmd='POWR') + + def get_shutter_status(self): + """ + Send command to retrive shutter status. + """ + return self.send_command(cmd='AVMT') + + def set_input_source(self, src=None): + """ + Verify input source available as listed in 'INST' command, + then send the command to select the input source. + """ + if self.source_available is None: + return + elif src not in self.source_available: + return + self.send_command(cmd='INPT', opts=src) + self.waitForReadyRead() + self.poll_loop() + + def set_power_on(self): + """ + Send command to turn power to on. + """ + self.send_command(cmd='POWR', opts='1') + self.waitForReadyRead() + self.poll_loop() + + def set_power_off(self): + """ + Send command to turn power to standby. + """ + self.send_command(cmd='POWR', opts='0') + self.waitForReadyRead() + self.poll_loop() + + def set_shutter_closed(self): + """ + Send command to set shutter to closed position. + """ + self.send_command(cmd='AVMT', opts='11') + self.waitForReadyRead() + self.poll_loop() + + def set_shutter_open(self): + """ + Send command to set shutter to open position. + """ + self.send_command(cmd='AVMT', opts='10') + self.waitForReadyRead() + self.poll_loop() diff --git a/openlp/core/ui/__init__.py b/openlp/core/ui/__init__.py index 664074a87..9bece8dd9 100644 --- a/openlp/core/ui/__init__.py +++ b/openlp/core/ui/__init__.py @@ -124,9 +124,13 @@ from .shortcutlistform import ShortcutListForm from .mediadockmanager import MediaDockManager from .servicemanager import ServiceManager from .thememanager import ThemeManager +from .projector.manager import ProjectorManager +from .projector.wizard import ProjectorWizard +from .projector.tab import ProjectorTab __all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', 'ThemeForm', 'ThemeManager', 'MediaDockManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', 'Display', 'ServiceNoteForm', 'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay', 'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm', - 'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'SingleColumnTableWidget'] + 'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'SingleColumnTableWidget', + 'ProjectorManager', 'ProjectorTab', 'ProjectorWizard'] diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 77a903c5f..d2b3f5b60 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -53,6 +53,7 @@ from openlp.core.ui.media import MediaController from openlp.core.utils import LanguageManager, add_actions, get_application_version from openlp.core.utils.actions import ActionList, CategoryOrder from openlp.core.ui.firsttimeform import FirstTimeForm +from openlp.core.ui.projector.manager import ProjectorManager log = logging.getLogger(__name__) @@ -178,6 +179,14 @@ class Ui_MainWindow(object): self.theme_manager_contents.setObjectName('theme_manager_contents') self.theme_manager_dock.setWidget(self.theme_manager_contents) main_window.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.theme_manager_dock) + # Create the projector manager + self.projector_manager_dock = OpenLPDockWidget(parent=main_window, + name='projector_manager_dock', + icon=':/projector/projector_manager.png') + self.projector_manager_contents = ProjectorManager(self.projector_manager_dock) + self.projector_manager_contents.setObjectName('projector_manager_contents') + self.projector_manager_dock.setWidget(self.projector_manager_contents) + main_window.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.projector_manager_dock) # Create the menu items action_list = ActionList.get_instance() action_list.add_category(UiStrings().File, CategoryOrder.standard_menu) @@ -210,6 +219,16 @@ class Ui_MainWindow(object): can_shortcuts=True) self.export_language_item = create_action(main_window, 'exportLanguageItem') action_list.add_category(UiStrings().View, CategoryOrder.standard_menu) + # Projector items + self.import_projector_item = create_action(main_window, 'importProjectorItem', category=UiStrings().Import, + can_shortcuts=False) + action_list.add_category(UiStrings().Import, CategoryOrder.standard_menu) + self.view_projector_manager_item = create_action(main_window, 'viewProjectorManagerItem', + icon=':/projector/projector_manager.png', + checked=self.projector_manager_dock.isVisible(), + can_shortcuts=True, + category=UiStrings().View, + triggers=self.toggle_projector_manager) self.view_media_manager_item = create_action(main_window, 'viewMediaManagerItem', icon=':/system/system_mediamanager.png', checked=self.media_manager_dock.isVisible(), @@ -310,6 +329,11 @@ class Ui_MainWindow(object): 'searchShortcut', can_shortcuts=True, category=translate('OpenLP.MainWindow', 'General'), triggers=self.on_search_shortcut_triggered) + ''' + Leave until the import projector options are finished + add_actions(self.file_import_menu, (self.settings_import_item, self.import_theme_item, + self.import_projector_item, self.import_language_item, None)) + ''' add_actions(self.file_import_menu, (self.settings_import_item, self.import_theme_item, self.import_language_item, None)) add_actions(self.file_export_menu, (self.settings_export_item, self.export_theme_item, @@ -320,8 +344,8 @@ class Ui_MainWindow(object): self.print_service_order_item, self.file_exit_item)) add_actions(self.view_mode_menu, (self.mode_default_item, self.mode_setup_item, self.mode_live_item)) add_actions(self.view_menu, (self.view_mode_menu.menuAction(), None, self.view_media_manager_item, - self.view_service_manager_item, self.view_theme_manager_item, None, self.view_preview_panel, - self.view_live_panel, None, self.lock_panel)) + self.view_projector_manager_item, self.view_service_manager_item, self.view_theme_manager_item, + None, self.view_preview_panel, self.view_live_panel, None, self.lock_panel)) # i18n add Language Actions add_actions(self.settings_language_menu, (self.auto_language_item, None)) add_actions(self.settings_language_menu, self.language_group.actions()) @@ -375,6 +399,7 @@ class Ui_MainWindow(object): self.media_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Library')) self.service_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Service Manager')) self.theme_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Theme Manager')) + self.projector_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Projector Manager')) self.file_new_item.setText(translate('OpenLP.MainWindow', '&New')) self.file_new_item.setToolTip(UiStrings().NewService) self.file_new_item.setStatusTip(UiStrings().CreateService) @@ -406,6 +431,10 @@ class Ui_MainWindow(object): translate('OpenLP.MainWindow', 'Import OpenLP settings from a specified *.config file previously ' 'exported on this or another machine')) self.settings_import_item.setText(translate('OpenLP.MainWindow', 'Settings')) + self.view_projector_manager_item.setText(translate('OPenLP.MainWindow', '&ProjectorManager')) + self.view_projector_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Toogle Projector Manager')) + self.view_projector_manager_item.setStatusTip(translate('OpenLP.MainWindow', + 'Toggle the visibiilty of the Projector Manager')) self.view_media_manager_item.setText(translate('OpenLP.MainWindow', '&Media Manager')) self.view_media_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Toggle Media Manager')) self.view_media_manager_item.setStatusTip(translate('OpenLP.MainWindow', @@ -485,6 +514,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): self.service_manager_settings_section = 'servicemanager' self.songs_settings_section = 'songs' self.themes_settings_section = 'themes' + self.projector_settings_section = 'projector' self.players_settings_section = 'players' self.display_tags_section = 'displayTags' self.header_section = 'SettingsImport' @@ -515,6 +545,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): self.media_manager_dock.visibilityChanged.connect(self.view_media_manager_item.setChecked) self.service_manager_dock.visibilityChanged.connect(self.view_service_manager_item.setChecked) self.theme_manager_dock.visibilityChanged.connect(self.view_theme_manager_item.setChecked) + self.projector_manager_dock.visibilityChanged.connect(self.view_projector_manager_item.setChecked) self.import_theme_item.triggered.connect(self.theme_manager_contents.on_import_theme) self.export_theme_item.triggered.connect(self.theme_manager_contents.on_export_theme) self.web_site_item.triggered.connect(self.on_help_web_site_clicked) @@ -825,6 +856,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): setting_sections.extend([self.shortcuts_settings_section]) setting_sections.extend([self.service_manager_settings_section]) setting_sections.extend([self.themes_settings_section]) + setting_sections.extend([self.projector_settings_section]) setting_sections.extend([self.players_settings_section]) setting_sections.extend([self.display_tags_section]) setting_sections.extend([self.header_section]) @@ -1114,6 +1146,12 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): """ self.media_manager_dock.setVisible(not self.media_manager_dock.isVisible()) + def toggle_projector_manager(self): + """ + Toggle visibility of the projector manager + """ + self.projector_manager_dock.setVisible(not self.projector_manager_dock.isVisible()) + def toggle_service_manager(self): """ Toggle the visibility of the service manager diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py new file mode 100644 index 000000000..cb25f5ee3 --- /dev/null +++ b/openlp/core/ui/projector/manager.py @@ -0,0 +1,836 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod: projectormanager` module provides the functions for + the display/control of Projectors. +""" + +import logging +log = logging.getLogger(__name__) +log.debug('projectormanager loaded') + +from PyQt4 import QtCore, QtGui +from PyQt4.QtCore import QObject, QThread, pyqtSlot + +from openlp.core.common import Registry, RegistryProperties, Settings, OpenLPMixin, \ + RegistryMixin, translate +from openlp.core.lib import OpenLPToolbar, ImageSource, get_text_file_string, build_icon,\ + check_item_selected, create_thumb +from openlp.core.lib.ui import critical_error_message_box, create_widget_action +from openlp.core.utils import get_locale_key, get_filesystem_encoding + +from openlp.core.lib.projector.db import ProjectorDB +from openlp.core.lib.projector.pjlink1 import PJLink1 +from openlp.core.ui.projector.wizard import ProjectorWizard +from openlp.core.lib.projector.constants import * + +# Dict for matching projector status to display icon +STATUS_ICONS = {S_NOT_CONNECTED: ':/projector/projector_disconnect.png', + S_CONNECTING: ':/projector/projector_connect.png', + S_CONNECTED: ':/projector/projector_off.png', + S_OFF: ':/projector/projector_off.png', + S_INITIALIZE: ':/projector/projector_off.png', + S_STANDBY: ':/projector/projector_off.png', + S_WARMUP: ':/projector/projector_warmup.png', + S_ON: ':/projector/projector_on.png', + S_COOLDOWN: ':/projector/projector_cooldown.png', + E_ERROR: ':/projector/projector_error.png', + E_NETWORK: ':/projector/projector_not_connected.png', + E_AUTHENTICATION: ':/projector/projector_not_connected.png', + E_UNKNOWN_SOCKET_ERROR: ':/icons/openlp-logo-64x64.png' + } + + +class Ui_ProjectorManager(object): + """ + UI part of the Projector Manager + """ + def setup_ui(self, widget): + """ + Define the UI + :param widget: The screen object the dialog is to be attached to. + """ + log.debug('setup_ui()') + # Create ProjectorManager box + self.layout = QtGui.QVBoxLayout(widget) + self.layout.setSpacing(0) + self.layout.setMargin(0) + self.layout.setObjectName('layout') + # Add toolbar + self.toolbar = OpenLPToolbar(widget) + self.toolbar.add_toolbar_action('newProjector', + text=translate('OpenLP.Projector', 'Add Projector'), + icon=':/projector/projector_new.png', + tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'), + triggers=self.on_add_projector) + self.toolbar.addSeparator() + self.toolbar.add_toolbar_action('connect_all_projectors', + text=translate('OpenLP.ProjectorManager', 'Connect to all projectors'), + icon=':/projector/projector_connect.png', + tootip=translate('OpenLP.ProjectorManager', 'Connect to all projectors'), + triggers=self.on_connect_all_projectors) + self.toolbar.add_toolbar_action('disconnect_all_projectors', + text=translate('OpenLP.ProjectorManager', 'Disconnect from all projectors'), + icon=':/projector/projector_disconnect.png', + tooltip=translate('OpenLP.ProjectorManager', 'Disconnect from all projectors'), + triggers=self.on_disconnect_all_projectors) + self.toolbar.addSeparator() + self.toolbar.add_toolbar_action('poweron_all_projectors', + text=translate('OpenLP.ProjectorManager', 'Power On All Projectors'), + icon=':/projector/projector_power_on.png', + tooltip=translate('OpenLP.ProjectorManager', 'Power on all projectors'), + triggers=self.on_poweron_all_projectors) + self.toolbar.add_toolbar_action('poweroff_all_projectors', + text=translate('OpenLP.ProjectorManager', 'Standby All Projector'), + icon=':/projector/projector_power_off.png', + tooltip=translate('OpenLP.ProjectorManager', 'Put all projectors in standby'), + triggers=self.on_poweroff_all_projectors) + self.toolbar.addSeparator() + self.toolbar.add_toolbar_action('blank_projector', + text=translate('OpenLP.ProjectorManager', 'Blank All Projector Screens'), + icon=':/projector/projector_blank.png', + tooltip=translate('OpenLP.ProjectorManager', 'Blank all projector screens'), + triggers=self.on_blank_all_projectors) + self.toolbar.add_toolbar_action('show_all_projector', + text=translate('OpenLP.ProjectorManager', 'Show All Projector Screens'), + icon=':/projector/projector_show.png', + tooltip=translate('OpenLP.ProjectorManager', 'Show all projector screens'), + triggers=self.on_show_all_projectors) + self.layout.addWidget(self.toolbar) + # Add the projector list box + self.projector_widget = QtGui.QWidgetAction(self.toolbar) + self.projector_widget.setObjectName('projector_widget') + # Create projector manager list + self.projector_list_widget = QtGui.QListWidget(widget) + self.projector_list_widget.setAlternatingRowColors(True) + self.projector_list_widget.setIconSize(QtCore.QSize(90, 50)) + self.projector_list_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.projector_list_widget.setObjectName('projector_list_widget') + self.layout.addWidget(self.projector_list_widget) + self.projector_list_widget.customContextMenuRequested.connect(self.context_menu) + # Build the context menu + self.menu = QtGui.QMenu() + self.view_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&View Projector Information'), + icon=':/projector/projector_view.png', + triggers=self.on_view_projector) + self.status_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + 'View &Projector Status'), + icon=':/projector/projector_status.png', + triggers=self.on_status_projector) + self.edit_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&Edit Projector'), + icon=':/projector/projector_edit.png', + triggers=self.on_edit_projector) + self.menu.addSeparator() + self.connect_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&Connect Projector'), + icon=':/projector/projector_connect.png', + triggers=self.on_connect_projector) + self.disconnect_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + 'D&isconnect Projector'), + icon=':/projector/projector_disconnect.png', + triggers=self.on_disconnect_projector) + self.menu.addSeparator() + self.poweron_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + 'Power &On Projector'), + icon=':/projector/projector_power_on.png', + triggers=self.on_poweron_projector) + self.poweroff_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + 'Power O&ff Projector'), + icon=':/projector/projector_power_off.png', + triggers=self.on_poweroff_projector) + self.menu.addSeparator() + self.select_input_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + 'Select &Input'), + icon=':/projector/projector_connectors.png', + triggers=self.on_select_input) + self.blank_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&Blank Projector Screen'), + icon=':/projector/projector_blank.png', + triggers=self.on_blank_projector) + self.show_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&Show Projector Screen'), + icon=':/projector/projector_show.png', + triggers=self.on_show_projector) + self.menu.addSeparator() + self.delete_action = create_widget_action(self.menu, + text=translate('OpenLP.ProjectorManager', + '&Delete Projector'), + icon=':/general/general_delete.png', + triggers=self.on_delete_projector) + + +class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorManager, RegistryProperties): + """ + Manage the projectors. + """ + def __init__(self, parent=None, projectordb=None): + log.debug('__init__()') + super().__init__(parent) + self.settings_section = 'projector' + self.projectordb = projectordb + self.projector_list = [] + + def bootstrap_initialise(self): + self.setup_ui(self) + if self.projectordb is None: + # Work around for testing creating a ~/.openlp.data.projector.projector.sql file + log.debug('Creating new ProjectorDB() instance') + self.projectordb = ProjectorDB() + else: + log.debug('Using existing ProjectorDB() instance') + settings = Settings() + settings.beginGroup(self.settings_section) + self.autostart = settings.value('connect on start') + settings.endGroup() + del(settings) + + def bootstrap_post_set_up(self): + self.load_projectors() + self.projector_form = ProjectorWizard(self, projectordb=self.projectordb) + self.projector_form.edit_page.newProjector.connect(self.add_projector_from_wizard) + self.projector_form.edit_page.editProjector.connect(self.edit_projector_from_wizard) + + def context_menu(self, point): + """ + Build the Right Click Context menu and set state. + + :param point: The position of the mouse so the correct item can be found. + """ + # QListWidgetItem + item = self.projector_list_widget.itemAt(point) + if item is None: + return + real_projector = item.data(QtCore.Qt.UserRole) + projector_name = str(item.text()) + visible = real_projector.link.status_connect >= S_CONNECTED + log.debug('(%s) Building menu - visible = %s' % (projector_name, visible)) + self.delete_action.setVisible(True) + self.edit_action.setVisible(True) + self.view_action.setVisible(True) + self.connect_action.setVisible(not visible) + self.disconnect_action.setVisible(visible) + self.status_action.setVisible(visible) + if visible: + self.select_input_action.setVisible(real_projector.link.power == S_ON) + self.poweron_action.setVisible(real_projector.link.power == S_STANDBY) + self.poweroff_action.setVisible(real_projector.link.power == S_ON) + self.blank_action.setVisible(real_projector.link.power == S_ON and + not real_projector.link.shutter) + self.show_action.setVisible(real_projector.link.power == S_ON and + real_projector.link.shutter) + else: + self.select_input_action.setVisible(False) + self.poweron_action.setVisible(False) + self.poweroff_action.setVisible(False) + self.blank_action.setVisible(False) + self.show_action.setVisible(False) + self.menu.projector = real_projector + self.menu.exec_(self.projector_list_widget.mapToGlobal(point)) + + def _select_input_widget(self, parent, selected, code, text): + """ + Build the radio button widget for selecting source input menu + + :param parent: parent widget + :param selected: Selected widget text + :param code: PJLink code for this widget + :param text: Text to display + :returns: radio button widget + """ + widget = QtGui.QRadioButton(text, parent=parent) + widget.setChecked(True if code == selected else False) + widget.button_role = code + widget.clicked.connect(self._select_input_radio) + self.radio_buttons.append(widget) + return widget + + def _select_input_radio(self, opt1=None, opt2=None): + """ + Returns the currently selected radio button + + :param opt1: Needed by PyQt4 + :param op2: future + :returns: Selected button role + """ + for i in self.radio_buttons: + if i.isChecked(): + self.radio_button_selected = i.button_role + break + return + + def on_select_input(self, opt=None): + """ + Builds menu for 'Select Input' option, then calls the selected projector + item to change input source. + + :param opt: Needed by PyQt4 + :returns: None + """ + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + projector = list_item.data(QtCore.Qt.UserRole) + layout = QtGui.QVBoxLayout() + box = QtGui.QDialog(parent=self) + box.setModal(True) + title = QtGui.QLabel(translate('OpenLP.ProjectorManager', 'Select the input source below')) + layout.addWidget(title) + self.radio_button_selected = None + self.radio_buttons = [] + source_list = self.projectordb.get_source_list(make=projector.link.manufacturer, + model=projector.link.model, + sources=projector.link.source_available + ) + if source_list is None: + return + sort = [] + for i in source_list.keys(): + sort.append(i) + sort.sort() + for i in sort: + button = self._select_input_widget(parent=self, + selected=projector.link.source, + code=i, + text=source_list[i]) + layout.addWidget(button) + button_box = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | + QtGui.QDialogButtonBox.Cancel) + button_box.accepted.connect(box.accept) + button_box.rejected.connect(box.reject) + layout.addWidget(button_box) + box.setLayout(layout) + check = box.exec_() + if check == 0: + # Cancel button clicked or window closed - don't set source + return + selected = self.radio_button_selected + projector.link.set_input_source(self.radio_button_selected) + self.radio_button_selected = None + + def on_add_projector(self, opt=None): + """ + Calls wizard to add a new projector to the database + + :param opt: Needed by PyQt4 + :returns: None + """ + self.projector_form.exec_() + + def on_blank_all_projectors(self, opt=None): + """ + Cycles through projector list to send blank screen command + + :param opt: Needed by PyQt4 + :returns: None + """ + for item in self.projector_list: + self.on_blank_projector(item) + + def on_blank_projector(self, opt=None): + """ + Calls projector thread to send blank screen command + + :param opt: Needed by PyQt4 + :returns: None + """ + try: + ip = opt.link.ip + projector = opt + except AttributeError: + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + return projector.link.set_shutter_closed() + + def on_connect_projector(self, opt=None): + """ + Calls projector thread to connect to projector + + :param opt: Needed by PyQt4 + :returns: None + """ + try: + ip = opt.link.ip + projector = opt + except AttributeError: + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + return projector.link.connect_to_host() + + def on_connect_all_projectors(self, opt=None): + """ + Cycles through projector list to tell threads to connect to projectors + + :param opt: Needed by PyQt4 + :returns: None + """ + for item in self.projector_list: + self.on_connect_projector(item) + + def on_delete_projector(self, opt=None): + """ + Deletes a projector from the list and the database + + :param opt: Needed by PyQt4 + :returns: None + """ + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + msg = QtGui.QMessageBox() + msg.setText('Delete projector (%s) %s?' % (projector.link.ip, projector.link.name)) + msg.setInformativeText('Are you sure you want to delete this projector?') + msg.setStandardButtons(msg.Cancel | msg.Ok) + msg.setDefaultButton(msg.Cancel) + ans = msg.exec_() + if ans == msg.Cancel: + return + try: + projector.link.projectorNetwork.disconnect(self.update_status) + except TypeError: + pass + try: + projector.link.changeStatus.disconnect(self.update_status) + except TypeError: + pass + + try: + projector.timer.stop() + projector.timer.timeout.disconnect(projector.link.poll_loop) + except TypeError: + pass + projector.thread.quit() + new_list = [] + for item in self.projector_list: + if item.link.dbid == projector.link.dbid: + continue + new_list.append(item) + self.projector_list = new_list + list_item = self.projector_list_widget.takeItem(self.projector_list_widget.currentRow()) + list_item = None + deleted = self.projectordb.delete_projector(projector.db_item) + for item in self.projector_list: + log.debug('New projector list - item: %s %s' % (item.link.ip, item.link.name)) + + def on_disconnect_projector(self, opt=None): + """ + Calls projector thread to disconnect from projector + + :param opt: Needed by PyQt4 + :returns: None + """ + try: + ip = opt.link.ip + projector = opt + except AttributeError: + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + return projector.link.disconnect_from_host() + + def on_disconnect_all_projectors(self, opt=None): + """ + Cycles through projector list to have projector threads disconnect + + :param opt: Needed by PyQt4 + :returns: None + """ + for item in self.projector_list: + self.on_disconnect_projector(item) + + def on_edit_projector(self, opt=None): + """ + Calls wizard with selected projector to edit information + + :param opt: Needed by PyQt4 + :returns: None + """ + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + projector = list_item.data(QtCore.Qt.UserRole) + if projector is None: + return + self.old_projector = projector + projector.link.disconnect_from_host() + record = self.projectordb.get_projector_by_ip(projector.link.ip) + self.projector_form.exec_(record) + + def on_poweroff_all_projectors(self, opt=None): + """ + Cycles through projector list to send Power Off command + + :param opt: Needed by PyQt4 + :returns: None + """ + for item in self.projector_list: + self.on_poweroff_projector(item) + + def on_poweroff_projector(self, opt=None): + """ + Calls projector link to send Power Off command + + :param opt: Needed by PyQt4 + :returns: None + """ + try: + ip = opt.link.ip + projector = opt + except AttributeError: + # Must have been called by a mouse-click on item + list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + if list_item is None: + return + projector = list_item.data(QtCore.Qt.UserRole) + return projector.link.set_power_off() + + def on_poweron_all_projectors(self, opt=None): + """ + Cycles through projector list to send Power On command + + :param opt: Needed by PyQt4 + :returns: None + """ + for item in self.projector_list: + self.on_poweron_projector(item) + + def on_poweron_projector(self, opt=None): + """ + Calls projector link to send Power On command + + :param opt: Needed by PyQt4 + :returns: None + """ + try: + ip = opt.link.ip + projector = opt + except AttributeError: + lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + if lwi is None: + return + projector = lwi.data(QtCore.Qt.UserRole) + return projector.link.set_power_on() + + def on_show_all_projectors(self, opt=None): + """ + Cycles through projector list to send open shutter command + + :param opt: Needed by PyQt4 + :returns: None + """ + for i in self.projector_list: + self.on_show_projector(i.link) + + def on_show_projector(self, opt=None): + """ + Calls projector thread to send open shutter command + + :param opt: Needed by PyQt4 + :returns: None + """ + try: + ip = opt.link.ip + projector = opt + except AttributeError: + lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + if lwi is None: + return + projector = lwi.data(QtCore.Qt.UserRole) + return projector.link.set_shutter_open() + + def on_status_projector(self, opt=None): + """ + Builds message box with projector status information + + :param opt: Needed by PyQt4 + :returns: None + """ + lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + projector = lwi.data(QtCore.Qt.UserRole) + s = '%s: %s
' % (translate('OpenLP.ProjectorManager', 'Name'), projector.link.name) + s = '%s%s: %s
' % (s, translate('OpenLP.ProjectorManager', 'IP'), projector.link.ip) + s = '%s%s: %s
' % (s, translate('OpenLP.ProjectorManager', 'Port'), projector.link.port) + s = '%s

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

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

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

' % s + s = '%s%s
' % (s, translate('OpenLP.ProjectorManager', 'Lamp status')) + c = 1 + for i in projector.link.lamp: + s = '%s %s %s (%s) %s: %s
' % (s, + translate('OpenLP.ProjectorManager', 'Lamp'), + c, + translate('OpenLP.ProjectorManager', 'On') if i['On'] else + translate('OpenLP.ProjectorManager', 'Off'), + translate('OpenLP.ProjectorManager', 'Hours'), + i['Hours']) + c = c + 1 + QtGui.QMessageBox.information(self, translate('OpenLP.ProjectorManager', 'Projector Information'), s) + + def on_view_projector(self, opt=None): + """ + Builds message box with projector information stored in database + + :param opt: Needed by PyQt4 + :returns: None + """ + lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow()) + projector = lwi.data(QtCore.Qt.UserRole) + dbid = translate('OpenLP.ProjectorManager', 'DB Entry') + ip = translate('OpenLP.ProjectorManager', 'IP') + port = translate('OpenLP.ProjectorManager', 'Port') + name = translate('OpenLP.ProjectorManager', 'Name') + location = translate('OpenLP.ProjectorManager', 'Location') + notes = translate('OpenLP.ProjectorManager', 'Notes') + QtGui.QMessageBox.information(self, translate('OpenLP.ProjectorManager', + 'Projector %s Information' % projector.link.name), + '%s: %s

%s: %s

%s: %s

' + '%s: %s

%s: %s

' + '%s:
%s' % (dbid, projector.link.dbid, + ip, projector.link.ip, + port, projector.link.port, + name, projector.link.name, + location, projector.link.location, + notes, projector.link.notes)) + + def _add_projector(self, projector): + """ + Helper app to build a projector instance + + :param p: Dict of projector database information + :returns: PJLink() instance + """ + log.debug('_add_projector()') + return PJLink1(dbid=projector.id, + ip=projector.ip, + port=int(projector.port), + name=projector.name, + location=projector.location, + notes=projector.notes, + pin=projector.pin + ) + + def add_projector(self, opt1, opt2=None): + """ + Builds manager list item, projector thread, and timer for projector instance. + + If called by add projector wizard: + opt1 = wizard instance + opt2 = item + Otherwise: + opt1 = item + opt2 = None + + We are not concerned with the wizard instance, + just the projector item + + :param opt1: See docstring + :param opt2: See docstring + :returns: None + """ + if opt1 is None: + return + if opt2 is None: + projector = opt1 + else: + projector = opt2 + item = ProjectorItem(link=self._add_projector(projector)) + item.db_item = projector + icon = QtGui.QIcon(QtGui.QPixmap(STATUS_ICONS[S_NOT_CONNECTED])) + item.icon = icon + widget = QtGui.QListWidgetItem(icon, + item.link.name, + self.projector_list_widget + ) + widget.setData(QtCore.Qt.UserRole, item) + item.widget = widget + thread = QThread(parent=self) + thread.my_parent = self + item.moveToThread(thread) + thread.started.connect(item.link.thread_started) + thread.finished.connect(item.link.thread_stopped) + thread.finished.connect(thread.deleteLater) + item.link.projectorNetwork.connect(self.update_status) + item.link.changeStatus.connect(self.update_status) + timer = QtCore.QTimer(self) + timer.setInterval(20000) # 20 second poll interval + timer.timeout.connect(item.link.poll_loop) + item.timer = timer + thread.start() + item.thread = thread + item.link.timer = timer + item.link.widget = item.widget + self.projector_list.append(item) + if self.autostart: + item.link.connect_to_host() + for i in self.projector_list: + log.debug('New projector list - item: (%s) %s' % (i.link.ip, i.link.name)) + + @pyqtSlot(str) + def add_projector_from_wizard(self, ip, opts=None): + """ + Add a projector from the wizard + + :param ip: IP address of new record item + :param opts: Needed by PyQt4 + :returns: None + """ + log.debug('load_projector(ip=%s)' % ip) + item = self.projectordb.get_projector_by_ip(ip) + self.add_projector(item) + + @pyqtSlot(object) + def edit_projector_from_wizard(self, projector, opts=None): + """ + Update projector from the wizard edit page + + :param projector: Projector() instance of projector with updated information + :param opts: Needed by PyQt4 + :returns: None + """ + + self.old_projector.link.name = projector.name + self.old_projector.link.ip = projector.ip + self.old_projector.link.pin = projector.pin + self.old_projector.link.port = projector.port + self.old_projector.link.location = projector.location + self.old_projector.link.notes = projector.notes + self.old_projector.widget.setText(projector.name) + + def load_projectors(self): + """' + Load projectors - only call when initializing + """ + log.debug('load_projectors()') + self.projector_list_widget.clear() + for i in self.projectordb.get_projector_all(): + self.add_projector(i) + + def get_projector_list(self): + """ + Return the list of active projectors + + :returns: list + """ + return self.projector_list + + @pyqtSlot(str, int, str) + def update_status(self, ip, status=None, msg=None): + """ + Update the status information/icon for selected list item + + :param ip: IP address of projector + :param status: Optional status code + :param msg: Optional status message + :returns: None + """ + if status is None: + return + item = None + for list_item in self.projector_list: + if ip == list_item.link.ip: + item = list_item + break + message = 'No message' if msg is None else msg + if status in STATUS_STRING: + status_code = STATUS_STRING[status] + message = ERROR_MSG[status] if msg is None else msg + elif status in ERROR_STRING: + status_code = ERROR_STRING[status] + message = ERROR_MSG[status] if msg is None else msg + else: + status_code = status + message = ERROR_MSG[status] if msg is None else msg + log.debug('(%s) updateStatus(status=%s) message: "%s"' % (item.link.name, status_code, message)) + if status in STATUS_ICONS: + item.icon = QtGui.QIcon(QtGui.QPixmap(STATUS_ICONS[status])) + log.debug('(%s) Updating icon' % item.link.name) + item.widget.setIcon(item.icon) + + +class ProjectorItem(QObject): + """ + Class for the projector list widget item. + NOTE: Actual PJLink class instance should be saved as self.link + """ + def __init__(self, link=None): + self.link = link + self.thread = None + self.icon = None + self.widget = None + self.my_parent = None + self.timer = None + self.projectordb_item = None + super(ProjectorItem, self).__init__() + + +def not_implemented(function): + """ + Temporary function to build an information message box indicating function not implemented yet + + :param func: Function name + :returns: None + """ + QtGui.QMessageBox.information(None, + translate('OpenLP.ProjectorManager', 'Not Implemented Yet'), + translate('OpenLP.ProjectorManager', + 'Function "%s"
has not been implemented yet.' + '
Please check back again later.' % function)) diff --git a/openlp/core/ui/projector/tab.py b/openlp/core/ui/projector/tab.py new file mode 100644 index 000000000..1d0ed90f2 --- /dev/null +++ b/openlp/core/ui/projector/tab.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`projector.ui.projectortab` module provides the settings tab in the + settings dialog. +""" + +import logging +log = logging.getLogger(__name__) +log.debug('projectortab module loaded') + +from PyQt4 import QtCore, QtGui + +from openlp.core.common import Registry, Settings, UiStrings, translate +from openlp.core.lib import SettingsTab +from openlp.core.lib.ui import find_and_set_in_combo_box + + +class ProjectorTab(SettingsTab): + """ + Openlp Settings -> Projector settings + """ + def __init__(self, parent): + self.icon_path = ':/projector/projector_manager.png' + projector_translated = translate('OpenLP.ProjectorTab', 'Projector') + super(ProjectorTab, self).__init__(parent, 'Projector', projector_translated) + + def setupUi(self): + """ + Setup the UI + """ + self.setObjectName('ProjectorTab') + super(ProjectorTab, self).setupUi() + self.connect_box = QtGui.QGroupBox(self.left_column) + self.connect_box.setTitle('Communication Options') + self.connect_box.setObjectName('connect_box') + self.connect_box_layout = QtGui.QVBoxLayout(self.connect_box) + self.connect_box_layout.setObjectName('connect_box_layout') + # Start comms with projectors on startup + self.connect_on_startup = QtGui.QCheckBox(self.connect_box) + self.connect_on_startup.setObjectName('connect_on_startup') + self.connect_box_layout.addWidget(self.connect_on_startup) + self.left_layout.addWidget(self.connect_box) + self.left_layout.addStretch() + + def retranslateUi(self): + """ + Translate the UI on the fly + """ + self.tab_title_visible = UiStrings().Projectors + self.connect_on_startup.setText( + translate('OpenLP.ProjectorTab', 'Connect to projectors on startup')) + + def load(self): + """ + Load the projetor settings on startup + """ + settings = Settings() + settings.beginGroup(self.settings_section) + self.connect_on_startup.setChecked(settings.value('connect on start')) + settings.endGroup() + + def save(self): + """ + Save the projector settings + """ + settings = Settings() + settings.beginGroup(self.settings_section) + settings.setValue('connect on start', self.connect_on_startup.isChecked()) + settings.endGroup diff --git a/openlp/core/ui/projector/wizard.py b/openlp/core/ui/projector/wizard.py new file mode 100644 index 000000000..82168aed0 --- /dev/null +++ b/openlp/core/ui/projector/wizard.py @@ -0,0 +1,551 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`projector.projectorwizard` module handles the GUI Wizard for adding + new projetor entries. +""" + +import logging +log = logging.getLogger(__name__) +log.debug('projectorwizard loaded') + +from ipaddress import IPv4Address, IPv6Address, AddressValueError + +from PyQt4 import QtCore, QtGui +from PyQt4.QtCore import pyqtSlot, pyqtSignal + +from openlp.core.common import Registry, RegistryProperties, translate +from openlp.core.lib import build_icon + +from openlp.core.common import verify_ip_address +from openlp.core.lib.projector.db import ProjectorDB, Projector +from openlp.core.lib.projector.pjlink1 import PJLink1 +from openlp.core.lib.projector.constants import * + +PAGE_COUNT = 4 +(ConnectWelcome, + ConnectHost, + ConnectEdit, + ConnectFinish) = range(PAGE_COUNT) + +PAGE_NEXT = {ConnectWelcome: ConnectHost, + ConnectHost: ConnectEdit, + ConnectEdit: ConnectFinish, + ConnectFinish: -1} + + +class ProjectorWizard(QtGui.QWizard, RegistryProperties): + """ + Wizard for adding/editing projector entries. + """ + def __init__(self, parent, projectordb): + log.debug('__init__()') + super().__init__(parent) + self.db = projectordb + self.projector = None + self.setObjectName('projector_wizard') + self.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + self.setModal(True) + self.setWizardStyle(QtGui.QWizard.ModernStyle) + self.setMinimumSize(650, 550) + self.setOption(QtGui.QWizard.NoBackButtonOnStartPage) + self.spacer = QtGui.QSpacerItem(10, 0, + QtGui.QSizePolicy.Fixed, + QtGui.QSizePolicy.Minimum) + self.setOption(self.HaveHelpButton, True) + self.welcome_page = ConnectWelcomePage(self, ConnectWelcome) + self.host_page = ConnectHostPage(self, ConnectHost) + self.edit_page = ConnectEditPage(self, ConnectEdit) + self.finish_page = ConnectFinishPage(self, ConnectFinish) + self.setPage(self.welcome_page.pageId, self.welcome_page) + self.setPage(self.host_page.pageId, self.host_page) + self.setPage(self.edit_page.pageId, self.edit_page) + self.setPage(self.finish_page.pageId, self.finish_page) + self.registerFields() + self.retranslateUi() + # Connect signals + self.button(QtGui.QWizard.HelpButton).clicked.connect(self.showHelp) + log.debug('ProjectorWizard() started') + + def exec_(self, projector=None): + """ + Override function to determine whether we are called to add a new + projector or edit an old projector. + + :param projector: Projector instance + :returns: None + """ + self.projector = projector + if self.projector is None: + log.debug('ProjectorWizard() Adding new projector') + self.setWindowTitle(translate('OpenLP.ProjectorWizard', + 'New Projector Wizard')) + self.setStartId(ConnectWelcome) + self.restart() + else: + log.debug('ProjectorWizard() Editing existing projector') + self.setWindowTitle(translate('OpenLP.ProjectorWizard', + 'Edit Projector Wizard')) + self.setStartId(ConnectEdit) + self.restart() + saved = QtGui.QWizard.exec_(self) + self.projector = None + return saved + + def registerFields(self): + """ + Register selected fields for use by all pages. + """ + self.host_page.registerField('ip_number*', self.host_page.ip_number_text) + self.edit_page.registerField('pjlink_port', self.host_page.pjlink_port_text) + self.edit_page.registerField('pjlink_pin', self.host_page.pjlink_pin_text) + self.edit_page.registerField('projector_name*', self.edit_page.name_text) + self.edit_page.registerField('projector_location', self.edit_page.location_text) + self.edit_page.registerField('projector_notes', self.edit_page.notes_text, 'plainText') + self.edit_page.registerField('projector_make', self.host_page.manufacturer_text) + self.edit_page.registerField('projector_model', self.host_page.model_text) + + @pyqtSlot() + def showHelp(self): + """ + Show the pop-up help message. + """ + page = self.currentPage() + try: + help_page = page.help_ + except: + help_page = self.no_help + QtGui.QMessageBox.information(self, self.title_help, help_page) + + def retranslateUi(self): + """ + Fixed-text strings used for translations + """ + self.title_help = translate('OpenLP.ProjectorWizard', 'Projector Wizard Help') + self.no_help = translate('OpenLP.ProjectorWizard', + 'Sorry - no help available for this page.') + self.welcome_page.title_label.setText('%s' % + translate('OpenLP.ProjectorWizard', + 'Welcome to the
Projector Wizard')) + self.welcome_page.information_label.setText(translate('OpenLP.ProjectorWizard', 'This wizard will help you to ' + 'create and edit your Projector control.

' + 'Press "Next" button below to continue.')) + self.host_page.setTitle(translate('OpenLP.ProjectorWizard', 'Host IP Number')) + self.host_page.setSubTitle(translate('OpenLP.ProjectorWizard', + 'Enter the IP address, port, and PIN used to conenct to the projector. ' + 'The port should only be changed if you know what you\'re doing, and ' + 'the pin should only be entered if it\'s required.' + '

Once the IP address is checked and is ' + 'not in the database, you can continue to the next page')) + self.host_page.help_ = translate('OpenLP.ProjectorWizard', + 'IP: The IP address of the projector to connect to.
' + 'Port: The port number. Default is 4352.
' + 'PIN: If needed, enter the PIN access code for the projector.
' + '
Once I verify the address is a valid IP and not in the database, you ' + 'can then add the rest of the information on the next page.') + self.host_page.ip_number_label.setText(translate('OpenLP.ProjectorWizard', 'IP Number: ')) + self.host_page.pjlink_port_label.setText(translate('OpenLP.ProjectorWizard', 'Port: ')) + self.host_page.pjlink_pin_label.setText(translate('OpenLP.ProjectorWizard', 'PIN: ')) + self.edit_page.setTitle(translate('OpenLP.ProjectorWizard', 'Add/Edit Projector Information')) + self.edit_page.setSubTitle(translate('OpenLP.ProjectorWizard', + 'Enter the information below in the left panel for the projector.')) + self.edit_page.help_ = translate('OpenLP.ProjectorWizard', + 'Please enter the following information:' + '

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

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

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

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

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

The "Manufacturer" and "Model" ' + 'information will only be available if the projector is connected to the ' + 'network and can be accessed while running this wizard. ' + '(Currently not implemented)' % PJLINK_PORT) + self.edit_page.ip_number_label.setText(translate('OpenLP.ProjectorWizard', 'IP Number: ')) + self.edit_page.pjlink_port_label.setText(translate('OpenLP.ProjectorWizard', 'PJLink port: ')) + self.edit_page.pjlink_pin_label.setText(translate('OpenLP.ProjectorWizard', 'PJLink PIN: ')) + self.edit_page.name_label.setText(translate('OpenLP.ProjectorWizard', 'Name: ')) + self.edit_page.location_label.setText(translate('OpenLP.ProjectorWizard', 'Location: ')) + self.edit_page.notes_label.setText(translate('OpenLP.ProjectorWizard', 'Notes: ')) + self.edit_page.projector_make_label.setText(translate('OpenLP.ProjectorWizard', 'Manufacturer: ')) + self.edit_page.projector_model_label.setText(translate('OpenLP.ProjectorWizard', 'Model: ')) + self.finish_page.title_label.setText('%s' % + translate('OpenLP.ProjectorWizard', 'Projector Added')) + self.finish_page.information_label.setText(translate('OpenLP.ProjectorWizard', + '
Have fun with your new projector.')) + + +class ConnectBase(QtGui.QWizardPage): + """ + Base class for the projector wizard pages. + """ + def __init__(self, parent=None, page=None): + super().__init__(parent) + self.pageId = page + + def nextId(self): + """ + Returns next page to show. + """ + return PAGE_NEXT[self.pageId] + + def setVisible(self, visible): + """ + Set buttons for bottom of page. + """ + QtGui.QWizardPage.setVisible(self, visible) + if visible: + try: + self.myCustomButton() + except: + try: + self.wizard().setButtonLayout(self.myButtons) + except: + self.wizard().setButtonLayout([QtGui.QWizard.Stretch, + QtGui.QWizard.BackButton, + QtGui.QWizard.NextButton, + QtGui.QWizard.CancelButton]) + + +class ConnectWelcomePage(ConnectBase): + """ + Splash screen + """ + def __init__(self, parent, page): + super().__init__(parent, page) + self.setPixmap(QtGui.QWizard.WatermarkPixmap, + QtGui.QPixmap(':/wizards/wizard_createprojector.png')) + self.setObjectName('welcome_page') + self.myButtons = [QtGui.QWizard.Stretch, + QtGui.QWizard.NextButton] + self.layout = QtGui.QVBoxLayout(self) + self.layout.setObjectName('layout') + self.title_label = QtGui.QLabel(self) + self.title_label.setObjectName('title_label') + self.layout.addWidget(self.title_label) + self.layout.addSpacing(40) + self.information_label = QtGui.QLabel(self) + self.information_label.setWordWrap(True) + self.information_label.setObjectName('information_label') + self.layout.addWidget(self.information_label) + self.layout.addStretch() + + +class ConnectHostPage(ConnectBase): + """ + Get initial information. + """ + def __init__(self, parent, page): + super().__init__(parent, page) + self.setObjectName('host_page') + self.myButtons = [QtGui.QWizard.HelpButton, + QtGui.QWizard.Stretch, + QtGui.QWizard.BackButton, + QtGui.QWizard.NextButton, + QtGui.QWizard.CancelButton] + self.hostPageLayout = QtGui.QHBoxLayout(self) + self.hostPageLayout.setObjectName('layout') + # Projector DB information + self.localAreaBox = QtGui.QGroupBox(self) + self.localAreaBox.setObjectName('host_local_area_box') + self.localAreaForm = QtGui.QFormLayout(self.localAreaBox) + self.localAreaForm.setObjectName('host_local_area_form') + self.ip_number_label = QtGui.QLabel(self.localAreaBox) + self.ip_number_label.setObjectName('host_ip_number_label') + self.ip_number_text = QtGui.QLineEdit(self.localAreaBox) + self.ip_number_text.setObjectName('host_ip_number_text') + self.localAreaForm.addRow(self.ip_number_label, self.ip_number_text) + self.pjlink_port_label = QtGui.QLabel(self.localAreaBox) + self.pjlink_port_label.setObjectName('host_pjlink_port_label') + self.pjlink_port_text = QtGui.QLineEdit(self.localAreaBox) + self.pjlink_port_text.setMaxLength(5) + self.pjlink_port_text.setText(str(PJLINK_PORT)) + self.pjlink_port_text.setObjectName('host_pjlink_port_text') + self.localAreaForm.addRow(self.pjlink_port_label, self.pjlink_port_text) + self.pjlink_pin_label = QtGui.QLabel(self.localAreaBox) + self.pjlink_pin_label.setObjectName('host_pjlink_pin_label') + self.pjlink_pin_text = QtGui.QLineEdit(self.localAreaBox) + self.pjlink_pin_text.setObjectName('host_pjlink_pin_text') + self.localAreaForm.addRow(self.pjlink_pin_label, self.pjlink_pin_text) + self.hostPageLayout.addWidget(self.localAreaBox) + self.manufacturer_text = QtGui.QLineEdit(self) + self.manufacturer_text.setVisible(False) + self.model_text = QtGui.QLineEdit(self) + self.model_text.setVisible(False) + + def validatePage(self): + """ + Validate IP number/FQDN before continuing to next page. + """ + adx = self.wizard().field('ip_number') + port = self.wizard().field('pjlink_port') + pin = self.wizard().field('pjlink_pin') + log.debug('ip="%s" port="%s" pin="%s"' % (adx, port, pin)) + valid = verify_ip_address(adx) + if valid: + ip = self.wizard().db.get_projector_by_ip(adx) + if ip is None: + valid = True + else: + QtGui.QMessageBox.warning(self, + translate('OpenLP.ProjectorWizard', 'Already Saved'), + translate('OpenLP.ProjectorWizard', + 'IP "%s"
is already in the database as ID %s.' + '

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

Please enter a valid IP address.' % adx)) + valid = False + """ + FIXME - Future plan to retrieve manufacture/model input source information. Not implemented yet. + new = PJLink(host=adx, port=port, pin=pin if pin.strip() != '' else None) + if new.connect(): + mfg = new.get_manufacturer() + log.debug('Setting manufacturer_text to %s' % mfg) + self.manufacturer_text.setText(mfg) + model = new.get_model() + log.debug('Setting model_text to %s' % model) + self.model_text.setText(model) + else: + if new.status_error == E_AUTHENTICATION: + QtGui.QMessageBox.warning(self, + translate('OpenLP.ProjectorWizard', 'Requires Authorization'), + translate('OpenLP.ProjectorWizard', + 'Projector requires authorization and either PIN not set ' + 'or invalid PIN set.' + '
Enter a valid PIN before hitting "NEXT"') + ) + elif new.status_error == E_NO_AUTHENTICATION: + QtGui.QMessageBox.warning(self, + translate('OpenLP.ProjectorWizard', 'No Authorization Required'), + translate('OpenLP.ProjectorWizard', + 'Projector does not require authorization and PIN set.' + '
Remove PIN entry before hitting "NEXT"') + ) + valid = False + new.disconnect() + del(new) + """ + return valid + + +class ConnectEditPage(ConnectBase): + """ + Full information page. + """ + newProjector = QtCore.pyqtSignal(str) + editProjector = QtCore.pyqtSignal(object) + + def __init__(self, parent, page): + super().__init__(parent, page) + self.setObjectName('edit_page') + self.editPageLayout = QtGui.QHBoxLayout(self) + self.editPageLayout.setObjectName('layout') + # Projector DB information + self.localAreaBox = QtGui.QGroupBox(self) + self.localAreaBox.setObjectName('edit_local_area_box') + self.localAreaForm = QtGui.QFormLayout(self.localAreaBox) + self.localAreaForm.setObjectName('edit_local_area_form') + self.ip_number_label = QtGui.QLabel(self.localAreaBox) + self.ip_number_label.setObjectName('edit_ip_number_label') + self.ip_number_text = QtGui.QLineEdit(self.localAreaBox) + self.ip_number_text.setObjectName('edit_ip_number_text') + self.localAreaForm.addRow(self.ip_number_label, self.ip_number_text) + self.pjlink_port_label = QtGui.QLabel(self.localAreaBox) + self.pjlink_port_label.setObjectName('edit_pjlink_port_label') + self.pjlink_port_text = QtGui.QLineEdit(self.localAreaBox) + self.pjlink_port_text.setMaxLength(5) + self.pjlink_port_text.setObjectName('edit_pjlink_port_text') + self.localAreaForm.addRow(self.pjlink_port_label, self.pjlink_port_text) + self.pjlink_pin_label = QtGui.QLabel(self.localAreaBox) + self.pjlink_pin_label.setObjectName('pjlink_pin_label') + self.pjlink_pin_text = QtGui.QLineEdit(self.localAreaBox) + self.pjlink_pin_text.setObjectName('pjlink_pin_text') + self.localAreaForm.addRow(self.pjlink_pin_label, self.pjlink_pin_text) + self.name_label = QtGui.QLabel(self.localAreaBox) + self.name_label.setObjectName('name_label') + self.name_text = QtGui.QLineEdit(self.localAreaBox) + self.name_text.setObjectName('name_label') + self.name_text.setMaxLength(20) + self.localAreaForm.addRow(self.name_label, self.name_text) + self.location_label = QtGui.QLabel(self.localAreaBox) + self.location_label.setObjectName('location_label') + self.location_text = QtGui.QLineEdit(self.localAreaBox) + self.location_text.setObjectName('location_text') + self.location_text.setMaxLength(30) + self.localAreaForm.addRow(self.location_label, self.location_text) + self.notes_label = QtGui.QLabel(self.localAreaBox) + self.notes_label.setObjectName('notes_label') + self.notes_text = QtGui.QPlainTextEdit(self.localAreaBox) + self.notes_text.setObjectName('notes_text') + self.localAreaForm.addRow(self.notes_label, self.notes_text) + self.editPageLayout.addWidget(self.localAreaBox) + # Projector retrieved information + self.remoteAreaBox = QtGui.QGroupBox(self) + self.remoteAreaBox.setObjectName('edit_remote_area_box') + self.remoteAreaForm = QtGui.QFormLayout(self.remoteAreaBox) + self.remoteAreaForm.setObjectName('edit_remote_area_form') + self.projector_make_label = QtGui.QLabel(self.remoteAreaBox) + self.projector_make_label.setObjectName('projector_make_label') + self.projector_make_text = QtGui.QLabel(self.remoteAreaBox) + self.projector_make_text.setObjectName('projector_make_text') + self.remoteAreaForm.addRow(self.projector_make_label, self.projector_make_text) + self.projector_model_label = QtGui.QLabel(self.remoteAreaBox) + self.projector_model_label.setObjectName('projector_model_text') + self.projector_model_text = QtGui.QLabel(self.remoteAreaBox) + self.projector_model_text.setObjectName('projector_model_text') + self.remoteAreaForm.addRow(self.projector_model_label, self.projector_model_text) + self.editPageLayout.addWidget(self.remoteAreaBox) + + def initializePage(self): + """ + Fill in the blanks for information from previous page/projector to edit. + """ + if self.wizard().projector is not None: + log.debug('ConnectEditPage.initializePage() Editing existing projector') + self.ip_number_text.setText(self.wizard().projector.ip) + self.pjlink_port_text.setText(str(self.wizard().projector.port)) + self.pjlink_pin_text.setText(self.wizard().projector.pin) + self.name_text.setText(self.wizard().projector.name) + self.location_text.setText(self.wizard().projector.location) + self.notes_text.insertPlainText(self.wizard().projector.notes) + self.myButtons = [QtGui.QWizard.HelpButton, + QtGui.QWizard.Stretch, + QtGui.QWizard.FinishButton, + QtGui.QWizard.CancelButton] + else: + log.debug('Retrieving information from host page') + self.ip_number_text.setText(self.wizard().field('ip_number')) + self.pjlink_port_text.setText(self.wizard().field('pjlink_port')) + self.pjlink_pin_text.setText(self.wizard().field('pjlink_pin')) + make = self.wizard().field('projector_make') + model = self.wizard().field('projector_model') + if make is None or make.strip() == '': + self.projector_make_text.setText('Unavailable ') + else: + self.projector_make_text.setText(make) + if model is None or model.strip() == '': + self.projector_model_text.setText('Unavailable ') + else: + self.projector_model_text.setText(model) + self.myButtons = [QtGui.QWizard.HelpButton, + QtGui.QWizard.Stretch, + QtGui.QWizard.BackButton, + QtGui.QWizard.NextButton, + QtGui.QWizard.CancelButton] + + def validatePage(self): + """ + Last verification if editiing existing entry in case of IP change. Add entry to DB. + """ + log.debug('ConnectEditPage().validatePage()') + if self.wizard().projector is not None: + ip = self.ip_number_text.text() + port = self.pjlink_port_text.text() + name = self.name_text.text() + location = self.location_text.text() + notes = self.notes_text.toPlainText() + pin = self.pjlink_pin_text.text() + log.debug('edit-page() Verifying info : ip="%s"' % ip) + valid = verify_ip_address(ip) + if not valid: + QtGui.QMessageBox.warning(self, + translate('OpenLP.ProjectorWizard', 'Invalid IP'), + translate('OpenLP.ProjectorWizard', + 'IP "%s"
is not a valid IP address.' + '

Please enter a valid IP address.' % ip)) + return False + log.debug('Saving edited projector %s' % ip) + self.wizard().projector.ip = ip + self.wizard().projector.port = port + self.wizard().projector.name = name + self.wizard().projector.location = location + self.wizard().projector.notes = notes + self.wizard().projector.pin = pin + saved = self.db.update_projector(self.wizard().projector) + if not saved: + QtGui.QMessageBox.error(self, translate('OpenLP.ProjectorWizard', 'Database Error'), + translate('OpenLP.ProjectorWizard', 'There was an error saving projector ' + 'information. See the log for the error')) + return False + self.editProjector.emit(self.wizard().projector) + else: + projector = Projector(ip=self.wizard().field('ip_number'), + port=self.wizard().field('pjlink_port'), + name=self.wizard().field('projector_name'), + location=self.wizard().field('projector_location'), + notes=self.wizard().field('projector_notes'), + pin=self.wizard().field('pjlink_pin')) + log.debug('Adding new projector %s' % projector.ip) + if self.wizard().db.get_projector_by_ip(projector.ip) is None: + saved = self.wizard().db.add_projector(projector) + if not saved: + QtGui.QMessageBox.error(self, translate('OpenLP.ProjectorWizard', 'Database Error'), + translate('OpenLP.ProjectorWizard', 'There was an error saving projector ' + 'information. See the log for the error')) + return False + self.newProjector.emit('%s' % projector.ip) + return True + + def nextId(self): + """ + Returns the next page ID if new entry or end of wizard if editing entry. + """ + if self.wizard().projector is None: + return PAGE_NEXT[self.pageId] + else: + return -1 + + +class ConnectFinishPage(ConnectBase): + """ + Buh-Bye page + """ + def __init__(self, parent, page): + super().__init__(parent, page) + self.setObjectName('connect_finish_page') + self.setPixmap(QtGui.QWizard.WatermarkPixmap, QtGui.QPixmap(':/wizards/wizard_createprojector.png')) + self.myButtons = [QtGui.QWizard.Stretch, + QtGui.QWizard.FinishButton] + self.isFinalPage() + self.layout = QtGui.QVBoxLayout(self) + self.layout.setObjectName('layout') + self.title_label = QtGui.QLabel(self) + self.title_label.setObjectName('title_label') + self.layout.addWidget(self.title_label) + self.layout.addSpacing(40) + self.information_label = QtGui.QLabel(self) + self.information_label.setWordWrap(True) + self.information_label.setObjectName('information_label') + self.layout.addWidget(self.information_label) + self.layout.addStretch() diff --git a/openlp/core/ui/settingsform.py b/openlp/core/ui/settingsform.py index a1077e1f4..7045e7bd1 100644 --- a/openlp/core/ui/settingsform.py +++ b/openlp/core/ui/settingsform.py @@ -38,6 +38,7 @@ from openlp.core.lib import PluginStatus, build_icon from openlp.core.ui import AdvancedTab, GeneralTab, ThemesTab from openlp.core.ui.media import PlayerTab from .settingsdialog import Ui_SettingsDialog +from openlp.core.ui.projector.tab import ProjectorTab log = logging.getLogger(__name__) @@ -67,9 +68,10 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog, RegistryProperties): self.stacked_layout.takeAt(0) self.insert_tab(self.general_tab, 0, PluginStatus.Active) self.insert_tab(self.themes_tab, 1, PluginStatus.Active) - self.insert_tab(self.advanced_tab, 2, PluginStatus.Active) - self.insert_tab(self.player_tab, 3, PluginStatus.Active) - count = 4 + self.insert_tab(self.projector_tab, 2, PluginStatus.Active) + self.insert_tab(self.advanced_tab, 3, PluginStatus.Active) + self.insert_tab(self.player_tab, 4, PluginStatus.Active) + count = 5 for plugin in self.plugin_manager.plugins: if plugin.settings_tab: self.insert_tab(plugin.settings_tab, count, plugin.status) @@ -125,6 +127,8 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog, RegistryProperties): self.general_tab = GeneralTab(self) # Themes tab self.themes_tab = ThemesTab(self) + # Projector Tab + self.projector_tab = ProjectorTab(self) # Advanced tab self.advanced_tab = AdvancedTab(self) # Advanced tab diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index 0347cc3c7..2337a3f64 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -106,6 +106,7 @@ wizard_firsttime.bmp wizard_createtheme.bmp wizard_duplicateremoval.bmp + wizard_createprojector.png service_collapse_all.png @@ -169,6 +170,26 @@ theme_new.png theme_edit.png + + projector_blank.png + projector_connect.png + projector_connectors.png + projector_cooldown.png + projector_disconnect.png + projector_edit.png + projector_error.png + projector_manager.png + projector_new.png + projector_not_connected.png + projector_off.png + projector_on.png + projector_power_off.png + projector_power_on.png + projector_show.png + projector_status.png + projector_warmup.png + projector_view.png + android_app_qr.png diff --git a/resources/images/projector_blank.png b/resources/images/projector_blank.png new file mode 100644 index 0000000000000000000000000000000000000000..28feae350b406189502ce551014cdef402ffbcbc GIT binary patch literal 385 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=E9BL736%&ErQvLG}_)Usv{foUEeqN_T!{#{%W}JY5_^EKZ*dUg&qkL7?`st&^Us zuA*auil}I4M{dx;s1#G*U7UUV54=6Hv;;+cwFHhTuJT$H)!xK%^s11@&7%cP-rYNt z8>ict>Ko_BrLD-lt*o@kph@TSIotE6WsgTDFilZ8X`Gx>y(Xh)CUb@QqW+D$u2nhA zd#=xL#!9CvDN#`H8mr*JLv1%sFgC;&d|1x=&%I;wy4UkgZQ63|4ew!t^q#$f6aVcx zlRs-&rd(pr%oGvV>4h@y|1X(&#zC|6#jbZ7ul@4T+-fTMWR>pHEA!s}77q>G`>l3= z_U&)E0xZi*WA8KX@VgwczV1$uxsX!~r()icDeMe6=YGmQw_({J-1e|@%BRNScZ@T1 c+v}NfzD(FN)gWavFt8XrUHx3vIVCg!08;auM*si- literal 0 HcmV?d00001 diff --git a/resources/images/projector_connect.png b/resources/images/projector_connect.png new file mode 100644 index 0000000000000000000000000000000000000000..2e9b08c026466c9608bef059c886beadb4ecac23 GIT binary patch literal 1436 zcmX|93sll)6o+UI-RZ0>YqO5lY3t!5TFuAG)=WFaaj6NYh$Zspr6{5x3WSx*7Bro- z#L6d z2q{cKMtm9rm64ILA%V-`Bk?@OhLki`(O7^h3N`Oq__xHJb8~YL&tx(a6B82>50XZFs5irzhwZUM3I*2R{Drhtsd*Ax{dH{_^BdtJKTwF{$A*B5zl*weYlj(ra zl)~obX28##I|sV8wUsDFNMZz-Lr1Y#EIpW|*XzLsB<`X(u2cda^{gmDOanfRjg2rq zK|+g4rvowQ8z)KZ?(QZ^5QRdq{~#wfH@CdJ90;5)Is-TurKP1{ARMpx~Zwjo>Ey^+1}n>pi)-+a`o4n4FdxM8;dAW69wcaKO#qY zN~KbzQb8#g8C+iHW4uaYyo+PLD2m-yK;5Q{C+2ZT(}kqjQ>3}mkyC|{)2E_FPeiN5 z(a-Y9Wg>F9IQ}S~c8o_qmdeaYVaoWtoJ>*P;fjiis;VlOv97KTD(iGQxWl%#Hek}x z(E)7X{^7yEL=PT3=+3U{%@&JgczAeZWW;8(O-@eE%*?=Z)&GIH3s1I25D`j1 zq0wl}{P_+JSVu=Er;k2fxNwoPv-9F5OP77(x_tTa74EB6uUqfo>FMc>+q7x(mwtX* zw(rL8jfjXKfDlPhlw>ZK%j1a;XUk-A`N_i5XD+BNm8olQY3gq`fWRMVY;0-Kw*AYp zv&ZxgJz&`T5E%CNKLUCDc<||vW#swj=!=(QV`Jmv6BB>E2C-VLzh7N?@`^XU4%be-vPx_= zBW1C%xxw;$l%~dgx2BR%m+|DnK69;Fro@IgPNdd$&;u1w?EYKh>%!fx(57FRhNvkY zeoe&qmzvzrh7@z{(|d!@f^mxkYk9(#{SF^&4aGSXYI#UQc`~mGsr5Ljp}&nhlAXNr zP6_6v?>vtiWu~X3#(f&G*6}kJvtekj<#=s>pJvcM_U@Y;@s%vE{4dYuZ z+DmEin|)%xYDwh2I$(T(N_i{Wr5L@$3Kq literal 0 HcmV?d00001 diff --git a/resources/images/projector_connectors.png b/resources/images/projector_connectors.png new file mode 100644 index 0000000000000000000000000000000000000000..77a6a4dd7c49080621802da157606d39bd8c5892 GIT binary patch literal 8853 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 XFutHc`9y>{00000NkvXXu0mjfaclIX literal 0 HcmV?d00001 diff --git a/resources/images/projector_cooldown.png b/resources/images/projector_cooldown.png new file mode 100644 index 0000000000000000000000000000000000000000..c4a4140f5567cd5cad3228214b8ea0de4b29ee74 GIT binary patch literal 4487 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 Z`yaLb;o(OVCQ$$Y002ovPDHLkV1g`aU+(|_ literal 0 HcmV?d00001 diff --git a/resources/images/projector_disconnect.png b/resources/images/projector_disconnect.png new file mode 100644 index 0000000000000000000000000000000000000000..2c49e920de0d14c0a71d74dd585697db13d4e10e GIT binary patch literal 1267 zcmX|B2~3k`7;f>ZXfe#8;LKtO5tWXONw^t@E!e`!RYw6GS13ZqQOYPd3iX#FXM=#8 ziV7%kuyV*{!G;#bRiqs4=qMG+IiTez6hob`4^8$Y-~YY;o9};)H(<PrNHq`+3()3_u zI5?!GrPv8EL?RLN>+0%8M@NN1;b(@dtSmO0 z4OBQBPES{tKqx??(M%?@q@)Bi%gV}HTU*=P+h2b|czAeHQWBu8sHnhVv4DX@A^~qX zIXS>aO-)U0Z7oRD-Q8_sVq#`yW?^9g%v)Pq+uGU!dybBd&d$z&AA`ZTxVYeOI6NNj z;o;%s<>lk!Lm&`*eSQ6YLI>W&g#}{(GmyJ|`*sA05J|#CMn*4qoae% z<#tVU@pwG`&`2M^OkaP0|A0XB_U+r@;bAb?v9Yo7@o|wzBo>P&CMF~j$>ijuR4SdC znv%(6?*NpW(?a3&^z_Wkj9f0Cot>SVo134XUszaJTwGiNSXKWckt{DSudJ-BuC6LN zI~59rQmI^9Tie*!P^nbg+uLfjdS_>6cXwB#(fs>ye{W<2+#-8V%`tGR+~U3QJ}@`{ z0)a&8=$ryDGO{6)V`HgQI{hGOMMYh6b7yCFw?H7D1>!*Hlg9cdY^meY+1*9#oup!n_wwx&Gb-YX-oJz_v0}g#dI6ZLmG$DZoQ{> z-VWj2aH1GGo)ocrnsP-lDqTWqoz^6*cU1;0w<>@5u7PZO=7^g`%$8ccbqcZZeoMxU zy8HtAv)sb<@KI(OlX(~uE$uw)!>dG=L)W^iLP9F}RhBXSIWL3c8e}77^YRhD%bW7A zEKz(yLy(e_ktR~S$XAH^)vISJao=lgc8bZZaNds6F+^Fmdr5WPKN6KsEvhHJxr5ZN z`}s|XX_H;?4p*=DevQSqf!yRH`l!HnP#r@U+ov*k?8^GF%>FE#WH=yoe`58V6!zi4 zwJ(ST@F(i+ia~zFLICTWSxCWBi`L+hBG8t+^q1c6lKF3Vs=l4G_5NIs)7e@dm+gOD zcHEts+*6h*N>sn#zWzg;S#Vb+D>!M@sDB}HU3}B)K_R@bZHBjq)*IKbn>fAcYuH8N zf`R!>nION`hGSex2^F4D={z6Cu%fLNp4(rIy5+9;QXShbxI?lTcxjT|FxyY&b4u~v tiK#y0gJCgvG}}1{`VSDWogM%H literal 0 HcmV?d00001 diff --git a/resources/images/projector_edit.png b/resources/images/projector_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..84e345d22430175e80c4ea1f1f17e6790649ee35 GIT binary patch literal 726 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 Rgpa{%LKrj-Rfi-}{{w~vNoxQA literal 0 HcmV?d00001 diff --git a/resources/images/projector_off.png b/resources/images/projector_off.png new file mode 100644 index 0000000000000000000000000000000000000000..88e1ccb0cf339fb992f687d5a4a23178c1c6c04d GIT binary patch literal 3749 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 P00000NkvXXu0mjfT81f` literal 0 HcmV?d00001 diff --git a/resources/images/projector_on.png b/resources/images/projector_on.png new file mode 100644 index 0000000000000000000000000000000000000000..6555c7aac7d712b32db1e4d2ddde866428d58069 GIT binary patch literal 4266 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 M07*qoM6N<$g4j#*CjbBd literal 0 HcmV?d00001 diff --git a/resources/images/projector_power_off.png b/resources/images/projector_power_off.png new file mode 100644 index 0000000000000000000000000000000000000000..c415f34109b17f47a5a6831cba2c4393f6aa8c42 GIT binary patch literal 1219 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

*(LsvIJ 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 M07*qoM6N<$f=l|)^8f$< literal 0 HcmV?d00001 diff --git a/resources/images/projector_warmup.png b/resources/images/projector_warmup.png new file mode 100644 index 0000000000000000000000000000000000000000..b692bb6faa8d308f1fc99044a07cf4f9cd81d5e7 GIT binary patch literal 4554 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 literal 0 HcmV?d00001 diff --git a/resources/images/wizard_createprojector.png b/resources/images/wizard_createprojector.png new file mode 100644 index 0000000000000000000000000000000000000000..d17a01e41ff1c816f9dcfa6c3bb88e8ac98c11bf GIT binary patch literal 149097 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
vjP;&^G) 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 C?P6QrH<&R#T05%T!)i7DzbCAwZ#TYs-hL9 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ư>yj$)05Z6W52^WKf;Nz-y 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 literal 0 HcmV?d00001 diff --git a/scripts/generate_resources.sh b/scripts/generate_resources.sh index eb2cd7c46..3aa545756 100755 --- a/scripts/generate_resources.sh +++ b/scripts/generate_resources.sh @@ -53,5 +53,6 @@ cat openlp/core/resources.py.new | sed '/# Created: /d;/# by: /d' > openlp/ patch --posix -s openlp/core/resources.py scripts/resources.patch # Remove temporary file -rm openlp/core/resources.py.new - +rm openlp/core/resources.py.new 2>/dev/null +rm openlp/core/resources.py.old 2>/dev/null +rm openlp/core/resources.py.orig 2>/dev/null diff --git a/tests/functional/openlp_core_common/test_projector_utilities.py b/tests/functional/openlp_core_common/test_projector_utilities.py new file mode 100644 index 000000000..1a3063321 --- /dev/null +++ b/tests/functional/openlp_core_common/test_projector_utilities.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Package to test the openlp.core.ui.projector.networkutils package. +""" + +from unittest import TestCase + +from openlp.core.common import verify_ip_address, md5_hash, qmd5_hash + +salt = '498e4a67' +pin = 'JBMIAProjectorLink' +test_hash = '5d8409bc1c3fa39749434aa3a5c38682' + +ip4_loopback = '127.0.0.1' +ip4_local = '192.168.1.1' +ip4_broadcast = '255.255.255.255' +ip4_bad = '192.168.1.256' + +ip6_loopback = '::1' +ip6_link_local = 'fe80::223:14ff:fe99:d315' +ip6_bad = 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' + + +class testProjectorUtilities(TestCase): + """ + Validate functions in the projector utilities module + """ + def test_ip4_loopback_valid(self): + """ + Test IPv4 loopbackvalid + """ + # WHEN: Test with a local loopback test + valid = verify_ip_address(addr=ip4_loopback) + + # THEN: Verify we received True + self.assertTrue(valid, 'IPv4 loopback address should have been valid') + + def test_ip4_local_valid(self): + """ + Test IPv4 local valid + """ + # WHEN: Test with a local loopback test + valid = verify_ip_address(addr=ip4_local) + + # THEN: Verify we received True + self.assertTrue(valid, 'IPv4 local address should have been valid') + + def test_ip4_broadcast_valid(self): + """ + Test IPv4 broadcast valid + """ + # WHEN: Test with a local loopback test + valid = verify_ip_address(addr=ip4_broadcast) + + # THEN: Verify we received True + self.assertTrue(valid, 'IPv4 broadcast address should have been valid') + + def test_ip4_address_invalid(self): + """ + Test IPv4 address invalid + """ + # WHEN: Test with a local loopback test + valid = verify_ip_address(addr=ip4_bad) + + # THEN: Verify we received True + self.assertFalse(valid, 'Bad IPv4 address should not have been valid') + + def test_ip6_loopback_valid(self): + """ + Test IPv6 loopback valid + """ + # WHEN: Test IPv6 loopback address + valid = verify_ip_address(addr=ip6_loopback) + + # THEN: Validate return + self.assertTrue(valid, 'IPv6 loopback address should have been valid') + + def test_ip6_local_valid(self): + """ + Test IPv6 link-local valid + """ + # WHEN: Test IPv6 link-local address + valid = verify_ip_address(addr=ip6_link_local) + + # THEN: Validate return + self.assertTrue(valid, 'IPv6 link-local address should have been valid') + + def test_ip6_address_invalid(self): + """ + Test NetworkUtils IPv6 address invalid + """ + # WHEN: Given an invalid IPv6 address + valid = verify_ip_address(addr=ip6_bad) + + # THEN: Validate bad return + self.assertFalse(valid, 'IPv6 bad address should have been invalid') + + def test_md5_hash(self): + """ + Test MD5 hash from salt+data pass (python) + """ + # WHEN: Given a known salt+data + hash_ = md5_hash(salt=salt, data=pin) + + # THEN: Validate return has is same + self.assertEquals(hash_, test_hash, 'MD5 should have returned a good hash') + + def test_md5_hash_bad(self): + """ + Test MD5 hash from salt+data fail (python) + """ + # WHEN: Given a different salt+hash + hash_ = md5_hash(salt=pin, data=salt) + + # THEN: return data is different + self.assertNotEquals(hash_, test_hash, 'MD5 should have returned a bad hash') + + def test_qmd5_hash(self): + """ + Test MD5 hash from salt+data pass (Qt) + """ + # WHEN: Given a known salt+data + hash_ = qmd5_hash(salt=salt, data=pin) + + # THEN: Validate return has is same + self.assertEquals(hash_, test_hash, 'Qt-MD5 should have returned a good hash') + + def test_qmd5_hash_bad(self): + """ + Test MD5 hash from salt+hash fail (Qt) + """ + # WHEN: Given a different salt+hash + hash_ = qmd5_hash(salt=pin, data=salt) + + # THEN: return data is different + self.assertNotEquals(hash_, test_hash, 'Qt-MD5 should have returned a bad hash') diff --git a/tests/functional/openlp_core_lib/__init__.py b/tests/functional/openlp_core_lib/__init__.py index 0f0461449..0691295a6 100644 --- a/tests/functional/openlp_core_lib/__init__.py +++ b/tests/functional/openlp_core_lib/__init__.py @@ -27,5 +27,28 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -Package to test the openlp.core.lib package. +Module-level functions for the functional test suite """ + +import os +from tests.functional import patch + +from openlp.core.common import is_win + +from .test_projectordb import tmpfile + + +def setUp(): + if not is_win(): + # Wine creates a sharing violation during tests. Ignore. + try: + os.remove(tmpfile) + except: + pass + + +def tearDown(): + """ + Ensure test suite has been cleaned up after tests + """ + patch.stopall() diff --git a/tests/functional/openlp_core_lib/test_projectordb.py b/tests/functional/openlp_core_lib/test_projectordb.py new file mode 100644 index 000000000..8717825bb --- /dev/null +++ b/tests/functional/openlp_core_lib/test_projectordb.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Package to test the openlp.core.ui.projectordb find, edit, delete +record functions. + +PREREQUISITE: add_record() and get_all() functions validated. +""" + +from unittest import TestCase +from tests.functional import MagicMock, patch + +from openlp.core.lib.projectordb import Projector, ProjectorDB + +from tests.resources.projector.data import TEST1_DATA, TEST2_DATA, TEST3_DATA + +tmpfile = '/tmp/openlp-test-projectordb.sql' + + +def compare_data(one, two): + """ + Verify two Projector() instances contain the same data + """ + return one is not None and \ + two is not None and \ + one.ip == two.ip and \ + one.port == two.port and \ + one.name == two.name and \ + one.location == two.location and \ + one.notes == two.notes + + +def add_records(self, test): + """ + Add record if not in database + """ + record_list = self.projector.get_projector_all() + if len(record_list) < 1: + added = False + for record in test: + added = self.projector.add_projector(record) or added + return added + + for new_record in test: + added = None + for record in record_list: + if compare_data(record, new_record): + break + added = self.projector.add_projector(new_record) + return added + + +class TestProjectorDB(TestCase): + """ + Test case for ProjectorDB + """ + def setUp(self): + """ + Set up anything necessary for all tests + """ + if not hasattr(self, 'projector'): + with patch('openlp.core.lib.projectordb.init_url') as mocked_init_url: + mocked_init_url.start() + mocked_init_url.return_value = 'sqlite:///%s' % tmpfile + self.projector = ProjectorDB() + + def find_record_by_ip_test(self): + """ + Test find record by IP + """ + # GIVEN: Record entries in database + add_records(self, [TEST1_DATA, TEST2_DATA]) + + # WHEN: Search for record using IP + record = self.projector.get_projector_by_ip(TEST2_DATA.ip) + + # THEN: Verify proper record returned + self.assertTrue(compare_data(TEST2_DATA, record), + 'Record found should have been test_2 data') + + def find_record_by_name_test(self): + """ + Test find record by name + """ + # GIVEN: Record entries in database + add_records(self, [TEST1_DATA, TEST2_DATA]) + + # WHEN: Search for record using name + record = self.projector.get_projector_by_name(TEST2_DATA.name) + + # THEN: Verify proper record returned + self.assertTrue(compare_data(TEST2_DATA, record), + 'Record found should have been test_2 data') + + def record_delete_test(self): + """ + Test record can be deleted + """ + # GIVEN: Record in database + add_records(self, [TEST3_DATA, ]) + record = self.projector.get_projector_by_ip(TEST3_DATA.ip) + + # WHEN: Record deleted + self.projector.delete_projector(record) + + # THEN: Verify record not retrievable + found = self.projector.get_projector_by_ip(TEST3_DATA.ip) + self.assertFalse(found, 'test_3 record should have been deleted') + + def record_edit_test(self): + """ + Test edited record returns the same record ID with different data + """ + # GIVEN: Record entries in database + add_records(self, [TEST1_DATA, TEST2_DATA]) + + # WHEN: We retrieve a specific record + record = self.projector.get_projector_by_ip(TEST1_DATA.ip) + record_id = record.id + + # WHEN: Data is changed + record.ip = TEST3_DATA.ip + record.port = TEST3_DATA.port + record.pin = TEST3_DATA.pin + record.name = TEST3_DATA.name + record.location = TEST3_DATA.location + record.notes = TEST3_DATA.notes + updated = self.projector.update_projector(record) + self.assertTrue(updated, 'Save updated record should have returned True') + record = self.projector.get_projector_by_ip(TEST3_DATA.ip) + + # THEN: Record ID should remain the same, but data should be changed + self.assertEqual(record_id, record.id, 'Edited record should have the same ID') + self.assertTrue(compare_data(TEST3_DATA, record), 'Edited record should have new data') diff --git a/tests/interfaces/openlp_core_ui/__init__.py b/tests/interfaces/openlp_core_ui/__init__.py index 6b241e7fc..34420bdb8 100644 --- a/tests/interfaces/openlp_core_ui/__init__.py +++ b/tests/interfaces/openlp_core_ui/__init__.py @@ -26,3 +26,28 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +""" +Module-level functions for the functional test suite +""" + +from tests.interfaces import patch + +from openlp.core.common import is_win + +from .test_projectormanager import tmpfile + + +def setUp(): + if not is_win(): + # Wine creates a sharing violation during tests. Ignore. + try: + os.remove(tmpfile) + except: + pass + + +def tearDown(): + """ + Ensure test suite has been cleaned up after tests + """ + patch.stopall() diff --git a/tests/interfaces/openlp_core_ui/test_projectormanager.py b/tests/interfaces/openlp_core_ui/test_projectormanager.py new file mode 100644 index 000000000..e12f9a74a --- /dev/null +++ b/tests/interfaces/openlp_core_ui/test_projectormanager.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Interface tests to test the themeManager class and related methods. +""" +from unittest import TestCase + +from openlp.core.common import Registry, Settings +from tests.functional import patch, MagicMock +from tests.helpers.testmixin import TestMixin + +from openlp.core.ui import ProjectorManager, ProjectorWizard +from openlp.core.lib.projectordb import Projector, ProjectorDB + +from tests.resources.projector.data import TEST1_DATA, TEST2_DATA, TEST3_DATA + +tmpfile = '/tmp/openlp-test-projectormanager.sql' + + +class TestProjectorManager(TestCase, TestMixin): + """ + Test the functions in the ProjectorManager module + """ + def setUp(self): + """ + Create the UI and setup necessary options + """ + self.build_settings() + self.get_application() + Registry.create() + if not hasattr(self, 'projector_manager'): + with patch('openlp.core.lib.projectordb.init_url') as mocked_init_url: + mocked_init_url.start() + mocked_init_url.return_value = 'sqlite:///%s' % tmpfile + self.projectordb = ProjectorDB() + if not hasattr(self, 'projector_manager'): + self.projector_manager = ProjectorManager(projectordb=self.projectordb) + + def tearDown(self): + """ + Delete all the C++ objects at the end so that we don't have a segfault + """ + self.destroy_settings() + + def bootstrap_initialise_test(self): + """ + Test initialize calls correct startup functions + """ + # WHEN: we call bootstrap_initialise + self.projector_manager.bootstrap_initialise() + # THEN: ProjectorDB is setup + self.assertEqual(type(self.projector_manager.projectordb), ProjectorDB, + 'Initialization should have created a ProjectorDB() instance') + + def bootstrap_post_set_up_test(self): + """ + Test post-initialize calls proper setups + """ + # GIVEN: setup mocks + self.projector_manager.load_projectors = MagicMock() + + # WHEN: Call to initialize is run + self.projector_manager.bootstrap_initialise() + self.projector_manager.bootstrap_post_set_up() + + # THEN: verify calls to retrieve saved projectors + self.assertEqual(1, self.projector_manager.load_projectors.call_count, + 'Initialization should have called load_projectors()') + + # THEN: Verify wizard page is initialized + self.assertEqual(type(self.projector_manager.projector_form), ProjectorWizard, + 'Initialization should have created a Wizard') + self.assertIs(self.projector_manager.projectordb, + self.projector_manager.projector_form.db, + 'Wizard should be using same ProjectorDB() instance') diff --git a/tests/resources/projector/data.py b/tests/resources/projector/data.py new file mode 100644 index 000000000..798e8af36 --- /dev/null +++ b/tests/resources/projector/data.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`tests.resources.projector.data file contains test data +""" + +from openlp.core.lib.projectordb import Projector + +# Test data +TEST1_DATA = Projector(ip='111.111.111.111', + port='1111', + pin='1111', + name='___TEST_ONE___', + location='location one', + notes='notes one') + +TEST2_DATA = Projector(ip='222.222.222.222', + port='2222', + pin='2222', + name='___TEST_TWO___', + location='location two', + notes='notes two') + +TEST3_DATA = Projector(ip='333.333.333.333', + port='3333', + pin='3333', + name='___TEST_THREE___', + location='location three', + notes='notes three') From 7545fb534f0d9c10e3ff25c91246ac8b3a99a3ee Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 6 Oct 2014 14:19:08 -0700 Subject: [PATCH 002/115] Fix changed imports in mockups --- tests/functional/openlp_core_lib/test_projectordb.py | 4 ++-- tests/interfaces/openlp_core_ui/test_projectormanager.py | 4 ++-- tests/resources/projector/data.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/functional/openlp_core_lib/test_projectordb.py b/tests/functional/openlp_core_lib/test_projectordb.py index 8717825bb..ba79b59c3 100644 --- a/tests/functional/openlp_core_lib/test_projectordb.py +++ b/tests/functional/openlp_core_lib/test_projectordb.py @@ -36,7 +36,7 @@ PREREQUISITE: add_record() and get_all() functions validated. from unittest import TestCase from tests.functional import MagicMock, patch -from openlp.core.lib.projectordb import Projector, ProjectorDB +from openlp.core.lib.projector.db import Projector, ProjectorDB from tests.resources.projector.data import TEST1_DATA, TEST2_DATA, TEST3_DATA @@ -85,7 +85,7 @@ class TestProjectorDB(TestCase): Set up anything necessary for all tests """ if not hasattr(self, 'projector'): - with patch('openlp.core.lib.projectordb.init_url') as mocked_init_url: + 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() diff --git a/tests/interfaces/openlp_core_ui/test_projectormanager.py b/tests/interfaces/openlp_core_ui/test_projectormanager.py index e12f9a74a..c9218e0aa 100644 --- a/tests/interfaces/openlp_core_ui/test_projectormanager.py +++ b/tests/interfaces/openlp_core_ui/test_projectormanager.py @@ -36,7 +36,7 @@ from tests.functional import patch, MagicMock from tests.helpers.testmixin import TestMixin from openlp.core.ui import ProjectorManager, ProjectorWizard -from openlp.core.lib.projectordb import Projector, ProjectorDB +from openlp.core.lib.projector.db import Projector, ProjectorDB from tests.resources.projector.data import TEST1_DATA, TEST2_DATA, TEST3_DATA @@ -55,7 +55,7 @@ class TestProjectorManager(TestCase, TestMixin): self.get_application() Registry.create() if not hasattr(self, 'projector_manager'): - with patch('openlp.core.lib.projectordb.init_url') as mocked_init_url: + 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() diff --git a/tests/resources/projector/data.py b/tests/resources/projector/data.py index 798e8af36..37923d5b7 100644 --- a/tests/resources/projector/data.py +++ b/tests/resources/projector/data.py @@ -30,7 +30,7 @@ The :mod:`tests.resources.projector.data file contains test data """ -from openlp.core.lib.projectordb import Projector +from openlp.core.lib.projector.db import Projector # Test data TEST1_DATA = Projector(ip='111.111.111.111', From c789d1c0eda0abc99a329c6a1a78c3f6776b9eed Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 7 Oct 2014 12:37:55 -0700 Subject: [PATCH 003/115] Missed db reference in edit page --- .bzrignore | 6 ++++++ openlp/core/ui/projector/wizard.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.bzrignore b/.bzrignore index 390fde6af..6b7b989a6 100644 --- a/.bzrignore +++ b/.bzrignore @@ -34,3 +34,9 @@ __pycache__ *.dll .directory *.kate-swp +# Git files +.git +.gitignore +# Rejected diff's +*.rej +*.~\?~ diff --git a/openlp/core/ui/projector/wizard.py b/openlp/core/ui/projector/wizard.py index 82168aed0..d60019471 100644 --- a/openlp/core/ui/projector/wizard.py +++ b/openlp/core/ui/projector/wizard.py @@ -492,7 +492,7 @@ class ConnectEditPage(ConnectBase): self.wizard().projector.location = location self.wizard().projector.notes = notes self.wizard().projector.pin = pin - saved = self.db.update_projector(self.wizard().projector) + saved = self.wizard().db.update_projector(self.wizard().projector) if not saved: QtGui.QMessageBox.error(self, translate('OpenLP.ProjectorWizard', 'Database Error'), translate('OpenLP.ProjectorWizard', 'There was an error saving projector ' From 6a75726eed6d29dcc68b5253ca67502cac6d6615 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 8 Oct 2014 13:01:47 -0700 Subject: [PATCH 004/115] Update wizard setup for Mac OSX --- openlp/core/ui/projector/wizard.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openlp/core/ui/projector/wizard.py b/openlp/core/ui/projector/wizard.py index d60019471..e11ef962f 100644 --- a/openlp/core/ui/projector/wizard.py +++ b/openlp/core/ui/projector/wizard.py @@ -72,7 +72,10 @@ class ProjectorWizard(QtGui.QWizard, RegistryProperties): self.setObjectName('projector_wizard') self.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) self.setModal(True) - self.setWizardStyle(QtGui.QWizard.ModernStyle) + if is_macosx(): + self.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) + else: + self.setWizardStyle(QtGui.QWizard.ModernStyle) self.setMinimumSize(650, 550) self.setOption(QtGui.QWizard.NoBackButtonOnStartPage) self.spacer = QtGui.QSpacerItem(10, 0, From 06da940310a2bc2ecc0588c766e7805ec710e93d Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 8 Oct 2014 13:03:33 -0700 Subject: [PATCH 005/115] Update wizard setup for Mac OSX on wizard pages --- openlp/core/ui/projector/wizard.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openlp/core/ui/projector/wizard.py b/openlp/core/ui/projector/wizard.py index e11ef962f..7db026ba2 100644 --- a/openlp/core/ui/projector/wizard.py +++ b/openlp/core/ui/projector/wizard.py @@ -242,8 +242,11 @@ class ConnectWelcomePage(ConnectBase): """ def __init__(self, parent, page): super().__init__(parent, page) - self.setPixmap(QtGui.QWizard.WatermarkPixmap, - QtGui.QPixmap(':/wizards/wizard_createprojector.png')) + if is_macosx(): + self.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) + else: + self.setPixmap(QtGui.QWizard.WatermarkPixmap, + QtGui.QPixmap(':/wizards/wizard_createprojector.png')) self.setObjectName('welcome_page') self.myButtons = [QtGui.QWizard.Stretch, QtGui.QWizard.NextButton] @@ -537,7 +540,10 @@ class ConnectFinishPage(ConnectBase): def __init__(self, parent, page): super().__init__(parent, page) self.setObjectName('connect_finish_page') - self.setPixmap(QtGui.QWizard.WatermarkPixmap, QtGui.QPixmap(':/wizards/wizard_createprojector.png')) + if is_macosx(): + self.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) + else: + self.setPixmap(QtGui.QWizard.WatermarkPixmap, QtGui.QPixmap(':/wizards/wizard_createprojector.png')) self.myButtons = [QtGui.QWizard.Stretch, QtGui.QWizard.FinishButton] self.isFinalPage() From 1eee08ff6b80200fd18785a6ef8e9905ef6944ab Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 8 Oct 2014 13:04:50 -0700 Subject: [PATCH 006/115] pep8 on wizard.py --- openlp/core/ui/projector/wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/ui/projector/wizard.py b/openlp/core/ui/projector/wizard.py index 7db026ba2..2583f041d 100644 --- a/openlp/core/ui/projector/wizard.py +++ b/openlp/core/ui/projector/wizard.py @@ -246,7 +246,7 @@ class ConnectWelcomePage(ConnectBase): self.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) else: self.setPixmap(QtGui.QWizard.WatermarkPixmap, - QtGui.QPixmap(':/wizards/wizard_createprojector.png')) + QtGui.QPixmap(':/wizards/wizard_createprojector.png')) self.setObjectName('welcome_page') self.myButtons = [QtGui.QWizard.Stretch, QtGui.QWizard.NextButton] From db2d2ca3b33fa235aef12f4b372201f57543a3b6 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 8 Oct 2014 13:21:59 -0700 Subject: [PATCH 007/115] Missed import for is_macosx --- openlp/core/ui/projector/wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/ui/projector/wizard.py b/openlp/core/ui/projector/wizard.py index 2583f041d..73dadd109 100644 --- a/openlp/core/ui/projector/wizard.py +++ b/openlp/core/ui/projector/wizard.py @@ -40,7 +40,7 @@ from ipaddress import IPv4Address, IPv6Address, AddressValueError from PyQt4 import QtCore, QtGui from PyQt4.QtCore import pyqtSlot, pyqtSignal -from openlp.core.common import Registry, RegistryProperties, translate +from openlp.core.common import Registry, RegistryProperties, translate, is_macosx from openlp.core.lib import build_icon from openlp.core.common import verify_ip_address From d4b6c067ef1db9a7bfe92e11a36439502856e8d7 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 8 Oct 2014 15:36:23 -0700 Subject: [PATCH 008/115] Fix issue with source select being None --- openlp/core/lib/projector/pjlink1.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index f5866dd8e..d049173d9 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -191,6 +191,8 @@ class PJLink1(QTcpSocket): for i in ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']: self.send_command(i) self.waitForReadyRead() + if self.source_available is None: + self.send_command('INST') def _get_status(self, status): """ From 853826a501b8b2ed635ba929d02c102b72cc5b0a Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 8 Oct 2014 18:43:34 -0700 Subject: [PATCH 009/115] Icon/tranlate fixes --- openlp/core/lib/projector/pjlink1.py | 22 ++++++++++-------- openlp/core/ui/projector/manager.py | 4 ++-- resources/images/openlp-2.qrc | 2 ++ resources/images/projector_item_connect.png | Bin 0 -> 1369 bytes .../images/projector_item_disconnect.png | Bin 0 -> 1408 bytes resources/images/projector_manager.png | Bin 7512 -> 842 bytes resources/images/projector_new.png | Bin 720 -> 781 bytes resources/images/projector_not_connected.png | Bin 1160 -> 1416 bytes 8 files changed, 16 insertions(+), 12 deletions(-) create mode 100644 resources/images/projector_item_connect.png create mode 100644 resources/images/projector_item_disconnect.png diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index d049173d9..07efaebb7 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -45,7 +45,7 @@ The :mod:`projector.pjlink1` module provides the necessary functions import logging log = logging.getLogger(__name__) -log.debug('projectorpjlink1 loaded') +log.debug('rpjlink1 loaded') __all__ = ['PJLink1'] @@ -188,10 +188,10 @@ class PJLink1(QTcpSocket): log.debug('(%s) Updating projector status' % self.ip) # Reset timer in case we were called from a set command self.timer.start() - for i in ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']: - self.send_command(i) + for command in ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']: + self.send_command(command) self.waitForReadyRead() - if self.source_available is None: + if self.power == S_ON and self.source_available is None: self.send_command('INST') def _get_status(self, status): @@ -204,14 +204,14 @@ class PJLink1(QTcpSocket): elif status in STATUS_STRING: return (STATUS_STRING[status], ERROR_MSG[status]) else: - return (status, 'Unknown status') + 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. """ - message = 'No message' if msg is None else msg + 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 @@ -275,8 +275,8 @@ class PJLink1(QTcpSocket): self.waitForReadyRead() # These should never change once we get this information if self.manufacturer is None: - for i in ['INF1', 'INF2', 'INFO', 'NAME', 'INST']: - self.send_command(cmd=i) + for command in ['INF1', 'INF2', 'INFO', 'NAME', 'INST']: + self.send_command(cmd=command) self.waitForReadyRead() self.change_status(S_CONNECTED) if not self.new_wizard: @@ -364,7 +364,8 @@ class PJLink1(QTcpSocket): if sent == -1: # Network error? self.projectorNetwork.emit(S_NETWORK_RECEIVED) - self.change_status(E_NETWORK, 'Error while sending data to projector') + self.change_status(E_NETWORK, + translate('OpenLP.PJLink1', 'Error while sending data to projector')) def process_command(self, cmd, data): """ @@ -379,7 +380,8 @@ class PJLink1(QTcpSocket): return elif data.upper() == 'ERR1': # Undefined command - self.change_status(E_UNDEFINED, 'Undefined command: "%s"' % cmd) + self.change_status(E_UNDEFINED, '%s "%s"' % + (translate('OpenLP.PJLink1', 'Undefined command:'), cmd)) return elif data.upper() == 'ERR2': # Invalid parameter diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index cb25f5ee3..b082840ae 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -51,8 +51,8 @@ from openlp.core.ui.projector.wizard import ProjectorWizard from openlp.core.lib.projector.constants import * # Dict for matching projector status to display icon -STATUS_ICONS = {S_NOT_CONNECTED: ':/projector/projector_disconnect.png', - S_CONNECTING: ':/projector/projector_connect.png', +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', diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index 2337a3f64..2471ddda5 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -178,6 +178,8 @@ projector_disconnect.png projector_edit.png projector_error.png + projector_item_connect.png + projector_item_disconnect.png projector_manager.png projector_new.png projector_not_connected.png diff --git a/resources/images/projector_item_connect.png b/resources/images/projector_item_connect.png new file mode 100644 index 0000000000000000000000000000000000000000..a976fcecd76f857a13021e440f472505e236bf21 GIT binary patch literal 1369 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 A1poj5 literal 0 HcmV?d00001 diff --git a/resources/images/projector_manager.png b/resources/images/projector_manager.png index 69a481cb0b3b2c3ee71569cbe7a218f1748ef0b8..770fa572e6620d9fd491e3daebc5cebb777c4e87 100644 GIT binary patch delta 825 zcmV-91IGN=I?4t!iBL{Q4GJ0x0000DNk~Le0000G0000G2nGNE03Y-JVE_OC24YJ` zL;(K){{a7>y{D6rAs>GV2>}x`45i9v0008lNkl|cQRj@ zv^LX(q0^QGO_4zTs3>h(b>TF0=@x1W}?Q5e>u! zH6~7N^C6S@x^vGx=e+I&OLu*iKX?`oJU_aKMyCA0Z`)A`Hai{qK(ny@7@QCKSg!-ac0e z^yT$BaWX4OQ6X~yMa}5<`kLd+N!UG<6}81d`QXKco5~>|L%<};@}c}?%-7!?M^qjc zC3{GF2MEIvM)`j>a@mWHHj;M>cCL;Z!r5Aru6_G7y@scvcCk%gjkanwcm|LG)v89N z;;FMJL{_n10h?PP=!wMs<2`g01!MDET73MJM4$EZrX*}#_9lia=Va`GWf|OhMx1*T;wKRXvw%*=$JBCK zGH2p;=SLOXfqJje*b8?5>P2yv&Dzpb@Vi%nDc?k}F^P!)VW^ZLTmh6SSh+(hzh57i z(!=7NsMSA?%A0gvpMU;Dp0&2#lFlb{aiDj|4)%RO_0R$4~mO^xH{#{r0taOA@=Qr@q1V2K3U{q5w^u*`NL%zOqagaN68U z69)x?fDurJmVJO29vURiVkUfnb!I5g2ahRpKRW*l-P%;N|F4Vh00000NkvXXu0mjf DC?kP> literal 7512 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 UR1}-oX#fBK07*qoM6N<$g1vs`Jpcdz From 0feca36ede38078e10829f5dd7daf8af362c1e29 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 8 Oct 2014 18:56:45 -0700 Subject: [PATCH 010/115] Add multi-selection to projector item widgets --- openlp/core/ui/projector/manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index b082840ae..afcd80c90 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -128,6 +128,7 @@ class Ui_ProjectorManager(object): self.projector_widget.setObjectName('projector_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) From 6c4b8e5b91202488c013c4e98affa98c8613e0dc Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 8 Oct 2014 19:05:53 -0700 Subject: [PATCH 011/115] Revert multiselection --- openlp/core/ui/projector/manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index afcd80c90..b082840ae 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -128,7 +128,6 @@ class Ui_ProjectorManager(object): self.projector_widget.setObjectName('projector_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) From b9199f8856b89e770a433987cfbf6d0d2c0a1705 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 8 Oct 2014 19:47:32 -0700 Subject: [PATCH 012/115] Fixed pixelated icons --- resources/images/projector_item_connect.png | Bin 1369 -> 2329 bytes .../images/projector_item_disconnect.png | Bin 1408 -> 2735 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/images/projector_item_connect.png b/resources/images/projector_item_connect.png index a976fcecd76f857a13021e440f472505e236bf21..6d5b27b5b4d1cabe1ddc0230010be1eaf596c389 100644 GIT binary patch literal 2329 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 delta 1400 zcmX9;2~d)082&+9vdgUP(lw9RbaH3XS+f*ej|EdCOrgZA)pqlUk|Y}gQ-35i&ong= z(bB<<91rqJJVR+09aB(oATPXI6Rk{TJ9gQx?V0&z-uFA6_j%rV=M(K=Z_Pab8$b|L z+MJ|?AY{O4A_Wi9;NW19NTk>6LCj{eZES4n>gv#FbVWsliHV7|wRKur8jr_2e*Abs zLIQ)qaB*>gVc5~pF*Y{#>)XV{M1ery=H`~1oQ%a{0f~o)2Z+ICUteDk5Q)T?n3$51 z61cm&n@XjEgX-$)!NEbfT>h0JBO{}>wic9WZEbxnk@U&?a5!8_N=i{t5!fs)E`Iv- zsaPy-`5zGx5iAx9@IH9(z{kf2FocJP194edSwM$SD6Fcg0+yswshypjgM)*UlM_(y z?CgwzF&H4w-QC^O(-RcH2%=X`n3x;V*;5>CQuU}9}g5HGS9F%Yz~KWo5M*-3IhgG zQ&aDVw4jCQ>FF66a7HG8GBZ0n`|jPlxw*M{d3gl|1^2Uq3JVLlTrQu_F9mq6FDol6 ze^m~wfIa~$_4V})4GoQrjZIBW&CShiZEc;Mof4I#tE;Q~g+c}}CF|+w>FrbY_xBGB z41m@S4Gj$s4=a_*k&zLVN~Kn-M@L8Dv9Yo7@$rd?iAjLvcN)1|qtQ%FO-)Zv&&Q}uTqgmAxOs1Z`V!UdeaeH zZe4hB4@w+uT)h>QVTM4_!+5r2h2bH=nK?ZzV05%?!{f!hr3=rFS{Xm{JBXZ~nSC!u zLH2O*9~?{L$g_^xG|RnPw_S0>L0qn<6^B~m#GhD+j`xT;p^fGD)AnpZu=hi$A#72k z8^X}abY~{&8y5@;_5H3HL}u6(Uv!+#u8xY1V?Lp#A(XVuoM|1LY-vy2Ru;hP-$}XS zWqq&LLeX<+S7q&vR4ebA!^qP|P91LG!6cDLc>1w2Cz5g`r90F;_hof}3BvYUFF~(T zp-_7Z9C!~EMMTq@%HFF^yaIwX)G{7wZ_4KfpQHZZbSv_4Gq*pyF0zqtCh#w}M2f|V z#kDw>j;8z4>FfLU$^I0d-*KnQe*aorOUdRbEm1H?E4R34Ox4+xlB!(Ybcf-cW@ZP{ z&HYFaWO&Rpi)DmCbq$l>AQ+5T%;rbGySgqf8yQM;5>592x3C>mQ4yL`b^CVHG2!rh z9w8^kPd_?cev|zFT^JXa&Jv) z+%Rn&+oht;pXip3jjeGkR$OaMRD{e`qs}Gd=Z7Sqq7Cr)rY2-pMB~QB3SEcAjNLwJ zLbAkS5B)}`CzQ%&-|ySI@T!h=wh*O~ZO5Gqq)^VhW15?j$&9KhHTK70L5LOI()O{K z8Q)%g^%D5#>W5~X=@Pl<(IcU-tgIdD+VR95jdmE6F3lPukxy8cFNcOwC{(rCFV)co wMn>v!m9AeDD^8l4`kM*E-yP+Oll1M^R$p#9a?sYQ4U976gD2r?JwrMF0m6E~{{R30 From 842898aaf9c96306f22da61a788c47da423bdae6 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 9 Oct 2014 07:41:32 -0700 Subject: [PATCH 013/115] Remove extraneous imports --- openlp/core/lib/projector/db.py | 7 +++---- openlp/core/ui/projector/manager.py | 11 ++++------- openlp/core/ui/projector/tab.py | 3 +-- openlp/core/ui/projector/wizard.py | 2 +- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/openlp/core/lib/projector/db.py b/openlp/core/lib/projector/db.py index d7abf6fe0..07881a1df 100644 --- a/openlp/core/lib/projector/db.py +++ b/openlp/core/lib/projector/db.py @@ -37,14 +37,13 @@ log.debug('projector.lib.db module loaded') from os import path -from sqlalchemy import Column, ForeignKey, Integer, MetaData, Sequence, String, and_ +from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, and_ from sqlalchemy.ext.declarative import declarative_base, declared_attr -from sqlalchemy.orm import backref, joinedload, relationship +from sqlalchemy.orm import backref, relationship from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound -from sqlalchemy.sql import select from openlp.core.common import translate -from openlp.core.lib.db import BaseModel, Manager, init_db, init_url +from openlp.core.lib.db import Manager, init_db, init_url from openlp.core.lib.projector.constants import PJLINK_DEFAULT_SOURCES metadata = MetaData() diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index b082840ae..ec9207a2b 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -38,17 +38,14 @@ log.debug('projectormanager loaded') from PyQt4 import QtCore, QtGui from PyQt4.QtCore import QObject, QThread, pyqtSlot -from openlp.core.common import Registry, RegistryProperties, Settings, OpenLPMixin, \ +from openlp.core.common import RegistryProperties, Settings, OpenLPMixin, \ RegistryMixin, translate -from openlp.core.lib import OpenLPToolbar, ImageSource, get_text_file_string, build_icon,\ - check_item_selected, create_thumb -from openlp.core.lib.ui import critical_error_message_box, create_widget_action -from openlp.core.utils import get_locale_key, get_filesystem_encoding - +from openlp.core.lib import OpenLPToolbar +from openlp.core.lib.ui import create_widget_action +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.wizard import ProjectorWizard -from openlp.core.lib.projector.constants import * # Dict for matching projector status to display icon STATUS_ICONS = {S_NOT_CONNECTED: ':/projector/projector_item_disconnect.png', diff --git a/openlp/core/ui/projector/tab.py b/openlp/core/ui/projector/tab.py index 1d0ed90f2..0a79595c9 100644 --- a/openlp/core/ui/projector/tab.py +++ b/openlp/core/ui/projector/tab.py @@ -37,9 +37,8 @@ log.debug('projectortab module loaded') from PyQt4 import QtCore, QtGui -from openlp.core.common import Registry, Settings, UiStrings, translate +from openlp.core.common import Settings, UiStrings, translate from openlp.core.lib import SettingsTab -from openlp.core.lib.ui import find_and_set_in_combo_box class ProjectorTab(SettingsTab): diff --git a/openlp/core/ui/projector/wizard.py b/openlp/core/ui/projector/wizard.py index 73dadd109..e95c68e30 100644 --- a/openlp/core/ui/projector/wizard.py +++ b/openlp/core/ui/projector/wizard.py @@ -40,7 +40,7 @@ from ipaddress import IPv4Address, IPv6Address, AddressValueError from PyQt4 import QtCore, QtGui from PyQt4.QtCore import pyqtSlot, pyqtSignal -from openlp.core.common import Registry, RegistryProperties, translate, is_macosx +from openlp.core.common import RegistryProperties, translate, is_macosx from openlp.core.lib import build_icon from openlp.core.common import verify_ip_address From b8f84f79eb01517828d01408434da047f6bfded9 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 9 Oct 2014 13:30:07 -0700 Subject: [PATCH 014/115] Fix previous reviews --- openlp/core/common/__init__.py | 4 +-- openlp/core/common/uistrings.py | 6 ++--- openlp/core/lib/projector/constants.py | 7 +++-- openlp/core/lib/projector/db.py | 8 +++--- openlp/core/lib/projector/pjlink1.py | 36 +++++++++++++++----------- openlp/core/ui/mainwindow.py | 4 +-- openlp/core/ui/projector/manager.py | 2 +- 7 files changed, 36 insertions(+), 31 deletions(-) diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index 47023a588..833aa5d10 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -217,8 +217,8 @@ def md5_hash(salt, data): def qmd5_hash(salt, data): """ - Returns the hashed output of MD%Sum on salt, data - using PyQt4.QCryptograhicHash. + Returns the hashed output of MD5Sum on salt, data + using PyQt4.QCryptographicHash. :param salt: Initial salt :param data: Data to hash diff --git a/openlp/core/common/uistrings.py b/openlp/core/common/uistrings.py index c1e7ebb8b..6b952cc39 100644 --- a/openlp/core/common/uistrings.py +++ b/openlp/core/common/uistrings.py @@ -99,9 +99,9 @@ class UiStrings(object): self.LiveBGError = translate('OpenLP.Ui', 'Live Background Error') self.LiveToolbar = translate('OpenLP.Ui', 'Live Toolbar') self.Load = translate('OpenLP.Ui', 'Load') - self.Manufacturer = translate('OpenLP.Ui', 'Manufacturer', 'Singluar') + self.Manufacturer = translate('OpenLP.Ui', 'Manufacturer', 'Singular') self.Manufacturers = translate('OpenLP.Ui', 'Manufacturers', 'Plural') - self.Model = translate('OpenLP.Ui', 'Model', 'Singluar') + 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') @@ -122,7 +122,7 @@ class UiStrings(object): self.PlaySlidesToEnd = translate('OpenLP.Ui', 'Play Slides to End') self.Preview = translate('OpenLP.Ui', 'Preview') self.PrintService = translate('OpenLP.Ui', 'Print Service') - self.Projector = translate('OpenLP.Ui', 'Projector', 'Singluar') + self.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.') diff --git a/openlp/core/lib/projector/constants.py b/openlp/core/lib/projector/constants.py index eeb969987..72903f45f 100644 --- a/openlp/core/lib/projector/constants.py +++ b/openlp/core/lib/projector/constants.py @@ -69,7 +69,7 @@ PJLINK_VALID_CMD = {'1': ['POWR', # Power option 'INST', # Input sources available query 'NAME', # Projector name query 'INF1', # Manufacturer name query - 'INF2', # Projuct name query + 'INF2', # Product name query 'INFO', # Other information query 'CLSS' # PJLink class support query ]} @@ -221,7 +221,6 @@ STATUS_STRING = {S_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'S_NOT_ ERROR_MSG = {E_OK: translate('OpenLP.ProjectorConstants', 'OK'), # E_OK | S_OK E_GENERAL: translate('OpenLP.ProjectorConstants', 'General projector error'), E_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected error'), - E_NETWORK: translate('OpenLP.ProjectorConstants', 'Network error'), E_LAMP: translate('OpenLP.ProjectorConstants', 'Lamp error'), E_FAN: translate('OpenLP.ProjectorConstants', 'Fan error'), E_TEMP: translate('OpenLP.ProjectorConstants', 'High temperature detected'), @@ -232,7 +231,7 @@ ERROR_MSG = {E_OK: translate('OpenLP.ProjectorConstants', 'OK'), # E_OK | S_OK E_PARAMETER: translate('OpenLP.ProjectorConstants', 'Invalid Parameter'), E_UNAVAILABLE: translate('OpenLP.ProjectorConstants', 'Projector Busy'), E_PROJECTOR: translate('OpenLP.ProjectorConstants', 'Projector/Display Error'), - E_INVALID_DATA: translate('OpenLP.ProjectorConstants', 'Invald packet received'), + E_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'), @@ -295,7 +294,7 @@ ERROR_MSG = {E_OK: translate('OpenLP.ProjectorConstants', 'OK'), # E_OK | S_OK S_WARMUP: translate('OpenLP.ProjectorConstants', 'Warmup in progress'), S_ON: translate('OpenLP.ProjectorConstants', 'Power is on'), S_COOLDOWN: translate('OpenLP.ProjectorConstants', 'Cooldown in progress'), - S_INFO: translate('OpenLP.ProjectorConstants', 'Projector Information availble'), + S_INFO: translate('OpenLP.ProjectorConstants', 'Projector Information available'), S_NETWORK_SENDING: translate('OpenLP.ProjectorConstants', 'Sending data'), S_NETWORK_RECEIVED: translate('OpenLP.ProjectorConstants', 'Received data')} diff --git a/openlp/core/lib/projector/db.py b/openlp/core/lib/projector/db.py index 07881a1df..3663ef4bf 100644 --- a/openlp/core/lib/projector/db.py +++ b/openlp/core/lib/projector/db.py @@ -96,7 +96,7 @@ class Model(CommonBase, Base): class Source(CommonBase, Base): """ - Input ource table. + Input source table. Model table links here. These entries map PJLink source codes to text strings. @@ -170,7 +170,7 @@ class ProjectorDB(Manager): Locate a projector by host IP/Name. :param ip: Host IP/Name - :returns: Projetor() instance + :returns: Projector() instance """ log.debug('get_projector_by_ip(ip="%s")' % ip) projector = self.get_object_filtered(Projector, Projector.ip == ip) @@ -186,7 +186,7 @@ class ProjectorDB(Manager): Locate a projector by name field :param name: Name of projector - :returns: Projetor() instance + :returns: Projector() instance """ log.debug('get_projector_by_name(name="%s")' % name) projector = self.get_object_filtered(Projector, Projector.name == name) @@ -203,7 +203,7 @@ class ProjectorDB(Manager): NOTE: Will not add new entry if IP is the same as already in the table. - :param projector: Projetor() instance to add + :param projector: Projector() instance to add :returns: bool """ old_projector = self.get_object_filtered(Projector, Projector.ip == projector.ip) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 07efaebb7..e557839d6 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -123,7 +123,7 @@ class PJLink1(QTcpSocket): self.projector_status = S_NOT_CONNECTED self.error_status = S_OK # Socket information - # Account for self.readLine appending \0 and/or exraneous \r + # Account for self.readLine appending \0 and/or extraneous \r self.maxSize = PJLINK_MAX_PACKET + 2 self.setReadBufferSize(self.maxSize) # PJLink projector information @@ -239,21 +239,24 @@ class PJLink1(QTcpSocket): return self.pjlink_class in PJLINK_VALID_CMD and \ cmd in PJLINK_VALID_CMD[self.pjlink_class] - def check_login(self): + @pyqtSlot() + def check_login(self, data=None): """ Processes the initial connection and authentication (if needed). """ - self.waitForReadyRead(5000) # 5 seconds should be more than enough - read = self.readLine(self.maxSize) - dontcare = self.readLine(self.maxSize) # Clean out the trailing \r\n - if len(read) < 8: - log.warn('(%s) Not enough data read)' % self.ip) - return - data = decode(read, 'ascii') - # Possibility of extraneous data on input when reading. - # Clean out extraneous characters in buffer. - dontcare = self.readLine(self.maxSize) - log.debug('(%s) check_login() read "%s"' % (self.ip, data)) + if data is None: + # Reconnected setup? + self.waitForReadyRead(5000) # 5 seconds should be more than enough + read = self.readLine(self.maxSize) + dontcare = self.readLine(self.maxSize) # Clean out the trailing \r\n + if len(read) < 8: + log.warn('(%s) Not enough data read)' % self.ip) + return + data = decode(read, 'ascii') + # Possibility of extraneous data on input when reading. + # Clean out extraneous characters in buffer. + dontcare = self.readLine(self.maxSize) + log.debug('(%s) check_login() read "%s"' % (self.ip, data)) # At this point, we should only have the initial login prompt with # possible authentication if not data.upper().startswith('PJLINK'): @@ -304,10 +307,13 @@ class PJLink1(QTcpSocket): log.debug('(%s) get_data(): Packet length < 8: "%s"' % (self.ip, data)) return log.debug('(%s) Checking new data "%s"' % (self.ip, data)) + if data.upper().startswith('PJLINK'): + # Reconnected from remote host disconnect ? + return self.check_login(data) if '=' in data: pass else: - log.warn('(%s) Invalid packet received') + log.warn('(%s) Invalid packet received' % self.ip) return data_split = data.split('=') try: @@ -622,7 +628,7 @@ class PJLink1(QTcpSocket): def get_shutter_status(self): """ - Send command to retrive shutter status. + Send command to retrieve shutter status. """ return self.send_command(cmd='AVMT') diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index d2b3f5b60..774e7039d 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -432,9 +432,9 @@ class Ui_MainWindow(object): 'exported on this or another machine')) self.settings_import_item.setText(translate('OpenLP.MainWindow', 'Settings')) self.view_projector_manager_item.setText(translate('OPenLP.MainWindow', '&ProjectorManager')) - self.view_projector_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Toogle Projector Manager')) + self.view_projector_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Toggle Projector Manager')) self.view_projector_manager_item.setStatusTip(translate('OpenLP.MainWindow', - 'Toggle the visibiilty of the Projector Manager')) + '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', diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index ec9207a2b..8e8bcf692 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -786,7 +786,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa if ip == list_item.link.ip: item = list_item break - message = 'No message' if msg is None else msg + 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 From 303eabdfa6492788b7be6bc2b7239b76707b5406 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 9 Oct 2014 14:46:05 -0700 Subject: [PATCH 015/115] Wizard text updates --- openlp/core/ui/projector/wizard.py | 45 +++++++++++++++--------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/openlp/core/ui/projector/wizard.py b/openlp/core/ui/projector/wizard.py index e95c68e30..26ccf9d7c 100644 --- a/openlp/core/ui/projector/wizard.py +++ b/openlp/core/ui/projector/wizard.py @@ -159,22 +159,22 @@ class ProjectorWizard(QtGui.QWizard, RegistryProperties): self.welcome_page.information_label.setText(translate('OpenLP.ProjectorWizard', 'This wizard will help you to ' 'create and edit your Projector control.

' 'Press "Next" button below to continue.')) - self.host_page.setTitle(translate('OpenLP.ProjectorWizard', 'Host IP Number')) + self.host_page.setTitle(translate('OpenLP.ProjectorWizard', 'Host Address')) self.host_page.setSubTitle(translate('OpenLP.ProjectorWizard', 'Enter the IP address, port, and PIN used to conenct to the projector. ' 'The port should only be changed if you know what you\'re doing, and ' 'the pin should only be entered if it\'s required.' - '

Once the IP address is checked and is ' - 'not in the database, you can continue to the next page')) + '

Once the IP address has been verified as correct and not ' + 'in the database, the rest of the information can be added on the next page.')) self.host_page.help_ = translate('OpenLP.ProjectorWizard', - 'IP: The IP address of the projector to connect to.
' - 'Port: The port number. Default is 4352.
' - 'PIN: If needed, enter the PIN access code for the projector.
' - '
Once I verify the address is a valid IP and not in the database, you ' - 'can then add the rest of the information on the next page.') - self.host_page.ip_number_label.setText(translate('OpenLP.ProjectorWizard', 'IP Number: ')) - self.host_page.pjlink_port_label.setText(translate('OpenLP.ProjectorWizard', 'Port: ')) - self.host_page.pjlink_pin_label.setText(translate('OpenLP.ProjectorWizard', 'PIN: ')) + 'IP Address: The IP address of the projector to connect to.
' + 'PJLink Port: The port number. Default is 4352.
' + 'PJLink PIN: If needed, enter the PIN access code for the projector.
' + '
Once I verify the address is a valid IP address and not in the ' + 'database, you can then add the rest of the information on the next page.') + self.host_page.ip_number_label.setText(translate('OpenLP.ProjectorWizard', 'IP Address: ')) + self.host_page.pjlink_port_label.setText(translate('OpenLP.ProjectorWizard', 'PJLink Port: ')) + self.host_page.pjlink_pin_label.setText(translate('OpenLP.ProjectorWizard', 'PJLink PIN: ')) self.edit_page.setTitle(translate('OpenLP.ProjectorWizard', 'Add/Edit Projector Information')) self.edit_page.setSubTitle(translate('OpenLP.ProjectorWizard', 'Enter the information below in the left panel for the projector.')) @@ -190,7 +190,7 @@ class ProjectorWizard(QtGui.QWizard, RegistryProperties): 'information will only be available if the projector is connected to the ' 'network and can be accessed while running this wizard. ' '(Currently not implemented)' % PJLINK_PORT) - self.edit_page.ip_number_label.setText(translate('OpenLP.ProjectorWizard', 'IP Number: ')) + self.edit_page.ip_number_label.setText(translate('OpenLP.ProjectorWizard', 'IP Address: ')) self.edit_page.pjlink_port_label.setText(translate('OpenLP.ProjectorWizard', 'PJLink port: ')) self.edit_page.pjlink_pin_label.setText(translate('OpenLP.ProjectorWizard', 'PJLink PIN: ')) self.edit_page.name_label.setText(translate('OpenLP.ProjectorWizard', 'Name: ')) @@ -272,7 +272,6 @@ class ConnectHostPage(ConnectBase): self.setObjectName('host_page') self.myButtons = [QtGui.QWizard.HelpButton, QtGui.QWizard.Stretch, - QtGui.QWizard.BackButton, QtGui.QWizard.NextButton, QtGui.QWizard.CancelButton] self.hostPageLayout = QtGui.QHBoxLayout(self) @@ -320,16 +319,16 @@ class ConnectHostPage(ConnectBase): valid = True else: QtGui.QMessageBox.warning(self, - translate('OpenLP.ProjectorWizard', 'Already Saved'), + translate('OpenLP.ProjectorWizard', 'Duplicate IP Address'), translate('OpenLP.ProjectorWizard', - 'IP "%s"
is already in the database as ID %s.' - '

Please Enter a different IP.' % (adx, ip.id))) + 'IP address "%s"
is already in the database as ID %s.' + '

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

Please enter a valid IP address.' % adx)) valid = False """ @@ -457,11 +456,13 @@ class ConnectEditPage(ConnectBase): make = self.wizard().field('projector_make') model = self.wizard().field('projector_model') if make is None or make.strip() == '': - self.projector_make_text.setText('Unavailable ') + self.projector_make_text.setText(translate('OpenLP.ProjectorWizard', + 'Unavailable ')) else: self.projector_make_text.setText(make) if model is None or model.strip() == '': - self.projector_model_text.setText('Unavailable ') + self.projector_model_text.setText(translate('OpenLP.ProjectorWizard', + 'Unavailable ')) else: self.projector_model_text.setText(model) self.myButtons = [QtGui.QWizard.HelpButton, @@ -486,9 +487,9 @@ class ConnectEditPage(ConnectBase): valid = verify_ip_address(ip) if not valid: QtGui.QMessageBox.warning(self, - translate('OpenLP.ProjectorWizard', 'Invalid IP'), + translate('OpenLP.ProjectorWizard', 'Invalid IP Address'), translate('OpenLP.ProjectorWizard', - 'IP "%s"
is not a valid IP address.' + 'IP address "%s"
is not a valid IP address.' '

Please enter a valid IP address.' % ip)) return False log.debug('Saving edited projector %s' % ip) From 66a674c8de5042ca395e3bc852c44a5f56895e4a Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 9 Oct 2014 15:00:29 -0700 Subject: [PATCH 016/115] more wizard text updates --- openlp/core/ui/projector/wizard.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/openlp/core/ui/projector/wizard.py b/openlp/core/ui/projector/wizard.py index 26ccf9d7c..4c6da818d 100644 --- a/openlp/core/ui/projector/wizard.py +++ b/openlp/core/ui/projector/wizard.py @@ -163,26 +163,24 @@ class ProjectorWizard(QtGui.QWizard, RegistryProperties): self.host_page.setSubTitle(translate('OpenLP.ProjectorWizard', 'Enter the IP address, port, and PIN used to conenct to the projector. ' 'The port should only be changed if you know what you\'re doing, and ' - 'the pin should only be entered if it\'s required.' - '

Once the IP address has been verified as correct and not ' - 'in the database, the rest of the information can be added on the next page.')) + 'the pin should only be entered if it\'s required.')) self.host_page.help_ = translate('OpenLP.ProjectorWizard', 'IP Address: The IP address of the projector to connect to.
' - 'PJLink Port: The port number. Default is 4352.
' - 'PJLink PIN: If needed, enter the PIN access code for the projector.
' - '
Once I verify the address is a valid IP address and not in the ' - 'database, you can then add the rest of the information on the next page.') + 'Port: The port number. Default is 4352.
' + 'PIN: If needed, enter the PIN access code for the projector.
' + '
Once the IP address has been verified as correct and not ' + 'in the database, the rest of the information can be added on the next page.') self.host_page.ip_number_label.setText(translate('OpenLP.ProjectorWizard', 'IP Address: ')) - self.host_page.pjlink_port_label.setText(translate('OpenLP.ProjectorWizard', 'PJLink Port: ')) - self.host_page.pjlink_pin_label.setText(translate('OpenLP.ProjectorWizard', 'PJLink PIN: ')) + self.host_page.pjlink_port_label.setText(translate('OpenLP.ProjectorWizard', 'Port: ')) + self.host_page.pjlink_pin_label.setText(translate('OpenLP.ProjectorWizard', 'PIN: ')) self.edit_page.setTitle(translate('OpenLP.ProjectorWizard', 'Add/Edit Projector Information')) self.edit_page.setSubTitle(translate('OpenLP.ProjectorWizard', 'Enter the information below in the left panel for the projector.')) self.edit_page.help_ = translate('OpenLP.ProjectorWizard', 'Please enter the following information:' - '

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

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

Name: ' + '

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

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

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

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

Notes: Any notes you want to add about this ' @@ -191,8 +189,8 @@ class ProjectorWizard(QtGui.QWizard, RegistryProperties): 'network and can be accessed while running this wizard. ' '(Currently not implemented)' % PJLINK_PORT) self.edit_page.ip_number_label.setText(translate('OpenLP.ProjectorWizard', 'IP Address: ')) - self.edit_page.pjlink_port_label.setText(translate('OpenLP.ProjectorWizard', 'PJLink port: ')) - self.edit_page.pjlink_pin_label.setText(translate('OpenLP.ProjectorWizard', 'PJLink PIN: ')) + self.edit_page.pjlink_port_label.setText(translate('OpenLP.ProjectorWizard', 'Port: ')) + self.edit_page.pjlink_pin_label.setText(translate('OpenLP.ProjectorWizard', 'PIN: ')) self.edit_page.name_label.setText(translate('OpenLP.ProjectorWizard', 'Name: ')) self.edit_page.location_label.setText(translate('OpenLP.ProjectorWizard', 'Location: ')) self.edit_page.notes_label.setText(translate('OpenLP.ProjectorWizard', 'Notes: ')) From c0169bb76234c28e564f9c322afa222db5d135c5 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 9 Oct 2014 15:12:08 -0700 Subject: [PATCH 017/115] Test for MacOSX for wizard buttons --- openlp/core/ui/projector/wizard.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/openlp/core/ui/projector/wizard.py b/openlp/core/ui/projector/wizard.py index 4c6da818d..d5d7db9fe 100644 --- a/openlp/core/ui/projector/wizard.py +++ b/openlp/core/ui/projector/wizard.py @@ -221,17 +221,18 @@ class ConnectBase(QtGui.QWizardPage): Set buttons for bottom of page. """ QtGui.QWizardPage.setVisible(self, visible) - if visible: - try: - self.myCustomButton() - except: + if not is_macosx(): + if visible: try: - self.wizard().setButtonLayout(self.myButtons) + self.myCustomButton() except: - self.wizard().setButtonLayout([QtGui.QWizard.Stretch, - QtGui.QWizard.BackButton, - QtGui.QWizard.NextButton, - QtGui.QWizard.CancelButton]) + try: + self.wizard().setButtonLayout(self.myButtons) + except: + self.wizard().setButtonLayout([QtGui.QWizard.Stretch, + QtGui.QWizard.BackButton, + QtGui.QWizard.NextButton, + QtGui.QWizard.CancelButton]) class ConnectWelcomePage(ConnectBase): From be2b16a17c34b0baa17a6d63c1563ccd06f0feeb Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 9 Oct 2014 19:07:43 -0700 Subject: [PATCH 018/115] Rescaled icons --- resources/images/projector_connect.png | Bin 1436 -> 928 bytes resources/images/projector_disconnect.png | Bin 1267 -> 855 bytes resources/images/projector_power_off.png | Bin 1219 -> 851 bytes resources/images/projector_power_on.png | Bin 1258 -> 889 bytes resources/images/projector_status.png | Bin 4255 -> 807 bytes resources/images/projector_view.png | Bin 5930 -> 694 bytes 6 files changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/images/projector_connect.png b/resources/images/projector_connect.png index 2e9b08c026466c9608bef059c886beadb4ecac23..cc3540629985b9435ff42b4f14abd1959733dc13 100644 GIT binary patch delta 232 zcmbQky?|Y_Gr-TCmrII^fq{Y7)59eQNDF{42Q!e=(B%(iU|?Dw;1lAyv2;C?+=tnx zn}OntB|(0{3=Yq3q=7g|-tI089jvk*6AdKl*-JcqUD@w(aWPsM=?0$q0#uOY>Eak7 zaXC3bLF~%g+0sWJG(IqYb9ZOy>n+n1T2Az>btur{IhTK%i!G2t^z{W7vwSPINBPa4 z^=+z(m`>PFV14Mnv*h>5o7H!3Uwh^#`o%rQwwV8B=vmF18h<>cr+-uUygbk4^D&-3 c`&bx^g!@ctwjA9s6=*Gkr>mdKI;Vst03JS64gdfE delta 773 zcmZ3$K8L%$Gr-TCmrII^fq{Y7)59eQNGpIa2Q!d#S<-b3NHG^Xd4_OsaNK_A{{YD4 zEbxdd28xz}Fk|i2rZYf6$r9IylHmNblJdl&REF~Ma=pyF?Be9af>gcyqV(DCY@`?% zm^KCYgt-3y{~xG6EiEl8D=TxO@lvMxldqVM0)@7g1o;IsFfuYSv#_wSad2{SadGnt z2nY%Z2?>jciAzXHOH0ehD=KSfYH4e0>*^aB8Jn1!TiJVf1_lKM1p`4yXn1sPUS3{7 zK~-Z@XJ=Q}l<6~PEnK>6_1bN__w3zw00<6(z>y=z&Yiz-;lf2AxO%Pr`t=(SaP!vf z+js8V1%i9`9zA~Y^u^29uiw0V_wL>M_a8rg{sIJFzkdAz0zZHL`t#@SKNygb6F$Vi zz&P8}#WBRQ(g)mzzz9z?bI{)^z$7dcrbI}(pQ7dmMT&c2CtYP8HGHr=J8q?cR3h)}H!D7BNS^wN|aD)45ah z`q5JN)qRekIrnQVoH?BTeZ0I}+dr@MguKlJ-M$Iw?I-PR%%5Kg>!?{LGKrBXczw*i zg6Et8|E~S@w2j_=K7PBpeB?aE0_U$^tHs~jFr3ogdO1%k)c=I@#Pl!o1>`Hg-}<@P zp)#6{O;?r8k5NQlBVWg}rrUd0&DU>#mrlD8oRQ&Om6dbu>~HV08&6yN&553H^XC49 zPR+8)p*eFk|4ie28U-i(m?NMQuI%@?xEQS!j(_g@3sg|&>Eak7 zaXC5R0PD1OcQ)t$zjyca1lFR@6IiF^d}eE!%hBjMo2ki}dAnX4w@!ro>^YL#6BT0; z-o08Iy*)S9nE%w>Y?IWRo1UJYu0Nakq5teXj|?_RFAIG$d#5XFXZ-H`__Jmztn;3B xnhH<+q5dk*^X-WmHh$@3)|7pH%FJ;L3?FR`l~kX65CS@d!PC{xWt~$(697YRS_=RG delta 697 zcmV;q0!IDU2J;DjiBL{Q4GJ0x0000DNk~Le0000W0000W2m=5B07RsU#sB~S19L)2 zR0s$N+u={(00009a7bBm000Aa000Aa0e#hi%m4rY8FWQhbW?9;ba!ELWdL_~cP?pe zYja~^aAhuUa%Y?FJQ@H10##5bW%=J z009930SE{U4GsTeSMpoot>VZthBVeyu-uG%i-eV+J08 z?d|XH@9*#L@caAw`~Lp^{{H^{{{H?zODcB&00Fs4L_t(I%VS_56)fMreaEidyZ7wb zy?fV=?W-{rENk0;wP)|X{Ra;0-?w*9+j1-lwu2QMJP1;-3zvckd1?6*aVpr++vg1e z6Z&@Vz@lL3w(UMJuxmRO1xq$>^Y!)f4+!x0^WDB}DW-zOn>I%T28D!$g#<-z+q?u* z!J-YDfVOH!Q+`RIp&}x`>3-?52|J)X4Sg z7NSZr0`a`nYqkNw_U%Ao?V9;0@*G-fERu7UuUNf$4H&Fmy<#r1JeT&q6~>3BHZNbk zd<7URU*3YOfX{Hp@)?~+j!l|97XfA>$=ghyIHhJ~`;kKuSd#&t_4J;~!fEAOjv6sy zljpSp%IEBVUps5())lJQ zf%4sx3^=hEAZqW`Tv3sf0hG7k!RAC2)B3Kmy2gg?N#E0`dS*JwsVj@H;>c?-!n&DGQh4&BfVhH=48F|DEGQ;~ fKRYw=>*d+{kpZ)3S$p@e1fAW7CEG(=ppF8)Y z9)Rd5Qg(ZrLMhUN2T{gQT3scr*Kt6p-6pBmu|Ys-c^S_+(l}NF5M@Eqe>fJ??ZSZr zG>#snaqJkT->1=R(r7kGhWW9X8SLaFrrRYmMjVLBKujFVAd1+3nV0t?#jydbLdhDnV7(|uHzQxDY_fvopsV5zhmptMe;!7`2o?wEs7$OTzlvo zK`J5J^EXJ=+U$MmY1%W-5?uKP{poAqpuG7Hae@OdcnwTmAmnOCNbz=8Y#0000<2SrXqu0mjf?VgEY delta 1187 zcmV;U1YG;m2Ez#6+!%-ns0qXb3Bee{ zlAXF_0dWBE<(|x=9-dlCcMK_~S zObq@>rRpSqo&0rPC3G+f+ym+6f9X&M=0KR;v26hEZ`?Rt7A@MA z7uysH$mY%0Z=E`|ci!>iH9H36OP1_5n>Ou|b?Xq3|Ex0n)eyn=^iUrj4r8#}D#+#i z`eVVXj_JuV03I4ce*k87IOEom zRVqxIn4sKwrK*y8ucLsifOya>jtO~=2khFltQr^~O%m=OIz)&fp4++=fRV$8q0wOB zmMxUZWo{1+(mHt(1DIkF4_X1yy!`~t;+U*nC!ar`c>@DjYw5C}|wOV-49MA^fLDEKpwArL@%^Hj`7*%e3eSmag9N%go zB8Uii(juK0=jOqKSYxo(^6cu>B+Vwi)xv=^08IcdqOQ?Es#T`<_F{}-^1=mbLxX4% zBUyti%gC|}NgHSyQyaQO?czm@F-)sgk)9rWlHf%&0kj1;5p_w6f2g+?RYg^)51z-k z80%xyH*wD4oI`yR>tc+H+Zp*f5nH(8mMv@>Rs4705z-D!9 z4(i&D0gO*57r|o*S#wIr{WE76{_0CK1aTmm0$z}Zmo74P;>4e^EEPzvp2Lq^fO}W) z-s7AjA30Ba@_Qa0e?3U6S)1ZF@7_U-;#DLCEbSO0MtOAhBy?K3x_%RnzSxT$pCmth z7xUc;2!ZhH50rz)Ri`s^)0^$i$aBVjIfYs!dtB21xd>vF?7=9%9XkrZjM;PPes>?* z9nn?v_(LDy4}VBk(PO$J$~!({`mDJCJUDg~_xl)Xl)N($f2I^0tDtc8<2}rNc|B6; zV&VGD-0JJ+?zf*4-9ASMfq3ro6kmIr1*=|Yhb*o!yzf1XLMSM{qqyx@L5zSv^Y#q} zcWz_lrw1^mz?|i)NEUrUv)Lp~Q=Talh@uEp#iT7R@7zXm=O!u;j7%x+greiPgYnZp za^EfiL5$$@cJ6!* z@wFjFcDzn?@CPzA1EWu2J@xRYo74TdU(c@>e{9==fV2E7XDtp$=L}yY9#KPhFb-?u@zc)VKYa`drt`t;Mx3aTa zQ?;lp4l*bnwOe z`-II9=Nt}bMiJ-dKOpZF80tAm-QOmR8zNRji4sCTl7q!V>`y(y&FyQHSIZ=^rI`dY z+YQ=rLV2ak&8=znrMfxXe|3b=JrF@*bXrOM&p?beXk}OrRuQYP*3!1Ez1LGh{-F$8 zVTF{=uu>^A^X;-^iz!iFMpONxAwOLD1_basFyA zvUqueR9+MLF}m2B?euc`v~7{|)fU73FZ0~tL8RnUUcXJ0gq#>T&g(C{Lq1jDqi;Us z*6cmJj0)l)`P2X?tq@6=n!G~ak#mdQ0l+3;V~f&4rqkf%v`gpB)y>7mjsQGDhm&G{{|ogVR9DSPDlU%002ovPDHLkV1ix4nl1nU delta 1227 zcmV;+1T_2k2I>hRiBL{Q4GJ0x0000DNk~Le0000M0000M2nGNE0K~LxJCPwVe+dN@ z3m7z8Wx4s#eiOwXOJwZ8b@)NnhzB@h10`@0*$9;-)t) z+*l7Bm^m}^=RfDn|BNti$ARl_e_AHS31?%`E8p%sq5bo7QQ*NhuIgI0rfbVD9~~J` z)%NCp4FAf1p3x=Z@qQp!&|Q0bQBSDR*VIqJHM1e zi^p{mwz~Cq_DkG0yk_N#KTvdK{*Ofs?0Ws+n)P?~i}THhp9`j%RS22^e-Jd#neJIxyVyOs0}(^sb^sXT<`b$BQ%h&5W)+r~2Uy#?4l4;c zTVN)gqQiAjPP!>4-4tD!f7$deN=X-192YIVn*P!%s#%4GuYqC!O!SJX%3Re`uh+P` z?-r~iSg~yX?PX?a6&k+UPRU$oGE=Lt{kNBKCLu8i*Yw>+yKnUWc>Rn zejFG^E^bLWXA2zrf4M@w(@|bJfsYxAa%B2X_J4Ye`l*zaPp`(z3+mLtD#i)S(aQ0e zq8KZTetEL}Ue_y4q3FuoykY~rkDtS52j64r&l(z`dqoGg4BXGcrAS$D^OLA*T?y3WH%Fg zrfF1CR27X7gJKp4<63M5>jaHT$RZ(adDaL_?49QLf4&`9XAlEEjrh6NT1XY2Mxs|@ zP(%bVt(W0|*t4oMcHUJzsI~_iaWc5JS3}lVK zqUA-V4%NqiC{F<&uj^e_>`9)h9BB*?&wNYzpPIZ$3@&fRH0bkp@0mU!08as*T)#v# pwsd}0AzzaLF%s|k_LRN?{0$kmEHMTxYi$4k002ovPDHLkV1o2rLcRb1 diff --git a/resources/images/projector_status.png b/resources/images/projector_status.png index c65f61630906527bedf32d85522e690b8dca99f6..516a7260225e304c90be894853cd69838dd4e1b2 100644 GIT binary patch delta 797 zcmV+&1LFLjA*Tj2iBL{Q4GJ0x0000DNk~Le0000G0000G2nGNE03Y-JVE_OC24YJ` zL;(K){{a7>y{D6rF&KXbbV*G`2i^(_0t703zW0Uz00Oj0L_t(I%XO2@PZehr#((cS zGk3Ui3kb-S1~8x)sEf9`Fk0=(bb)M2TdIkPw74++3+SdSh$bfOqTQId!Px4?rbUw) zlE$TpX-Zp?l2&OUMId|#!`#cgcfQ`4*Togu82YSE&U4PmbH0BByFhDg05w1k5I`C5 zg%HWhvErpf`DDRuzw>Z*w6(eJj1Y3K6cR`_mfg}s|B23#?$(BRAw;@sfq~0+&%f5u zdaI?e0e36)H;9ttMx~J-tI{Uc$k`mK?Hz`6(5~`1AtGiJ;Jig z7i~gU+rIB+n)_{<8zX~fzrnHhue|g2u~UAeh~k7urTAfjDK$?PH~DqWBT3RKm!#yI z_VDvcA;aXC4om~yBnDNr)Q z=l5rKwi8POZCVr}Sa8ey^mvKs*)?_)EUyNb67PFMLz-MgVs#WRUi{W z9DoWSjzNF0w4;E~8z(b$2tIf3vXWfm;+(Q$7nwSRu*grq=e9i*oXOFVD8Zn zQ`c`mFEH_+U+C(Ezb7zyhW6^lrmH%C=smKv4G001q9@n>`d)Y&!|IQ*_3hZ*VQBsr byKv6|9TGCbhf~c!00000NkvXXu0mjfs1JMf literal 4255 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

*(LsvIJ z4G=^-fW_;8P)lh_FeZhlvc7bL6hJO-Q6rx{37hr zeXLK;`8g9ny5VLGt5z%^X5fAp>V1r;ZO{Thb2k^_T2YJ3gho*Du%!8)RhiY904qU~ z_7`3KV{=L1%v(URA8-m#z~XS|Hkz^j6GspC%S!Lk2q3G)W%or9G%wS_+TT)D`4j~S zK{J6!J23BZpn0LF9sJr|kg6zdScL$jatT}UTCetqp9O~c{1LaH5|&pO`-z<@d>z@` zDgK0)l7&v=0@Uw@6)T!aAoP2BRNLx;ZEz_uOLo^wbwgVZ5NRukVTOdA<+BBKy-|go zFWp+xBf!WBppZs+{%b0V8o~Zz(t4B?3rEn~o2)VFms$WBWHqlBao#sz3xj7Jn)bs`9>DFBt}P*n{oRblaMxZ(02Ccz*!T*Za!I&k^6 zkt*O&4=4Q)McvW30CO)6j|NFLF2L_{DEJ+Z{|AOTqevA*D0z*keK~y-)-IF~`Q(+i|&v*NOxgQm*6{EG-NU%zbwGEKaQ31gPc3jJ< zBu*x3AQ)rBn3!lx>fmCGLLEU})M%uQ(f}<8ili|@X-oRuA6xo!KPS8B;AlM4llQ#m zo!$fdU%GX3qQ=3lV2vFRYdkhOfBNTeXGbRhVD!q>mp!Mu70x;N<*g={ZGF9G)NAA8 zj{u;LoG6NJi^T#}Q9$b{RK}24`qcIy9DZhqMx%Fk zR%g9dHI3Fo2a#neM7j$@e?#XGkH=$Xi-ivlhgvtjZy@(=4VLT|>@k&Lmqc8?KH((o zUQe}M(uBiJ4bXG}pSKHmwPl>yzt=ehLLTeYs-ddu1G#*D@R(KW^X(JSNJV624U@|X z7g(9ZJdGmST@7fF2r>nZQmJ(3&AYevRaIrGrft8E&W2@KxyMabXtdakIO(*aQ7EH8 z%rOI{^hrvuZ!1TTM{O;Os6H8 z4v+w8+CqlXhLqqKNE*9;V_7 z(DtAEojEh_d(N}{w(sxvoaewQR3@_Q;$l1A_qBU@$L+TP@P*I+g&rRt_lm`$ zKQlcoPo6rJpE`9ae(=EuI}>Ln4Zt6N{1g29kACEr8v9?~@h|v`JHG(H_A9pQU%%-l zf5V0i?%FkL#o*AOsa9*zU*B_g{6GHsuPnf0fBysC@Wy|7QF7>uRY01iE5D5~+p;XX z#TfIpJkJMnW9IWb|F+{eUjin8{3553QiyGNp1&tI`I}8{LSxM7EKBbRg5Y6b@gM8S z7v%w;`>+3v4}9?Vu-1xmXQu}4y6dh#$n*T&TI-=|waV=5EQ^baXPeFDzLDYK4}bN( zduLzm9M)QK^V@FTGCw!>ABKj8_KXY<5B3lA%ii9;c9vzQOm4pX;otxLKfTcYv!D44 z|LQ{@`hVjAv$Mol7@)7OFB}{gyrQeCeCwCL_{9Tz_g??R z@aWpSTCHE4i~TZ<{`=?toOj-K`^x{1|M4dQc=6=Pb&CrN?-Q^#GlpifNxR)?{il3o^~JEsuWxp>BX{cJAVhZ+z1$&hz%0Z)NAzI{=uRnCP?CzIJYYE;~DQmg(tf z=I7^`o}Q*rual+OrPXTnckjOYD`Nm3`=gJojPr*d{n-C~4*1lc{Kw6}2TH}_RcR9Y z?RJaBN`+RdNq2WQf$vkTR{6nWKPWtV_$RM=p3mR;1t(3@qU*Y%x3>@1b*NUW3?6AL7uVL!@cS?|k5QUlyB}ZM&=(#qsNV zdV1a_r6{*M9qNq+aS|h?Bn$$IVSz9Rb!M`yi;I;2;J{N)zHE+LZ@I19+t+38yz?(x z01tfQ>nqQaB=H^XY#13HQLgKfWf|>Gn<$ECG#W%vL{D#Tujl)>CXL34T~}TG%_sli znGV1oe*BLB_{u&16M%QU=iRT41iyaoePAuOo=dpPN3nw$W$| zy!JJ(-M#J7OZR3bUNC=UUDB zmU_LuA&%oUV0*(bG`+ojbA5gNljZL27ysa6pFDlz>vwMQJn!9( zqf{<){f#&L@*Z&O?YCjA<8wx7>bPa?5S+;9KAL=Bwv`2k!fN2>iP+3~%r0?b%Xq)J5P23=R%5Iy%b2!UBg5 z9b$TVnmCT>?(Smk+OfhY`#drSJdsA!eFAjl+zW03qZoFwP_x#P>1HSLy>ihnO@;u+NRH-mA zImwwbXAnY=BngX)i_~g0;yA|hJc`94#u$plBGy`})hbaG5k(PMmLY^72n(cHMi_<+ z4GmqeKGs^&Bw==Dh9rqmO5yuHVHi>_m$5*r*#w}~YLR9szUNc!>SAouY{0&8&`2c;B32-@v7ohYKyZc!|jkWwNMbUGcDs#QAeHibgS=FOXN zT^C1dE43Cvh}Q`r?rII#bTm79P2=)SdD>o-a%U#FD9k(%eHP| z-I_J@_VyAMN)!r3wBxKOElpDv78a=2YACJgbUGMgkV@e=4qfFkozAiXlO!dIW1=Jx zCd-gg5(L3bg+k%lFW>dW;syB*|K!>KGY2F|@&?Co-e9dIj$IHa5nZHERgN5a0KAzveZs z`7yBX6-n@#J-b(KHk!>{Ns?UIZnaogSfE<1Qmfa9q6o)v@O&QxiPkl@gxk5d;CbF@$q-AOumTLlVX0xdEk6=dTNd_)@s8 z%kc0py*)jY%VmnC65U;06bc1=&!f?3V6COAt7~@<1S<--{r1~mu7IbXe2N{rb}~CP zEvILuFL~^TKirmE>uIIvbUGlG7oVmXr7Xh&Cd-J4=|mAW&v9KB-}mu6Z#iX2iQ_nA zS%xu&IF4vEY6M}xx#?-)I8JN*x^;7o+_ClS-)vY@T-=K63P?si~>)rKJkSWN58dJn82NpJo}3 z$)xg zN=k(5VvM2HY*NdrEX>cHS(u-@H;&^qtyb$QYwfsF3eWRaGRMyo-WanS_dp!SG@DJj z%4I|tA`s~Fd~{(9$8qTC?jh<#+b1R`{_VPT>ka|WuY|yp`=4Ofp546g+=-17XD0u7 zqtV>l*V`+yJY)Y8PcS_*L%ZEx7B0_;lY}%)u+|dz0bv-DBne59{FD>UGrv>{eLjAr z5D2YTUZkXHnjAWGs9Go#x{AeOb|LLuu)4MOWlA_-<&k9>S(ag~CCf4@mC8z0n&!~N7IDp=Dp8S@UD%+cl#yekl z;Y8sD1X^pF%?6En4Qq28t#P!*3PGA08jU7PwHlpHXJv6xF84FW5XH;Ij0yszQrJAl znB|3@7fucj4{NEEl0pitm*YLpbF|jWDffKwWUZxGC{Qk!S*li7LU3kwmMBf=E|>9K z7h#vVVL22nNXh9JU!+{>BDXm}xaE>d-nF!}^f+*8*Y0bULtt@kzW>pE`>vdrm{@=4 z&>>ErK21+onaj3ZLVs6*u2PX8@DWmxWQN6Rom#a*vstIpX|K4N<2aN`rSp|viZO;T z3@H=~q-jE}R>xXaJi=LyU%R~8^H%m;y1TlOQvP(Gk>`}kWyZ$FXtr9USw^GPq|@mT zMQx->8S3kys}N8K16G+T_1ZxZ7hH?B>RtNx+98iyn7EBiBQ%C`=(BcYh|S|0@Ph*L z3yak24a%hw{rv+gT+lzz&(57Y*t&HqgM&jjj>A%A>4IgSlTvo6|r*It2Ke>Dxy{HR_CwjFKCJ=liT*zkyBTo0yrI zCCySctlz-t)2B(21S#b~Ywg`a2xH;b9LL=>G&sn0*Ih@wUT0=z1}P+3X#@h#^8msA zC!V0YyNltTGST8WY^RRSQ+#C!w8T>aR|z~Naka$vTteTYt6ZW~E;BVf%fjN~;Gu*6 zYxcS4pMO}t`Au)W^5CJLZ13sr3OB78#x6_{HI~ril#2zliE%Q8lSyQQiVIjnEmXl0he;IEC7;m7oSU1X95{r<0>0FEWOx`no8ge6twx%J zJZ@7A3v3$h2Rx?NdoKbXTcCExAu^S~_4;wwwhckmVX(k^RN zf))ZLEy7x`mNbr-pPFD{@+48G13?$Fb8{4m#h2_1OI&^R4h9DYId<$Ar%#_IP1Ai! zsRz!SJ@fp`+?-v%ZvBACvNc(jjd-5N_{MRzT(SkNHP1Zr3|W@3P+8>gv7>Ar--y#* zB&|#nx{89U2pvh_NJ2*wxC-AfsMB=Jv+ zB%51~96rpM$;p*XO`4`hl~Q*#8jbJ1q@Mq(Qp%=j`q2Xi4y-?M;sjc2TCLV{Iu(*? zwMM(uLgxu_98=ViO@n3Xtq#H(ygb3pHJ)9*bgqyom^31(B}~sQa{SCRX_nJnC?ky? zZnj!`wK3*WEu`=3oFq>ucn&7d$*rcR7}8VlaFj#{3EDw99!NzyibxX4#;qF=M^Di_ zF+(R#aI~fqMQ0lI#t|vx$uvz(tJTV~EbFw}?FG;Cjwq%6;U|ZmH8X;Kt9));&x2(>`P7SD4@l4NpjZtlA|JnSfClO)NqENcfrP<{M~ z{Sm-TH|}L=X^B18>}LOgC-~vxKbix+d)=OEb)`}%d7f7&6bfFm*|d{q&b%c_vu@vY zH|1GEAT3Hl6sIUG&UsQ+LLhR3H7R);|y z+O6kKKlAi+uXOOaxVXqu&pZvl&Z~D&DwWvxcYn)|_CKBhOXsg;67B83L_4}Eqy_<6 z35vd^%agPkb&^(tLg3L;E>Z}5^4t)|F;N_&EfidZRF+ztp`{?VU~&xDSR=%-Rwvrj zMD&)t5Kmir3LYg_p{*fQ0#}2S9dgsb7dftHiL^&1H0{)owGvV*u-ZrF;47!1!k{|8 zu<$E6iSFs?UW($RVlCv-#pD7<3HnMtp|iYBUs+1FO_)Y#DX@VhaU3FJF;db>Gn&na z6h&+-sWEjS#3@|}!|yeljd$tX>+R|3CNmZCEF&oT^q0FSg&vO92r0>}AWkznaZ0V# zVZPpCsU4B!7V88cOPIKZ5aQUHv318^IC=8t3iPA>kH7q={k$i<5;knuaQxV@W5 z&`J&GI#9DdsZ|J@KiAXU{eYU8ot-I{N`q4AjTxf9ohu3|rzlcH7GskRHi^mN4xLt$ zMy*1%TBFu%Q;Ra@BACnywB2rsVYtw4xBskGtL=-TsIhD3F4l~#VQOmXq6FWyb0=%Y z#+aI#qEIMCK@enGtJkH{4VBWAL`onHB8$o64mN3nNyy_4acg-aw^VPj(26@5xiA?pFZYw{Jhv>2#X8F<0rZSemZKEH*oY8cL2ri5!!r#BoBBWwcYn zOf%*9QpSn6z`*9q>F()UJbCiuy;+uhc6w%d24KzD8i0#dz?!kKmHw?(tKQYsbs~=A zq2At}aTOGTY7EvSgbE5;ps?gwip>(zG@}t`oLoxy!I>5(q5!XNlqgANrIdeC2n&BT zH$R`OY`8DG^wPmJ%iiL8{<~a9zqQ*F;gFLN##OpRL_t}EkknF3;+D|8W2nAS7Gpsx zPM)sUYWE#Ia^#!SGc(TttY5dDiOGpyNeHZ4yN<~-lK_m4jBXts8Gd^h7T!_t-K+Wo zNhxa)BsBulqNohymX4LQWq>IUGn;uVbh5bFj{dIQZrvk(0h2EiR&+yo^l4}b9e@Bi-m?zqDM?6`Wz@=NCH-||)-+4tzmWOv;1Z#Zz` z(aQ_P!kad3+;~M0hHJIfy;^IhR4O%r*@gM}qt86^^!`H!5B><~0E}^{k#c`Yh$^Wk})h5oIX>H!T`B4jxA%qrENecq05K`rUv5>_{n!Hr_SH$jXcC-7M z-50f(Jr~*UVzB#~Yc8tawR`q(?VdfW-#D&f6{}dqDps+IRjdN|-)cB$wiJ)pzW@LL M07*qoM6N<$f=l|)^8f$< From 857a1d06f6e9baaf11b78561a03fd513dd04ae63 Mon Sep 17 00:00:00 2001 From: Jonathan Springer Date: Thu, 9 Oct 2014 22:08:59 -0400 Subject: [PATCH 019/115] Intial changes for multiple toolbars for projector manager --- openlp/core/ui/projector/manager.py | 219 +++++++++++++++++++--------- 1 file changed, 147 insertions(+), 72 deletions(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 8e8bcf692..26283a346 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -80,56 +80,108 @@ class Ui_ProjectorManager(object): self.layout.setMargin(0) self.layout.setObjectName('layout') # Add toolbar - self.toolbar = OpenLPToolbar(widget) - self.toolbar.add_toolbar_action('newProjector', - text=translate('OpenLP.Projector', 'Add Projector'), - icon=':/projector/projector_new.png', - tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'), - triggers=self.on_add_projector) - self.toolbar.addSeparator() - self.toolbar.add_toolbar_action('connect_all_projectors', - text=translate('OpenLP.ProjectorManager', 'Connect to all projectors'), - icon=':/projector/projector_connect.png', - tootip=translate('OpenLP.ProjectorManager', 'Connect to all projectors'), - triggers=self.on_connect_all_projectors) - self.toolbar.add_toolbar_action('disconnect_all_projectors', - text=translate('OpenLP.ProjectorManager', 'Disconnect from all projectors'), - icon=':/projector/projector_disconnect.png', - tooltip=translate('OpenLP.ProjectorManager', 'Disconnect from all projectors'), - triggers=self.on_disconnect_all_projectors) - self.toolbar.addSeparator() - self.toolbar.add_toolbar_action('poweron_all_projectors', - text=translate('OpenLP.ProjectorManager', 'Power On All Projectors'), - icon=':/projector/projector_power_on.png', - tooltip=translate('OpenLP.ProjectorManager', 'Power on all projectors'), - triggers=self.on_poweron_all_projectors) - self.toolbar.add_toolbar_action('poweroff_all_projectors', - text=translate('OpenLP.ProjectorManager', 'Standby All Projector'), - icon=':/projector/projector_power_off.png', - tooltip=translate('OpenLP.ProjectorManager', 'Put all projectors in standby'), - triggers=self.on_poweroff_all_projectors) - self.toolbar.addSeparator() - self.toolbar.add_toolbar_action('blank_projector', - text=translate('OpenLP.ProjectorManager', 'Blank All Projector Screens'), - icon=':/projector/projector_blank.png', - tooltip=translate('OpenLP.ProjectorManager', 'Blank all projector screens'), - triggers=self.on_blank_all_projectors) - self.toolbar.add_toolbar_action('show_all_projector', - text=translate('OpenLP.ProjectorManager', 'Show All Projector Screens'), - icon=':/projector/projector_show.png', - tooltip=translate('OpenLP.ProjectorManager', 'Show all projector screens'), - triggers=self.on_show_all_projectors) - self.layout.addWidget(self.toolbar) + self.top_toolbar = OpenLPToolbar(widget) + self.top_toolbar.add_toolbar_action('newProjector', + text=translate('OpenLP.Projector', 'Add Projector'), + icon=':/projector/projector_new.png', + tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'), + triggers=self.on_add_projector) + self.top_toolbar.addSeparator() + self.top_toolbar.add_toolbar_action('connect_all_projectors', + text=translate('OpenLP.ProjectorManager', + 'Connect to selected projectors'), + icon=':/projector/projector_connect.png', + tootip=translate('OpenLP.ProjectorManager', + 'Connect to selected projectors'), + triggers=self.on_connect_projector) + self.top_toolbar.add_toolbar_action('disconnect_all_projectors', + text=translate('OpenLP.ProjectorManager', + 'Disconnect from selected projectors'), + icon=':/projector/projector_disconnect.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Disconnect from selected projectors'), + triggers=self.on_disconnect_projector) + self.top_toolbar.addSeparator() + self.top_toolbar.add_toolbar_action('poweron_all_projectors', + text=translate('OpenLP.ProjectorManager', + 'Power on selected projectors'), + icon=':/projector/projector_power_on.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Power on selected projectors'), + triggers=self.on_poweron_projector) + self.top_toolbar.add_toolbar_action('poweroff_all_projectors', + text=translate('OpenLP.ProjectorManager', 'Standby selected projectors'), + icon=':/projector/projector_power_off.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Put selected projectors in standby'), + triggers=self.on_poweroff_projector) + self.top_toolbar.addSeparator() + self.top_toolbar.add_toolbar_action('blank_projector', + text=translate('OpenLP.ProjectorManager', + 'Blank selected projector screens'), + icon=':/projector/projector_blank.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Blank selected projector screens'), + triggers=self.on_blank_projector) + self.top_toolbar.add_toolbar_action('show_all_projector', + text=translate('OpenLP.ProjectorManager', + 'Show selected projector screens'), + icon=':/projector/projector_show.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Show selected projector screens'), + triggers=self.on_show_projector) + self.top_toolbar.addSeparator() + self.layout.addWidget(self.top_toolbar) # Add the projector list box - self.projector_widget = QtGui.QWidgetAction(self.toolbar) + self.projector_widget = QtGui.QWidgetAction(self.top_toolbar) self.projector_widget.setObjectName('projector_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.bottom_toolbar = OpenLPToolbar(widget) + self.bottom_toolbar.add_toolbar_action('connect_all_projectors', + text=translate('OpenLP.ProjectorManager', 'Connect to all projectors'), + icon=':/projector/projector_connect.png', + tootip=translate('OpenLP.ProjectorManager', 'Connect to all projectors'), + triggers=self.on_connect_all_projectors) + self.bottom_toolbar.add_toolbar_action('disconnect_all_projectors', + text=translate('OpenLP.ProjectorManager', + 'Disconnect from all projectors'), + icon=':/projector/projector_disconnect.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Disconnect from all projectors'), + triggers=self.on_disconnect_all_projectors) + self.bottom_toolbar.addSeparator() + self.bottom_toolbar.add_toolbar_action('poweron_all_projectors', + text=translate('OpenLP.ProjectorManager', 'Power On All Projectors'), + icon=':/projector/projector_power_on.png', + tooltip=translate('OpenLP.ProjectorManager', 'Power on all projectors'), + triggers=self.on_poweron_all_projectors) + self.bottom_toolbar.add_toolbar_action('poweroff_all_projectors', + text=translate('OpenLP.ProjectorManager', 'Standby All Projector'), + icon=':/projector/projector_power_off.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Put all projectors in standby'), + triggers=self.on_poweroff_all_projectors) + self.bottom_toolbar.addSeparator() + self.bottom_toolbar.add_toolbar_action('blank_projector', + text=translate('OpenLP.ProjectorManager', 'Blank All Projector Screens'), + icon=':/projector/projector_blank.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Blank all projector screens'), + triggers=self.on_blank_all_projectors) + self.bottom_toolbar.add_toolbar_action('show_all_projector', + text=translate('OpenLP.ProjectorManager', 'Show All Projector Screens'), + icon=':/projector/projector_show.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Show all projector screens'), + triggers=self.on_show_all_projectors) + self.layout.addWidget(self.bottom_toolbar, alignment=QtCore.Qt.AlignBottom) self.projector_list_widget.customContextMenuRequested.connect(self.context_menu) # Build the context menu self.menu = QtGui.QMenu() @@ -369,12 +421,16 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa try: ip = opt.link.ip projector = opt + projector.link.set_shutter_closed() except AttributeError: - list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) - if list_item is None: - return - projector = list_item.data(QtCore.Qt.UserRole) - return projector.link.set_shutter_closed() + 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_connect_projector(self, opt=None): """ @@ -386,12 +442,16 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa try: ip = opt.link.ip projector = opt + projector.link.connect_to_host() except AttributeError: - list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) - if list_item is None: - return - projector = list_item.data(QtCore.Qt.UserRole) - return projector.link.connect_to_host() + 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_connect_all_projectors(self, opt=None): """ @@ -459,12 +519,16 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa try: ip = opt.link.ip projector = opt + projector.link.disconnect_from_host() except AttributeError: - list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) - if list_item is None: - return - projector = list_item.data(QtCore.Qt.UserRole) - return projector.link.disconnect_from_host() + 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_disconnect_all_projectors(self, opt=None): """ @@ -512,13 +576,16 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa try: ip = opt.link.ip projector = opt + projector.link.set_power_off() except AttributeError: - # Must have been called by a mouse-click on item - list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) - if list_item is None: - return - projector = list_item.data(QtCore.Qt.UserRole) - return projector.link.set_power_off() + 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_all_projectors(self, opt=None): """ @@ -540,12 +607,16 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa try: ip = opt.link.ip projector = opt + projector.link.set_power_on() except AttributeError: - lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow()) - if lwi is None: - return - projector = lwi.data(QtCore.Qt.UserRole) - return projector.link.set_power_on() + 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_all_projectors(self, opt=None): """ @@ -567,12 +638,16 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa try: ip = opt.link.ip projector = opt + projector.link.set_shutter_open() except AttributeError: - lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow()) - if lwi is None: - return - projector = lwi.data(QtCore.Qt.UserRole) - return projector.link.set_shutter_open() + 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): """ From 859ae13eb3f9365ca37105c1a2cc2d21d080825a Mon Sep 17 00:00:00 2001 From: Jonathan Springer Date: Thu, 9 Oct 2014 22:49:18 -0400 Subject: [PATCH 020/115] More work for top and bottom toolbars for the projector manager --- openlp/core/lib/toolbar.py | 13 +++++++++++ openlp/core/ui/projector/manager.py | 35 +++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 7 deletions(-) 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/projector/manager.py b/openlp/core/ui/projector/manager.py index 26283a346..7a4087209 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -87,14 +87,14 @@ class Ui_ProjectorManager(object): tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'), triggers=self.on_add_projector) self.top_toolbar.addSeparator() - self.top_toolbar.add_toolbar_action('connect_all_projectors', + self.top_toolbar.add_toolbar_action('connect_selected_projectors', text=translate('OpenLP.ProjectorManager', 'Connect to selected projectors'), icon=':/projector/projector_connect.png', tootip=translate('OpenLP.ProjectorManager', 'Connect to selected projectors'), triggers=self.on_connect_projector) - self.top_toolbar.add_toolbar_action('disconnect_all_projectors', + self.top_toolbar.add_toolbar_action('disconnect_selected_projectors', text=translate('OpenLP.ProjectorManager', 'Disconnect from selected projectors'), icon=':/projector/projector_disconnect.png', @@ -102,28 +102,28 @@ class Ui_ProjectorManager(object): 'Disconnect from selected projectors'), triggers=self.on_disconnect_projector) self.top_toolbar.addSeparator() - self.top_toolbar.add_toolbar_action('poweron_all_projectors', + self.top_toolbar.add_toolbar_action('poweron_selected_projectors', text=translate('OpenLP.ProjectorManager', 'Power on selected projectors'), icon=':/projector/projector_power_on.png', tooltip=translate('OpenLP.ProjectorManager', 'Power on selected projectors'), triggers=self.on_poweron_projector) - self.top_toolbar.add_toolbar_action('poweroff_all_projectors', + self.top_toolbar.add_toolbar_action('poweroff_selected_projectors', text=translate('OpenLP.ProjectorManager', 'Standby selected projectors'), icon=':/projector/projector_power_off.png', tooltip=translate('OpenLP.ProjectorManager', 'Put selected projectors in standby'), triggers=self.on_poweroff_projector) self.top_toolbar.addSeparator() - self.top_toolbar.add_toolbar_action('blank_projector', + self.top_toolbar.add_toolbar_action('blank_selected_projectors', text=translate('OpenLP.ProjectorManager', 'Blank selected projector screens'), icon=':/projector/projector_blank.png', tooltip=translate('OpenLP.ProjectorManager', 'Blank selected projector screens'), triggers=self.on_blank_projector) - self.top_toolbar.add_toolbar_action('show_all_projector', + self.top_toolbar.add_toolbar_action('show_selected_projectors', text=translate('OpenLP.ProjectorManager', 'Show selected projector screens'), icon=':/projector/projector_show.png', @@ -169,7 +169,7 @@ class Ui_ProjectorManager(object): 'Put all projectors in standby'), triggers=self.on_poweroff_all_projectors) self.bottom_toolbar.addSeparator() - self.bottom_toolbar.add_toolbar_action('blank_projector', + self.bottom_toolbar.add_toolbar_action('blank_all_projectors', text=translate('OpenLP.ProjectorManager', 'Blank All Projector Screens'), icon=':/projector/projector_blank.png', tooltip=translate('OpenLP.ProjectorManager', @@ -274,8 +274,13 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa def bootstrap_post_set_up(self): self.load_projectors() self.projector_form = ProjectorWizard(self, projectordb=self.projectordb) + self.top_toolbar.set_widget_enabled(['connect_selected_projectors', 'disconnect_selected_projectors', + 'poweron_selected_projectors', 'poweroff_selected_projectors', + 'blank_selected_projectors', 'show_selected_projectors'], + enabled=False) self.projector_form.edit_page.newProjector.connect(self.add_projector_from_wizard) self.projector_form.edit_page.editProjector.connect(self.edit_projector_from_wizard) + self.projector_list_widget.itemSelectionChanged.connect(self.update_icons) def context_menu(self, point): """ @@ -877,6 +882,22 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa log.debug('(%s) Updating icon' % item.link.name) item.widget.setIcon(item.icon) + @pyqtSlot() + def update_icons(self): + """ + Update the icons when the selected projectors change + """ + if not self.projector_list_widget.selectedItems(): + self.top_toolbar.set_widget_enabled(['connect_selected_projectors', 'disconnect_selected_projectors', + 'poweron_selected_projectors', 'poweroff_selected_projectors', + 'blank_selected_projectors', 'show_selected_projectors'], + enabled=False) + else: + self.top_toolbar.set_widget_enabled(['connect_selected_projectors', 'disconnect_selected_projectors', + 'poweron_selected_projectors', 'poweroff_selected_projectors', + 'blank_selected_projectors', 'show_selected_projectors'], + enabled=True) + class ProjectorItem(QObject): """ From d6bb5aad520afb82f56efc8f1c06fd394db98954 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 9 Oct 2014 20:16:46 -0700 Subject: [PATCH 021/115] More single character cleanups --- openlp/core/ui/projector/manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 8e8bcf692..4cea25991 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -287,9 +287,9 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa :param op2: future :returns: Selected button role """ - for i in self.radio_buttons: - if i.isChecked(): - self.radio_button_selected = i.button_role + for button in self.radio_buttons: + if button.isChecked(): + self.radio_button_selected = button.button_role break return From cbb995c4c673653e926169294043c5b2dbac74bb Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 9 Oct 2014 20:28:24 -0700 Subject: [PATCH 022/115] More single character cleanups --- openlp/core/ui/projector/manager.py | 89 +++++++++++++++-------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 4cea25991..8fbd1f7db 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -317,14 +317,14 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa if source_list is None: return sort = [] - for i in source_list.keys(): - sort.append(i) + for item in source_list.keys(): + sort.append(item) sort.sort() - for i in sort: + for item in sort: button = self._select_input_widget(parent=self, selected=projector.link.source, - code=i, - text=source_list[i]) + code=item, + text=source_list[item]) layout.addWidget(button) button_box = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) @@ -554,8 +554,8 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa :param opt: Needed by PyQt4 :returns: None """ - for i in self.projector_list: - self.on_show_projector(i.link) + for item in self.projector_list: + self.on_show_projector(item.link) def on_show_projector(self, opt=None): """ @@ -583,44 +583,49 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa """ lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow()) projector = lwi.data(QtCore.Qt.UserRole) - s = '%s: %s
' % (translate('OpenLP.ProjectorManager', 'Name'), projector.link.name) - s = '%s%s: %s
' % (s, translate('OpenLP.ProjectorManager', 'IP'), projector.link.ip) - s = '%s%s: %s
' % (s, translate('OpenLP.ProjectorManager', 'Port'), projector.link.port) - s = '%s



' % s + 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

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

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

' % s + 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', 'Power status'), + ERROR_MSG[projector.link.power]) + message = '%s%s: %s
' % (message, translate('OpenLP.ProjectorManager', 'Shutter is'), + 'Closed' if projector.link.shutter else 'Open') + message = '%s%s: %s
' % (message, + translate('OpenLP.ProjectorManager', 'Current source input is'), + projector.link.source) + message = '%s

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

' % s - s = '%s%s
' % (s, translate('OpenLP.ProjectorManager', 'Lamp status')) - c = 1 - for i in projector.link.lamp: - s = '%s %s %s (%s) %s: %s
' % (s, - translate('OpenLP.ProjectorManager', 'Lamp'), - c, - translate('OpenLP.ProjectorManager', 'On') if i['On'] else - translate('OpenLP.ProjectorManager', 'Off'), - translate('OpenLP.ProjectorManager', 'Hours'), - i['Hours']) - c = c + 1 - QtGui.QMessageBox.information(self, translate('OpenLP.ProjectorManager', 'Projector Information'), s) + message = '%s%s: %s
' % (message, key, ERROR_MSG[val]) + message = '%s

' % message + message = '%s%s
' % (message, translate('OpenLP.ProjectorManager', 'Lamp status')) + 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 + QtGui.QMessageBox.information(self, translate('OpenLP.ProjectorManager', 'Projector Information'), message) def on_view_projector(self, opt=None): """ @@ -758,8 +763,8 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa """ log.debug('load_projectors()') self.projector_list_widget.clear() - for i in self.projectordb.get_projector_all(): - self.add_projector(i) + for item in self.projectordb.get_projector_all(): + self.add_projector(item) def get_projector_list(self): """ From 5da20ec01dbe89b5b65174eff2e974a9f35bd3e6 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 9 Oct 2014 20:31:51 -0700 Subject: [PATCH 023/115] pep8 cleanups --- openlp/core/ui/projector/wizard.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openlp/core/ui/projector/wizard.py b/openlp/core/ui/projector/wizard.py index d5d7db9fe..44f456493 100644 --- a/openlp/core/ui/projector/wizard.py +++ b/openlp/core/ui/projector/wizard.py @@ -169,7 +169,7 @@ class ProjectorWizard(QtGui.QWizard, RegistryProperties): 'Port: The port number. Default is 4352.
' 'PIN: If needed, enter the PIN access code for the projector.
' '
Once the IP address has been verified as correct and not ' - 'in the database, the rest of the information can be added on the next page.') + 'in the database, the rest of the information can be added on the next page.') self.host_page.ip_number_label.setText(translate('OpenLP.ProjectorWizard', 'IP Address: ')) self.host_page.pjlink_port_label.setText(translate('OpenLP.ProjectorWizard', 'Port: ')) self.host_page.pjlink_pin_label.setText(translate('OpenLP.ProjectorWizard', 'PIN: ')) @@ -230,9 +230,9 @@ class ConnectBase(QtGui.QWizardPage): self.wizard().setButtonLayout(self.myButtons) except: self.wizard().setButtonLayout([QtGui.QWizard.Stretch, - QtGui.QWizard.BackButton, - QtGui.QWizard.NextButton, - QtGui.QWizard.CancelButton]) + QtGui.QWizard.BackButton, + QtGui.QWizard.NextButton, + QtGui.QWizard.CancelButton]) class ConnectWelcomePage(ConnectBase): From fcd14a12cd9011609014c01a57f290ace4ae48d4 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Fri, 10 Oct 2014 08:58:48 -0700 Subject: [PATCH 024/115] tranlate strings and cleanups --- openlp/core/ui/projector/manager.py | 3 ++- openlp/core/ui/projector/wizard.py | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 8fbd1f7db..542853243 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -601,7 +601,8 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa 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'), - 'Closed' if projector.link.shutter else 'Open') + 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) diff --git a/openlp/core/ui/projector/wizard.py b/openlp/core/ui/projector/wizard.py index 44f456493..ffcecaee4 100644 --- a/openlp/core/ui/projector/wizard.py +++ b/openlp/core/ui/projector/wizard.py @@ -161,9 +161,7 @@ class ProjectorWizard(QtGui.QWizard, RegistryProperties): 'Press "Next" button below to continue.')) self.host_page.setTitle(translate('OpenLP.ProjectorWizard', 'Host Address')) self.host_page.setSubTitle(translate('OpenLP.ProjectorWizard', - 'Enter the IP address, port, and PIN used to conenct to the projector. ' - 'The port should only be changed if you know what you\'re doing, and ' - 'the pin should only be entered if it\'s required.')) + 'Enter the IP address, port, and PIN used to conenct to the projector.')) self.host_page.help_ = translate('OpenLP.ProjectorWizard', 'IP Address: The IP address of the projector to connect to.
' 'Port: The port number. Default is 4352.
' @@ -331,7 +329,7 @@ class ConnectHostPage(ConnectBase): '

Please enter a valid IP address.' % adx)) valid = False """ - FIXME - Future plan to retrieve manufacture/model input source information. Not implemented yet. + TODO - Future plan to retrieve manufacture/model input source information. Not implemented yet. new = PJLink(host=adx, port=port, pin=pin if pin.strip() != '' else None) if new.connect(): mfg = new.get_manufacturer() From 91ab6a64bf42ba5568f2c4ead5cd11b9c00ec28c Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Fri, 10 Oct 2014 09:54:35 -0700 Subject: [PATCH 025/115] Tiled icons for toolbar --- openlp/core/ui/projector/wizard.py | 3 +-- resources/images/projector_blank-tiled.png | Bin 0 -> 652 bytes resources/images/projector_connect-tiled.png | Bin 0 -> 920 bytes resources/images/projector_disconnect-tiled.png | Bin 0 -> 914 bytes resources/images/projector_power_off-tiled.png | Bin 0 -> 894 bytes resources/images/projector_power_on-tiled.png | Bin 0 -> 935 bytes resources/images/projector_show-tiled.png | Bin 0 -> 893 bytes 7 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 resources/images/projector_blank-tiled.png create mode 100644 resources/images/projector_connect-tiled.png create mode 100644 resources/images/projector_disconnect-tiled.png create mode 100644 resources/images/projector_power_off-tiled.png create mode 100644 resources/images/projector_power_on-tiled.png create mode 100644 resources/images/projector_show-tiled.png diff --git a/openlp/core/ui/projector/wizard.py b/openlp/core/ui/projector/wizard.py index ffcecaee4..a43f8a3af 100644 --- a/openlp/core/ui/projector/wizard.py +++ b/openlp/core/ui/projector/wizard.py @@ -110,13 +110,12 @@ class ProjectorWizard(QtGui.QWizard, RegistryProperties): self.setWindowTitle(translate('OpenLP.ProjectorWizard', 'New Projector Wizard')) self.setStartId(ConnectWelcome) - self.restart() else: log.debug('ProjectorWizard() Editing existing projector') self.setWindowTitle(translate('OpenLP.ProjectorWizard', 'Edit Projector Wizard')) self.setStartId(ConnectEdit) - self.restart() + self.restart() saved = QtGui.QWizard.exec_(self) self.projector = None return saved diff --git a/resources/images/projector_blank-tiled.png b/resources/images/projector_blank-tiled.png new file mode 100644 index 0000000000000000000000000000000000000000..f70fd807414d811d5bc2c3e154375ce98a0f9150 GIT binary patch literal 652 zcmV;70(1R|P)us!KP>l9)a5v9fLVg#|2v+SBXGq20;GFp&+;Kz^O7v6i{_xrt{l*{G2wrwX= zRRL_g0MI3yAYIu%uwp`jtF)hex4iy#Pa9LK8FYL4&w zJ%fXTUjfWc)up8+4i68x=)`qhR231y^St|E7#_Q>`xCeUV2TI;nM{V|W(6beP( z_a6sA&|_H^VHl#Sn5NkX!!RA6RNJD!%_V@Siot&I}866$1&dkhw92gkT&Ul}j#CY(;#KbKTc~PlUUU{Aus_I!f mo&L74uu!sX`=y9f0R8~g6XuII&ixbs0000e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00RI?L_t(I%axH&NRx3K#-HE& zzRtbxn#`=EJXD8<(jht|6@|DWD6uZltz#E45KvL?V&N z?(Xg-LP!n(=(X~tjqPNm(X@G0{njPCTPU{F+CePqtW(15K_w~`=?e^+_V25hn3z}| z92`sn0JXNDsIiQEnHS3zmKIA&Srar~hxl9+8GR1Z><35jdc(>0Z{E3{zj$sLA0Mw) z6ovV1Lieq!8x)Q_4QX%f8ckrbm4SEKkQkeVoJxOgRh>_dHnl7+%b8b`lanJqO&A&) z(mU;rpl7So!+s@&*Xc#V84XCkPe9CPkkfZf^G_cMgu@wTN_EGQgv3V)yHuh zl}e?Eo@KG6!iFlY0?*=ekac-bARqv8I+U##wH-~hd%_d|xLhv6^L!o8^KS(~hyZ}1 zC;&xZD=Eim>k*WGErv=JpaCc@JQO+`LW_w@7_+S}XxiA2H~3+9=eS>_^< z2xqg|7ywi{lSXNgaWND7rz zSS+W);jlg$jlLv=bhWm&2JtrpL7@8k`#qhVotuY;hrOXtDDU(6uHk?2`~8;D(a}e} uy}iDFb(pmw9*@W8=jTuK_4OtHwctCKa7GZy*X1k#0000LPA>lr z?e>MJD^Uo7Tp(u=^I^Yu7Uv>h{r<++bOZ^{ze8n^pe!sbl*{FzPzZ7i!{XxN6beOb zY^+2giH?qriHT`xY3b|hOG-+rt*tFDFHcTRuCK38OiZk(sQ7fZuC8umWF##ut+BB& zH8mAT($mu+hr`*~*^p4FR4$i0FfgFk>kA4Bpf8ijR##VzM&l<2fj}@eHU_U~G@3b` zZo#-fBodpNntFSCp(zrHrlzLUYW1uCQCwVHU0n^dLqkJkG8q`yY&K|XYik1;qoboz zsT3T|&d$ci$0sBt;BYt)kH_OvQc^%3fk4Q}$N+v4iIka{Nuf|^G+J(MZeCsLmX?;4m4St)PwrQ8nN?L)Af~3K1_abq z-NkqqkH>q?<25xDf&S*^=0D^<7$Bd|2al})OKW?3dq+oyP$=x|?Ck35`m>$Y-QC^O z)6?JIKM0r$iN)gKx5HorrUXXD$H$dQ<;29q&Bn{H-iM!&c+5Ae(z z42CxgmZhbo<>h4;Nal!@bKv9 zC>RVLA0MBboSdGXe*7o&@5&1NB86RIgP#=kET2Y42wpq;tazAxn~1c0ek}`%^S@@X zxCjzaaTZ~k>5Om8zo5wTB4?$4eR41?tUD0M zb!o4XQT_*S&m=R>=VYS#_6$ZSh-dzn~V40s=3pB0Hwsy)2FAdUAk0%dtiXw5_kLiUooy;{lW7*L=X@N zf!JAY%>l4AIr+`N&6}^!0Wa_;o<}j0 zdG3!`EY*Ad{L%*(FS?ggscnD<(pg)heQ1cJuHz;WwD~?LMO(9p0FaXQ;o)OV-=F(? zdV2bDDs}Qd2Odb*b?NEsq094l3iHwYJxVxLe!~`7&4p7bKu`CNhA+{bqWRI@DaA$b<``D2qZbpc+T3soM zKTaXv8KbhfN&oTV{53j?aqb+YoiM#tsHqehM16SGX>Vs~e0)TDLJ$fo=EvToyqZNS zg<}_~77En*`beBQMa6O0n@;28^K_m$L)r7F*)}zh(vu){Mfv^$`I%d2;V?nMqFO4^ zeefW~>tCTYn;3>c&3;T^+jNbNQpxAH0gaF*0s&qfLb_z`LYhEnn7zGdO5xo34dvQY zT^HT8d357CamzxQ<~CrzC%$uK_@_ccEh=46oVr1I`7U-QgHnp%{tW8T5>;Qo($7?~ z3+SfF{HNncrG#jmS8+wJY%Lzj^aR UyUsj^y8r+H07*qoM6N<$f{0MD#Q*>R literal 0 HcmV?d00001 diff --git a/resources/images/projector_power_on-tiled.png b/resources/images/projector_power_on-tiled.png new file mode 100644 index 0000000000000000000000000000000000000000..b38970c609abc291d7549450859bcf1c8b92f5dc GIT binary patch literal 935 zcmV;Y16cftP)qC12joQ zK~y-)ZIfMSm317)KmY&pI?tJtogLC`%(YxAC5aRV5?eEtg1fLz6e}X5h+x&~wl1Q} zq8lR!^2Q=TL`=##VfnH-mdYgGg3Tt!z3*_&*?E6?&U2pU--T1K`d)qcec{XJ;`bE* zFCE(4xo|aWqCoMel&6ro5PRNzZla?nT>VHKdn6ZqhqrVc-1Et3(aWw~1pk&$aBghm z_>uR{zYcKV(6gKxyna8cZ87>j`}M8glq*Q3R+;@PhEfWFSfjb`7pCKJW1Sf9Jh1JH zue*P_^`KnWg&|jp^E9N}Y2CbqwoOl=)c=6=im+1t%fmm8HUXUv3xpIRw_0F-cpqb> zpD?`|UTAuiABMhRQ?QkIagjPJOjT7#m*d2eD~;RU3=KTp-BLO6{-1|+-I7vebhNNZ zEVD#^$ERGpI>L+Hy(ko0#gi!IBM=B7>FuFeDfL~@SkKM=nW(hu3^9J}HkN7Oqzh3W@n%(Sjt9;mFJkfkh3KUoP^Ki_%JKCGH6mGU0Nkk ziV=BYfbOPOu(TkrwGR?6F4NN-VQc*^5~XD{sY$7Pu!9yyzW`>VSN5HosaD$@1~JTN~x4WGbC=o!>~-gJUWQs+k}D*7}CUbTs-Ac z_KIkhU@CJBTVKbGp(w%n8Yu$}W+l`Xz^>CVERC}#e`ht5wmbmjtKYll}>W(;|9y&)lcRW!IxZf6zJcg?g6)y!_U0WMrZbCWH*q&_I9XS0@88I`Z=ayz zJEWW#LA?%33lRCZ7dI(L%@-m9pmleMdlO08x*GIiqM~Z*&C7mO?cC5(L#QQ4-y6L& z1=|?CGsKNEQPK+~P>R%SG4k+XACLg-dZ%^RuloA}VVgj`iBYSe=>pGDl(Q~{WChKX zOr2TT``_sSwHs~Q4C-CVRm!XSD1U9|0;DDoQlQspPJB8y_74>jZ17_ZU+Dk<002ov JPDHLkV1g}gxp)8o literal 0 HcmV?d00001 diff --git a/resources/images/projector_show-tiled.png b/resources/images/projector_show-tiled.png new file mode 100644 index 0000000000000000000000000000000000000000..9d7ff8d08b21758e1bf058749a6a5c955ecc30e5 GIT binary patch literal 893 zcmV-@1A_dCP)KfmW~-qcCgLSY+|g$i6W!o+Na%Du^wsGAiGBCKw^@Gklbx{Z|( zg{}q{x`@3{gkTqKQE=N*c5rPS^96LAd1joMXPnXJ?Q|vBe(uiU`#pU5a@4}Y!uytG z=^`Qku~^JqUS9sPx3~A#NF?HM1<&(JqobpLU)>pnd|~1FzdNx&LM+R|G)?q~!RE8f z*D8c_-{t(`g4S3Q-QC^yflmQ4nG8KWJs6_6xPGEyAHV$}N>dODxB`YhYs41MWLp+`Kc^ z01%NDL)SDOG;-Vyd=mALdc@^0RznCt6e=o&Nu5F%pox&;P4@QoI%Z~OoY~n~Ukk&K zhpT^a?z#k=(caO->iRnFb`kyPoM=I4u{Bz3Mf^CRdB1_NnOBFaPn(*Wo0Xvy>eP!6 zh{85q_QNXWVu^23pP|}-r~`Ht>NPlPK&644dfY1)@d6*iwu^uKHvh{jwq1Pd=|4MW zUoKDJ`)GQOaI}QhQHP}i*e-xyL!5K)d?Yjw0b^=v>Jxy)#l^#XxqiKR0h+1vz{=n( z)QGp3aFD0CpQBtUQK_7wc^+9hc@Kqg`B57i8%ICi{iz*@KKy-&yGsYWaU)4*tIy+2 zo8w%SO2xqsz_d(4%jXt?c6lJ3PR~wEOnhaU<~_$bXtS{ Date: Fri, 10 Oct 2014 10:29:52 -0700 Subject: [PATCH 026/115] Relocate second toolbar to top --- openlp/core/ui/projector/manager.py | 28 +++++++++++++++----------- resources/images/openlp-2.qrc | 2 ++ resources/images/projector_spacer.png | Bin 0 -> 170 bytes 3 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 resources/images/projector_spacer.png diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 7adc3ac28..bad8d9ecd 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -132,34 +132,29 @@ class Ui_ProjectorManager(object): triggers=self.on_show_projector) self.top_toolbar.addSeparator() self.layout.addWidget(self.top_toolbar) - # Add the projector list box self.projector_widget = QtGui.QWidgetAction(self.top_toolbar) self.projector_widget.setObjectName('projector_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.bottom_toolbar = OpenLPToolbar(widget) + self.bottom_toolbar.add_toolbar_action('connect_all_projectors', + text=translate('OpenLP.ProjectorManager', 'Nothing here'), + icon=':/projector/projector_spacer.png') + self.bottom_toolbar.addSeparator() self.bottom_toolbar.add_toolbar_action('connect_all_projectors', text=translate('OpenLP.ProjectorManager', 'Connect to all projectors'), - icon=':/projector/projector_connect-tiled.png', + icon=':/projector/projector_connect_tiled.png', tootip=translate('OpenLP.ProjectorManager', 'Connect to all projectors'), triggers=self.on_connect_all_projectors) self.bottom_toolbar.add_toolbar_action('disconnect_all_projectors', text=translate('OpenLP.ProjectorManager', 'Disconnect from all projectors'), - icon=':/projector/projector_disconnect-tiled.png', + icon=':/projector/projector_disconnect_tiled.png', tooltip=translate('OpenLP.ProjectorManager', 'Disconnect from all projectors'), triggers=self.on_disconnect_all_projectors) self.bottom_toolbar.addSeparator() self.bottom_toolbar.add_toolbar_action('poweron_all_projectors', text=translate('OpenLP.ProjectorManager', 'Power On All Projectors'), - icon=':/projector/projector_power_on-tiled.png', + icon=':/projector/projector_power_on_tiled.png', tooltip=translate('OpenLP.ProjectorManager', 'Power on all projectors'), triggers=self.on_poweron_all_projectors) self.bottom_toolbar.add_toolbar_action('poweroff_all_projectors', @@ -181,7 +176,16 @@ class Ui_ProjectorManager(object): tooltip=translate('OpenLP.ProjectorManager', 'Show all projector screens'), triggers=self.on_show_all_projectors) + self.bottom_toolbar.addSeparator() self.layout.addWidget(self.bottom_toolbar, alignment=QtCore.Qt.AlignBottom) + # 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) # Build the context menu self.menu = QtGui.QMenu() diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index fd37afcc4..0d7c19e63 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -174,6 +174,7 @@ projector_blank.png projector_blank_tiled.png projector_connect.png + projector_connect_tiled.png projector_connectors.png projector_cooldown.png projector_disconnect.png @@ -193,6 +194,7 @@ projector_power_on_tiled.png projector_show.png projector_show_tiled.png + projector_spacer.png projector_status.png projector_warmup.png projector_view.png diff --git a/resources/images/projector_spacer.png b/resources/images/projector_spacer.png new file mode 100644 index 0000000000000000000000000000000000000000..ca5a54a04af8bc1b5e6b64c0f3821d01664d0393 GIT binary patch literal 170 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmPq7nh&}t6oLdYM_v0iEBiObAE1aYF-J0b5UwyNotBh zd1gt5g1e`0KzJjcI8c$0r;B5V#p&b(3D(64B0WqT3=Av+42-SztBQcK44$rjF6*2U FngGWaD7pXu literal 0 HcmV?d00001 From 26db1f16ad19902f21be34ac871eab2693c19466 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Fri, 10 Oct 2014 12:14:15 -0700 Subject: [PATCH 027/115] pep8 --- openlp/core/ui/projector/manager.py | 65 +++++++++++++++-------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index bad8d9ecd..6012ab693 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -88,52 +88,53 @@ class Ui_ProjectorManager(object): triggers=self.on_add_projector) self.top_toolbar.addSeparator() self.top_toolbar.add_toolbar_action('connect_selected_projectors', - text=translate('OpenLP.ProjectorManager', - 'Connect to selected projectors'), - icon=':/projector/projector_connect.png', - tootip=translate('OpenLP.ProjectorManager', - 'Connect to selected projectors'), - triggers=self.on_connect_projector) + text=translate('OpenLP.ProjectorManager', + 'Connect to selected projectors'), + icon=':/projector/projector_connect.png', + tootip=translate('OpenLP.ProjectorManager', + 'Connect to selected projectors'), + triggers=self.on_connect_projector) self.top_toolbar.add_toolbar_action('disconnect_selected_projectors', - text=translate('OpenLP.ProjectorManager', + text=translate('OpenLP.ProjectorManager', + 'Disconnect from selected projectors'), + icon=':/projector/projector_disconnect.png', + tooltip=translate('OpenLP.ProjectorManager', 'Disconnect from selected projectors'), - icon=':/projector/projector_disconnect.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Disconnect from selected projectors'), - triggers=self.on_disconnect_projector) + triggers=self.on_disconnect_projector) self.top_toolbar.addSeparator() self.top_toolbar.add_toolbar_action('poweron_selected_projectors', - text=translate('OpenLP.ProjectorManager', + text=translate('OpenLP.ProjectorManager', + 'Power on selected projectors'), + icon=':/projector/projector_power_on.png', + tooltip=translate('OpenLP.ProjectorManager', 'Power on selected projectors'), - icon=':/projector/projector_power_on.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Power on selected projectors'), - triggers=self.on_poweron_projector) + triggers=self.on_poweron_projector) self.top_toolbar.add_toolbar_action('poweroff_selected_projectors', - text=translate('OpenLP.ProjectorManager', 'Standby selected projectors'), - icon=':/projector/projector_power_off.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Put selected projectors in standby'), - triggers=self.on_poweroff_projector) + text=translate('OpenLP.ProjectorManager', 'Standby selected projectors'), + icon=':/projector/projector_power_off.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Put selected projectors in standby'), + triggers=self.on_poweroff_projector) self.top_toolbar.addSeparator() self.top_toolbar.add_toolbar_action('blank_selected_projectors', - text=translate('OpenLP.ProjectorManager', + text=translate('OpenLP.ProjectorManager', + 'Blank selected projector screens'), + icon=':/projector/projector_blank.png', + tooltip=translate('OpenLP.ProjectorManager', 'Blank selected projector screens'), - icon=':/projector/projector_blank.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Blank selected projector screens'), - triggers=self.on_blank_projector) + triggers=self.on_blank_projector) self.top_toolbar.add_toolbar_action('show_selected_projectors', - text=translate('OpenLP.ProjectorManager', + ext=translate('OpenLP.ProjectorManager', + 'Show selected projector screens'), + icon=':/projector/projector_show.png', + tooltip=translate('OpenLP.ProjectorManager', 'Show selected projector screens'), - icon=':/projector/projector_show.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Show selected projector screens'), - triggers=self.on_show_projector) + triggers=self.on_show_projector) self.top_toolbar.addSeparator() self.layout.addWidget(self.top_toolbar) self.projector_widget = QtGui.QWidgetAction(self.top_toolbar) - self.projector_widget.setObjectName('projector_widget') + self.projector_widget.setObjectName('projector_top_toolbar_widget') + # Create the second toolbar self.bottom_toolbar = OpenLPToolbar(widget) self.bottom_toolbar.add_toolbar_action('connect_all_projectors', text=translate('OpenLP.ProjectorManager', 'Nothing here'), From 72cca862080243a85f0d3dc92c15072227e1a9ad Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 13 Oct 2014 07:51:23 -0700 Subject: [PATCH 028/115] Refactor pjlink/remove db id in view projector/add pjlink command to constants --- openlp/core/lib/projector/constants.py | 3 ++- openlp/core/lib/projector/pjlink1.py | 25 ++++++++++++++----------- openlp/core/ui/projector/manager.py | 14 +++++++++----- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/openlp/core/lib/projector/constants.py b/openlp/core/lib/projector/constants.py index 72903f45f..eec17bf47 100644 --- a/openlp/core/lib/projector/constants.py +++ b/openlp/core/lib/projector/constants.py @@ -61,7 +61,8 @@ LF = chr(0x0A) # \n PJLINK_PORT = 4352 TIMEOUT = 30.0 PJLINK_MAX_PACKET = 136 -PJLINK_VALID_CMD = {'1': ['POWR', # Power option +PJLINK_VALID_CMD = {'1': ['PJLINK', # Initial connection + 'POWR', # Power option 'INPT', # Video sources option 'AVMT', # Shutter option 'ERST', # Error status option diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index e557839d6..e6d63362f 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -128,17 +128,7 @@ class PJLink1(QTcpSocket): self.setReadBufferSize(self.maxSize) # PJLink projector information self.pjlink_class = '1' # Default class - self.power = S_OFF - self.pjlink_name = None - self.manufacturer = None - self.model = None - self.shutter = None - self.mute = None - self.lamp = None - self.fan = None - self.source_available = None - self.source = None - self.projector_errors = None + self.reset_information() # Set from ProjectorManager.add_projector() self.widget = None # QListBox entry self.timer = None # Timer that calls the poll_loop @@ -156,6 +146,19 @@ class PJLink1(QTcpSocket): 'POWR': self.process_powr } + def reset_information(self): + self.power = S_OFF + self.pjlink_name = None + self.manufacturer = None + self.model = None + self.shutter = None + self.mute = None + self.lamp = None + self.fan = None + self.source_available = None + self.source = None + self.projector_errors = None + def thread_started(self): """ Connects signals to methods when thread is started. diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 6012ab693..e62a2b4f2 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -60,7 +60,8 @@ STATUS_ICONS = {S_NOT_CONNECTED: ':/projector/projector_item_disconnect.png', E_ERROR: ':/projector/projector_error.png', E_NETWORK: ':/projector/projector_not_connected.png', E_AUTHENTICATION: ':/projector/projector_not_connected.png', - E_UNKNOWN_SOCKET_ERROR: ':/icons/openlp-logo-64x64.png' + E_UNKNOWN_SOCKET_ERROR: ':/icons/openlp-logo-64x64.png', + E_NOT_CONNECTED: ':/projector/projector_not_connected.png' } @@ -730,10 +731,9 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa notes = translate('OpenLP.ProjectorManager', 'Notes') QtGui.QMessageBox.information(self, translate('OpenLP.ProjectorManager', 'Projector %s Information' % projector.link.name), - '%s: %s

%s: %s

%s: %s

' + '
%s: %s

%s: %s

' '%s: %s

%s: %s

' - '%s:
%s' % (dbid, projector.link.dbid, - ip, projector.link.ip, + '%s:
%s' % (ip, projector.link.ip, port, projector.link.port, name, projector.link.name, location, projector.link.location, @@ -890,7 +890,11 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa log.debug('(%s) updateStatus(status=%s) message: "%s"' % (item.link.name, status_code, message)) if status in STATUS_ICONS: item.icon = QtGui.QIcon(QtGui.QPixmap(STATUS_ICONS[status])) - log.debug('(%s) Updating icon' % item.link.name) + 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) @pyqtSlot() From 498ecb5b6a34e85e32ea149becfdb16fbee9214c Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 13 Oct 2014 08:11:55 -0700 Subject: [PATCH 029/115] increase pin length to allow test pin --- openlp/core/lib/projector/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/lib/projector/db.py b/openlp/core/lib/projector/db.py index 3663ef4bf..7a7e7c550 100644 --- a/openlp/core/lib/projector/db.py +++ b/openlp/core/lib/projector/db.py @@ -118,7 +118,7 @@ class Projector(CommonBase, Base): """ ip = Column(String(100)) port = Column(String(8)) - pin = Column(String(6)) + pin = Column(String(20)) name = Column(String(20)) location = Column(String(30)) notes = Column(String(200)) From e7e53501dfbcc9dd5523d099c412b385904b0329 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 13 Oct 2014 09:40:58 -0700 Subject: [PATCH 030/115] Fix authentication error processing --- openlp/core/lib/projector/pjlink1.py | 51 ++++++++++++++++------------ 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index e6d63362f..ae1d75c90 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -143,6 +143,7 @@ class PJLink1(QTcpSocket): 'INST': self.process_inst, 'LAMP': self.process_lamp, 'NAME': self.process_name, + 'PJLINK': self.check_login, 'POWR': self.process_powr } @@ -157,7 +158,8 @@ class PJLink1(QTcpSocket): self.fan = None self.source_available = None self.source = None - self.projector_errors = None + if hasattr(self, 'timer'): + self.timer.stop() def thread_started(self): """ @@ -192,10 +194,16 @@ class PJLink1(QTcpSocket): # Reset timer in case we were called from a set command self.timer.start() for command in ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']: + # Changeable information self.send_command(command) self.waitForReadyRead() if self.power == S_ON and self.source_available is None: self.send_command('INST') + if self.manufacturer is None: + for command in ['INF1', 'INF2', 'INFO', 'NAME', 'INST']: + log.debug('(%s) Updating %s information' % (self.ip, command)) + self.send_command(cmd=command) + self.waitForReadyRead() def _get_status(self, status): """ @@ -265,27 +273,25 @@ class PJLink1(QTcpSocket): if not data.upper().startswith('PJLINK'): # Invalid response return self.disconnect_from_host() - data_check = data.strip().split(' ') + if '=' in data: + data_check = data.strip().split('=') + else: + data_check = data.strip().split(' ') log.debug('(%s) data_check="%s"' % (self.ip, data_check)) - salt = None # PJLink initial login will be: # 'PJLink 0' - Unauthenticated login - no extra steps required. # 'PJLink 1 XXXXXX' Authenticated login - extra processing required. if data_check[1] == '1': # Authenticated login with salt salt = qmd5_hash(salt=data_check[2], data=self.pin) + else: + salt = None # We're connected at this point, so go ahead and do regular I/O self.readyRead.connect(self.get_data) # Initial data we should know about self.send_command(cmd='CLSS', salt=salt) self.waitForReadyRead() - # These should never change once we get this information - if self.manufacturer is None: - for command in ['INF1', 'INF2', 'INFO', 'NAME', 'INST']: - self.send_command(cmd=command) - self.waitForReadyRead() - self.change_status(S_CONNECTED) - if not self.new_wizard: + if not self.new_wizard and self.state() == self.ConnectedState: self.timer.start() self.poll_loop() @@ -313,9 +319,7 @@ class PJLink1(QTcpSocket): if data.upper().startswith('PJLINK'): # Reconnected from remote host disconnect ? return self.check_login(data) - if '=' in data: - pass - else: + if not '=' in data: log.warn('(%s) Invalid packet received' % self.ip) return data_split = data.split('=') @@ -339,7 +343,7 @@ class PJLink1(QTcpSocket): """ log.debug('(%s) get_error(err=%s): %s' % (self.ip, err, self.errorString())) if err <= 18: - # QSocket errors. Redefined in projectorconstants so we don't mistake + # QSocket errors. Redefined in projector.constants so we don't mistake # them for system errors check = err + E_CONNECTION_REFUSED self.timer.stop() @@ -368,13 +372,17 @@ class PJLink1(QTcpSocket): out = '%s%s %s%s' % (PJLINK_HEADER, cmd, opts, CR) else: out = '%s%s %s%s' % (salt, cmd, opts, CR) - sent = self.write(out) - self.waitForBytesWritten(5000) # 5 seconds should be enough - if sent == -1: - # Network error? + try: + sent = self.write(out) + self.waitForBytesWritten(2000) # 2 seconds should be enough self.projectorNetwork.emit(S_NETWORK_RECEIVED) - self.change_status(E_NETWORK, - translate('OpenLP.PJLink1', 'Error while sending data to projector')) + 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() + self.changeStatus(E_NETWORK, '%s : %s' % (e.error(), e.errorString())) def process_command(self, cmd, data): """ @@ -385,6 +393,7 @@ class PJLink1(QTcpSocket): # Oops - projector error if data.upper() == 'ERRA': # Authentication error + self.disconnect_from_host() self.change_status(E_AUTHENTICATION) return elif data.upper() == 'ERR1': @@ -573,7 +582,7 @@ class PJLink1(QTcpSocket): except TypeError: pass self.change_status(S_NOT_CONNECTED) - self.timer.stop() + self.reset_information() def get_available_inputs(self): """ From 5a1aa5c88c7803566138cb3c7229de4dcf65c88f Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 13 Oct 2014 10:09:23 -0700 Subject: [PATCH 031/115] pep8 --- openlp/core/lib/projector/pjlink1.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index ae1d75c90..f4ed483be 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -76,6 +76,7 @@ class PJLink1(QTcpSocket): changeStatus = pyqtSignal(str, int, str) projectorNetwork = pyqtSignal(int) # Projector network activity projectorStatus = pyqtSignal(int) + projectorAuthentication = pyqtSignal(str) # Authentication error - str=self.name def __init__(self, name=None, ip=None, port=PJLINK_PORT, pin=None, *args, **kwargs): """ @@ -319,7 +320,7 @@ class PJLink1(QTcpSocket): if data.upper().startswith('PJLINK'): # Reconnected from remote host disconnect ? return self.check_login(data) - if not '=' in data: + if '=' not in data: log.warn('(%s) Invalid packet received' % self.ip) return data_split = data.split('=') @@ -379,7 +380,7 @@ class PJLink1(QTcpSocket): if sent == -1: # Network error? self.change_status(E_NETWORK, - translate('OpenLP.PJLink1', 'Error while sending data to projector')) + translate('OpenLP.PJLink1', 'Error while sending data to projector')) except SocketError as e: self.disconnect_from_host() self.changeStatus(E_NETWORK, '%s : %s' % (e.error(), e.errorString())) @@ -395,6 +396,7 @@ class PJLink1(QTcpSocket): # Authentication error self.disconnect_from_host() self.change_status(E_AUTHENTICATION) + self.projectorAuthentication.emit(self.name) return elif data.upper() == 'ERR1': # Undefined command From 5938ccc9a6a2a292a42d594bd5805152e750b68b Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 13 Oct 2014 13:16:03 -0700 Subject: [PATCH 032/115] added get_projector_by_id --- openlp/core/lib/projector/db.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openlp/core/lib/projector/db.py b/openlp/core/lib/projector/db.py index 7a7e7c550..eafbecd85 100644 --- a/openlp/core/lib/projector/db.py +++ b/openlp/core/lib/projector/db.py @@ -150,6 +150,22 @@ class ProjectorDB(Manager): 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 + :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 so they can be added to the Projector From 713a25524117bd79be44117ddf0eaf90b4f4f55b Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 13 Oct 2014 13:16:25 -0700 Subject: [PATCH 033/115] Fix authentication errors --- openlp/core/lib/projector/pjlink1.py | 51 ++++++++++++++++++++++------ openlp/core/ui/projector/manager.py | 32 +++++++++++++++-- 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index f4ed483be..8a126a80a 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -76,7 +76,8 @@ class PJLink1(QTcpSocket): changeStatus = pyqtSignal(str, int, str) projectorNetwork = pyqtSignal(int) # Projector network activity projectorStatus = pyqtSignal(int) - projectorAuthentication = pyqtSignal(str) # Authentication error - str=self.name + projectorAuthentication = pyqtSignal(str) # Authentication error + projectorNoAuthentication = pyqtSignal(str) # PIN set and no authentication needed def __init__(self, name=None, ip=None, port=PJLINK_PORT, pin=None, *args, **kwargs): """ @@ -200,11 +201,19 @@ class PJLink1(QTcpSocket): self.waitForReadyRead() if self.power == S_ON and self.source_available is None: self.send_command('INST') + self.waitForReadyRead() if self.manufacturer is None: - for command in ['INF1', 'INF2', 'INFO', 'NAME', 'INST']: - log.debug('(%s) Updating %s information' % (self.ip, command)) - self.send_command(cmd=command) - self.waitForReadyRead() + self.send_command('INF1') + self.waitForReadyRead() + if self.model is None: + self.send_command('INF2') + self.waitForReadyRead() + if self.pjlink_name is None: + self.send_command('NAME') + self.waitForReadyRead() + if self.power == S_ON and self.source_available is None: + self.send_command('INST') + self.waitForReadyRead() def _get_status(self, status): """ @@ -256,24 +265,29 @@ class PJLink1(QTcpSocket): """ Processes the initial connection and authentication (if needed). """ + log.debug('(%s) check_login(data="%s")' % (self.ip, data)) if data is None: # Reconnected setup? self.waitForReadyRead(5000) # 5 seconds should be more than enough read = self.readLine(self.maxSize) dontcare = self.readLine(self.maxSize) # Clean out the trailing \r\n - if len(read) < 8: + 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)) + 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 if not data.upper().startswith('PJLINK'): # Invalid response return self.disconnect_from_host() + # Test for authentication error if '=' in data: data_check = data.strip().split('=') else: @@ -282,8 +296,25 @@ class PJLink1(QTcpSocket): # PJLink initial login will be: # 'PJLink 0' - Unauthenticated login - no extra steps required. # 'PJLink 1 XXXXXX' Authenticated login - extra processing required. - if data_check[1] == '1': + # Oops - projector 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) + self.projectorAuthentication.emit(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 @@ -294,7 +325,6 @@ class PJLink1(QTcpSocket): self.waitForReadyRead() if not self.new_wizard and self.state() == self.ConnectedState: self.timer.start() - self.poll_loop() def get_data(self): """ @@ -333,7 +363,7 @@ class PJLink1(QTcpSocket): return if not self.check_command(cmd): - log.warn('(%s) Invalid packet - unknown command "%s"' % self.ip, cmd) + log.warn('(%s) Invalid packet - unknown command "%s"' % (self.ip, cmd)) return return self.process_command(cmd, data) @@ -396,6 +426,7 @@ class PJLink1(QTcpSocket): # Authentication error self.disconnect_from_host() self.change_status(E_AUTHENTICATION) + log.debug('(%s) emitting projectorAuthentication() signal' % self.ip) self.projectorAuthentication.emit(self.name) return elif data.upper() == 'ERR1': diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index e62a2b4f2..1e1347c21 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -501,7 +501,14 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa projector.link.changeStatus.disconnect(self.update_status) except TypeError: pass - + try: + projector.link.authentication_error.disconnect(self.authentication_error) + except TypeError: + pass + try: + projector.link.no_authentication_error.disconnect(self.no_authentication_error) + except TypeError: + pass try: projector.timer.stop() projector.timer.timeout.disconnect(projector.link.poll_loop) @@ -566,6 +573,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa 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_all_projectors(self, opt=None): """ @@ -753,7 +761,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa name=projector.name, location=projector.location, notes=projector.notes, - pin=projector.pin + pin=None if projector.pin == '' else projector.pin ) def add_projector(self, opt1, opt2=None): @@ -798,6 +806,8 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa 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) timer = QtCore.QTimer(self) timer.setInterval(20000) # 20 second poll interval timer.timeout.connect(item.link.poll_loop) @@ -837,7 +847,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa self.old_projector.link.name = projector.name self.old_projector.link.ip = projector.ip - self.old_projector.link.pin = projector.pin + self.old_projector.link.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 @@ -913,6 +923,22 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa 'blank_selected_projectors', 'show_selected_projectors'], enabled=True) + @pyqtSlot(str) + def authentication_error(self, name): + QtGui.QMessageBox.warning(self, translate('OpenLP.ProjectorManager', + '"%s" Authentication Error' % name), + '
There was an authentictaion error while trying to connect.' + '

Please verify your PIN setting ' + 'for projector item "%s"' % name) + + @pyqtSlot(str) + def no_authentication_error(self, name): + 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): """ From 39e574dba7acf6bd24dde13d23410e9cfb0fea38 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 13 Oct 2014 13:58:39 -0700 Subject: [PATCH 034/115] Change timer setting based on initial connect/poll --- openlp/core/lib/projector/pjlink1.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 8a126a80a..32e07093a 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -194,6 +194,9 @@ class PJLink1(QTcpSocket): return log.debug('(%s) Updating projector status' % self.ip) # Reset timer in case we were called from a set command + if self.timer.interval() < 5000: + # Reset timer to 5 seconds + self.timer.setInterval(5000) self.timer.start() for command in ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']: # Changeable information @@ -324,6 +327,7 @@ class PJLink1(QTcpSocket): self.send_command(cmd='CLSS', salt=salt) self.waitForReadyRead() if not self.new_wizard and self.state() == self.ConnectedState: + self.timer.setInterval(1000) # Set 1 second for initial information self.timer.start() def get_data(self): From e5743462e231c38a8e8e0ca05e8ec4c68f1531db Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 13 Oct 2014 17:07:54 -0700 Subject: [PATCH 035/115] Added default socket timeout/poll time in settings --- openlp/core/common/settings.py | 3 ++- openlp/core/lib/projector/pjlink1.py | 21 +++++++++++++---- openlp/core/ui/projector/manager.py | 10 ++++++-- openlp/core/ui/projector/tab.py | 34 +++++++++++++++++++++++++--- 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index db925c77f..18e71f837 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -302,7 +302,8 @@ class Settings(QtCore.QSettings): 'projector/connect on start': False, 'projector/last directory import': '', 'projector/last directory export': '', - 'projector/query time': 20 # PJLink socket timeout is 30 seconds + 'projector/poll time': 20, # PJLink timeout is 30 seconds + 'projector/socket timeout': 5 # 5 second socket timeout } __file_path__ = '' __obsolete_settings__ = [ diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 32e07093a..f2bc14c5d 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -119,6 +119,15 @@ class PJLink1(QTcpSocket): self.new_wizard = True else: self.new_wizard = False + if 'poll_time' in kwargs: + # Convert seconds to milliseconds + self.poll_time = kwargs['poll_time'] * 1000 + else: + # Default + self.poll_time = 20000 + if 'socket_timeout' in kwargs: + # Convert seconds to milliseconds + self.socket_timeout = kwargs['socket_timeout'] * 1000 self.i_am_running = False self.status_connect = S_NOT_CONNECTED self.last_command = '' @@ -194,9 +203,9 @@ class PJLink1(QTcpSocket): return log.debug('(%s) Updating projector status' % self.ip) # Reset timer in case we were called from a set command - if self.timer.interval() < 5000: + if self.timer.interval() < self.poll_time: # Reset timer to 5 seconds - self.timer.setInterval(5000) + self.timer.setInterval(self.poll_time) self.timer.start() for command in ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']: # Changeable information @@ -271,7 +280,11 @@ class PJLink1(QTcpSocket): log.debug('(%s) check_login(data="%s")' % (self.ip, data)) if data is None: # Reconnected setup? - self.waitForReadyRead(5000) # 5 seconds should be more than enough + 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: @@ -327,7 +340,7 @@ class PJLink1(QTcpSocket): self.send_command(cmd='CLSS', salt=salt) self.waitForReadyRead() if not self.new_wizard and self.state() == self.ConnectedState: - self.timer.setInterval(1000) # Set 1 second for initial information + self.timer.setInterval(2000) # Set 2 seconds for initial information self.timer.start() def get_data(self): diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 1e1347c21..19648130c 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -274,6 +274,8 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa 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') settings.endGroup() del(settings) @@ -761,7 +763,9 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa name=projector.name, location=projector.location, notes=projector.notes, - pin=None if projector.pin == '' else projector.pin + pin=None if projector.pin == '' else projector.pin, + poll_time=self.poll_time, + socket_timeout=self.socket_timeout ) def add_projector(self, opt1, opt2=None): @@ -809,7 +813,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa item.link.projectorAuthentication.connect(self.authentication_error) item.link.projectorNoAuthentication.connect(self.no_authentication_error) timer = QtCore.QTimer(self) - timer.setInterval(20000) # 20 second poll interval + timer.setInterval(self.poll_time) timer.timeout.connect(item.link.poll_loop) item.timer = timer thread.start() @@ -953,6 +957,8 @@ class ProjectorItem(QObject): self.my_parent = None self.timer = None self.projectordb_item = None + self.poll_time = None + self.socket_timeout = None super(ProjectorItem, self).__init__() diff --git a/openlp/core/ui/projector/tab.py b/openlp/core/ui/projector/tab.py index 0a79595c9..b15fcd2e8 100644 --- a/openlp/core/ui/projector/tab.py +++ b/openlp/core/ui/projector/tab.py @@ -57,15 +57,33 @@ class ProjectorTab(SettingsTab): self.setObjectName('ProjectorTab') super(ProjectorTab, self).setupUi() self.connect_box = QtGui.QGroupBox(self.left_column) - self.connect_box.setTitle('Communication Options') self.connect_box.setObjectName('connect_box') - self.connect_box_layout = QtGui.QVBoxLayout(self.connect_box) + self.connect_box_layout = 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.addWidget(self.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) + self.left_layout.addStretch() def retranslateUi(self): @@ -73,8 +91,14 @@ class ProjectorTab(SettingsTab): 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)')) def load(self): """ @@ -83,6 +107,8 @@ class ProjectorTab(SettingsTab): 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')) settings.endGroup() def save(self): @@ -92,4 +118,6 @@ class ProjectorTab(SettingsTab): 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.endGroup From 5f232ce5fdd8f808b1750455086543283c40e485 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 14 Oct 2014 08:14:16 -0700 Subject: [PATCH 036/115] Fix wrong status on sending data --- openlp/core/lib/projector/pjlink1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index f2bc14c5d..10a03adfa 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -421,9 +421,9 @@ class PJLink1(QTcpSocket): else: out = '%s%s %s%s' % (salt, cmd, opts, CR) try: + self.projectorNetwork.emit(S_NETWORK_SENDING) sent = self.write(out) self.waitForBytesWritten(2000) # 2 seconds should be enough - self.projectorNetwork.emit(S_NETWORK_RECEIVED) if sent == -1: # Network error? self.change_status(E_NETWORK, From cc43efa8090d7f1d3cfe3307250b655f3f4de9c1 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 14 Oct 2014 10:12:49 -0700 Subject: [PATCH 037/115] Toolbar change based on selected projector items --- openlp/core/ui/projector/manager.py | 193 +++++++++++++++++----------- 1 file changed, 117 insertions(+), 76 deletions(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 19648130c..aa225973a 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -58,10 +58,10 @@ STATUS_ICONS = {S_NOT_CONNECTED: ':/projector/projector_item_disconnect.png', S_ON: ':/projector/projector_on.png', S_COOLDOWN: ':/projector/projector_cooldown.png', E_ERROR: ':/projector/projector_error.png', - E_NETWORK: ':/projector/projector_not_connected.png', - E_AUTHENTICATION: ':/projector/projector_not_connected.png', - E_UNKNOWN_SOCKET_ERROR: ':/icons/openlp-logo-64x64.png', - E_NOT_CONNECTED: ':/projector/projector_not_connected.png' + E_NETWORK: ':/projector/projector_error.png', + E_AUTHENTICATION: ':/projector/projector_error.png', + E_UNKNOWN_SOCKET_ERROR: ':/icons/projector_error.png', + E_NOT_CONNECTED: ':/projector/projector_error.png' } @@ -80,106 +80,142 @@ class Ui_ProjectorManager(object): self.layout.setSpacing(0) self.layout.setMargin(0) self.layout.setObjectName('layout') - # Add toolbar - self.top_toolbar = OpenLPToolbar(widget) - self.top_toolbar.add_toolbar_action('newProjector', + # Add spacer toolbar + self.no_toolbar = OpenLPToolbar(widget) + self.no_toolbar.add_toolbar_action('new_projector', text=translate('OpenLP.Projector', 'Add Projector'), icon=':/projector/projector_new.png', tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'), triggers=self.on_add_projector) - self.top_toolbar.addSeparator() - self.top_toolbar.add_toolbar_action('connect_selected_projectors', + self.no_toolbar.addSeparator() + self.no_toolbar.add_toolbar_action('connect_no_projectors', + icon=':/projector/projector_spacer.png') + self.no_toolbar.add_toolbar_action('disconnect_no_projectors', + icon=':/projector/projector_spacer.png') + self.no_toolbar.addSeparator() + self.no_toolbar.add_toolbar_action('poweron_no_projectors', + icon=':/projector/projector_spacer.png') + self.no_toolbar.add_toolbar_action('poweroff_no_projectors', + icon=':/projector/projector_spacer.png') + self.no_toolbar.addSeparator() + self.no_toolbar.add_toolbar_action('blank_no_projectors', + icon=':/projector/projector_spacer.png') + self.no_toolbar.add_toolbar_action('show_no_projectors', + icon=':/projector/projector_spacer.png') + self.no_toolbar.addSeparator() + self.layout.addWidget(self.no_toolbar) + self.projector_one_widget = QtGui.QWidgetAction(self.no_toolbar) + self.projector_one_widget.setObjectName('projector_no_toolbar_widget') + # Add one selection toolbar + self.one_toolbar = OpenLPToolbar(widget) + self.one_toolbar.add_toolbar_action('new_projector', + text=translate('OpenLP.Projector', 'Add Projector'), + icon=':/projector/projector_new.png', + tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'), + triggers=self.on_add_projector) + self.one_toolbar.addSeparator() + self.one_toolbar.add_toolbar_action('connect_selected_projectors', text=translate('OpenLP.ProjectorManager', 'Connect to selected projectors'), icon=':/projector/projector_connect.png', tootip=translate('OpenLP.ProjectorManager', 'Connect to selected projectors'), triggers=self.on_connect_projector) - self.top_toolbar.add_toolbar_action('disconnect_selected_projectors', + self.one_toolbar.add_toolbar_action('disconnect_selected_projectors', text=translate('OpenLP.ProjectorManager', 'Disconnect from selected projectors'), icon=':/projector/projector_disconnect.png', tooltip=translate('OpenLP.ProjectorManager', 'Disconnect from selected projectors'), triggers=self.on_disconnect_projector) - self.top_toolbar.addSeparator() - self.top_toolbar.add_toolbar_action('poweron_selected_projectors', + self.one_toolbar.addSeparator() + self.one_toolbar.add_toolbar_action('poweron_selected_projectors', text=translate('OpenLP.ProjectorManager', 'Power on selected projectors'), icon=':/projector/projector_power_on.png', tooltip=translate('OpenLP.ProjectorManager', 'Power on selected projectors'), triggers=self.on_poweron_projector) - self.top_toolbar.add_toolbar_action('poweroff_selected_projectors', + self.one_toolbar.add_toolbar_action('poweroff_selected_projectors', text=translate('OpenLP.ProjectorManager', 'Standby selected projectors'), icon=':/projector/projector_power_off.png', tooltip=translate('OpenLP.ProjectorManager', 'Put selected projectors in standby'), triggers=self.on_poweroff_projector) - self.top_toolbar.addSeparator() - self.top_toolbar.add_toolbar_action('blank_selected_projectors', + self.one_toolbar.addSeparator() + self.one_toolbar.add_toolbar_action('blank_selected_projectors', text=translate('OpenLP.ProjectorManager', 'Blank selected projector screens'), icon=':/projector/projector_blank.png', tooltip=translate('OpenLP.ProjectorManager', 'Blank selected projector screens'), triggers=self.on_blank_projector) - self.top_toolbar.add_toolbar_action('show_selected_projectors', + self.one_toolbar.add_toolbar_action('show_selected_projectors', ext=translate('OpenLP.ProjectorManager', 'Show selected projector screens'), icon=':/projector/projector_show.png', tooltip=translate('OpenLP.ProjectorManager', 'Show selected projector screens'), triggers=self.on_show_projector) - self.top_toolbar.addSeparator() - self.layout.addWidget(self.top_toolbar) - self.projector_widget = QtGui.QWidgetAction(self.top_toolbar) - self.projector_widget.setObjectName('projector_top_toolbar_widget') - # Create the second toolbar - self.bottom_toolbar = OpenLPToolbar(widget) - self.bottom_toolbar.add_toolbar_action('connect_all_projectors', - text=translate('OpenLP.ProjectorManager', 'Nothing here'), - icon=':/projector/projector_spacer.png') - self.bottom_toolbar.addSeparator() - self.bottom_toolbar.add_toolbar_action('connect_all_projectors', - text=translate('OpenLP.ProjectorManager', 'Connect to all projectors'), - icon=':/projector/projector_connect_tiled.png', - tootip=translate('OpenLP.ProjectorManager', 'Connect to all projectors'), - triggers=self.on_connect_all_projectors) - self.bottom_toolbar.add_toolbar_action('disconnect_all_projectors', - text=translate('OpenLP.ProjectorManager', - 'Disconnect from all projectors'), - icon=':/projector/projector_disconnect_tiled.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Disconnect from all projectors'), - triggers=self.on_disconnect_all_projectors) - self.bottom_toolbar.addSeparator() - self.bottom_toolbar.add_toolbar_action('poweron_all_projectors', - text=translate('OpenLP.ProjectorManager', 'Power On All Projectors'), - icon=':/projector/projector_power_on_tiled.png', - tooltip=translate('OpenLP.ProjectorManager', 'Power on all projectors'), - triggers=self.on_poweron_all_projectors) - self.bottom_toolbar.add_toolbar_action('poweroff_all_projectors', - text=translate('OpenLP.ProjectorManager', 'Standby All Projector'), - icon=':/projector/projector_power_off_tiled.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Put all projectors in standby'), - triggers=self.on_poweroff_all_projectors) - self.bottom_toolbar.addSeparator() - self.bottom_toolbar.add_toolbar_action('blank_all_projectors', - text=translate('OpenLP.ProjectorManager', 'Blank All Projector Screens'), - icon=':/projector/projector_blank_tiled.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Blank all projector screens'), - triggers=self.on_blank_all_projectors) - self.bottom_toolbar.add_toolbar_action('show_all_projector', - text=translate('OpenLP.ProjectorManager', 'Show All Projector Screens'), - icon=':/projector/projector_show_tiled.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Show all projector screens'), - triggers=self.on_show_all_projectors) - self.bottom_toolbar.addSeparator() - self.layout.addWidget(self.bottom_toolbar, alignment=QtCore.Qt.AlignBottom) + self.one_toolbar.addSeparator() + self.layout.addWidget(self.one_toolbar) + self.projector_one_widget = QtGui.QWidgetAction(self.one_toolbar) + self.projector_one_widget.setObjectName('projector_one_toolbar_widget') + # Add many selection toolbar + self.many_toolbar = OpenLPToolbar(widget) + self.many_toolbar.add_toolbar_action('new_projector', + text=translate('OpenLP.Projector', 'Add Projector'), + icon=':/projector/projector_new.png', + tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'), + triggers=self.on_add_projector) + self.many_toolbar.addSeparator() + self.many_toolbar.add_toolbar_action('connect_selected_projectors', + text=translate('OpenLP.ProjectorManager', + 'Connect to selected projectors'), + icon=':/projector/projector_connect_tiled.png', + tootip=translate('OpenLP.ProjectorManager', + 'Connect to selected projectors'), + triggers=self.on_connect_projector) + self.many_toolbar.add_toolbar_action('disconnect_selected_projectors', + text=translate('OpenLP.ProjectorManager', + 'Disconnect from selected projectors'), + icon=':/projector/projector_disconnect_tiled.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Disconnect from selected projectors'), + triggers=self.on_disconnect_projector) + self.many_toolbar.addSeparator() + self.many_toolbar.add_toolbar_action('poweron_selected_projectors', + text=translate('OpenLP.ProjectorManager', + 'Power on selected projectors'), + icon=':/projector/projector_power_on_tiled.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Power on selected projectors'), + triggers=self.on_poweron_projector) + self.many_toolbar.add_toolbar_action('poweroff_selected_projectors', + text=translate('OpenLP.ProjectorManager', 'Standby selected projectors'), + icon=':/projector/projector_power_off_tiled.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Put selected projectors in standby'), + triggers=self.on_poweroff_projector) + self.many_toolbar.addSeparator() + self.many_toolbar.add_toolbar_action('blank_selected_projectors', + text=translate('OpenLP.ProjectorManager', + 'Blank selected projector screens'), + icon=':/projector/projector_blank_tiled.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Blank selected projector screens'), + triggers=self.on_blank_projector) + self.many_toolbar.add_toolbar_action('show_selected_projectors', + ext=translate('OpenLP.ProjectorManager', + 'Show selected projector screens'), + icon=':/projector/projector_show_tiled.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Show selected projector screens'), + triggers=self.on_show_projector) + self.many_toolbar.addSeparator() + self.layout.addWidget(self.many_toolbar) + self.projector_one_widget = QtGui.QWidgetAction(self.many_toolbar) + self.projector_one_widget.setObjectName('projector_many_toolbar_widget') # Create projector manager list self.projector_list_widget = QtGui.QListWidget(widget) self.projector_list_widget.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) @@ -250,6 +286,7 @@ class Ui_ProjectorManager(object): '&Delete Projector'), icon=':/general/general_delete.png', triggers=self.on_delete_projector) + self.update_icons() class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorManager, RegistryProperties): @@ -282,10 +319,12 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa def bootstrap_post_set_up(self): self.load_projectors() self.projector_form = ProjectorWizard(self, projectordb=self.projectordb) - self.top_toolbar.set_widget_enabled(['connect_selected_projectors', 'disconnect_selected_projectors', + ''' + self.one_toolbar.set_widget_enabled(['connect_selected_projectors', 'disconnect_selected_projectors', 'poweron_selected_projectors', 'poweroff_selected_projectors', 'blank_selected_projectors', 'show_selected_projectors'], enabled=False) + ''' self.projector_form.edit_page.newProjector.connect(self.add_projector_from_wizard) self.projector_form.edit_page.editProjector.connect(self.edit_projector_from_wizard) self.projector_list_widget.itemSelectionChanged.connect(self.update_icons) @@ -916,16 +955,18 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa """ Update the icons when the selected projectors change """ - if not self.projector_list_widget.selectedItems(): - self.top_toolbar.set_widget_enabled(['connect_selected_projectors', 'disconnect_selected_projectors', - 'poweron_selected_projectors', 'poweroff_selected_projectors', - 'blank_selected_projectors', 'show_selected_projectors'], - enabled=False) - else: - self.top_toolbar.set_widget_enabled(['connect_selected_projectors', 'disconnect_selected_projectors', - 'poweron_selected_projectors', 'poweroff_selected_projectors', - 'blank_selected_projectors', 'show_selected_projectors'], - enabled=True) + count = len(self.projector_list_widget.selectedItems()) + self.no_toolbar.setHidden(False if count == 0 else True) + self.one_toolbar.setHidden(False if count == 1 else True) + self.many_toolbar.setHidden(False if count > 1 else True) + ''' + enable = False if count == 0 else True + item_list = ['connect_selected_projectors', 'disconnect_selected_projectors', + 'poweron_selected_projectors', 'poweroff_selected_projectors', + 'blank_selected_projectors', 'show_selected_projectors'] + for item in item_list: + self.one_toolbar.set_widget_enabled([item,], enabled=enable) + ''' @pyqtSlot(str) def authentication_error(self, name): From ad6a8ec5ce373e7014ab7bbdfc4706744f193a1a Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 14 Oct 2014 10:40:07 -0700 Subject: [PATCH 038/115] Test cleanups and fix network error icons --- openlp/core/ui/projector/manager.py | 204 +++++++++------------------- 1 file changed, 65 insertions(+), 139 deletions(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index aa225973a..73c2a3d13 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -58,10 +58,10 @@ STATUS_ICONS = {S_NOT_CONNECTED: ':/projector/projector_item_disconnect.png', S_ON: ':/projector/projector_on.png', S_COOLDOWN: ':/projector/projector_cooldown.png', E_ERROR: ':/projector/projector_error.png', - E_NETWORK: ':/projector/projector_error.png', - E_AUTHENTICATION: ':/projector/projector_error.png', - E_UNKNOWN_SOCKET_ERROR: ':/icons/projector_error.png', - E_NOT_CONNECTED: ':/projector/projector_error.png' + E_NETWORK: ':/projector/projector_not_connected.png', + E_AUTHENTICATION: ':/projector/projector_not_connected.png', + E_UNKNOWN_SOCKET_ERROR: ':/icons/projector_not_connected.png', + E_NOT_CONNECTED: ':/projector/projector_not_connected.png' } @@ -83,25 +83,25 @@ class Ui_ProjectorManager(object): # Add spacer toolbar self.no_toolbar = OpenLPToolbar(widget) self.no_toolbar.add_toolbar_action('new_projector', - text=translate('OpenLP.Projector', 'Add Projector'), - icon=':/projector/projector_new.png', - tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'), - triggers=self.on_add_projector) + text=translate('OpenLP.Projector', 'Add Projector'), + icon=':/projector/projector_new.png', + tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'), + triggers=self.on_add_projector) self.no_toolbar.addSeparator() self.no_toolbar.add_toolbar_action('connect_no_projectors', - icon=':/projector/projector_spacer.png') + icon=':/projector/projector_spacer.png') self.no_toolbar.add_toolbar_action('disconnect_no_projectors', - icon=':/projector/projector_spacer.png') + icon=':/projector/projector_spacer.png') self.no_toolbar.addSeparator() self.no_toolbar.add_toolbar_action('poweron_no_projectors', - icon=':/projector/projector_spacer.png') + icon=':/projector/projector_spacer.png') self.no_toolbar.add_toolbar_action('poweroff_no_projectors', - icon=':/projector/projector_spacer.png') + icon=':/projector/projector_spacer.png') self.no_toolbar.addSeparator() self.no_toolbar.add_toolbar_action('blank_no_projectors', - icon=':/projector/projector_spacer.png') + icon=':/projector/projector_spacer.png') self.no_toolbar.add_toolbar_action('show_no_projectors', - icon=':/projector/projector_spacer.png') + icon=':/projector/projector_spacer.png') self.no_toolbar.addSeparator() self.layout.addWidget(self.no_toolbar) self.projector_one_widget = QtGui.QWidgetAction(self.no_toolbar) @@ -116,46 +116,46 @@ class Ui_ProjectorManager(object): self.one_toolbar.addSeparator() self.one_toolbar.add_toolbar_action('connect_selected_projectors', text=translate('OpenLP.ProjectorManager', - 'Connect to selected projectors'), + 'Connect to selected projector'), icon=':/projector/projector_connect.png', tootip=translate('OpenLP.ProjectorManager', - 'Connect to selected projectors'), + 'Connect to selected projector'), triggers=self.on_connect_projector) self.one_toolbar.add_toolbar_action('disconnect_selected_projectors', text=translate('OpenLP.ProjectorManager', - 'Disconnect from selected projectors'), + 'Disconnect from selected projector'), icon=':/projector/projector_disconnect.png', tooltip=translate('OpenLP.ProjectorManager', - 'Disconnect from selected projectors'), + 'Disconnect from selected projector'), triggers=self.on_disconnect_projector) self.one_toolbar.addSeparator() self.one_toolbar.add_toolbar_action('poweron_selected_projectors', text=translate('OpenLP.ProjectorManager', - 'Power on selected projectors'), + 'Power on selected projector'), icon=':/projector/projector_power_on.png', tooltip=translate('OpenLP.ProjectorManager', - 'Power on selected projectors'), + 'Power on selected projector'), triggers=self.on_poweron_projector) self.one_toolbar.add_toolbar_action('poweroff_selected_projectors', - text=translate('OpenLP.ProjectorManager', 'Standby selected projectors'), + text=translate('OpenLP.ProjectorManager', 'Standby selected projector'), icon=':/projector/projector_power_off.png', tooltip=translate('OpenLP.ProjectorManager', - 'Put selected projectors in standby'), + 'Put selected projector in standby'), triggers=self.on_poweroff_projector) self.one_toolbar.addSeparator() self.one_toolbar.add_toolbar_action('blank_selected_projectors', text=translate('OpenLP.ProjectorManager', - 'Blank selected projector screens'), + 'Blank selected projector screen'), icon=':/projector/projector_blank.png', tooltip=translate('OpenLP.ProjectorManager', - 'Blank selected projector screens'), + 'Blank selected projector screen'), triggers=self.on_blank_projector) self.one_toolbar.add_toolbar_action('show_selected_projectors', ext=translate('OpenLP.ProjectorManager', - 'Show selected projector screens'), + 'Show selected projector screen'), icon=':/projector/projector_show.png', tooltip=translate('OpenLP.ProjectorManager', - 'Show selected projector screens'), + 'Show selected projector screen'), triggers=self.on_show_projector) self.one_toolbar.addSeparator() self.layout.addWidget(self.one_toolbar) @@ -164,54 +164,54 @@ class Ui_ProjectorManager(object): # Add many selection toolbar self.many_toolbar = OpenLPToolbar(widget) self.many_toolbar.add_toolbar_action('new_projector', - text=translate('OpenLP.Projector', 'Add Projector'), - icon=':/projector/projector_new.png', - tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'), - triggers=self.on_add_projector) + text=translate('OpenLP.Projector', 'Add Projector'), + icon=':/projector/projector_new.png', + tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'), + triggers=self.on_add_projector) self.many_toolbar.addSeparator() self.many_toolbar.add_toolbar_action('connect_selected_projectors', - text=translate('OpenLP.ProjectorManager', - 'Connect to selected projectors'), - icon=':/projector/projector_connect_tiled.png', - tootip=translate('OpenLP.ProjectorManager', - 'Connect to selected projectors'), - triggers=self.on_connect_projector) + text=translate('OpenLP.ProjectorManager', + 'Connect to selected projectors'), + icon=':/projector/projector_connect_tiled.png', + tootip=translate('OpenLP.ProjectorManager', + 'Connect to selected projectors'), + triggers=self.on_connect_projector) self.many_toolbar.add_toolbar_action('disconnect_selected_projectors', - text=translate('OpenLP.ProjectorManager', - 'Disconnect from selected projectors'), - icon=':/projector/projector_disconnect_tiled.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Disconnect from selected projectors'), - triggers=self.on_disconnect_projector) + text=translate('OpenLP.ProjectorManager', + 'Disconnect from selected projectors'), + icon=':/projector/projector_disconnect_tiled.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Disconnect from selected projectors'), + triggers=self.on_disconnect_projector) self.many_toolbar.addSeparator() self.many_toolbar.add_toolbar_action('poweron_selected_projectors', - text=translate('OpenLP.ProjectorManager', - 'Power on selected projectors'), - icon=':/projector/projector_power_on_tiled.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Power on selected projectors'), - triggers=self.on_poweron_projector) + text=translate('OpenLP.ProjectorManager', + 'Power on selected projectors'), + icon=':/projector/projector_power_on_tiled.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Power on selected projectors'), + triggers=self.on_poweron_projector) self.many_toolbar.add_toolbar_action('poweroff_selected_projectors', - text=translate('OpenLP.ProjectorManager', 'Standby selected projectors'), - icon=':/projector/projector_power_off_tiled.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Put selected projectors in standby'), - triggers=self.on_poweroff_projector) + text=translate('OpenLP.ProjectorManager', 'Standby selected projectors'), + icon=':/projector/projector_power_off_tiled.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Put selected projectors in standby'), + triggers=self.on_poweroff_projector) self.many_toolbar.addSeparator() self.many_toolbar.add_toolbar_action('blank_selected_projectors', - text=translate('OpenLP.ProjectorManager', - 'Blank selected projector screens'), - icon=':/projector/projector_blank_tiled.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Blank selected projector screens'), - triggers=self.on_blank_projector) + text=translate('OpenLP.ProjectorManager', + 'Blank selected projector screens'), + icon=':/projector/projector_blank_tiled.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Blank selected projector screens'), + triggers=self.on_blank_projector) self.many_toolbar.add_toolbar_action('show_selected_projectors', - ext=translate('OpenLP.ProjectorManager', - 'Show selected projector screens'), - icon=':/projector/projector_show_tiled.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Show selected projector screens'), - triggers=self.on_show_projector) + ext=translate('OpenLP.ProjectorManager', + 'Show selected projector screens'), + icon=':/projector/projector_show_tiled.png', + tooltip=translate('OpenLP.ProjectorManager', + 'Show selected projector screens'), + triggers=self.on_show_projector) self.many_toolbar.addSeparator() self.layout.addWidget(self.many_toolbar) self.projector_one_widget = QtGui.QWidgetAction(self.many_toolbar) @@ -319,12 +319,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa def bootstrap_post_set_up(self): self.load_projectors() self.projector_form = ProjectorWizard(self, projectordb=self.projectordb) - ''' - self.one_toolbar.set_widget_enabled(['connect_selected_projectors', 'disconnect_selected_projectors', - 'poweron_selected_projectors', 'poweroff_selected_projectors', - 'blank_selected_projectors', 'show_selected_projectors'], - enabled=False) - ''' self.projector_form.edit_page.newProjector.connect(self.add_projector_from_wizard) self.projector_form.edit_page.editProjector.connect(self.edit_projector_from_wizard) self.projector_list_widget.itemSelectionChanged.connect(self.update_icons) @@ -453,16 +447,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa """ self.projector_form.exec_() - def on_blank_all_projectors(self, opt=None): - """ - Cycles through projector list to send blank screen command - - :param opt: Needed by PyQt4 - :returns: None - """ - for item in self.projector_list: - self.on_blank_projector(item) - def on_blank_projector(self, opt=None): """ Calls projector thread to send blank screen command @@ -505,16 +489,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa except: continue - def on_connect_all_projectors(self, opt=None): - """ - Cycles through projector list to tell threads to connect to projectors - - :param opt: Needed by PyQt4 - :returns: None - """ - for item in self.projector_list: - self.on_connect_projector(item) - def on_delete_projector(self, opt=None): """ Deletes a projector from the list and the database @@ -589,16 +563,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa except: continue - def on_disconnect_all_projectors(self, opt=None): - """ - Cycles through projector list to have projector threads disconnect - - :param opt: Needed by PyQt4 - :returns: None - """ - for item in self.projector_list: - self.on_disconnect_projector(item) - def on_edit_projector(self, opt=None): """ Calls wizard with selected projector to edit information @@ -616,16 +580,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa self.projector_form.exec_(record) new_record = self.projectordb.get_projector_by_id(record.id) - def on_poweroff_all_projectors(self, opt=None): - """ - Cycles through projector list to send Power Off command - - :param opt: Needed by PyQt4 - :returns: None - """ - for item in self.projector_list: - self.on_poweroff_projector(item) - def on_poweroff_projector(self, opt=None): """ Calls projector link to send Power Off command @@ -647,16 +601,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa except: continue - def on_poweron_all_projectors(self, opt=None): - """ - Cycles through projector list to send Power On command - - :param opt: Needed by PyQt4 - :returns: None - """ - for item in self.projector_list: - self.on_poweron_projector(item) - def on_poweron_projector(self, opt=None): """ Calls projector link to send Power On command @@ -678,16 +622,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa except: continue - def on_show_all_projectors(self, opt=None): - """ - Cycles through projector list to send open shutter command - - :param opt: Needed by PyQt4 - :returns: None - """ - for item in self.projector_list: - self.on_show_projector(item.link) - def on_show_projector(self, opt=None): """ Calls projector thread to send open shutter command @@ -959,14 +893,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa self.no_toolbar.setHidden(False if count == 0 else True) self.one_toolbar.setHidden(False if count == 1 else True) self.many_toolbar.setHidden(False if count > 1 else True) - ''' - enable = False if count == 0 else True - item_list = ['connect_selected_projectors', 'disconnect_selected_projectors', - 'poweron_selected_projectors', 'poweroff_selected_projectors', - 'blank_selected_projectors', 'show_selected_projectors'] - for item in item_list: - self.one_toolbar.set_widget_enabled([item,], enabled=enable) - ''' @pyqtSlot(str) def authentication_error(self, name): From 14ed9966471671145d935f09bdadf2962cda7f11 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 14 Oct 2014 10:41:49 -0700 Subject: [PATCH 039/115] Fix icon names to match function --- resources/images/openlp-2.qrc | 2 +- ...nected.png => projector_not_connected_error.png} | Bin 2 files changed, 1 insertion(+), 1 deletion(-) rename resources/images/{projector_not_connected.png => projector_not_connected_error.png} (100%) diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index 0d7c19e63..c922558b2 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -185,7 +185,7 @@ projector_item_disconnect.png projector_manager.png projector_new.png - projector_not_connected.png + projector_not_connected_error.png projector_off.png projector_on.png projector_power_off.png diff --git a/resources/images/projector_not_connected.png b/resources/images/projector_not_connected_error.png similarity index 100% rename from resources/images/projector_not_connected.png rename to resources/images/projector_not_connected_error.png From 6e96851e2ec13c836f17599b064384fbdda27c91 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 14 Oct 2014 11:59:57 -0700 Subject: [PATCH 040/115] Fix toolbar icons disabled state --- openlp/core/ui/projector/manager.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 73c2a3d13..21cdebedc 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -89,19 +89,25 @@ class Ui_ProjectorManager(object): triggers=self.on_add_projector) self.no_toolbar.addSeparator() self.no_toolbar.add_toolbar_action('connect_no_projectors', - icon=':/projector/projector_spacer.png') + icon=':/projector/projector_connect.png', + enabled=False) self.no_toolbar.add_toolbar_action('disconnect_no_projectors', - icon=':/projector/projector_spacer.png') + icon=':/projector/projector_disconnect.png', + enabled=False) self.no_toolbar.addSeparator() self.no_toolbar.add_toolbar_action('poweron_no_projectors', - icon=':/projector/projector_spacer.png') + icon=':/projector/projector_power_on.png', + enabled=False) self.no_toolbar.add_toolbar_action('poweroff_no_projectors', - icon=':/projector/projector_spacer.png') + icon=':/projector/projector_power_off.png', + enabled=False) self.no_toolbar.addSeparator() self.no_toolbar.add_toolbar_action('blank_no_projectors', - icon=':/projector/projector_spacer.png') + icon=':/projector/projector_blank.png', + enabled=False) self.no_toolbar.add_toolbar_action('show_no_projectors', - icon=':/projector/projector_spacer.png') + icon=':/projector/projector_show.png', + enabled=False) self.no_toolbar.addSeparator() self.layout.addWidget(self.no_toolbar) self.projector_one_widget = QtGui.QWidgetAction(self.no_toolbar) From 7931c97698f8353d6120ad80cf458c4a56e66374 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 14 Oct 2014 18:40:21 -0700 Subject: [PATCH 041/115] Toolbar icons/change source icon --- openlp/core/lib/projector/pjlink1.py | 5 +- openlp/core/ui/projector/manager.py | 226 +++++++++++---------------- resources/images/openlp-2.qrc | 2 +- 3 files changed, 98 insertions(+), 135 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 10a03adfa..e3777acfd 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -123,11 +123,14 @@ class PJLink1(QTcpSocket): # Convert seconds to milliseconds self.poll_time = kwargs['poll_time'] * 1000 else: - # Default + # Default 20 seconds self.poll_time = 20000 if 'socket_timeout' in kwargs: # Convert seconds to milliseconds self.socket_timeout = kwargs['socket_timeout'] * 1000 + else: + # Default is 5 seconds + self.socket_timeout = 5000 self.i_am_running = False self.status_connect = S_NOT_CONNECTED self.last_command = '' diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 21cdebedc..8b9a5b238 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -58,10 +58,10 @@ STATUS_ICONS = {S_NOT_CONNECTED: ':/projector/projector_item_disconnect.png', S_ON: ':/projector/projector_on.png', S_COOLDOWN: ':/projector/projector_cooldown.png', E_ERROR: ':/projector/projector_error.png', - E_NETWORK: ':/projector/projector_not_connected.png', - E_AUTHENTICATION: ':/projector/projector_not_connected.png', - E_UNKNOWN_SOCKET_ERROR: ':/icons/projector_not_connected.png', - E_NOT_CONNECTED: ':/projector/projector_not_connected.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' } @@ -80,54 +80,46 @@ class Ui_ProjectorManager(object): self.layout.setSpacing(0) self.layout.setMargin(0) self.layout.setObjectName('layout') - # Add spacer toolbar - self.no_toolbar = OpenLPToolbar(widget) - self.no_toolbar.add_toolbar_action('new_projector', - text=translate('OpenLP.Projector', 'Add Projector'), - icon=':/projector/projector_new.png', - tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'), - triggers=self.on_add_projector) - self.no_toolbar.addSeparator() - self.no_toolbar.add_toolbar_action('connect_no_projectors', - icon=':/projector/projector_connect.png', - enabled=False) - self.no_toolbar.add_toolbar_action('disconnect_no_projectors', - icon=':/projector/projector_disconnect.png', - enabled=False) - self.no_toolbar.addSeparator() - self.no_toolbar.add_toolbar_action('poweron_no_projectors', - icon=':/projector/projector_power_on.png', - enabled=False) - self.no_toolbar.add_toolbar_action('poweroff_no_projectors', - icon=':/projector/projector_power_off.png', - enabled=False) - self.no_toolbar.addSeparator() - self.no_toolbar.add_toolbar_action('blank_no_projectors', - icon=':/projector/projector_blank.png', - enabled=False) - self.no_toolbar.add_toolbar_action('show_no_projectors', - icon=':/projector/projector_show.png', - enabled=False) - self.no_toolbar.addSeparator() - self.layout.addWidget(self.no_toolbar) - self.projector_one_widget = QtGui.QWidgetAction(self.no_toolbar) - self.projector_one_widget.setObjectName('projector_no_toolbar_widget') # Add one selection toolbar self.one_toolbar = OpenLPToolbar(widget) self.one_toolbar.add_toolbar_action('new_projector', - text=translate('OpenLP.Projector', 'Add 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_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=':/general/general_find.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_selected_projectors', + 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('disconnect_selected_projectors', + self.one_toolbar.add_toolbar_action('disconnect_projector', text=translate('OpenLP.ProjectorManager', 'Disconnect from selected projector'), icon=':/projector/projector_disconnect.png', @@ -135,28 +127,28 @@ class Ui_ProjectorManager(object): 'Disconnect from selected projector'), triggers=self.on_disconnect_projector) self.one_toolbar.addSeparator() - self.one_toolbar.add_toolbar_action('poweron_selected_projectors', + 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('poweroff_selected_projectors', + 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.addSeparator() - self.one_toolbar.add_toolbar_action('blank_selected_projectors', + 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('show_selected_projectors', + self.one_toolbar.add_toolbar_action('show_projector', ext=translate('OpenLP.ProjectorManager', 'Show selected projector screen'), icon=':/projector/projector_show.png', @@ -167,61 +159,6 @@ class Ui_ProjectorManager(object): self.layout.addWidget(self.one_toolbar) self.projector_one_widget = QtGui.QWidgetAction(self.one_toolbar) self.projector_one_widget.setObjectName('projector_one_toolbar_widget') - # Add many selection toolbar - self.many_toolbar = OpenLPToolbar(widget) - self.many_toolbar.add_toolbar_action('new_projector', - text=translate('OpenLP.Projector', 'Add Projector'), - icon=':/projector/projector_new.png', - tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'), - triggers=self.on_add_projector) - self.many_toolbar.addSeparator() - self.many_toolbar.add_toolbar_action('connect_selected_projectors', - text=translate('OpenLP.ProjectorManager', - 'Connect to selected projectors'), - icon=':/projector/projector_connect_tiled.png', - tootip=translate('OpenLP.ProjectorManager', - 'Connect to selected projectors'), - triggers=self.on_connect_projector) - self.many_toolbar.add_toolbar_action('disconnect_selected_projectors', - text=translate('OpenLP.ProjectorManager', - 'Disconnect from selected projectors'), - icon=':/projector/projector_disconnect_tiled.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Disconnect from selected projectors'), - triggers=self.on_disconnect_projector) - self.many_toolbar.addSeparator() - self.many_toolbar.add_toolbar_action('poweron_selected_projectors', - text=translate('OpenLP.ProjectorManager', - 'Power on selected projectors'), - icon=':/projector/projector_power_on_tiled.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Power on selected projectors'), - triggers=self.on_poweron_projector) - self.many_toolbar.add_toolbar_action('poweroff_selected_projectors', - text=translate('OpenLP.ProjectorManager', 'Standby selected projectors'), - icon=':/projector/projector_power_off_tiled.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Put selected projectors in standby'), - triggers=self.on_poweroff_projector) - self.many_toolbar.addSeparator() - self.many_toolbar.add_toolbar_action('blank_selected_projectors', - text=translate('OpenLP.ProjectorManager', - 'Blank selected projector screens'), - icon=':/projector/projector_blank_tiled.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Blank selected projector screens'), - triggers=self.on_blank_projector) - self.many_toolbar.add_toolbar_action('show_selected_projectors', - ext=translate('OpenLP.ProjectorManager', - 'Show selected projector screens'), - icon=':/projector/projector_show_tiled.png', - tooltip=translate('OpenLP.ProjectorManager', - 'Show selected projector screens'), - triggers=self.on_show_projector) - self.many_toolbar.addSeparator() - self.layout.addWidget(self.many_toolbar) - self.projector_one_widget = QtGui.QWidgetAction(self.many_toolbar) - self.projector_one_widget.setObjectName('projector_many_toolbar_widget') # Create projector manager list self.projector_list_widget = QtGui.QListWidget(widget) self.projector_list_widget.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) @@ -233,14 +170,9 @@ class Ui_ProjectorManager(object): self.projector_list_widget.customContextMenuRequested.connect(self.context_menu) # Build the context menu self.menu = QtGui.QMenu() - self.view_action = create_widget_action(self.menu, - text=translate('OpenLP.ProjectorManager', - '&View Projector Information'), - icon=':/projector/projector_view.png', - triggers=self.on_view_projector) self.status_action = create_widget_action(self.menu, text=translate('OpenLP.ProjectorManager', - 'View &Projector Status'), + '&View Projector Information'), icon=':/projector/projector_status.png', triggers=self.on_status_projector) self.edit_action = create_widget_action(self.menu, @@ -345,7 +277,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa log.debug('(%s) Building menu - visible = %s' % (projector_name, visible)) self.delete_action.setVisible(True) self.edit_action.setVisible(True) - self.view_action.setVisible(True) self.connect_action.setVisible(not visible) self.disconnect_action.setVisible(visible) self.status_action.setVisible(visible) @@ -664,11 +595,15 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa 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'), @@ -703,31 +638,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa count = count + 1 QtGui.QMessageBox.information(self, translate('OpenLP.ProjectorManager', 'Projector Information'), message) - def on_view_projector(self, opt=None): - """ - Builds message box with projector information stored in database - - :param opt: Needed by PyQt4 - :returns: None - """ - lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow()) - projector = lwi.data(QtCore.Qt.UserRole) - dbid = translate('OpenLP.ProjectorManager', 'DB Entry') - ip = translate('OpenLP.ProjectorManager', 'IP') - port = translate('OpenLP.ProjectorManager', 'Port') - name = translate('OpenLP.ProjectorManager', 'Name') - location = translate('OpenLP.ProjectorManager', 'Location') - notes = translate('OpenLP.ProjectorManager', 'Notes') - QtGui.QMessageBox.information(self, translate('OpenLP.ProjectorManager', - 'Projector %s Information' % projector.link.name), - '
%s: %s

%s: %s

' - '%s: %s

%s: %s

' - '%s:
%s' % (ip, projector.link.ip, - port, projector.link.port, - name, projector.link.name, - location, projector.link.location, - notes, projector.link.notes)) - def _add_projector(self, projector): """ Helper app to build a projector instance @@ -889,6 +799,16 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa 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) + else: + log.debug('item "%s" updating enabled=%s hidden=%s' % (name, enabled, hidden)) + item.setVisible(False if hidden else True) + item.setEnabled(True if enabled else False) @pyqtSlot() def update_icons(self): @@ -896,9 +816,49 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa Update the icons when the selected projectors change """ count = len(self.projector_list_widget.selectedItems()) - self.no_toolbar.setHidden(False if count == 0 else True) - self.one_toolbar.setHidden(False if count == 1 else True) - self.many_toolbar.setHidden(False if count > 1 else True) + 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_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') + 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 + if connected: + self.get_toolbar_item('view_projector', enabled=True) + self.get_toolbar_item('source_projector', enabled=connected & power) + 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_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=True) + self.get_toolbar_item('disconnect_projector', enabled=True) + self.get_toolbar_item('poweron_projector', enabled=True) + self.get_toolbar_item('poweroff_projector', enabled=True) + self.get_toolbar_item('blank_projector', enabled=True) + self.get_toolbar_item('show_projector', enabled=True) + 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_projector', hidden=True) + self.get_toolbar_item('connect_projector', enabled=True) + self.get_toolbar_item('disconnect_projector', enabled=True) + self.get_toolbar_item('poweron_projector', enabled=True) + self.get_toolbar_item('poweroff_projector', enabled=True) + self.get_toolbar_item('blank_projector', enabled=True) + self.get_toolbar_item('show_projector', enabled=True) @pyqtSlot(str) def authentication_error(self, name): diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index c922558b2..b3afe9978 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -175,7 +175,7 @@ projector_blank_tiled.png projector_connect.png projector_connect_tiled.png - projector_connectors.png + projector_hdmi.png projector_cooldown.png projector_disconnect.png projector_disconnect_tiled.png From 0d5709b8f532ac13dfd28dea1396d1a246e38561 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 14 Oct 2014 19:04:56 -0700 Subject: [PATCH 042/115] New connector icon --- resources/images/projector_hdmi.png | Bin 0 -> 351 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/images/projector_hdmi.png diff --git a/resources/images/projector_hdmi.png b/resources/images/projector_hdmi.png new file mode 100644 index 0000000000000000000000000000000000000000..b4a64cb282c381e9eba371bb435b3038665cd197 GIT binary patch literal 351 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmPq7eAwbIcpyiNaq_*7sn8b({Cr}_B98HxSfCeT=l#_ z*#*-Ck5}3o{Dow?6{m(qlpSE&ux0^YZ&r{yhln-qT7b#ZvDysx@c;`qelj~msMo>cnX|ITyx!0WG0E;qja4KO)oaNgK8P~f=X zz3`~ZCLJ8tqHNCpR_x}8%D%PjY2E(dX{rqOw&hABdt9EfYu(Nmy*00^ED~><9V>iN zVs)|fZo2mILYHjcX{jonhrU+1X7_f;ytAKwULtwN-8`SmElfuWWxmxOkMb6A)j8et rdRIH|yz}azp&Qq|emMW$pGN)<-C4}v99o=!{$}uW^>bP0l+XkKeeRBR literal 0 HcmV?d00001 From cf7756d6c745d9a36b71a533dbe13327c000f065 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 14 Oct 2014 19:18:00 -0700 Subject: [PATCH 043/115] Fix packet length check for valid command with empty data --- openlp/core/lib/projector/pjlink1.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index e3777acfd..e3d126dcd 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -172,6 +172,7 @@ class PJLink1(QTcpSocket): self.fan = None self.source_available = None self.source = None + self.other_info = None if hasattr(self, 'timer'): self.timer.stop() @@ -217,6 +218,9 @@ class PJLink1(QTcpSocket): if self.power == S_ON and self.source_available is None: self.send_command('INST') self.waitForReadyRead() + if self.other_info is None: + self.send_command('INFO') + self.waitForReadyRead() if self.manufacturer is None: self.send_command('INF1') self.waitForReadyRead() @@ -362,9 +366,9 @@ class PJLink1(QTcpSocket): self.projectorNetwork.emit(S_NETWORK_RECEIVED) data_in = decode(read, 'ascii') data = data_in.strip() - if len(data) < 8: + if len(data) < 7: # Not enough data for a packet - log.debug('(%s) get_data(): Packet length < 8: "%s"' % (self.ip, data)) + log.debug('(%s) get_data(): Packet length < 7: "%s"' % (self.ip, data)) return log.debug('(%s) Checking new data "%s"' % (self.ip, data)) if data.upper().startswith('PJLINK'): From ef5b597a8bcea1e7e3150f1265926599fd1ebc9f Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 14 Oct 2014 19:18:40 -0700 Subject: [PATCH 044/115] Fix missing information from view item --- openlp/core/ui/projector/manager.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 8b9a5b238..0617a1ae8 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -606,8 +606,10 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa 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', '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'), @@ -616,15 +618,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa message = '%s%s: %s
' % (message, translate('OpenLP.ProjectorManager', 'Current source input is'), projector.link.source) - 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]) - message = '%s

' % message - message = '%s%s
' % (message, translate('OpenLP.ProjectorManager', 'Lamp status')) count = 1 for item in projector.link.lamp: message = '%s %s %s (%s) %s: %s
' % (message, @@ -636,6 +629,13 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa 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): From 03a31f9bc9fcd112313af25f93a454477eb3769b Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 14 Oct 2014 19:30:38 -0700 Subject: [PATCH 045/115] Remove extra spacers in toolbar --- openlp/core/ui/projector/manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 0617a1ae8..422c3843d 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -140,7 +140,7 @@ class Ui_ProjectorManager(object): tooltip=translate('OpenLP.ProjectorManager', 'Put selected projector in standby'), triggers=self.on_poweroff_projector) - self.one_toolbar.addSeparator() + #self.one_toolbar.addSeparator() self.one_toolbar.add_toolbar_action('blank_projector', text=translate('OpenLP.ProjectorManager', 'Blank selected projector screen'), @@ -155,7 +155,6 @@ class Ui_ProjectorManager(object): tooltip=translate('OpenLP.ProjectorManager', 'Show selected projector screen'), triggers=self.on_show_projector) - self.one_toolbar.addSeparator() self.layout.addWidget(self.one_toolbar) self.projector_one_widget = QtGui.QWidgetAction(self.one_toolbar) self.projector_one_widget.setObjectName('projector_one_toolbar_widget') From fb51f505d521e2f9378f51a9ade37df937129c82 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 14 Oct 2014 19:57:13 -0700 Subject: [PATCH 046/115] Extra poll after command returned OK --- openlp/core/lib/projector/pjlink1.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index e3d126dcd..2517d7174 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -474,6 +474,8 @@ class PJLink1(QTcpSocket): # Command succeeded - no extra information if data.upper() == 'OK': log.debug('(%s) Command returned OK' % self.ip) + # A command returned successfully, recheck data + self.poll_loop() return if cmd in self.PJLINK1_FUNC: From e3439172f84325fea35608d97ccefd371bdc3908 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 14 Oct 2014 19:57:58 -0700 Subject: [PATCH 047/115] Toolbar icon enable/disable on power on/off based on projector state --- openlp/core/ui/projector/manager.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 422c3843d..f19a5cd67 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -841,12 +841,16 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa self.get_toolbar_item('source_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=True) - self.get_toolbar_item('disconnect_projector', enabled=True) - self.get_toolbar_item('poweron_projector', enabled=True) - self.get_toolbar_item('poweroff_projector', enabled=True) - self.get_toolbar_item('blank_projector', enabled=True) - self.get_toolbar_item('show_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=projector.link.power == S_STANDBY) + self.get_toolbar_item('poweroff_projector', enabled=projector.link.power == S_ON) + if projector.link.shutter is not None: + self.get_toolbar_item('blank_projector', enabled=not projector.link.shutter) + self.get_toolbar_item('show_projector', enabled=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) From 4d127cd6fac2c9b9bdef37369a87a9daa069534f Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 14 Oct 2014 20:34:06 -0700 Subject: [PATCH 048/115] Fix icon update only when status changes --- openlp/core/ui/projector/manager.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index f19a5cd67..19ebdc6ab 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -791,6 +791,9 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa 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] @@ -895,6 +898,7 @@ class ProjectorItem(QObject): self.projectordb_item = None self.poll_time = None self.socket_timeout = None + self.status = S_NOT_CONNECTED super(ProjectorItem, self).__init__() From 2e3391eff4146664e9eee7e2753f236393571293 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 15 Oct 2014 10:22:12 -0700 Subject: [PATCH 049/115] Remove blocking calls on sockets --- openlp/core/lib/projector/pjlink1.py | 117 ++++++++++++++++++--------- openlp/core/ui/projector/manager.py | 11 ++- 2 files changed, 86 insertions(+), 42 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 2517d7174..546278c1e 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -75,9 +75,11 @@ class PJLink1(QTcpSocket): """ changeStatus = pyqtSignal(str, int, str) projectorNetwork = pyqtSignal(int) # Projector network activity - projectorStatus = pyqtSignal(int) + 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): """ @@ -146,6 +148,8 @@ class PJLink1(QTcpSocket): # 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 # Map command returned to function self.PJLINK1_FUNC = {'AVMT': self.process_avmt, 'CLSS': self.process_clss, @@ -194,6 +198,7 @@ class PJLink1(QTcpSocket): self.connected.disconnect(self.check_login) self.disconnected.disconnect(self.disconnect_from_host) self.error.disconnect(self.get_error) + self.projectorReceivedData.disconnect(self._send_command) self.disconnect_from_host() self.deleteLater() self.i_am_running = False @@ -214,25 +219,18 @@ class PJLink1(QTcpSocket): for command in ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']: # Changeable information self.send_command(command) - self.waitForReadyRead() if self.power == S_ON and self.source_available is None: self.send_command('INST') - self.waitForReadyRead() if self.other_info is None: self.send_command('INFO') - self.waitForReadyRead() if self.manufacturer is None: self.send_command('INF1') - self.waitForReadyRead() if self.model is None: self.send_command('INF2') - self.waitForReadyRead() if self.pjlink_name is None: self.send_command('NAME') - self.waitForReadyRead() if self.power == S_ON and self.source_available is None: self.send_command('INST') - self.waitForReadyRead() def _get_status(self, status): """ @@ -325,7 +323,6 @@ class PJLink1(QTcpSocket): self.disconnect_from_host() self.change_status(E_AUTHENTICATION) log.debug('(%s) emitting projectorAuthentication() signal' % self.name) - self.projectorAuthentication.emit(self.name) return elif data_check[1] == '0' and self.pin is not None: # Pin set and no authentication needed @@ -346,6 +343,7 @@ class PJLink1(QTcpSocket): # Initial data we should know about self.send_command(cmd='CLSS', salt=salt) self.waitForReadyRead() + self.projectorReceivedData.connect(self._send_command) if not self.new_wizard and self.state() == self.ConnectedState: self.timer.setInterval(2000) # Set 2 seconds for initial information self.timer.start() @@ -362,6 +360,7 @@ class PJLink1(QTcpSocket): if read == -1: # No data available log.debug('(%s) get_data(): No data available (-1)' % self.ip) + self.projectorReceivedData.emit() return self.projectorNetwork.emit(S_NETWORK_RECEIVED) data_in = decode(read, 'ascii') @@ -369,13 +368,17 @@ class PJLink1(QTcpSocket): if len(data) < 7: # Not enough data for a packet log.debug('(%s) get_data(): Packet length < 7: "%s"' % (self.ip, data)) + self.projectorReceivedData.emit() return log.debug('(%s) Checking new data "%s"' % (self.ip, data)) if data.upper().startswith('PJLINK'): # Reconnected from remote host disconnect ? - return self.check_login(data) - if '=' not in data: + self.check_login(data) + self.projectorReceivedData.emit() + return + elif '=' not in data: log.warn('(%s) Invalid packet received' % self.ip) + self.projectorReceivedData.emit() return data_split = data.split('=') try: @@ -384,10 +387,12 @@ class PJLink1(QTcpSocket): log.warn('(%s) Invalid packet - expected header + command + data' % self.ip) log.warn('(%s) Received data: "%s"' % (self.ip, read)) self.change_status(E_INVALID_DATA) + self.projectorReceivedData.emit() return if not self.check_command(cmd): log.warn('(%s) Invalid packet - unknown command "%s"' % (self.ip, cmd)) + self.projectorReceivedData.emit() return return self.process_command(cmd, data) @@ -413,10 +418,11 @@ class PJLink1(QTcpSocket): def send_command(self, cmd, opts='?', salt=None): """ - Socket interface to send commands to projector. + Add command to output queue if not already in queue """ 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) Sending cmd="%s" opts="%s" %s' % (self.ip, @@ -427,6 +433,41 @@ class PJLink1(QTcpSocket): out = '%s%s %s%s' % (PJLINK_HEADER, cmd, opts, CR) else: out = '%s%s %s%s' % (salt, 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)) + else: + self.send_queue.append(out) + if not self.send_busy: + self._send_string() + + @pyqtSlot() + def _send_command(self): + log.debug('Received projectorReceivedData signal') + return self._send_string() + + def _send_string(self, data=None): + """ + Socket interface to send data. If data=None, then check queue. + + :param data: Immediate data to send + :returns: None + """ + log.debug('(%s) _send_string()' % self.ip) + if data is not None: + out = data + log.debug('(%s) _send_string(data=%s)' % (self.ip, out)) + elif len(self.send_queue) != 0: + out = self.send_queue.pop(0) + log.debug('(%s) _send_string(queued data=%s)' % (self.ip, out)) + 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) _sed_string(): Sending "%s"' % (self.ip, out)) + log.debug('(%s) _send_string(): Queue = %s' % ( self.ip, self.send_queue)) try: self.projectorNetwork.emit(S_NETWORK_SENDING) sent = self.write(out) @@ -452,36 +493,32 @@ class PJLink1(QTcpSocket): self.change_status(E_AUTHENTICATION) log.debug('(%s) emitting projectorAuthentication() signal' % self.ip) self.projectorAuthentication.emit(self.name) - return elif data.upper() == 'ERR1': # Undefined command self.change_status(E_UNDEFINED, '%s "%s"' % (translate('OpenLP.PJLink1', 'Undefined command:'), cmd)) - return elif data.upper() == 'ERR2': # Invalid parameter self.change_status(E_PARAMETER) - return elif data.upper() == 'ERR3': # Projector busy self.change_status(E_UNAVAILABLE) - return elif data.upper() == 'ERR4': # Projector/display error self.change_status(E_PROJECTOR) - return + self.projectorReceivedData.emit() # Command succeeded - no extra information if data.upper() == 'OK': log.debug('(%s) Command returned OK' % self.ip) # A command returned successfully, recheck data - self.poll_loop() - return + self.projectorReceivedData.emit() if cmd in self.PJLINK1_FUNC: - return self.PJLINK1_FUNC[cmd](data) + self.PJLINK1_FUNC[cmd](data) else: log.warn('(%s) Invalid command %s' % (self.ip, cmd)) + self.projectorReceivedData.emit() def process_lamp(self, data): """ @@ -502,8 +539,12 @@ class PJLink1(QTcpSocket): Power status. See PJLink specification for format. """ if data in PJLINK_POWR_STATUS: - self.power = PJLINK_POWR_STATUS[data] + 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() else: # Log unknown status response log.warn('Unknown power response: %s' % data) @@ -513,20 +554,28 @@ class PJLink1(QTcpSocket): """ Shutter open/closed. See PJLink specification for format. """ + shutter = self.shutter + mute = self.mute if data == '11': - self.shutter = True - self.mute = False + shutter = True + mute = False elif data == '21': - self.shutter = False - self.mute = True + shutter = False + mute = True elif data == '30': - self.shutter = False - self.mute = False + shutter = False + mute = False elif data == '31': - self.shutter = True - self.mute = True + 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): @@ -713,37 +762,27 @@ class PJLink1(QTcpSocket): elif src not in self.source_available: return self.send_command(cmd='INPT', opts=src) - self.waitForReadyRead() - self.poll_loop() def set_power_on(self): """ Send command to turn power to on. """ self.send_command(cmd='POWR', opts='1') - self.waitForReadyRead() - self.poll_loop() def set_power_off(self): """ Send command to turn power to standby. """ self.send_command(cmd='POWR', opts='0') - self.waitForReadyRead() - self.poll_loop() def set_shutter_closed(self): """ Send command to set shutter to closed position. """ self.send_command(cmd='AVMT', opts='11') - self.waitForReadyRead() - self.poll_loop() def set_shutter_open(self): """ Send command to set shutter to open position. """ self.send_command(cmd='AVMT', opts='10') - self.waitForReadyRead() - self.poll_loop() diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 19ebdc6ab..df94e2c64 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -140,7 +140,6 @@ class Ui_ProjectorManager(object): 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'), @@ -260,6 +259,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa self.projector_form.edit_page.editProjector.connect(self.edit_projector_from_wizard) self.projector_list_widget.itemSelectionChanged.connect(self.update_icons) + def context_menu(self, point): """ Build the Right Click Context menu and set state. @@ -460,6 +460,10 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa projector.link.no_authentication_error.disconnect(self.no_authentication_error) except TypeError: pass + try: + projector.link.projectorUpdateIcons.disconnect(self.update_icons) + except TypeError: + pass try: projector.timer.stop() projector.timer.timeout.disconnect(projector.link.poll_loop) @@ -700,6 +704,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa 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) @@ -849,8 +854,8 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa self.get_toolbar_item('poweron_projector', enabled=projector.link.power == S_STANDBY) self.get_toolbar_item('poweroff_projector', enabled=projector.link.power == S_ON) if projector.link.shutter is not None: - self.get_toolbar_item('blank_projector', enabled=not projector.link.shutter) - self.get_toolbar_item('show_projector', enabled=projector.link.shutter) + 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) From 650c5007b259ba9f5b19ed6025c18054d8264fb0 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 15 Oct 2014 10:29:15 -0700 Subject: [PATCH 050/115] Add poll_loop after send command --- openlp/core/lib/projector/pjlink1.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 546278c1e..1d29fc9b0 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -762,27 +762,32 @@ class PJLink1(QTcpSocket): elif src not in self.source_available: return 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() From f468fbb47d0d9199dc74c42229b9ec29a67b5139 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 15 Oct 2014 10:34:27 -0700 Subject: [PATCH 051/115] Fix bad data in process command --- openlp/core/lib/projector/pjlink1.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 1d29fc9b0..f773fb7dd 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -636,6 +636,11 @@ class PJLink1(QTcpSocket): """ Error status. See PJLink Specifications for format. """ + try: + dontcare = int(data) + except ValueError: + # Bad data - ignore + return if int(data) == 0: self.projector_errors = None else: From 8ea09172307eee3cef9dd0aa6e005a6e47073bfe Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 15 Oct 2014 10:38:39 -0700 Subject: [PATCH 052/115] pep8 --- openlp/core/lib/projector/pjlink1.py | 6 +++--- openlp/core/ui/projector/manager.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index f773fb7dd..014bb1efc 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -467,7 +467,7 @@ class PJLink1(QTcpSocket): return self.send_busy = True log.debug('(%s) _sed_string(): Sending "%s"' % (self.ip, out)) - log.debug('(%s) _send_string(): Queue = %s' % ( self.ip, self.send_queue)) + log.debug('(%s) _send_string(): Queue = %s' % (self.ip, self.send_queue)) try: self.projectorNetwork.emit(S_NETWORK_SENDING) sent = self.write(out) @@ -637,11 +637,11 @@ class PJLink1(QTcpSocket): Error status. See PJLink Specifications for format. """ try: - dontcare = int(data) + datacheck = int(data) except ValueError: # Bad data - ignore return - if int(data) == 0: + if datacheck == 0: self.projector_errors = None else: self.projector_errors = {} diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index df94e2c64..93b15537a 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -259,7 +259,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa self.projector_form.edit_page.editProjector.connect(self.edit_projector_from_wizard) self.projector_list_widget.itemSelectionChanged.connect(self.update_icons) - def context_menu(self, point): """ Build the Right Click Context menu and set state. From dc321155a08ade04b3745a6621766e0b4fce4b6e Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 15 Oct 2014 11:18:00 -0700 Subject: [PATCH 053/115] Fix localhost connection --- openlp/core/lib/projector/pjlink1.py | 37 +++++++++++++++++++--------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 014bb1efc..2cbcdbb8d 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -348,11 +348,16 @@ class PJLink1(QTcpSocket): self.timer.setInterval(2000) # Set 2 seconds for initial information self.timer.start() + @pyqtSlot() def get_data(self): + log.debug('(%s) get_data() Received readyRead signal' % self.ip) + return self._get_data() + + def _get_data(self): """ Socket interface to retrieve data. """ - log.debug('(%s) Reading data' % self.ip) + log.debug('(%s) get_data(): Reading data' % self.ip) if self.state() != self.ConnectedState: log.debug('(%s) get_data(): Not connected - returning' % self.ip) return @@ -370,28 +375,28 @@ class PJLink1(QTcpSocket): log.debug('(%s) get_data(): Packet length < 7: "%s"' % (self.ip, data)) self.projectorReceivedData.emit() return - log.debug('(%s) Checking new data "%s"' % (self.ip, data)) + 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.projectorReceivedData.emit() return elif '=' not in data: - log.warn('(%s) Invalid packet received' % self.ip) + log.warn('(%s) get_data(): Invalid packet received' % self.ip) 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) Invalid packet - expected header + command + data' % self.ip) - log.warn('(%s) Received data: "%s"' % (self.ip, read)) + 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.projectorReceivedData.emit() return if not self.check_command(cmd): - log.warn('(%s) Invalid packet - unknown command "%s"' % (self.ip, cmd)) + log.warn('(%s) get_data(): Invalid packet - unknown command "%s"' % (self.ip, cmd)) self.projectorReceivedData.emit() return return self.process_command(cmd, data) @@ -414,6 +419,7 @@ class PJLink1(QTcpSocket): self.change_status(err, self.errorString()) else: self.change_status(E_NETWORK, self.errorString()) + self.projectorUpdateIcons.emit() return def send_command(self, cmd, opts='?', salt=None): @@ -425,17 +431,19 @@ class PJLink1(QTcpSocket): self.send_queue = [] return self.projectorNetwork.emit(S_NETWORK_SENDING) - log.debug('(%s) Sending cmd="%s" opts="%s" %s' % (self.ip, - cmd, - opts, - '' if salt is None else 'with hash')) + log.debug('(%s) send_command(): Sending cmd="%s" opts="%s" %s' % (self.ip, + cmd, + opts, + '' if salt is None else 'with hash')) if salt is None: out = '%s%s %s%s' % (PJLINK_HEADER, cmd, opts, CR) else: out = '%s%s %s%s' % (salt, cmd, opts, CR) 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)) + log.debug('(%s) send_command(out=%s) Already in queue - skipping' % (self.ip, out.strip())) + elif len(self.send_queue) == 0: + return self._send_string(out) else: self.send_queue.append(out) if not self.send_busy: @@ -454,6 +462,11 @@ class PJLink1(QTcpSocket): :returns: None """ log.debug('(%s) _send_string()' % self.ip) + if self.state() != self.ConnectedState: + log.debug('(%s) _send_string() Not connected - abort' % self.ip) + self.send_queue = [] + self.send_busy = False + return if data is not None: out = data log.debug('(%s) _send_string(data=%s)' % (self.ip, out)) @@ -466,7 +479,7 @@ class PJLink1(QTcpSocket): self.send_busy = False return self.send_busy = True - log.debug('(%s) _sed_string(): Sending "%s"' % (self.ip, out)) + log.debug('(%s) _send_string(): Sending "%s"' % (self.ip, out)) log.debug('(%s) _send_string(): Queue = %s' % (self.ip, self.send_queue)) try: self.projectorNetwork.emit(S_NETWORK_SENDING) From 58792096273029ef090f3ee0cbda61d26431adc8 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 15 Oct 2014 11:41:12 -0700 Subject: [PATCH 054/115] Fix toolbar icon status on socket error --- openlp/core/lib/projector/pjlink1.py | 23 ++++++++++++----------- openlp/core/ui/projector/manager.py | 4 ++-- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 2cbcdbb8d..5ca94d7bf 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -218,19 +218,19 @@ class PJLink1(QTcpSocket): self.timer.start() for command in ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']: # Changeable information - self.send_command(command) + self.send_command(command, queue=True) if self.power == S_ON and self.source_available is None: - self.send_command('INST') + self.send_command('INST', queue=True) if self.other_info is None: - self.send_command('INFO') + self.send_command('INFO', queue=True) if self.manufacturer is None: - self.send_command('INF1') + self.send_command('INF1', queue=True) if self.model is None: - self.send_command('INF2') + self.send_command('INF2', queue=True) if self.pjlink_name is None: - self.send_command('NAME') + self.send_command('NAME', queue=True) if self.power == S_ON and self.source_available is None: - self.send_command('INST') + self.send_command('INST', queue=True) def _get_status(self, status): """ @@ -422,7 +422,7 @@ class PJLink1(QTcpSocket): self.projectorUpdateIcons.emit() return - def send_command(self, cmd, opts='?', salt=None): + def send_command(self, cmd, opts='?', salt=None, queue=False): """ Add command to output queue if not already in queue """ @@ -442,12 +442,11 @@ class PJLink1(QTcpSocket): 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 len(self.send_queue) == 0: + elif not queue and len(self.send_queue) == 0 and not self.send_busy: return self._send_string(out) else: self.send_queue.append(out) - if not self.send_busy: - self._send_string() + self.projectorReceivedData.emit() @pyqtSlot() def _send_command(self): @@ -701,6 +700,7 @@ class PJLink1(QTcpSocket): """ if self.state() != self.ConnectedState: log.warn('(%s) disconnect_from_host(): Not connected - returning' % self.ip) + self.projectorUpdateIcons.emit() return self.disconnectFromHost() try: @@ -709,6 +709,7 @@ class PJLink1(QTcpSocket): pass self.change_status(S_NOT_CONNECTED) self.reset_information() + self.projectorUpdateIcons.emit() def get_available_inputs(self): """ diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 93b15537a..2b0f46b0b 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -850,8 +850,8 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa 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=projector.link.power == S_STANDBY) - self.get_toolbar_item('poweroff_projector', enabled=projector.link.power == S_ON) + 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)) From c064a17eceeebe5ba354350a5995ee1d67f0e24c Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 15 Oct 2014 11:45:41 -0700 Subject: [PATCH 055/115] Add toolbar separator --- openlp/core/ui/projector/manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 2b0f46b0b..aa3299e7a 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -140,6 +140,7 @@ class Ui_ProjectorManager(object): 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'), From e4926416f885e4c522d27e2aa2412aaffad3160c Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 15 Oct 2014 13:27:43 -0700 Subject: [PATCH 056/115] Fix unlock send queue --- openlp/core/lib/projector/pjlink1.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 5ca94d7bf..7ed8f3687 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -431,10 +431,10 @@ class PJLink1(QTcpSocket): self.send_queue = [] return self.projectorNetwork.emit(S_NETWORK_SENDING) - log.debug('(%s) send_command(): Sending cmd="%s" opts="%s" %s' % (self.ip, - cmd, - opts, - '' if salt is None else 'with hash')) + 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: @@ -442,11 +442,18 @@ class PJLink1(QTcpSocket): 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 and not self.send_busy: + elif not queue and len(self.send_queue) == 0: + return self._send_string(out) else: + log.debug('(%s) send_command(out=%s) adding to queue' % (self.ip, out.strip())) self.send_queue.append(out) - self.projectorReceivedData.emit() + if not self.send_busy: + 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_string() @pyqtSlot() def _send_command(self): @@ -519,17 +526,21 @@ class PJLink1(QTcpSocket): # Projector/display error self.change_status(E_PROJECTOR) self.projectorReceivedData.emit() - + self.send_busy = False + return # Command succeeded - no extra information - if data.upper() == 'OK': + elif data.upper() == 'OK': log.debug('(%s) Command returned OK' % self.ip) # A command returned successfully, recheck data self.projectorReceivedData.emit() + self.send_busy = False + 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): From f60f8d211bfb984b74430ab53959d4cdb56f5361 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 15 Oct 2014 13:53:17 -0700 Subject: [PATCH 057/115] Fix source select when sources not known yet --- openlp/core/lib/projector/pjlink1.py | 14 ++++++++++---- openlp/core/ui/projector/manager.py | 3 ++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 7ed8f3687..6cf50cb07 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -475,17 +475,17 @@ class PJLink1(QTcpSocket): return if data is not None: out = data - log.debug('(%s) _send_string(data=%s)' % (self.ip, out)) + 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)) + 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)) + log.debug('(%s) _send_string(): Sending "%s"' % (self.ip, out.strip())) log.debug('(%s) _send_string(): Queue = %s' % (self.ip, self.send_queue)) try: self.projectorNetwork.emit(S_NETWORK_SENDING) @@ -550,7 +550,12 @@ class PJLink1(QTcpSocket): lamps = [] data_dict = data.split() while data_dict: - fill = {'Hours': int(data_dict[0]), 'On': False if data_dict[1] == '0' else True} + 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 @@ -653,6 +658,7 @@ class PJLink1(QTcpSocket): for source in check: sources.append(source) self.source_available = sources + self.projectorUpdateIcons.emit() return def process_erst(self, data): diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index aa3299e7a..9330b66f3 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -841,7 +841,8 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa power = projector.link.power == S_ON if connected: self.get_toolbar_item('view_projector', enabled=True) - self.get_toolbar_item('source_projector', enabled=connected & power) + self.get_toolbar_item('source_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: From 645f92e7d4c7a6b445d9650400197022c06f7d3e Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 16 Oct 2014 11:08:44 -0700 Subject: [PATCH 058/115] Change projector wizard to basic edit form --- openlp/core/lib/projector/pjlink1.py | 20 +- openlp/core/ui/__init__.py | 4 +- openlp/core/ui/projector/editform.py | 262 +++++++++++++ openlp/core/ui/projector/manager.py | 21 +- openlp/core/ui/projector/wizard.py | 557 --------------------------- 5 files changed, 294 insertions(+), 570 deletions(-) create mode 100644 openlp/core/ui/projector/editform.py delete mode 100644 openlp/core/ui/projector/wizard.py diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 6cf50cb07..b68300859 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -195,10 +195,22 @@ class PJLink1(QTcpSocket): Cleanups when thread is stopped. """ log.debug('(%s) Thread stopped' % self.ip) - self.connected.disconnect(self.check_login) - self.disconnected.disconnect(self.disconnect_from_host) - self.error.disconnect(self.get_error) - self.projectorReceivedData.disconnect(self._send_command) + 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 diff --git a/openlp/core/ui/__init__.py b/openlp/core/ui/__init__.py index 9bece8dd9..4d9bf2c40 100644 --- a/openlp/core/ui/__init__.py +++ b/openlp/core/ui/__init__.py @@ -125,12 +125,12 @@ from .mediadockmanager import MediaDockManager from .servicemanager import ServiceManager from .thememanager import ThemeManager from .projector.manager import ProjectorManager -from .projector.wizard import ProjectorWizard from .projector.tab import ProjectorTab +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', - 'ProjectorManager', 'ProjectorTab', 'ProjectorWizard'] + 'ProjectorManager', 'ProjectorTab',' ProjectorEditForm'] diff --git a/openlp/core/ui/projector/editform.py b/openlp/core/ui/projector/editform.py new file mode 100644 index 000000000..b40a95cfa --- /dev/null +++ b/openlp/core/ui/projector/editform.py @@ -0,0 +1,262 @@ + +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # +# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod: projectormanager` module provides the functions for + the display/control of Projectors. +""" + +import logging +log = logging.getLogger(__name__) +log.debug('editform loaded') + +from PyQt4 import QtCore, QtGui +from PyQt4.QtCore import QObject, pyqtSlot, pyqtSignal +from PyQt4.QtGui import QDialog, QFormLayout, QPlainTextEdit, QPushButton, QVBoxLayout, \ + QLineEdit, QDialogButtonBox, QHBoxLayout, 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:`~opelp.core.lib.ui.projector.editform.Ui_ProjectorEdiForm` 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.projector.port is None: + title = translate('OpenLP.ProjectorEditForm', 'Add New Projector') + self.projector.port = PJLINK_PORT + self.new_projecor = True + else: + title = translate('OpenLP.ProjectorEditForm', 'Edit Projector') + self.new_projector = False + edit_projector_dialog.setWindowTitle(title) + self.ip_label.setText(translate('OpenLP.ProjetorEditForm', 'IP Address')) + self.ip_text.setText(self.projector.ip) + 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=Projector()): + 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() + + def help_me(self): + """ + Show a help message about the input fields. + """ + log.debug('help_me() signal received') + + 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 index 9330b66f3..eecf79025 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -45,7 +45,8 @@ from openlp.core.lib.ui import create_widget_action 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.wizard import ProjectorWizard +#from openlp.core.ui.projector.wizard import ProjectorWizard +from openlp.core.ui.projector.editform import ProjectorEditForm # Dict for matching projector status to display icon STATUS_ICONS = {S_NOT_CONNECTED: ':/projector/projector_item_disconnect.png', @@ -255,9 +256,15 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa def bootstrap_post_set_up(self): self.load_projectors() + ''' self.projector_form = ProjectorWizard(self, projectordb=self.projectordb) self.projector_form.edit_page.newProjector.connect(self.add_projector_from_wizard) self.projector_form.edit_page.editProjector.connect(self.edit_projector_from_wizard) + ''' + 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 context_menu(self, point): @@ -446,28 +453,28 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa return try: projector.link.projectorNetwork.disconnect(self.update_status) - except TypeError: + except (AttributeError, TypeError): pass try: projector.link.changeStatus.disconnect(self.update_status) - except TypeError: + except (AttributeError, TypeError): pass try: projector.link.authentication_error.disconnect(self.authentication_error) - except TypeError: + except (AttributeError, TypeError): pass try: projector.link.no_authentication_error.disconnect(self.no_authentication_error) - except TypeError: + except (AttributeError, TypeError): pass try: projector.link.projectorUpdateIcons.disconnect(self.update_icons) - except TypeError: + except (AttributeError, TypeError): pass try: projector.timer.stop() projector.timer.timeout.disconnect(projector.link.poll_loop) - except TypeError: + except (AttributeError, TypeError): pass projector.thread.quit() new_list = [] diff --git a/openlp/core/ui/projector/wizard.py b/openlp/core/ui/projector/wizard.py deleted file mode 100644 index a43f8a3af..000000000 --- a/openlp/core/ui/projector/wizard.py +++ /dev/null @@ -1,557 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2014 Raoul Snyman # -# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # -# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # -# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # -# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # -# Christian Richter, Philip Ridout, Ken Roberts, Simon Scudder, # -# Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # -# Dave Warnock, Frode Woldsund, Martin Zibricky, Patrick Zimmermann # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -The :mod:`projector.projectorwizard` module handles the GUI Wizard for adding - new projetor entries. -""" - -import logging -log = logging.getLogger(__name__) -log.debug('projectorwizard loaded') - -from ipaddress import IPv4Address, IPv6Address, AddressValueError - -from PyQt4 import QtCore, QtGui -from PyQt4.QtCore import pyqtSlot, pyqtSignal - -from openlp.core.common import RegistryProperties, translate, is_macosx -from openlp.core.lib import build_icon - -from openlp.core.common import verify_ip_address -from openlp.core.lib.projector.db import ProjectorDB, Projector -from openlp.core.lib.projector.pjlink1 import PJLink1 -from openlp.core.lib.projector.constants import * - -PAGE_COUNT = 4 -(ConnectWelcome, - ConnectHost, - ConnectEdit, - ConnectFinish) = range(PAGE_COUNT) - -PAGE_NEXT = {ConnectWelcome: ConnectHost, - ConnectHost: ConnectEdit, - ConnectEdit: ConnectFinish, - ConnectFinish: -1} - - -class ProjectorWizard(QtGui.QWizard, RegistryProperties): - """ - Wizard for adding/editing projector entries. - """ - def __init__(self, parent, projectordb): - log.debug('__init__()') - super().__init__(parent) - self.db = projectordb - self.projector = None - self.setObjectName('projector_wizard') - self.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) - self.setModal(True) - if is_macosx(): - self.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) - else: - self.setWizardStyle(QtGui.QWizard.ModernStyle) - self.setMinimumSize(650, 550) - self.setOption(QtGui.QWizard.NoBackButtonOnStartPage) - self.spacer = QtGui.QSpacerItem(10, 0, - QtGui.QSizePolicy.Fixed, - QtGui.QSizePolicy.Minimum) - self.setOption(self.HaveHelpButton, True) - self.welcome_page = ConnectWelcomePage(self, ConnectWelcome) - self.host_page = ConnectHostPage(self, ConnectHost) - self.edit_page = ConnectEditPage(self, ConnectEdit) - self.finish_page = ConnectFinishPage(self, ConnectFinish) - self.setPage(self.welcome_page.pageId, self.welcome_page) - self.setPage(self.host_page.pageId, self.host_page) - self.setPage(self.edit_page.pageId, self.edit_page) - self.setPage(self.finish_page.pageId, self.finish_page) - self.registerFields() - self.retranslateUi() - # Connect signals - self.button(QtGui.QWizard.HelpButton).clicked.connect(self.showHelp) - log.debug('ProjectorWizard() started') - - def exec_(self, projector=None): - """ - Override function to determine whether we are called to add a new - projector or edit an old projector. - - :param projector: Projector instance - :returns: None - """ - self.projector = projector - if self.projector is None: - log.debug('ProjectorWizard() Adding new projector') - self.setWindowTitle(translate('OpenLP.ProjectorWizard', - 'New Projector Wizard')) - self.setStartId(ConnectWelcome) - else: - log.debug('ProjectorWizard() Editing existing projector') - self.setWindowTitle(translate('OpenLP.ProjectorWizard', - 'Edit Projector Wizard')) - self.setStartId(ConnectEdit) - self.restart() - saved = QtGui.QWizard.exec_(self) - self.projector = None - return saved - - def registerFields(self): - """ - Register selected fields for use by all pages. - """ - self.host_page.registerField('ip_number*', self.host_page.ip_number_text) - self.edit_page.registerField('pjlink_port', self.host_page.pjlink_port_text) - self.edit_page.registerField('pjlink_pin', self.host_page.pjlink_pin_text) - self.edit_page.registerField('projector_name*', self.edit_page.name_text) - self.edit_page.registerField('projector_location', self.edit_page.location_text) - self.edit_page.registerField('projector_notes', self.edit_page.notes_text, 'plainText') - self.edit_page.registerField('projector_make', self.host_page.manufacturer_text) - self.edit_page.registerField('projector_model', self.host_page.model_text) - - @pyqtSlot() - def showHelp(self): - """ - Show the pop-up help message. - """ - page = self.currentPage() - try: - help_page = page.help_ - except: - help_page = self.no_help - QtGui.QMessageBox.information(self, self.title_help, help_page) - - def retranslateUi(self): - """ - Fixed-text strings used for translations - """ - self.title_help = translate('OpenLP.ProjectorWizard', 'Projector Wizard Help') - self.no_help = translate('OpenLP.ProjectorWizard', - 'Sorry - no help available for this page.') - self.welcome_page.title_label.setText('%s' % - translate('OpenLP.ProjectorWizard', - 'Welcome to the
Projector Wizard')) - self.welcome_page.information_label.setText(translate('OpenLP.ProjectorWizard', 'This wizard will help you to ' - 'create and edit your Projector control.

' - 'Press "Next" button below to continue.')) - self.host_page.setTitle(translate('OpenLP.ProjectorWizard', 'Host Address')) - self.host_page.setSubTitle(translate('OpenLP.ProjectorWizard', - 'Enter the IP address, port, and PIN used to conenct to the projector.')) - self.host_page.help_ = translate('OpenLP.ProjectorWizard', - 'IP Address: The IP address of the projector to connect to.
' - 'Port: The port number. Default is 4352.
' - 'PIN: If needed, enter the PIN access code for the projector.
' - '
Once the IP address has been verified as correct and not ' - 'in the database, the rest of the information can be added on the next page.') - self.host_page.ip_number_label.setText(translate('OpenLP.ProjectorWizard', 'IP Address: ')) - self.host_page.pjlink_port_label.setText(translate('OpenLP.ProjectorWizard', 'Port: ')) - self.host_page.pjlink_pin_label.setText(translate('OpenLP.ProjectorWizard', 'PIN: ')) - self.edit_page.setTitle(translate('OpenLP.ProjectorWizard', 'Add/Edit Projector Information')) - self.edit_page.setSubTitle(translate('OpenLP.ProjectorWizard', - 'Enter the information below in the left panel for the projector.')) - self.edit_page.help_ = translate('OpenLP.ProjectorWizard', - 'Please enter the following information:' - '

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

PIN: The PJLink access PIN. Only required if ' - 'PJLink PIN is set in projector.

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

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

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

The "Manufacturer" and "Model" ' - 'information will only be available if the projector is connected to the ' - 'network and can be accessed while running this wizard. ' - '(Currently not implemented)' % PJLINK_PORT) - self.edit_page.ip_number_label.setText(translate('OpenLP.ProjectorWizard', 'IP Address: ')) - self.edit_page.pjlink_port_label.setText(translate('OpenLP.ProjectorWizard', 'Port: ')) - self.edit_page.pjlink_pin_label.setText(translate('OpenLP.ProjectorWizard', 'PIN: ')) - self.edit_page.name_label.setText(translate('OpenLP.ProjectorWizard', 'Name: ')) - self.edit_page.location_label.setText(translate('OpenLP.ProjectorWizard', 'Location: ')) - self.edit_page.notes_label.setText(translate('OpenLP.ProjectorWizard', 'Notes: ')) - self.edit_page.projector_make_label.setText(translate('OpenLP.ProjectorWizard', 'Manufacturer: ')) - self.edit_page.projector_model_label.setText(translate('OpenLP.ProjectorWizard', 'Model: ')) - self.finish_page.title_label.setText('%s' % - translate('OpenLP.ProjectorWizard', 'Projector Added')) - self.finish_page.information_label.setText(translate('OpenLP.ProjectorWizard', - '
Have fun with your new projector.')) - - -class ConnectBase(QtGui.QWizardPage): - """ - Base class for the projector wizard pages. - """ - def __init__(self, parent=None, page=None): - super().__init__(parent) - self.pageId = page - - def nextId(self): - """ - Returns next page to show. - """ - return PAGE_NEXT[self.pageId] - - def setVisible(self, visible): - """ - Set buttons for bottom of page. - """ - QtGui.QWizardPage.setVisible(self, visible) - if not is_macosx(): - if visible: - try: - self.myCustomButton() - except: - try: - self.wizard().setButtonLayout(self.myButtons) - except: - self.wizard().setButtonLayout([QtGui.QWizard.Stretch, - QtGui.QWizard.BackButton, - QtGui.QWizard.NextButton, - QtGui.QWizard.CancelButton]) - - -class ConnectWelcomePage(ConnectBase): - """ - Splash screen - """ - def __init__(self, parent, page): - super().__init__(parent, page) - if is_macosx(): - self.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) - else: - self.setPixmap(QtGui.QWizard.WatermarkPixmap, - QtGui.QPixmap(':/wizards/wizard_createprojector.png')) - self.setObjectName('welcome_page') - self.myButtons = [QtGui.QWizard.Stretch, - QtGui.QWizard.NextButton] - self.layout = QtGui.QVBoxLayout(self) - self.layout.setObjectName('layout') - self.title_label = QtGui.QLabel(self) - self.title_label.setObjectName('title_label') - self.layout.addWidget(self.title_label) - self.layout.addSpacing(40) - self.information_label = QtGui.QLabel(self) - self.information_label.setWordWrap(True) - self.information_label.setObjectName('information_label') - self.layout.addWidget(self.information_label) - self.layout.addStretch() - - -class ConnectHostPage(ConnectBase): - """ - Get initial information. - """ - def __init__(self, parent, page): - super().__init__(parent, page) - self.setObjectName('host_page') - self.myButtons = [QtGui.QWizard.HelpButton, - QtGui.QWizard.Stretch, - QtGui.QWizard.NextButton, - QtGui.QWizard.CancelButton] - self.hostPageLayout = QtGui.QHBoxLayout(self) - self.hostPageLayout.setObjectName('layout') - # Projector DB information - self.localAreaBox = QtGui.QGroupBox(self) - self.localAreaBox.setObjectName('host_local_area_box') - self.localAreaForm = QtGui.QFormLayout(self.localAreaBox) - self.localAreaForm.setObjectName('host_local_area_form') - self.ip_number_label = QtGui.QLabel(self.localAreaBox) - self.ip_number_label.setObjectName('host_ip_number_label') - self.ip_number_text = QtGui.QLineEdit(self.localAreaBox) - self.ip_number_text.setObjectName('host_ip_number_text') - self.localAreaForm.addRow(self.ip_number_label, self.ip_number_text) - self.pjlink_port_label = QtGui.QLabel(self.localAreaBox) - self.pjlink_port_label.setObjectName('host_pjlink_port_label') - self.pjlink_port_text = QtGui.QLineEdit(self.localAreaBox) - self.pjlink_port_text.setMaxLength(5) - self.pjlink_port_text.setText(str(PJLINK_PORT)) - self.pjlink_port_text.setObjectName('host_pjlink_port_text') - self.localAreaForm.addRow(self.pjlink_port_label, self.pjlink_port_text) - self.pjlink_pin_label = QtGui.QLabel(self.localAreaBox) - self.pjlink_pin_label.setObjectName('host_pjlink_pin_label') - self.pjlink_pin_text = QtGui.QLineEdit(self.localAreaBox) - self.pjlink_pin_text.setObjectName('host_pjlink_pin_text') - self.localAreaForm.addRow(self.pjlink_pin_label, self.pjlink_pin_text) - self.hostPageLayout.addWidget(self.localAreaBox) - self.manufacturer_text = QtGui.QLineEdit(self) - self.manufacturer_text.setVisible(False) - self.model_text = QtGui.QLineEdit(self) - self.model_text.setVisible(False) - - def validatePage(self): - """ - Validate IP number/FQDN before continuing to next page. - """ - adx = self.wizard().field('ip_number') - port = self.wizard().field('pjlink_port') - pin = self.wizard().field('pjlink_pin') - log.debug('ip="%s" port="%s" pin="%s"' % (adx, port, pin)) - valid = verify_ip_address(adx) - if valid: - ip = self.wizard().db.get_projector_by_ip(adx) - if ip is None: - valid = True - else: - QtGui.QMessageBox.warning(self, - translate('OpenLP.ProjectorWizard', '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 - 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 - """ - TODO - Future plan to retrieve manufacture/model input source information. Not implemented yet. - new = PJLink(host=adx, port=port, pin=pin if pin.strip() != '' else None) - if new.connect(): - mfg = new.get_manufacturer() - log.debug('Setting manufacturer_text to %s' % mfg) - self.manufacturer_text.setText(mfg) - model = new.get_model() - log.debug('Setting model_text to %s' % model) - self.model_text.setText(model) - else: - if new.status_error == E_AUTHENTICATION: - QtGui.QMessageBox.warning(self, - translate('OpenLP.ProjectorWizard', 'Requires Authorization'), - translate('OpenLP.ProjectorWizard', - 'Projector requires authorization and either PIN not set ' - 'or invalid PIN set.' - '
Enter a valid PIN before hitting "NEXT"') - ) - elif new.status_error == E_NO_AUTHENTICATION: - QtGui.QMessageBox.warning(self, - translate('OpenLP.ProjectorWizard', 'No Authorization Required'), - translate('OpenLP.ProjectorWizard', - 'Projector does not require authorization and PIN set.' - '
Remove PIN entry before hitting "NEXT"') - ) - valid = False - new.disconnect() - del(new) - """ - return valid - - -class ConnectEditPage(ConnectBase): - """ - Full information page. - """ - newProjector = QtCore.pyqtSignal(str) - editProjector = QtCore.pyqtSignal(object) - - def __init__(self, parent, page): - super().__init__(parent, page) - self.setObjectName('edit_page') - self.editPageLayout = QtGui.QHBoxLayout(self) - self.editPageLayout.setObjectName('layout') - # Projector DB information - self.localAreaBox = QtGui.QGroupBox(self) - self.localAreaBox.setObjectName('edit_local_area_box') - self.localAreaForm = QtGui.QFormLayout(self.localAreaBox) - self.localAreaForm.setObjectName('edit_local_area_form') - self.ip_number_label = QtGui.QLabel(self.localAreaBox) - self.ip_number_label.setObjectName('edit_ip_number_label') - self.ip_number_text = QtGui.QLineEdit(self.localAreaBox) - self.ip_number_text.setObjectName('edit_ip_number_text') - self.localAreaForm.addRow(self.ip_number_label, self.ip_number_text) - self.pjlink_port_label = QtGui.QLabel(self.localAreaBox) - self.pjlink_port_label.setObjectName('edit_pjlink_port_label') - self.pjlink_port_text = QtGui.QLineEdit(self.localAreaBox) - self.pjlink_port_text.setMaxLength(5) - self.pjlink_port_text.setObjectName('edit_pjlink_port_text') - self.localAreaForm.addRow(self.pjlink_port_label, self.pjlink_port_text) - self.pjlink_pin_label = QtGui.QLabel(self.localAreaBox) - self.pjlink_pin_label.setObjectName('pjlink_pin_label') - self.pjlink_pin_text = QtGui.QLineEdit(self.localAreaBox) - self.pjlink_pin_text.setObjectName('pjlink_pin_text') - self.localAreaForm.addRow(self.pjlink_pin_label, self.pjlink_pin_text) - self.name_label = QtGui.QLabel(self.localAreaBox) - self.name_label.setObjectName('name_label') - self.name_text = QtGui.QLineEdit(self.localAreaBox) - self.name_text.setObjectName('name_label') - self.name_text.setMaxLength(20) - self.localAreaForm.addRow(self.name_label, self.name_text) - self.location_label = QtGui.QLabel(self.localAreaBox) - self.location_label.setObjectName('location_label') - self.location_text = QtGui.QLineEdit(self.localAreaBox) - self.location_text.setObjectName('location_text') - self.location_text.setMaxLength(30) - self.localAreaForm.addRow(self.location_label, self.location_text) - self.notes_label = QtGui.QLabel(self.localAreaBox) - self.notes_label.setObjectName('notes_label') - self.notes_text = QtGui.QPlainTextEdit(self.localAreaBox) - self.notes_text.setObjectName('notes_text') - self.localAreaForm.addRow(self.notes_label, self.notes_text) - self.editPageLayout.addWidget(self.localAreaBox) - # Projector retrieved information - self.remoteAreaBox = QtGui.QGroupBox(self) - self.remoteAreaBox.setObjectName('edit_remote_area_box') - self.remoteAreaForm = QtGui.QFormLayout(self.remoteAreaBox) - self.remoteAreaForm.setObjectName('edit_remote_area_form') - self.projector_make_label = QtGui.QLabel(self.remoteAreaBox) - self.projector_make_label.setObjectName('projector_make_label') - self.projector_make_text = QtGui.QLabel(self.remoteAreaBox) - self.projector_make_text.setObjectName('projector_make_text') - self.remoteAreaForm.addRow(self.projector_make_label, self.projector_make_text) - self.projector_model_label = QtGui.QLabel(self.remoteAreaBox) - self.projector_model_label.setObjectName('projector_model_text') - self.projector_model_text = QtGui.QLabel(self.remoteAreaBox) - self.projector_model_text.setObjectName('projector_model_text') - self.remoteAreaForm.addRow(self.projector_model_label, self.projector_model_text) - self.editPageLayout.addWidget(self.remoteAreaBox) - - def initializePage(self): - """ - Fill in the blanks for information from previous page/projector to edit. - """ - if self.wizard().projector is not None: - log.debug('ConnectEditPage.initializePage() Editing existing projector') - self.ip_number_text.setText(self.wizard().projector.ip) - self.pjlink_port_text.setText(str(self.wizard().projector.port)) - self.pjlink_pin_text.setText(self.wizard().projector.pin) - self.name_text.setText(self.wizard().projector.name) - self.location_text.setText(self.wizard().projector.location) - self.notes_text.insertPlainText(self.wizard().projector.notes) - self.myButtons = [QtGui.QWizard.HelpButton, - QtGui.QWizard.Stretch, - QtGui.QWizard.FinishButton, - QtGui.QWizard.CancelButton] - else: - log.debug('Retrieving information from host page') - self.ip_number_text.setText(self.wizard().field('ip_number')) - self.pjlink_port_text.setText(self.wizard().field('pjlink_port')) - self.pjlink_pin_text.setText(self.wizard().field('pjlink_pin')) - make = self.wizard().field('projector_make') - model = self.wizard().field('projector_model') - if make is None or make.strip() == '': - self.projector_make_text.setText(translate('OpenLP.ProjectorWizard', - 'Unavailable ')) - else: - self.projector_make_text.setText(make) - if model is None or model.strip() == '': - self.projector_model_text.setText(translate('OpenLP.ProjectorWizard', - 'Unavailable ')) - else: - self.projector_model_text.setText(model) - self.myButtons = [QtGui.QWizard.HelpButton, - QtGui.QWizard.Stretch, - QtGui.QWizard.BackButton, - QtGui.QWizard.NextButton, - QtGui.QWizard.CancelButton] - - def validatePage(self): - """ - Last verification if editiing existing entry in case of IP change. Add entry to DB. - """ - log.debug('ConnectEditPage().validatePage()') - if self.wizard().projector is not None: - ip = self.ip_number_text.text() - port = self.pjlink_port_text.text() - name = self.name_text.text() - location = self.location_text.text() - notes = self.notes_text.toPlainText() - pin = self.pjlink_pin_text.text() - log.debug('edit-page() Verifying info : ip="%s"' % ip) - valid = verify_ip_address(ip) - if not valid: - QtGui.QMessageBox.warning(self, - translate('OpenLP.ProjectorWizard', 'Invalid IP Address'), - translate('OpenLP.ProjectorWizard', - 'IP address "%s"
is not a valid IP address.' - '

Please enter a valid IP address.' % ip)) - return False - log.debug('Saving edited projector %s' % ip) - self.wizard().projector.ip = ip - self.wizard().projector.port = port - self.wizard().projector.name = name - self.wizard().projector.location = location - self.wizard().projector.notes = notes - self.wizard().projector.pin = pin - saved = self.wizard().db.update_projector(self.wizard().projector) - if not saved: - QtGui.QMessageBox.error(self, translate('OpenLP.ProjectorWizard', 'Database Error'), - translate('OpenLP.ProjectorWizard', 'There was an error saving projector ' - 'information. See the log for the error')) - return False - self.editProjector.emit(self.wizard().projector) - else: - projector = Projector(ip=self.wizard().field('ip_number'), - port=self.wizard().field('pjlink_port'), - name=self.wizard().field('projector_name'), - location=self.wizard().field('projector_location'), - notes=self.wizard().field('projector_notes'), - pin=self.wizard().field('pjlink_pin')) - log.debug('Adding new projector %s' % projector.ip) - if self.wizard().db.get_projector_by_ip(projector.ip) is None: - saved = self.wizard().db.add_projector(projector) - if not saved: - QtGui.QMessageBox.error(self, translate('OpenLP.ProjectorWizard', 'Database Error'), - translate('OpenLP.ProjectorWizard', 'There was an error saving projector ' - 'information. See the log for the error')) - return False - self.newProjector.emit('%s' % projector.ip) - return True - - def nextId(self): - """ - Returns the next page ID if new entry or end of wizard if editing entry. - """ - if self.wizard().projector is None: - return PAGE_NEXT[self.pageId] - else: - return -1 - - -class ConnectFinishPage(ConnectBase): - """ - Buh-Bye page - """ - def __init__(self, parent, page): - super().__init__(parent, page) - self.setObjectName('connect_finish_page') - if is_macosx(): - self.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) - else: - self.setPixmap(QtGui.QWizard.WatermarkPixmap, QtGui.QPixmap(':/wizards/wizard_createprojector.png')) - self.myButtons = [QtGui.QWizard.Stretch, - QtGui.QWizard.FinishButton] - self.isFinalPage() - self.layout = QtGui.QVBoxLayout(self) - self.layout.setObjectName('layout') - self.title_label = QtGui.QLabel(self) - self.title_label.setObjectName('title_label') - self.layout.addWidget(self.title_label) - self.layout.addSpacing(40) - self.information_label = QtGui.QLabel(self) - self.information_label.setWordWrap(True) - self.information_label.setObjectName('information_label') - self.layout.addWidget(self.information_label) - self.layout.addStretch() From ec59abdc85c3f82d061d4eb42475064db3c7cb70 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 16 Oct 2014 11:16:08 -0700 Subject: [PATCH 059/115] pep8 --- openlp/core/lib/projector/pjlink1.py | 2 +- openlp/core/ui/projector/editform.py | 8 +++++--- openlp/core/ui/projector/manager.py | 1 - 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index b68300859..e68862e7e 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -566,7 +566,7 @@ class PJLink1(QTcpSocket): 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)) + log.warn('(%s) process_lamp(): Invalid data "%s"' % (self.ip, data)) return lamps.append(fill) data_dict.pop(0) # Remove lamp hours diff --git a/openlp/core/ui/projector/editform.py b/openlp/core/ui/projector/editform.py index b40a95cfa..ad8c28efd 100644 --- a/openlp/core/ui/projector/editform.py +++ b/openlp/core/ui/projector/editform.py @@ -46,6 +46,7 @@ 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:`~opelp.core.lib.ui.projector.editform.Ui_ProjectorEdiForm` class defines @@ -63,7 +64,7 @@ class Ui_ProjectorEditForm(object): 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) + 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') @@ -108,8 +109,8 @@ class Ui_ProjectorEditForm(object): self.dialog_layout.addWidget(self.notes_text, 5, 1) # Time for the buttons self.button_box = QDialogButtonBox(QDialogButtonBox.Help | - QDialogButtonBox.Save | - QDialogButtonBox.Cancel) + QDialogButtonBox.Save | + QDialogButtonBox.Cancel) self.dialog_layout.addWidget(self.button_box, 8, 0, 1, 2) def retranslateUi(self, edit_projector_dialog): @@ -134,6 +135,7 @@ class Ui_ProjectorEditForm(object): 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. diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index eecf79025..f000e1fee 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -45,7 +45,6 @@ from openlp.core.lib.ui import create_widget_action 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.wizard import ProjectorWizard from openlp.core.ui.projector.editform import ProjectorEditForm # Dict for matching projector status to display icon From 81e7798c5091368f3cbb55a900840d65509ed551 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 16 Oct 2014 11:22:34 -0700 Subject: [PATCH 060/115] Change test from wizard to edit form --- .../interfaces/openlp_core_ui/test_projectormanager.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/interfaces/openlp_core_ui/test_projectormanager.py b/tests/interfaces/openlp_core_ui/test_projectormanager.py index c9218e0aa..c45c38b5a 100644 --- a/tests/interfaces/openlp_core_ui/test_projectormanager.py +++ b/tests/interfaces/openlp_core_ui/test_projectormanager.py @@ -35,7 +35,7 @@ from openlp.core.common import Registry, Settings from tests.functional import patch, MagicMock from tests.helpers.testmixin import TestMixin -from openlp.core.ui import ProjectorManager, ProjectorWizard +from openlp.core.ui import ProjectorManager from openlp.core.lib.projector.db import Projector, ProjectorDB from tests.resources.projector.data import TEST1_DATA, TEST2_DATA, TEST3_DATA @@ -94,8 +94,8 @@ class TestProjectorManager(TestCase, TestMixin): 'Initialization should have called load_projectors()') # THEN: Verify wizard page is initialized - self.assertEqual(type(self.projector_manager.projector_form), ProjectorWizard, - 'Initialization should have created a Wizard') + self.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.db, - 'Wizard should be using same ProjectorDB() instance') + self.projector_manager.projector_form.projectordb, + 'ProjectorEditForm should be using same ProjectorDB() instance') From f9f4296acfbb96d80e677386cdad4cd7701caf71 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 16 Oct 2014 11:25:08 -0700 Subject: [PATCH 061/115] Missed ProjectorEditForm import for test --- tests/interfaces/openlp_core_ui/test_projectormanager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/interfaces/openlp_core_ui/test_projectormanager.py b/tests/interfaces/openlp_core_ui/test_projectormanager.py index c45c38b5a..ce70f0b20 100644 --- a/tests/interfaces/openlp_core_ui/test_projectormanager.py +++ b/tests/interfaces/openlp_core_ui/test_projectormanager.py @@ -35,7 +35,7 @@ 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 +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 @@ -98,4 +98,4 @@ class TestProjectorManager(TestCase, TestMixin): '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') + 'ProjectorEditForm should be using same ProjectorDB() instance as ProjectorManager') From 4714cbfcf78de57144e63e1f842e90dcef12c10f Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 16 Oct 2014 11:30:29 -0700 Subject: [PATCH 062/115] pep8 --- openlp/core/ui/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/ui/__init__.py b/openlp/core/ui/__init__.py index 4d9bf2c40..8569fc100 100644 --- a/openlp/core/ui/__init__.py +++ b/openlp/core/ui/__init__.py @@ -133,4 +133,4 @@ __all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideCon 'Display', 'ServiceNoteForm', 'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay', 'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm', 'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'SingleColumnTableWidget', - 'ProjectorManager', 'ProjectorTab',' ProjectorEditForm'] + 'ProjectorManager', 'ProjectorTab', 'ProjectorEditForm'] From 77040050f4779bbe324842a2e17f7a9049866686 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 16 Oct 2014 13:03:20 -0700 Subject: [PATCH 063/115] Restructure send_busy flag --- openlp/core/lib/projector/pjlink1.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index e68862e7e..0cd2a6c9e 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -453,12 +453,12 @@ class PJLink1(QTcpSocket): out = '%s%s %s%s' % (salt, 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())) + log.debug('(%s) send_command(out="%s") Already in queue - skipping' % (self.ip, out.strip())) elif not queue and len(self.send_queue) == 0: return self._send_string(out) else: - log.debug('(%s) send_command(out=%s) adding to queue' % (self.ip, out.strip())) + log.debug('(%s) send_command(out="%s") adding to queue' % (self.ip, out.strip())) self.send_queue.append(out) if not self.send_busy: self.projectorReceivedData.emit() @@ -485,6 +485,9 @@ class PJLink1(QTcpSocket): 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())) @@ -520,32 +523,37 @@ class PJLink1(QTcpSocket): # Oops - projector error if data.upper() == 'ERRA': # Authentication error + self.send_busy = False 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.send_busy = False self.change_status(E_UNDEFINED, '%s "%s"' % (translate('OpenLP.PJLink1', 'Undefined command:'), cmd)) elif data.upper() == 'ERR2': # Invalid parameter + self.send_busy = False self.change_status(E_PARAMETER) elif data.upper() == 'ERR3': # Projector busy + self.send_busy = False self.change_status(E_UNAVAILABLE) elif data.upper() == 'ERR4': # Projector/display error + self.send_busy = False self.change_status(E_PROJECTOR) - self.projectorReceivedData.emit() 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.projectorReceivedData.emit() self.send_busy = False + self.projectorReceivedData.emit() return if cmd in self.PJLINK1_FUNC: From 99c7566f98a7a47383c8b7860beda3dc730163e9 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 16 Oct 2014 13:33:29 -0700 Subject: [PATCH 064/115] Update source selection as soon as power is on --- openlp/core/lib/projector/pjlink1.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 0cd2a6c9e..5827b068a 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -593,6 +593,9 @@ class PJLink1(QTcpSocket): 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) From 6264435acb8776b2154ef3903c2178f49e5a8405 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 16 Oct 2014 13:48:41 -0700 Subject: [PATCH 065/115] Fix view status icon on toolbar --- openlp/core/ui/projector/manager.py | 4 ++-- resources/images/openlp-2.qrc | 1 - resources/images/projector_status.png | Bin 807 -> 0 bytes 3 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 resources/images/projector_status.png diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index f000e1fee..306934021 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -107,7 +107,7 @@ class Ui_ProjectorManager(object): triggers=self.on_select_input) self.one_toolbar.add_toolbar_action('view_projector', text=translate('OpenLP.ProjectorManager', 'View Projector'), - icon=':/general/general_find.png', + icon=':/messagebox/messagebox_info.png', tooltip=translate('OpenLP.ProjectorManager', 'View selected projector information'), triggers=self.on_status_projector) @@ -172,7 +172,7 @@ class Ui_ProjectorManager(object): self.status_action = create_widget_action(self.menu, text=translate('OpenLP.ProjectorManager', '&View Projector Information'), - icon=':/projector/projector_status.png', + icon=':/messagebox/messagebox_info.png', triggers=self.on_status_projector) self.edit_action = create_widget_action(self.menu, text=translate('OpenLP.ProjectorManager', diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index b3afe9978..ba0f10e96 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -195,7 +195,6 @@ projector_show.png projector_show_tiled.png projector_spacer.png - projector_status.png projector_warmup.png projector_view.png diff --git a/resources/images/projector_status.png b/resources/images/projector_status.png deleted file mode 100644 index 516a7260225e304c90be894853cd69838dd4e1b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 807 zcmV+?1K9kDP)oTxG?<-=%y@) zCMNBo-I%z+*y_fnMUxtm#-)jAN?VeWR%szcAbbeJ+{?XpzTTPF#TD8Z`m9dQbI!?g zz685KYi$5EKn@T<8SsS=$;+|gr9}B;!EL|uaCWq{x$cY*a<3E;NH&(;(nSA>&XMlc zhIt`Gx@&=f%XiPe*3o*arLh5bEBcq!&{UUU%_}hTbmhYB;l6LSbBt|c|GBT;J^6a~ z9i+{c9}}sB&b&ii)?~%=Df%%g5j4~~{j2pyf8Ut;;ZFdQ!x!P7RqJ3|%XpZYgh2#= ziWMK7egl9{u06uC%olA!SKGetW}5qLnj0g7XTQO*_piM3_OVlbq=@2#NTv8;f+;mm z7B~5I&Lc_EDwm|>oA&VYN+HAKmk%UB+pGDrg-S@t4=Du^Zn44#Z|%pAQdBD0s#N`@ zAfn`lta@d7I$A!gwg5;&c5am;w64x0NHm2?%;4!R0KT~QnEIRnsX}XwVMt8F=md}e zvC@>om~yBnDNr)Q=l5rKwi8POZCVr}Sa8ey^mvKs*)?_)EUyNb67PF zMLz-MgVs#WRUi{W9DoWSjzO@rqkzyGC*u%eQA$iUV~a#tq$(y&60jV|z5+E4ST@)> z2;A{%fzkWmu8-h*V3=TLAQcSvwGb#xpi+kW@}L0IuJV-)a0{RD>DQQy^Z|G{&LDOB zZX!((7Irqv=#MT4f$ikLEQ5$431M-1keM-$t7F^8IskkNZ(jPC1INc`KLS=3YzL%- z(1+NE`Cnk}(GXMDZ$K|F@tV>~2FnWge>c*z4I)CUrvb7Be0hOXB*Z%rmcpJm& lkFoXb*xg}h{ujG&&jB40GQ)>c%|HME002ovPDHLkV1hi@dl3Kt From 0ec753468c532c5766740405dd1cc6147c03aad3 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 16 Oct 2014 14:01:37 -0700 Subject: [PATCH 066/115] projector view info icon update --- openlp/core/ui/projector/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 306934021..830b7b377 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -107,7 +107,7 @@ class Ui_ProjectorManager(object): triggers=self.on_select_input) self.one_toolbar.add_toolbar_action('view_projector', text=translate('OpenLP.ProjectorManager', 'View Projector'), - icon=':/messagebox/messagebox_info.png', + icon=':/system/system_about.png', tooltip=translate('OpenLP.ProjectorManager', 'View selected projector information'), triggers=self.on_status_projector) @@ -172,7 +172,7 @@ class Ui_ProjectorManager(object): self.status_action = create_widget_action(self.menu, text=translate('OpenLP.ProjectorManager', '&View Projector Information'), - icon=':/messagebox/messagebox_info.png', + icon=':/system/system_about.png', triggers=self.on_status_projector) self.edit_action = create_widget_action(self.menu, text=translate('OpenLP.ProjectorManager', From 187dee6217838fc0291361a3c7d4a371efe45112 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 16 Oct 2014 14:11:43 -0700 Subject: [PATCH 067/115] Add double-click to connect option --- openlp/core/ui/projector/manager.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 830b7b377..865ea1bc4 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -167,6 +167,7 @@ class Ui_ProjectorManager(object): 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, @@ -410,6 +411,14 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa except: continue + def on_doubleclick_item(self, item, opt=None): + projector = item.data(QtCore.Qt.UserRole) + try: + projector.link.connect_to_host() + except: + pass + return + def on_connect_projector(self, opt=None): """ Calls projector thread to connect to projector From 054be0135aeae3aaf277f36e93374257183d3e52 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 16 Oct 2014 14:21:01 -0700 Subject: [PATCH 068/115] Add check for connect before calling connect on doubleclick --- openlp/core/ui/projector/manager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 865ea1bc4..5b3139672 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -413,10 +413,11 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa def on_doubleclick_item(self, item, opt=None): projector = item.data(QtCore.Qt.UserRole) - try: - projector.link.connect_to_host() - except: - pass + if projector.link.state() != projector.link.ConnectedState: + try: + projector.link.connect_to_host() + except: + pass return def on_connect_projector(self, opt=None): From c1415bb891f9d3cdf21e6ff6792c8c2e9e5dbfad Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 16 Oct 2014 17:04:19 -0700 Subject: [PATCH 069/115] Fix authentication send, added current source to source select dialog --- openlp/core/lib/projector/pjlink1.py | 2 +- openlp/core/ui/projector/manager.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 5827b068a..8c05d7b06 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -450,7 +450,7 @@ class PJLink1(QTcpSocket): if salt is None: out = '%s%s %s%s' % (PJLINK_HEADER, cmd, opts, CR) else: - out = '%s%s %s%s' % (salt, cmd, opts, CR) + 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())) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 5b3139672..5b35baf21 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -361,6 +361,8 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa for item in source_list.keys(): sort.append(item) sort.sort() + current = QtGui.QLabel(translate('OpenLP.ProjectorManager', 'Current source is %s' % source_list[projector.link.source])) + layout.addWidget(current) for item in sort: button = self._select_input_widget(parent=self, selected=projector.link.source, From dfcbaa8c2884049c187dcd2b6105529d342cd526 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 16 Oct 2014 19:28:51 -0700 Subject: [PATCH 070/115] Add socket timer for lost connections --- openlp/core/lib/projector/pjlink1.py | 56 +++++++++++++++++----------- openlp/core/ui/projector/manager.py | 10 +++++ 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 8c05d7b06..e82212215 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -150,6 +150,7 @@ class PJLink1(QTcpSocket): self.timer = None # Timer that calls the poll_loop self.send_queue = [] self.send_busy = False + self.socket_timer = None # Test for send_busy and brain-dead projectors # Map command returned to function self.PJLINK1_FUNC = {'AVMT': self.process_avmt, 'CLSS': self.process_clss, @@ -166,6 +167,7 @@ class PJLink1(QTcpSocket): } def reset_information(self): + log.debug('(%s) reset_information() connect status is %s' % (self.ip, self.state())) self.power = S_OFF self.pjlink_name = None self.manufacturer = None @@ -179,6 +181,10 @@ class PJLink1(QTcpSocket): 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): """ @@ -215,6 +221,13 @@ class PJLink1(QTcpSocket): self.deleteLater() self.i_am_running = False + def socket_abort(self): + """ + Aborts connection and closes socket in case of brain-dead projectors. + """ + log.debug('(%s) socket_abort() - Killing connection' % self.ip) + self.disconnect_from_host(abort=True) + def poll_loop(self): """ Called by QTimer in ProjectorManager.ProjectorItem. @@ -362,10 +375,6 @@ class PJLink1(QTcpSocket): @pyqtSlot() def get_data(self): - log.debug('(%s) get_data() Received readyRead signal' % self.ip) - return self._get_data() - - def _get_data(self): """ Socket interface to retrieve data. """ @@ -379,6 +388,7 @@ class PJLink1(QTcpSocket): log.debug('(%s) get_data(): No data available (-1)' % self.ip) self.projectorReceivedData.emit() return + self.socket_timer.stop() self.projectorNetwork.emit(S_NETWORK_RECEIVED) data_in = decode(read, 'ascii') data = data_in.strip() @@ -432,6 +442,9 @@ class PJLink1(QTcpSocket): 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): @@ -455,24 +468,18 @@ class PJLink1(QTcpSocket): # 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: - - return self._send_string(out) + 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) - if not self.send_busy: - self.projectorReceivedData.emit() + 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_string() + self._send_command() @pyqtSlot() - def _send_command(self): - log.debug('Received projectorReceivedData signal') - return self._send_string() - - def _send_string(self, data=None): + def _send_command(self, data=None): """ Socket interface to send data. If data=None, then check queue. @@ -480,6 +487,7 @@ class PJLink1(QTcpSocket): :returns: None """ 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 = [] @@ -502,6 +510,7 @@ class PJLink1(QTcpSocket): 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) @@ -511,7 +520,7 @@ class PJLink1(QTcpSocket): self.change_status(E_NETWORK, translate('OpenLP.PJLink1', 'Error while sending data to projector')) except SocketError as e: - self.disconnect_from_host() + self.disconnect_from_host(abort=True) self.changeStatus(E_NETWORK, '%s : %s' % (e.error(), e.errorString())) def process_command(self, cmd, data): @@ -734,20 +743,25 @@ class PJLink1(QTcpSocket): self.connectToHost(self.ip, self.port if type(self.port) is int else int(self.port)) @pyqtSlot() - def disconnect_from_host(self): + def disconnect_from_host(self, abort=False): """ Close socket and cleanup. """ - if self.state() != self.ConnectedState: - log.warn('(%s) disconnect_from_host(): Not connected - returning' % self.ip) - self.projectorUpdateIcons.emit() - return + 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 - self.change_status(S_NOT_CONNECTED) + if abort: + self.change_status(E_NOT_CONNECTED) + else: + self.change_status(S_NOT_CONNECTED) self.reset_information() self.projectorUpdateIcons.emit() diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 5b35baf21..330d530d8 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -487,6 +487,11 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa 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: @@ -727,9 +732,14 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa timer.setInterval(self.poll_time) timer.timeout.connect(item.link.poll_loop) item.timer = timer + socket_timer = QtCore.QTimer(self) + socket_timer.setInterval(5000) # % second timer in case of brain-dead projectors + 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 self.autostart: From 81f26dd9a3c56781e4604d22b448ec872dd69746 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 16 Oct 2014 20:09:42 -0700 Subject: [PATCH 071/115] pep8 and extra logging info --- openlp/core/lib/projector/pjlink1.py | 5 ++++- openlp/core/ui/projector/manager.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index e82212215..6b5259929 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -761,7 +761,10 @@ class PJLink1(QTcpSocket): if abort: self.change_status(E_NOT_CONNECTED) else: - self.change_status(S_NOT_CONNECTED) + 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() diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 330d530d8..faf273b70 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -361,7 +361,8 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa for item in source_list.keys(): sort.append(item) sort.sort() - current = QtGui.QLabel(translate('OpenLP.ProjectorManager', 'Current source is %s' % source_list[projector.link.source])) + current = QtGui.QLabel(translate('OpenLP.ProjectorManager', 'Current source is %s' % + source_list[projector.link.source])) layout.addWidget(current) for item in sort: button = self._select_input_widget(parent=self, From d8f94ac3df513fa41ed79f763c86d303560095c3 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Fri, 17 Oct 2014 08:23:45 -0700 Subject: [PATCH 072/115] Remove extraneous imports --- openlp/core/ui/projector/editform.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openlp/core/ui/projector/editform.py b/openlp/core/ui/projector/editform.py index ad8c28efd..08d28630e 100644 --- a/openlp/core/ui/projector/editform.py +++ b/openlp/core/ui/projector/editform.py @@ -28,8 +28,8 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod: projectormanager` module provides the functions for - the display/control of Projectors. +The :mod: `editform` module provides the functions for adding/editing the + list of controlled projectors. """ import logging @@ -37,9 +37,8 @@ log = logging.getLogger(__name__) log.debug('editform loaded') from PyQt4 import QtCore, QtGui -from PyQt4.QtCore import QObject, pyqtSlot, pyqtSignal -from PyQt4.QtGui import QDialog, QFormLayout, QPlainTextEdit, QPushButton, QVBoxLayout, \ - QLineEdit, QDialogButtonBox, QHBoxLayout, QLabel, QGridLayout +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 10c415cf1a1905332fdc7f2a254183bab3ee9f3d Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Fri, 17 Oct 2014 10:28:12 -0700 Subject: [PATCH 073/115] Docstrings cleanup --- openlp/core/lib/projector/constants.py | 4 +- openlp/core/lib/projector/db.py | 89 ++++++++++++++++---- openlp/core/lib/projector/pjlink1.py | 107 ++++++++++++++++++++----- openlp/core/ui/projector/editform.py | 5 +- openlp/core/ui/projector/manager.py | 76 +++++++++++------- openlp/core/ui/projector/tab.py | 10 ++- 6 files changed, 217 insertions(+), 74 deletions(-) diff --git a/openlp/core/lib/projector/constants.py b/openlp/core/lib/projector/constants.py index eec17bf47..59bb7d718 100644 --- a/openlp/core/lib/projector/constants.py +++ b/openlp/core/lib/projector/constants.py @@ -27,7 +27,9 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod:`projector` module + :mod:`openlp.core.lib.projector.constants` module + + Provides the constants used for projector errors/status/defaults """ import logging diff --git a/openlp/core/lib/projector/db.py b/openlp/core/lib/projector/db.py index eafbecd85..46bc15f10 100644 --- a/openlp/core/lib/projector/db.py +++ b/openlp/core/lib/projector/db.py @@ -27,8 +27,18 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod:`projector.db` module provides the database functions for the - Projector module. + :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 @@ -37,12 +47,10 @@ log.debug('projector.lib.db module loaded') from os import path -from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, and_ +from sqlalchemy import Column, ForeignKey, Integer, MetaData, String from sqlalchemy.ext.declarative import declarative_base, declared_attr from sqlalchemy.orm import backref, relationship -from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound -from openlp.core.common import translate from openlp.core.lib.db import Manager, init_db, init_url from openlp.core.lib.projector.constants import PJLINK_DEFAULT_SOURCES @@ -57,16 +65,26 @@ class CommonBase(object): @declared_attr def __tablename__(cls): return cls.__name__.lower() + id = Column(Integer, primary_key=True) class Manufacturer(CommonBase, Base): """ - Manufacturer table. + 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', @@ -78,12 +96,22 @@ class Manufacturer(CommonBase, Base): class Model(CommonBase, Base): """ - Model table. + 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', @@ -96,12 +124,22 @@ class Model(CommonBase, Base): class Source(CommonBase, Base): """ - Input source table. + 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 source codes to text strings. + 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')) @@ -114,7 +152,18 @@ class Projector(CommonBase, Base): """ Projector table. - No relation. This keeps track of installed projectors. + 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) """ ip = Column(String(100)) port = Column(String(8)) @@ -143,7 +192,7 @@ class ProjectorDB(Manager): """ Setup the projector database and initialize the schema. - Change to Declarative means we really don't do much here. + Declarative uses table classes to define schema. """ url = init_url('projector') session, metadata = init_db(url, base=Base) @@ -154,7 +203,7 @@ class ProjectorDB(Manager): """ Locate a DB record by record ID. - :param dbid: DB record + :param dbid: DB record id :returns: Projector() instance """ log.debug('get_projector_by_id(id="%s")' % dbid) @@ -168,8 +217,9 @@ class ProjectorDB(Manager): def get_projector_all(self): """ - Retrieve all projector entries so they can be added to the Projector - Manager list pane. + Retrieve all projector entries. + + :returns: List with Projector() instances used in Manager() QListWidget. """ log.debug('get_all() called') return_list = [] @@ -217,10 +267,10 @@ class ProjectorDB(Manager): """ Add a new projector entry - NOTE: Will not add new entry if IP is the same as already in the table. - :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: @@ -239,6 +289,8 @@ class ProjectorDB(Manager): :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') @@ -266,6 +318,8 @@ class ProjectorDB(Manager): :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: @@ -282,7 +336,10 @@ class ProjectorDB(Manager): :param make: Manufacturer name as retrieved from projector :param model: Manufacturer model as retrieved from projector + :param sources: List of available sources (from projector) :returns: dict + key: (str) PJLink code for source + value: (str) From Sources table or default PJLink strings """ source_dict = {} model_list = self.get_all_objects(Model, Model.name == model) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 6b5259929..3f4aa6a49 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -27,19 +27,19 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod:`projector.pjlink1` module provides the necessary functions - for connecting to a PJLink-capable projector. + :mod:`openlp.core.lib.projector.pjlink1` module + Provides the necessary functions for connecting to a PJLink-capable projector. - See PJLink Specifications for Class 1 for details. + 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. - - See PJLINK_FUNC(...) for command returned from projector. - """ import logging @@ -49,11 +49,10 @@ log.debug('rpjlink1 loaded') __all__ = ['PJLink1'] -from time import sleep -from codecs import decode, encode +from codecs import decode from PyQt4 import QtCore, QtGui -from PyQt4.QtCore import QObject, pyqtSignal, pyqtSlot +from PyQt4.QtCore import pyqtSignal, pyqtSlot from PyQt4.QtNetwork import QAbstractSocket, QTcpSocket from openlp.core.common import translate, qmd5_hash @@ -73,6 +72,7 @@ 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 @@ -151,7 +151,7 @@ class PJLink1(QTcpSocket): self.send_queue = [] self.send_busy = False self.socket_timer = None # Test for send_busy and brain-dead projectors - # Map command returned to function + # Map command to function self.PJLINK1_FUNC = {'AVMT': self.process_avmt, 'CLSS': self.process_clss, 'ERST': self.process_erst, @@ -167,6 +167,9 @@ class PJLink1(QTcpSocket): } 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 @@ -224,14 +227,15 @@ class PJLink1(QTcpSocket): 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): """ - Called by QTimer in ProjectorManager.ProjectorItem. - Retrieves status information. + Retrieve information from projector that changes. + Normally called by timer(). """ if self.state() != self.ConnectedState: return @@ -260,8 +264,10 @@ class PJLink1(QTcpSocket): 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) """ - # Return the status code as a string if status in ERROR_STRING: return (ERROR_STRING[status], ERROR_MSG[status]) elif status in STATUS_STRING: @@ -273,6 +279,9 @@ class PJLink1(QTcpSocket): """ 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) @@ -298,6 +307,11 @@ class PJLink1(QTcpSocket): def check_command(self, cmd): """ Verifies command is valid based on PJLink class. + + :param cmd: PJLink command to validate. + :returns: bool + True if command is valid PJLink command + False if command is not a valid PJLink command """ return self.pjlink_class in PJLINK_VALID_CMD and \ cmd in PJLINK_VALID_CMD[self.pjlink_class] @@ -306,6 +320,9 @@ class PJLink1(QTcpSocket): 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: @@ -426,7 +443,10 @@ class PJLink1(QTcpSocket): @pyqtSlot(int) def get_error(self, err): """ - Process error from SocketError signal + 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: @@ -449,7 +469,12 @@ class PJLink1(QTcpSocket): def send_command(self, cmd, opts='?', salt=None, queue=False): """ - Add command to output queue if not already in queue + Add command to output queue if not already in queue. + + :param cmd: Command to send + :param opts: Optional command option - defaults to '?' (get information) + :param salt: Optional salt for md5 hash for 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) @@ -484,7 +509,6 @@ class PJLink1(QTcpSocket): Socket interface to send data. If data=None, then check queue. :param data: Immediate data to send - :returns: None """ log.debug('(%s) _send_string()' % self.ip) log.debug('(%s) _send_string(): Connection status: %s' % (self.ip, self.state())) @@ -526,6 +550,9 @@ class PJLink1(QTcpSocket): 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: @@ -575,6 +602,10 @@ class PJLink1(QTcpSocket): 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() @@ -594,6 +625,9 @@ class PJLink1(QTcpSocket): 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] @@ -612,7 +646,10 @@ class PJLink1(QTcpSocket): def process_avmt(self, data): """ - Shutter open/closed. See PJLink specification for format. + 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 @@ -641,6 +678,9 @@ class PJLink1(QTcpSocket): 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 @@ -648,6 +688,9 @@ class PJLink1(QTcpSocket): 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)) @@ -655,28 +698,40 @@ class PJLink1(QTcpSocket): def process_name(self, data): """ - Projector name set by customer. + 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 by manufacturer. + Manufacturer name set in projector. + Updates self.manufacturer + + :param data: Projector manufacturer """ self.manufacturer = data return def process_inf2(self, data): """ - Projector Model set by manufacturer. + 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 by manufacturer. + Any extra info set in projector. + Updates self.other_info. + + :param data: Projector other info """ self.other_info = data return @@ -684,6 +739,9 @@ class PJLink1(QTcpSocket): 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() @@ -696,6 +754,9 @@ class PJLink1(QTcpSocket): def process_erst(self, data): """ Error status. See PJLink Specifications for format. + Updates self.projector_errors + + :param data: Error status """ try: datacheck = int(data) @@ -734,7 +795,7 @@ class PJLink1(QTcpSocket): def connect_to_host(self): """ - Initiate connection. + Initiate connection to projector. """ if self.state() == self.ConnectedState: log.warn('(%s) connect_to_host(): Already connected - returning' % self.ip) @@ -832,6 +893,8 @@ class PJLink1(QTcpSocket): """ 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 """ if self.source_available is None: return diff --git a/openlp/core/ui/projector/editform.py b/openlp/core/ui/projector/editform.py index 08d28630e..bc3f2c417 100644 --- a/openlp/core/ui/projector/editform.py +++ b/openlp/core/ui/projector/editform.py @@ -28,8 +28,9 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod: `editform` module provides the functions for adding/editing the - list of controlled projectors. + :mod: `openlp.core.ui.projector.editform` module + + Provides the functions for adding/editing entries in the projector database. """ import logging diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index faf273b70..2a0b23f29 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -27,8 +27,9 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod: projectormanager` module provides the functions for - the display/control of Projectors. + :mod: openlp.core.ui.projector.manager` module + + Provides the functions for the display/control of Projectors. """ import logging @@ -232,6 +233,12 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa 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' @@ -239,6 +246,9 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa self.projector_list = [] 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 @@ -255,12 +265,10 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa del(settings) def bootstrap_post_set_up(self): + """ + Post-initialize setups. + """ self.load_projectors() - ''' - self.projector_form = ProjectorWizard(self, projectordb=self.projectordb) - self.projector_form.edit_page.newProjector.connect(self.add_projector_from_wizard) - self.projector_form.edit_page.editProjector.connect(self.edit_projector_from_wizard) - ''' 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) @@ -273,7 +281,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa :param point: The position of the mouse so the correct item can be found. """ - # QListWidgetItem + # QListWidgetItem to build menu for. item = self.projector_list_widget.itemAt(point) if item is None: return @@ -340,7 +348,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa item to change input source. :param opt: Needed by PyQt4 - :returns: None """ list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) projector = list_item.data(QtCore.Qt.UserRole) @@ -386,10 +393,9 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa def on_add_projector(self, opt=None): """ - Calls wizard to add a new projector to the database + Calls edit dialog to add a new projector to the database :param opt: Needed by PyQt4 - :returns: None """ self.projector_form.exec_() @@ -398,7 +404,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa Calls projector thread to send blank screen command :param opt: Needed by PyQt4 - :returns: None """ try: ip = opt.link.ip @@ -415,6 +420,12 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa 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: @@ -428,7 +439,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa Calls projector thread to connect to projector :param opt: Needed by PyQt4 - :returns: None """ try: ip = opt.link.ip @@ -449,7 +459,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa Deletes a projector from the list and the database :param opt: Needed by PyQt4 - :returns: None """ list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) if list_item is None: @@ -511,7 +520,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa Calls projector thread to disconnect from projector :param opt: Needed by PyQt4 - :returns: None """ try: ip = opt.link.ip @@ -529,10 +537,9 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa def on_edit_projector(self, opt=None): """ - Calls wizard with selected projector to edit information + Calls edit dialog with selected projector to edit information :param opt: Needed by PyQt4 - :returns: None """ list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) projector = list_item.data(QtCore.Qt.UserRole) @@ -549,7 +556,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa Calls projector link to send Power Off command :param opt: Needed by PyQt4 - :returns: None """ try: ip = opt.link.ip @@ -570,7 +576,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa Calls projector link to send Power On command :param opt: Needed by PyQt4 - :returns: None """ try: ip = opt.link.ip @@ -591,7 +596,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa Calls projector thread to send open shutter command :param opt: Needed by PyQt4 - :returns: None """ try: ip = opt.link.ip @@ -612,7 +616,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa Builds message box with projector status information :param opt: Needed by PyQt4 - :returns: None """ lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow()) projector = lwi.data(QtCore.Qt.UserRole) @@ -670,7 +673,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa Helper app to build a projector instance :param p: Dict of projector database information - :returns: PJLink() instance + :returns: PJLink1() instance """ log.debug('_add_projector()') return PJLink1(dbid=projector.id, @@ -698,9 +701,8 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa We are not concerned with the wizard instance, just the projector item - :param opt1: See docstring - :param opt2: See docstring - :returns: None + :param opt1: See above + :param opt2: See above """ if opt1 is None: return @@ -751,11 +753,10 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa @pyqtSlot(str) def add_projector_from_wizard(self, ip, opts=None): """ - Add a projector from the wizard + Add a projector from the edit dialog - :param ip: IP address of new record item + :param ip: IP address of new record item to find :param opts: Needed by PyQt4 - :returns: None """ log.debug('load_projector(ip=%s)' % ip) item = self.projectordb.get_projector_by_ip(ip) @@ -768,7 +769,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa :param projector: Projector() instance of projector with updated information :param opts: Needed by PyQt4 - :returns: None """ self.old_projector.link.name = projector.name @@ -804,7 +804,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa :param ip: IP address of projector :param status: Optional status code :param msg: Optional status message - :returns: None """ if status is None: return @@ -903,6 +902,11 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa @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 authentictaion error while trying to connect.' @@ -911,6 +915,12 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa @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.' @@ -924,6 +934,11 @@ class ProjectorItem(QObject): 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 @@ -942,7 +957,6 @@ def not_implemented(function): Temporary function to build an information message box indicating function not implemented yet :param func: Function name - :returns: None """ QtGui.QMessageBox.information(None, translate('OpenLP.ProjectorManager', 'Not Implemented Yet'), diff --git a/openlp/core/ui/projector/tab.py b/openlp/core/ui/projector/tab.py index b15fcd2e8..e7d6fcafa 100644 --- a/openlp/core/ui/projector/tab.py +++ b/openlp/core/ui/projector/tab.py @@ -27,8 +27,9 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod:`projector.ui.projectortab` module provides the settings tab in the - settings dialog. + :mod:`openlp.core.ui.projector.tab` + + Provides the settings tab in the settings dialog. """ import logging @@ -46,6 +47,11 @@ 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) From 4b64e24e36283ae4963e15bbd8d96d008036aa63 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Sun, 19 Oct 2014 12:20:51 -0700 Subject: [PATCH 074/115] Initial change to tabs for source select --- openlp/core/lib/projector/pjlink1.py | 1 + openlp/core/ui/projector/manager.py | 14 +- openlp/core/ui/projector/sourceselectform.py | 168 +++++++++++++++++++ 3 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 openlp/core/ui/projector/sourceselectform.py diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 3f4aa6a49..d200d48f4 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -747,6 +747,7 @@ class PJLink1(QTcpSocket): check = data.split() for source in check: sources.append(source) + sources.sort() self.source_available = sources self.projectorUpdateIcons.emit() return diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 2a0b23f29..8b1095402 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -38,6 +38,7 @@ 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 @@ -47,6 +48,7 @@ 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 SourceSelectDialog # Dict for matching projector status to display icon STATUS_ICONS = {S_NOT_CONNECTED: ':/projector/projector_item_disconnect.png', @@ -228,7 +230,7 @@ class Ui_ProjectorManager(object): self.update_icons() -class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorManager, RegistryProperties): +class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, RegistryProperties): """ Manage the projectors. """ @@ -244,6 +246,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa self.settings_section = 'projector' self.projectordb = projectordb self.projector_list = [] + self.source_select_form = None def bootstrap_initialise(self): """ @@ -272,7 +275,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa 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 context_menu(self, point): @@ -351,6 +353,14 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ProjectorMa """ list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) projector = list_item.data(QtCore.Qt.UserRole) + + # Testing tabwidget for source select + source_select_form = SourceSelectDialog(parent=self, + projectordb=self.projectordb) + + source = source_select_form.exec_(projector.link) + return + layout = QtGui.QVBoxLayout() box = QtGui.QDialog(parent=self) box.setModal(True) diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py new file mode 100644 index 000000000..0fd0e1031 --- /dev/null +++ b/openlp/core/ui/projector/sourceselectform.py @@ -0,0 +1,168 @@ +# -*- 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, pyqtSignal +from PyQt4.QtGui import QDialog, QButtonGroup, QDialogButtonBox, QGroupBox, QRadioButton, \ + QTabBar, QTabWidget, QVBoxLayout, QWidget + +from openlp.core.common import translate +from openlp.core.lib import build_icon +from openlp.core.lib.projector.constants import PJLINK_DEFAULT_SOURCES + + +def source_group(inputs, source_text): + """ + Return a dictionary where key is source[0] and values are inputs + grouped by source[0]. + ex: + dict{"1": "11 12 13 14 15", + "2": "21 22 23 24 25", + ... } + + :param inputs: List of inputs + :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): + """ + 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: { group: { code: string, + code: string, + ... + } + } + + :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 + """ + widget = QWidget() + layout = 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() + for key in sourcelist: + itemwidget = QRadioButton(source_key[tempkey][key]) + itemwidget.setAutoExclusive(True) + if default == key: + itemwidget.setChecked(True) + group.addButton(itemwidget, int(key)) + layout.addWidget(itemwidget) + layout.addStretch() + return widget + + +class SourceSelectDialog(QDialog): + """ + Class for handling selecting the source for the projector to use. + """ + def __init__(self, parent, projectordb): + """ + Build the source select dialog. + + :param projectordb: ProjectorDB session to use + """ + log.debug('Initializing SourceSelectDialog()') + self.projectordb = projectordb + super(SourceSelectDialog, self).__init__(parent) + self.setWindowTitle(translate('OpenLP.SourceSelectDialog', 'Select Projector Source')) + self.setObjectName('source_select_dialog') + self.setWindowIcon(build_icon(':/icon/openlp-log-32x32.png')) + self.setMinimumWidth(300) + self.setMinimumHeight(400) + self.setModal(True) + + self.layout = QVBoxLayout() + self.layout.setObjectName('source_select_dialog_layout') + self.tabwidget = QTabWidget(self) + self.tabwidget.setObjectName('source_select_dialog_tabwidget') + 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.manufacturer, + projector.model, + projector.source_available) + self.source_group = source_group(projector.source_available, self.source_text) + self.button_group = QButtonGroup() + keys = list(self.source_group.keys()) + keys.sort() + for key in keys: + self.tabwidget.addTab(Build_Tab(group=self.button_group, + source_key={key: self.source_group[key]}, + default=self.projector.source), PJLINK_DEFAULT_SOURCES[key]) + button_box = QDialogButtonBox(QtGui.QDialogButtonBox.Ok | + QtGui.QDialogButtonBox.Cancel) + button_box.accepted.connect(self.accepted) + button_box.rejected.connect(self.rejected) + self.layout.addWidget(button_box) + selected = super(SourceSelectDialog, self).exec_() + + def accepted(self): + self.accept() + return projector.set_input_source(self.button_group.checkedId()) + + def rejected(self): + self.reject() + return 0 \ No newline at end of file From c42af376ccf9e3e2266bb0144f25e9c6952e8400 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Sun, 19 Oct 2014 16:55:19 -0700 Subject: [PATCH 075/115] Fix tabbed source select gui size --- openlp/core/lib/projector/pjlink1.py | 2 + openlp/core/ui/projector/editform.py | 2 + openlp/core/ui/projector/manager.py | 8 ++- openlp/core/ui/projector/sourceselectform.py | 72 ++++++++++++-------- 4 files changed, 54 insertions(+), 30 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index d200d48f4..cf3eba2ce 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -897,10 +897,12 @@ class PJLink1(QTcpSocket): :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() diff --git a/openlp/core/ui/projector/editform.py b/openlp/core/ui/projector/editform.py index bc3f2c417..659924161 100644 --- a/openlp/core/ui/projector/editform.py +++ b/openlp/core/ui/projector/editform.py @@ -250,12 +250,14 @@ class ProjectorEditForm(QDialog, Ui_ProjectorEditForm): 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. diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 8b1095402..fecb120cf 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -353,14 +353,16 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, """ list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) projector = list_item.data(QtCore.Qt.UserRole) - - # Testing tabwidget for source select + # QTabwidget for source select source_select_form = SourceSelectDialog(parent=self, projectordb=self.projectordb) 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 - + # QDialog for source select - Delete when QTabWidget finished layout = QtGui.QVBoxLayout() box = QtGui.QDialog(parent=self) box.setModal(True) diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index 0fd0e1031..210a2ad5b 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -40,7 +40,7 @@ from PyQt4.QtCore import pyqtSlot, pyqtSignal from PyQt4.QtGui import QDialog, QButtonGroup, QDialogButtonBox, QGroupBox, QRadioButton, \ QTabBar, QTabWidget, QVBoxLayout, QWidget -from openlp.core.common import translate +from openlp.core.common import translate, is_macosx from openlp.core.lib import build_icon from openlp.core.lib.projector.constants import PJLINK_DEFAULT_SOURCES @@ -50,11 +50,14 @@ def source_group(inputs, source_text): Return a dictionary where key is source[0] and values are inputs grouped by source[0]. ex: - dict{"1": "11 12 13 14 15", - "2": "21 22 23 24 25", - ... } + dict{ "key": { "key1": source_text[key1], + "key2": source_text[key2], + "key3": source_text[key3], + ... } + "key": ... } :param inputs: List of inputs + :param source_text: Dictionary of {code: text} values to display :returns: dict """ groupdict = {} @@ -78,10 +81,15 @@ def Build_Tab(group, source_key, default): 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: { group: { code: string, - code: string, - ... - } + source_key: {"groupkey1": {"key1": string, + "key2": string, + ... + }, + "groupkey2": {"key1": string, + "key2": string + .... + }, + ... } :param group: Button group widget to add buttons to @@ -95,6 +103,7 @@ def Build_Tab(group, source_key, default): tempkey = list(source_key.keys())[0] # Should only be 1 key sourcelist = list(source_key[tempkey]) sourcelist.sort() + button_count = len(sourcelist) for key in sourcelist: itemwidget = QRadioButton(source_key[tempkey][key]) itemwidget.setAutoExclusive(True) @@ -103,7 +112,7 @@ def Build_Tab(group, source_key, default): group.addButton(itemwidget, int(key)) layout.addWidget(itemwidget) layout.addStretch() - return widget + return (widget, button_count) class SourceSelectDialog(QDialog): @@ -122,16 +131,18 @@ class SourceSelectDialog(QDialog): self.setWindowTitle(translate('OpenLP.SourceSelectDialog', 'Select Projector Source')) self.setObjectName('source_select_dialog') self.setWindowIcon(build_icon(':/icon/openlp-log-32x32.png')) - self.setMinimumWidth(300) - self.setMinimumHeight(400) self.setModal(True) - + self.button_count = 0 # Maximum number of buttons in a single page self.layout = QVBoxLayout() self.layout.setObjectName('source_select_dialog_layout') self.tabwidget = QTabWidget(self) self.tabwidget.setObjectName('source_select_dialog_tabwidget') - self.tabwidget.setTabPosition(QTabWidget.West) - self.layout.addWidget(self.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): @@ -145,24 +156,31 @@ class SourceSelectDialog(QDialog): projector.model, projector.source_available) 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 = QButtonGroup() keys = list(self.source_group.keys()) keys.sort() for key in keys: - self.tabwidget.addTab(Build_Tab(group=self.button_group, + (tab, button_count) = Build_Tab(group=self.button_group, source_key={key: self.source_group[key]}, - default=self.projector.source), PJLINK_DEFAULT_SOURCES[key]) - button_box = QDialogButtonBox(QtGui.QDialogButtonBox.Ok | - QtGui.QDialogButtonBox.Cancel) - button_box.accepted.connect(self.accepted) - button_box.rejected.connect(self.rejected) - self.layout.addWidget(button_box) + default=self.projector.source) + self.tabwidget.addTab(tab, PJLINK_DEFAULT_SOURCES[key]) + self.button_count = self.button_count if self.button_count > button_count else button_count + self.button_box = QDialogButtonBox(QtGui.QDialogButtonBox.Ok | + QtGui.QDialogButtonBox.Cancel) + self.button_box.accepted.connect(self.accept_me) + self.button_box.rejected.connect(self.reject_me) + self.layout.addWidget(self.button_box) selected = super(SourceSelectDialog, self).exec_() + return selected - def accepted(self): - self.accept() - return projector.set_input_source(self.button_group.checkedId()) + @pyqtSlot() + def accept_me(self): + selected = self.button_group.checkedId() + log.debug('SourceSelectDialog().accepted() Setting source to %s' % selected) + self.done(selected) - def rejected(self): - self.reject() - return 0 \ No newline at end of file + @pyqtSlot() + def reject_me(self): + log.debug('SourceSelectDialog() - Rejected') + self.done(0) From 7fb3ca8c173cb9133b3d880d054269cb630f3663 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Sun, 19 Oct 2014 17:11:06 -0700 Subject: [PATCH 076/115] Fix misplace tabwidget addWidget --- openlp/core/ui/projector/sourceselectform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index 210a2ad5b..168b59ea5 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -142,7 +142,7 @@ class SourceSelectDialog(QDialog): self.tabwidget.setTabPosition(QTabWidget.North) else: self.tabwidget.setTabPosition(QTabWidget.West) - self.layout.addWidget(self.tabwidget) + self.layout.addWidget(self.tabwidget) self.setLayout(self.layout) def exec_(self, projector): From 3cb21e4c13aef36275204c44342955c6e5b1136f Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Sun, 19 Oct 2014 17:31:09 -0700 Subject: [PATCH 077/115] Set tab focus to tab with selected source --- openlp/core/ui/projector/sourceselectform.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index 168b59ea5..53636e345 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -96,6 +96,7 @@ def Build_Tab(group, source_key, default): :param source_key: Dictionary of sources for radio buttons :param default: Default radio button to check """ + buttonchecked = False widget = QWidget() layout = QVBoxLayout() layout.setSpacing(10) @@ -109,10 +110,11 @@ def Build_Tab(group, source_key, default): 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) + return (widget, button_count, buttonchecked) class SourceSelectDialog(QDialog): @@ -132,7 +134,6 @@ class SourceSelectDialog(QDialog): self.setObjectName('source_select_dialog') self.setWindowIcon(build_icon(':/icon/openlp-log-32x32.png')) self.setModal(True) - self.button_count = 0 # Maximum number of buttons in a single page self.layout = QVBoxLayout() self.layout.setObjectName('source_select_dialog_layout') self.tabwidget = QTabWidget(self) @@ -161,11 +162,12 @@ class SourceSelectDialog(QDialog): keys = list(self.source_group.keys()) keys.sort() for key in keys: - (tab, button_count) = Build_Tab(group=self.button_group, - source_key={key: self.source_group[key]}, - default=self.projector.source) - self.tabwidget.addTab(tab, PJLINK_DEFAULT_SOURCES[key]) - self.button_count = self.button_count if self.button_count > button_count else button_count + (tab, button_count, buttonchecked) = Build_Tab(group=self.button_group, + source_key={key: self.source_group[key]}, + default=self.projector.source) + thistab = self.tabwidget.addTab(tab, PJLINK_DEFAULT_SOURCES[key]) + if buttonchecked: + self.tabwidget.setCurrentIndex(thistab) self.button_box = QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) self.button_box.accepted.connect(self.accept_me) From c028353ecde78197b714176f174db93589afc35f Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Sun, 19 Oct 2014 17:43:40 -0700 Subject: [PATCH 078/115] Fix docstrings --- openlp/core/ui/projector/sourceselectform.py | 24 ++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index 53636e345..567efe80a 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -49,12 +49,18 @@ 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", + ...} ex: - dict{ "key": { "key1": source_text[key1], - "key2": source_text[key2], - "key3": source_text[key3], - ... } - "key": ... } + 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 @@ -81,12 +87,12 @@ def Build_Tab(group, source_key, default): 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": {"key1": string, - "key2": string, + source_key: {"groupkey1": {"key11": "key11-text", + "key12": "key12-text", ... }, - "groupkey2": {"key1": string, - "key2": string + "groupkey2": {"key21": "key21-text", + "key22": "key22-text", .... }, ... From 6a2f675e843c4bebe5fcc70396af033af4d21e17 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 20 Oct 2014 09:34:33 -0700 Subject: [PATCH 079/115] Realign tabs to left-right --- openlp/core/ui/projector/sourceselectform.py | 36 ++++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index 567efe80a..d5f7fed2a 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -36,9 +36,9 @@ log = logging.getLogger(__name__) log.debug('editform loaded') from PyQt4 import QtCore, QtGui -from PyQt4.QtCore import pyqtSlot, pyqtSignal +from PyQt4.QtCore import pyqtSlot, pyqtSignal, QSize from PyQt4.QtGui import QDialog, QButtonGroup, QDialogButtonBox, QGroupBox, QRadioButton, \ - QTabBar, QTabWidget, QVBoxLayout, QWidget + QStyle, QStylePainter, QStyleOptionTab, QTabBar, QTabWidget, QVBoxLayout, QWidget from openlp.core.common import translate, is_macosx from openlp.core.lib import build_icon @@ -123,6 +123,36 @@ def Build_Tab(group, source_key, default): return (widget, button_count, buttonchecked) +class FingerTabBarWidget(QTabBar): + def __init__(self, parent=None, *args, **kwargs): + self.tabSize = QSize(kwargs.pop('width',100), kwargs.pop('height',25)) + QTabBar.__init__(self, parent, *args, **kwargs) + + def paintEvent(self, event): + 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 self.tabSize + +# Shamelessly stolen from this thread: +# http://www.riverbankcomputing.com/pipermail/pyqt/2005-December/011724.html +class FingerTabWidget(QtGui.QTabWidget): + """A QTabWidget equivalent which uses our FingerTabBarWidget""" + def __init__(self, parent, *args): + QTabWidget.__init__(self, parent, *args) + self.setTabBar(FingerTabBarWidget(self)) + + class SourceSelectDialog(QDialog): """ Class for handling selecting the source for the projector to use. @@ -142,7 +172,7 @@ class SourceSelectDialog(QDialog): self.setModal(True) self.layout = QVBoxLayout() self.layout.setObjectName('source_select_dialog_layout') - self.tabwidget = QTabWidget(self) + self.tabwidget = FingerTabWidget(self) self.tabwidget.setObjectName('source_select_dialog_tabwidget') self.tabwidget.setUsesScrollButtons(False) if is_macosx(): From 1e2cfcd7bd6a256d97c4b32e43d3e11a1c6cc3ad Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 20 Oct 2014 09:50:19 -0700 Subject: [PATCH 080/115] pep8 --- openlp/core/ui/projector/sourceselectform.py | 48 ++++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index d5f7fed2a..f25e2c5a8 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -124,11 +124,27 @@ def Build_Tab(group, source_key, default): 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): - self.tabSize = QSize(kwargs.pop('width',100), kwargs.pop('height',25)) + """ + 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() @@ -137,18 +153,32 @@ class FingerTabBarWidget(QTabBar): 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.drawText(tabRect, QtCore.Qt.AlignVCenter | + QtCore.Qt.TextDontClip, + self.tabText(index)) painter.end() - def tabSizeHint(self,index): + + def tabSizeHint(self, index): + """ + Return tabsize + + :param index: Tab index to fetch tabsize from + :returns: instance tabSize + """ return self.tabSize -# Shamelessly stolen from this thread: -# http://www.riverbankcomputing.com/pipermail/pyqt/2005-December/011724.html -class FingerTabWidget(QtGui.QTabWidget): - """A QTabWidget equivalent which uses our FingerTabBarWidget""" + +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)) From 8d4eb3b3688d426d218abb815e2374676e46134d Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 20 Oct 2014 10:07:53 -0700 Subject: [PATCH 081/115] Fix icon for source input menu item --- openlp/core/ui/projector/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index fecb120cf..3a92b56a6 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -209,7 +209,7 @@ class Ui_ProjectorManager(object): self.select_input_action = create_widget_action(self.menu, text=translate('OpenLP.ProjectorManager', 'Select &Input'), - icon=':/projector/projector_connectors.png', + icon=':/projector/projector_hdmi.png', triggers=self.on_select_input) self.blank_action = create_widget_action(self.menu, text=translate('OpenLP.ProjectorManager', From 4283ff750239f5f5a1625f55e63763554edcc059 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 20 Oct 2014 11:37:59 -0700 Subject: [PATCH 082/115] Add tiled icons on toolbar for muliple-selection --- openlp/core/ui/projector/manager.py | 73 ++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 3a92b56a6..32226f6b3 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -122,13 +122,27 @@ class Ui_ProjectorManager(object): 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 projector'), + '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', @@ -137,12 +151,25 @@ class Ui_ProjectorManager(object): 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', @@ -151,6 +178,13 @@ class Ui_ProjectorManager(object): 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'), @@ -158,6 +192,13 @@ class Ui_ProjectorManager(object): 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') @@ -875,10 +916,22 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, 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_projector', @@ -905,12 +958,18 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, self.get_toolbar_item('delete_projector', enabled=False) self.get_toolbar_item('view_projector', hidden=True) self.get_toolbar_item('source_projector', hidden=True) - self.get_toolbar_item('connect_projector', enabled=True) - self.get_toolbar_item('disconnect_projector', enabled=True) - self.get_toolbar_item('poweron_projector', enabled=True) - self.get_toolbar_item('poweroff_projector', enabled=True) - self.get_toolbar_item('blank_projector', enabled=True) - self.get_toolbar_item('show_projector', enabled=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): From 16f4fcbce33d6b5461e9a06c489177af466631a8 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 20 Oct 2014 16:17:15 -0700 Subject: [PATCH 083/115] Update settings to allow selecting source dialog layout --- openlp/core/common/settings.py | 3 +- openlp/core/lib/projector/__init__.py | 40 ++++++ openlp/core/ui/projector/manager.py | 100 +++------------ openlp/core/ui/projector/sourceselectform.py | 125 +++++++++++++++++-- openlp/core/ui/projector/tab.py | 25 +++- 5 files changed, 198 insertions(+), 95 deletions(-) create mode 100644 openlp/core/lib/projector/__init__.py diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 18e71f837..b5bbea842 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -303,7 +303,8 @@ class Settings(QtCore.QSettings): '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/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/lib/projector/__init__.py b/openlp/core/lib/projector/__init__.py new file mode 100644 index 000000000..23fdf82f8 --- /dev/null +++ b/openlp/core/lib/projector/__init__.py @@ -0,0 +1,40 @@ +# -*- 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/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 32226f6b3..2dc8ca2dc 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -44,11 +44,12 @@ 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 SourceSelectDialog +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', @@ -300,13 +301,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, self.projectordb = ProjectorDB() else: log.debug('Using existing ProjectorDB() instance') - 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') - settings.endGroup() - del(settings) + self.get_settings() def bootstrap_post_set_up(self): """ @@ -318,6 +313,16 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, self.projector_form.editProjector.connect(self.edit_projector_from_wizard) self.projector_list_widget.itemSelectionChanged.connect(self.update_icons) + def get_settings(self): + 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. @@ -354,37 +359,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, self.menu.projector = real_projector self.menu.exec_(self.projector_list_widget.mapToGlobal(point)) - def _select_input_widget(self, parent, selected, code, text): - """ - Build the radio button widget for selecting source input menu - - :param parent: parent widget - :param selected: Selected widget text - :param code: PJLink code for this widget - :param text: Text to display - :returns: radio button widget - """ - widget = QtGui.QRadioButton(text, parent=parent) - widget.setChecked(True if code == selected else False) - widget.button_role = code - widget.clicked.connect(self._select_input_radio) - self.radio_buttons.append(widget) - return widget - - def _select_input_radio(self, opt1=None, opt2=None): - """ - Returns the currently selected radio button - - :param opt1: Needed by PyQt4 - :param op2: future - :returns: Selected button role - """ - for button in self.radio_buttons: - if button.isChecked(): - self.radio_button_selected = button.button_role - break - return - def on_select_input(self, opt=None): """ Builds menu for 'Select Input' option, then calls the selected projector @@ -392,57 +366,21 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, :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_select_form = SourceSelectDialog(parent=self, + if self.source_select_dialog_type == DialogSourceStyle.Tabbed: + source_select_form = SourceSelectTabs(parent=self, projectordb=self.projectordb) - + else: + source_select_form = SourceSelectSingle(parent=self, + projectordb=self.projectordb) 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 - # QDialog for source select - Delete when QTabWidget finished - layout = QtGui.QVBoxLayout() - box = QtGui.QDialog(parent=self) - box.setModal(True) - title = QtGui.QLabel(translate('OpenLP.ProjectorManager', 'Select the input source below')) - layout.addWidget(title) - self.radio_button_selected = None - self.radio_buttons = [] - source_list = self.projectordb.get_source_list(make=projector.link.manufacturer, - model=projector.link.model, - sources=projector.link.source_available - ) - if source_list is None: - return - sort = [] - for item in source_list.keys(): - sort.append(item) - sort.sort() - current = QtGui.QLabel(translate('OpenLP.ProjectorManager', 'Current source is %s' % - source_list[projector.link.source])) - layout.addWidget(current) - for item in sort: - button = self._select_input_widget(parent=self, - selected=projector.link.source, - code=item, - text=source_list[item]) - layout.addWidget(button) - button_box = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | - QtGui.QDialogButtonBox.Cancel) - button_box.accepted.connect(box.accept) - button_box.rejected.connect(box.reject) - layout.addWidget(button_box) - box.setLayout(layout) - check = box.exec_() - if check == 0: - # Cancel button clicked or window closed - don't set source - return - selected = self.radio_button_selected - projector.link.set_input_source(self.radio_button_selected) - self.radio_button_selected = None def on_add_projector(self, opt=None): """ diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index f25e2c5a8..4db279bea 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -183,27 +183,28 @@ class FingerTabWidget(QTabWidget): self.setTabBar(FingerTabBarWidget(self)) -class SourceSelectDialog(QDialog): +class SourceSelectTabs(QDialog): """ Class for handling selecting the source for the projector to use. + Uses tabbed interface. """ def __init__(self, parent, projectordb): """ - Build the source select dialog. + Build the source select dialog using tabbed interface. :param projectordb: ProjectorDB session to use """ - log.debug('Initializing SourceSelectDialog()') + log.debug('Initializing SourceSelectTabs()') self.projectordb = projectordb - super(SourceSelectDialog, self).__init__(parent) - self.setWindowTitle(translate('OpenLP.SourceSelectDialog', 'Select Projector Source')) - self.setObjectName('source_select_dialog') + super(SourceSelectTabs, self).__init__(parent) + self.setWindowTitle(translate('OpenLP.SourceSelectForm', 'Select Projector Source')) + 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_dialog_layout') + self.layout.setObjectName('source_select_tabs_layout') self.tabwidget = FingerTabWidget(self) - self.tabwidget.setObjectName('source_select_dialog_tabwidget') + self.tabwidget.setObjectName('source_select_tabs_tabwidget') self.tabwidget.setUsesScrollButtons(False) if is_macosx(): self.tabwidget.setTabPosition(QTabWidget.North) @@ -239,16 +240,122 @@ class SourceSelectDialog(QDialog): self.button_box.accepted.connect(self.accept_me) self.button_box.rejected.connect(self.reject_me) self.layout.addWidget(self.button_box) - selected = super(SourceSelectDialog, self).exec_() + selected = super(SourceSelectTabs, self).exec_() return selected @pyqtSlot() def accept_me(self): + """ + Slot to accept 'OK' button + """ + selected = self.button_group.checkedId() + log.debug('SourceSelectTabs().accepted() Setting source to %s' % selected) + self.done(selected) + + @pyqtSlot() + def reject_me(self): + """ + Slot to accept 'Cancel' button + """ + log.debug('SourceSelectTabs() - Rejected') + self.done(0) + + +class SourceSelectSingle(QDialog): + """ + Class for handling selecting the source for the projector to use. + Uses single dialog interface. + """ + def __init__(self, parent, projectordb): + """ + 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.layout = QVBoxLayout() + self.layout.setObjectName('source_select_tabs_layout') + self.layout.setSpacing(10) + self.setLayout(self.layout) + self.setMinimumWidth(350) + self.button_group = QButtonGroup() + self.button_group.setObjectName('source_select_single_buttongroup') + + 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.manufacturer, + projector.model, + projector.source_available) + keys = list(self.source_text.keys()) + keys.sort() + key_count = len(keys) + button_list = [] + for key in keys: + button = QtGui.QRadioButton(self.source_text[key]) + 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.Ok | + QtGui.QDialogButtonBox.Cancel) + self.button_box.accepted.connect(self.accept_me) + self.button_box.rejected.connect(self.reject_me) + self.layout.addWidget(self.button_box) + self.setMinimumHeight(key_count*25) + selected = super(SourceSelectSingle, self).exec_() + return selected + + title = QtGui.QLabel(translate('OpenLP.SourceSelectSingle', 'Select the input source below')) + self.layout.addWidget(title) + self.radio_buttons = [] + source_list = self.projectordb.get_source_list(make=projector.link.manufacturer, + model=projector.link.model, + sources=projector.link.source_available) + sort = [] + for item in source_list.keys(): + sort.append(item) + sort.sort() + current = QtGui.QLabel(translate('OpenLP.SourceSelectSingle', 'Current source is %s' % + source_list[projector.link.source])) + layout.addWidget(current) + for item in sort: + button = self._select_input_widget(parent=self, + selected=projector.link.source, + code=item, + text=source_list[item]) + layout.addWidget(button) + button_box = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | + QtGui.QDialogButtonBox.Cancel) + button_box.accepted.connect(box.accept_me) + button_box.rejected.connect(box.reject_me) + layout.addWidget(button_box) + selected = super(SourceSelectSingle, self).exec_() + return selected + + @pyqtSlot() + def accept_me(self): + """ + Slot to accept 'OK' button + """ selected = self.button_group.checkedId() log.debug('SourceSelectDialog().accepted() Setting source to %s' % selected) self.done(selected) @pyqtSlot() def reject_me(self): + """ + Slot to accept 'Cancel' button + """ log.debug('SourceSelectDialog() - Rejected') self.done(0) diff --git a/openlp/core/ui/projector/tab.py b/openlp/core/ui/projector/tab.py index e7d6fcafa..b256aa20b 100644 --- a/openlp/core/ui/projector/tab.py +++ b/openlp/core/ui/projector/tab.py @@ -40,6 +40,7 @@ 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): @@ -66,7 +67,6 @@ class ProjectorTab(SettingsTab): 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') @@ -81,16 +81,22 @@ class ProjectorTab(SettingsTab): 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_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): """ @@ -105,6 +111,12 @@ class ProjectorTab(SettingsTab): 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): """ @@ -115,6 +127,7 @@ class ProjectorTab(SettingsTab): 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): @@ -126,4 +139,8 @@ class ProjectorTab(SettingsTab): 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() From 8bb9510622198f6e9e48a4ffdcec6ddc0ac1e599 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 20 Oct 2014 16:25:27 -0700 Subject: [PATCH 084/115] Fix tab text on mac --- openlp/core/ui/projector/sourceselectform.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index 4db279bea..91554d84b 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -203,7 +203,10 @@ class SourceSelectTabs(QDialog): self.setModal(True) self.layout = QVBoxLayout() self.layout.setObjectName('source_select_tabs_layout') - self.tabwidget = FingerTabWidget(self) + 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(): From 3def1baf9d9d5bea90ce65031aec6b87ba1ae66f Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 20 Oct 2014 16:36:07 -0700 Subject: [PATCH 085/115] pep8 --- openlp/core/lib/projector/__init__.py | 1 + openlp/core/ui/projector/manager.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/openlp/core/lib/projector/__init__.py b/openlp/core/lib/projector/__init__.py index 23fdf82f8..88dcc4882 100644 --- a/openlp/core/lib/projector/__init__.py +++ b/openlp/core/lib/projector/__init__.py @@ -32,6 +32,7 @@ Initialization for the openlp.core.ui.projector modules. """ + class DialogSourceStyle(object): """ An enumeration for projector dialog box type. diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 2dc8ca2dc..a890fa48f 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -76,6 +76,7 @@ class Ui_ProjectorManager(object): def setup_ui(self, widget): """ Define the UI + :param widget: The screen object the dialog is to be attached to. """ log.debug('setup_ui()') @@ -307,13 +308,16 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, """ Post-initialize setups. """ - self.load_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') @@ -372,7 +376,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, # QTabwidget for source select if self.source_select_dialog_type == DialogSourceStyle.Tabbed: source_select_form = SourceSelectTabs(parent=self, - projectordb=self.projectordb) + projectordb=self.projectordb) else: source_select_form = SourceSelectSingle(parent=self, projectordb=self.projectordb) @@ -663,7 +667,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, """ Helper app to build a projector instance - :param p: Dict of projector database information + :param projector: Dict of projector database information :returns: PJLink1() instance """ log.debug('_add_projector()') @@ -770,7 +774,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, self.old_projector.link.notes = projector.notes self.old_projector.widget.setText(projector.name) - def load_projectors(self): + def _load_projectors(self): """' Load projectors - only call when initializing """ From ed4605be61bd8b8623285b93f903d17fde5a7cc8 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 08:12:43 -0700 Subject: [PATCH 086/115] Remove old test db file in case of schema changes --- .../interfaces/openlp_core_ui/test_projectormanager.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/interfaces/openlp_core_ui/test_projectormanager.py b/tests/interfaces/openlp_core_ui/test_projectormanager.py index ce70f0b20..d0a30522d 100644 --- a/tests/interfaces/openlp_core_ui/test_projectormanager.py +++ b/tests/interfaces/openlp_core_ui/test_projectormanager.py @@ -29,6 +29,8 @@ """ Interface tests to test the themeManager class and related methods. """ + +import os from unittest import TestCase from openlp.core.common import Registry, Settings @@ -41,7 +43,11 @@ 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' - +try: + # In case of changed schema, remove old test file + os.remove(tmpfile) +except FileNotFoundError: + pass class TestProjectorManager(TestCase, TestMixin): """ @@ -93,7 +99,7 @@ class TestProjectorManager(TestCase, TestMixin): self.assertEqual(1, self.projector_manager.load_projectors.call_count, 'Initialization should have called load_projectors()') - # THEN: Verify wizard page is initialized + # 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, From 8fcba0aa65061d1b6db7329bb0a90fbb1371e2f3 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 08:15:20 -0700 Subject: [PATCH 087/115] fix call to changed function --- tests/interfaces/openlp_core_ui/test_projectormanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/interfaces/openlp_core_ui/test_projectormanager.py b/tests/interfaces/openlp_core_ui/test_projectormanager.py index d0a30522d..197c0c4a8 100644 --- a/tests/interfaces/openlp_core_ui/test_projectormanager.py +++ b/tests/interfaces/openlp_core_ui/test_projectormanager.py @@ -96,7 +96,7 @@ class TestProjectorManager(TestCase, TestMixin): self.projector_manager.bootstrap_post_set_up() # THEN: verify calls to retrieve saved projectors - self.assertEqual(1, self.projector_manager.load_projectors.call_count, + self.assertEqual(1, self.projector_manager._load_projectors.call_count, 'Initialization should have called load_projectors()') # THEN: Verify edit page is initialized From 232f0486163db5846b8cfb45f01300e6d799bef3 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 08:17:27 -0700 Subject: [PATCH 088/115] fix call to changed function --- tests/interfaces/openlp_core_ui/test_projectormanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/interfaces/openlp_core_ui/test_projectormanager.py b/tests/interfaces/openlp_core_ui/test_projectormanager.py index 197c0c4a8..54b9afe64 100644 --- a/tests/interfaces/openlp_core_ui/test_projectormanager.py +++ b/tests/interfaces/openlp_core_ui/test_projectormanager.py @@ -89,7 +89,7 @@ class TestProjectorManager(TestCase, TestMixin): Test post-initialize calls proper setups """ # GIVEN: setup mocks - self.projector_manager.load_projectors = MagicMock() + self.projector_manager._load_projectors = MagicMock() # WHEN: Call to initialize is run self.projector_manager.bootstrap_initialise() From 81f6214cc54bb039f80a8ec49bd371b91f4c8b54 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 08:21:19 -0700 Subject: [PATCH 089/115] pep8 --- tests/interfaces/openlp_core_ui/test_projectormanager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/interfaces/openlp_core_ui/test_projectormanager.py b/tests/interfaces/openlp_core_ui/test_projectormanager.py index 54b9afe64..65d43dec5 100644 --- a/tests/interfaces/openlp_core_ui/test_projectormanager.py +++ b/tests/interfaces/openlp_core_ui/test_projectormanager.py @@ -43,12 +43,14 @@ 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' + try: # In case of changed schema, remove old test file os.remove(tmpfile) except FileNotFoundError: pass + class TestProjectorManager(TestCase, TestMixin): """ Test the functions in the ProjectorManager module From d914deacd200197771ddb47db1628e826e41e482 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 08:41:09 -0700 Subject: [PATCH 090/115] Fix projector test cleanups --- tests/interfaces/openlp_core_ui/__init__.py | 7 +++++++ .../openlp_core_ui/test_projectormanager.py | 11 ++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/interfaces/openlp_core_ui/__init__.py b/tests/interfaces/openlp_core_ui/__init__.py index 34420bdb8..747685c24 100644 --- a/tests/interfaces/openlp_core_ui/__init__.py +++ b/tests/interfaces/openlp_core_ui/__init__.py @@ -30,6 +30,7 @@ Module-level functions for the functional test suite """ +import os from tests.interfaces import patch from openlp.core.common import is_win @@ -51,3 +52,9 @@ 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 index 65d43dec5..6ead7c336 100644 --- a/tests/interfaces/openlp_core_ui/test_projectormanager.py +++ b/tests/interfaces/openlp_core_ui/test_projectormanager.py @@ -44,12 +44,6 @@ from tests.resources.projector.data import TEST1_DATA, TEST2_DATA, TEST3_DATA tmpfile = '/tmp/openlp-test-projectormanager.sql' -try: - # In case of changed schema, remove old test file - os.remove(tmpfile) -except FileNotFoundError: - pass - class TestProjectorManager(TestCase, TestMixin): """ @@ -72,8 +66,11 @@ class TestProjectorManager(TestCase, TestMixin): def tearDown(self): """ - Delete all the C++ objects at the end so that we don't have a segfault + 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): From 21f6216574682133877c5573b6179b85eec6f34f Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 13:01:07 -0700 Subject: [PATCH 091/115] Switch PJLINK_DEFAULT_TEXT to dict --- openlp/core/lib/projector/constants.py | 47 +++++++++++++++ openlp/core/lib/projector/db.py | 60 +++++++++++++------- openlp/core/ui/projector/editform.py | 14 +++-- openlp/core/ui/projector/sourceselectform.py | 4 +- 4 files changed, 96 insertions(+), 29 deletions(-) diff --git a/openlp/core/lib/projector/constants.py b/openlp/core/lib/projector/constants.py index 59bb7d718..d3a1dc3d5 100644 --- a/openlp/core/lib/projector/constants.py +++ b/openlp/core/lib/projector/constants.py @@ -317,3 +317,50 @@ PJLINK_DEFAULT_SOURCES = {'1': translate('OpenLP.DB', 'RGB'), '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 index 46bc15f10..db59154a3 100644 --- a/openlp/core/lib/projector/db.py +++ b/openlp/core/lib/projector/db.py @@ -52,7 +52,7 @@ 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_SOURCES +from openlp.core.lib.projector.constants import PJLINK_DEFAULT_SOURCES, PJLINK_DEFAULT_CODES metadata = MetaData() Base = declarative_base(metadata) @@ -178,6 +178,22 @@ class Projector(CommonBase, Base): sources = Column(String(128)) +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 + """ + code = Column(String(3)) + text = Column(String(20)) + projector_id = Integer(ForeignKey('projector.id')) + + class ProjectorDB(Manager): """ Class to access the projector database. @@ -328,36 +344,40 @@ class ProjectorDB(Manager): log.error('delete_by_id() Entry id="%s" not deleted for some reason' % projector.id) return deleted - def get_source_list(self, make, model, sources): + 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 make: Manufacturer name as retrieved from projector - :param model: Manufacturer model as retrieved from projector - :param sources: List of available sources (from projector) + :param projector: Projector instance :returns: dict key: (str) PJLink code for source - value: (str) From Sources table or default PJLink strings + value: (str) From ProjectorSource, Sources tables or PJLink default code list """ - source_dict = {} - model_list = self.get_all_objects(Model, Model.name == model) + # Get manufacturer-defined source text + model_list = self.get_all_objects(Model, Model.name == projector.model) if model_list is None or len(model_list) < 1: # No entry for model, so see if there's a default entry - default_list = self.get_object_filtered(Manufacturer, Manufacturer.name == make) + default_list = self.get_object_filtered(Manufacturer, Manufacturer.name == projector.manufacturer) if default_list is None or len(default_list) < 1: # No entry for manufacturer, so can't check for default text - log.debug('Using default PJLink text for input select') - for source in sources: - log.debug('source = "%s"' % source) - source_dict[source] = '%s %s' % (PJLINK_DEFAULT_SOURCES[source[0]], source[1]) + model_list = {} else: - # We have a manufacturer entry, see if there's a default - # TODO: Finish this section once edit source input is done - pass - else: - # There's at least one model entry, see if there's more than one manufacturer - # TODO: Finish this section once edit source input text is done - pass + model_list = default_list.models['DEFAULT'] + # Get user-defined source text + local_list = self.get_all_objects(ProjectorSource, ProjectorSource.projector_id == projector.dbid) + if local_list is None or len(local_list) < 1: + local_list = {} + source_dict = {} + for source in projector.source_available: + if source in local_list: + # User defined source text + source_dict[source] = local_list[source] + elif source in model_list: + # Default manufacturer defined source text + source_dict[source] = model_list[source] + else: + # Default PJLink source text + source_dict[source] = PJLINK_DEFAULT_CODES[source] return source_dict diff --git a/openlp/core/ui/projector/editform.py b/openlp/core/ui/projector/editform.py index 659924161..d98bf6232 100644 --- a/openlp/core/ui/projector/editform.py +++ b/openlp/core/ui/projector/editform.py @@ -114,13 +114,11 @@ class Ui_ProjectorEditForm(object): self.dialog_layout.addWidget(self.button_box, 8, 0, 1, 2) def retranslateUi(self, edit_projector_dialog): - if self.projector.port is None: + if self.new_projector: title = translate('OpenLP.ProjectorEditForm', 'Add New Projector') self.projector.port = PJLINK_PORT - self.new_projecor = True else: title = translate('OpenLP.ProjectorEditForm', 'Edit Projector') - self.new_projector = False edit_projector_dialog.setWindowTitle(title) self.ip_label.setText(translate('OpenLP.ProjetorEditForm', 'IP Address')) self.ip_text.setText(self.projector.ip) @@ -159,9 +157,13 @@ class ProjectorEditForm(QDialog, Ui_ProjectorEditForm): self.button_box.helpRequested.connect(self.help_me) self.button_box.rejected.connect(self.cancel_me) - def exec_(self, projector=Projector()): - self.projector = projector - self.new_projector = False + 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 diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index 91554d84b..533437858 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -223,9 +223,7 @@ class SourceSelectTabs(QDialog): :param projector: Projector instance to build source list from """ self.projector = projector - self.source_text = self.projectordb.get_source_list(projector.manufacturer, - projector.model, - projector.source_available) + 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 = QButtonGroup() From 598f94f5c1257553038df2599238bff5bc5199b8 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 13:03:58 -0700 Subject: [PATCH 092/115] Add ProjectorSource schema for user-defined source display text --- openlp/core/lib/projector/constants.py | 47 --------------- openlp/core/lib/projector/db.py | 60 +++++++------------- openlp/core/ui/projector/editform.py | 14 ++--- openlp/core/ui/projector/sourceselectform.py | 4 +- 4 files changed, 29 insertions(+), 96 deletions(-) diff --git a/openlp/core/lib/projector/constants.py b/openlp/core/lib/projector/constants.py index d3a1dc3d5..59bb7d718 100644 --- a/openlp/core/lib/projector/constants.py +++ b/openlp/core/lib/projector/constants.py @@ -317,50 +317,3 @@ PJLINK_DEFAULT_SOURCES = {'1': translate('OpenLP.DB', 'RGB'), '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 index db59154a3..46bc15f10 100644 --- a/openlp/core/lib/projector/db.py +++ b/openlp/core/lib/projector/db.py @@ -52,7 +52,7 @@ 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_SOURCES, PJLINK_DEFAULT_CODES +from openlp.core.lib.projector.constants import PJLINK_DEFAULT_SOURCES metadata = MetaData() Base = declarative_base(metadata) @@ -178,22 +178,6 @@ class Projector(CommonBase, Base): sources = Column(String(128)) -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 - """ - code = Column(String(3)) - text = Column(String(20)) - projector_id = Integer(ForeignKey('projector.id')) - - class ProjectorDB(Manager): """ Class to access the projector database. @@ -344,40 +328,36 @@ class ProjectorDB(Manager): log.error('delete_by_id() Entry id="%s" not deleted for some reason' % projector.id) return deleted - def get_source_list(self, projector): + def get_source_list(self, make, model, sources): """ Retrieves the source inputs pjlink code-to-text if available based on manufacturer and model. If not available, then returns the PJLink code to default text. - :param projector: Projector instance + :param make: Manufacturer name as retrieved from projector + :param model: Manufacturer model as retrieved from projector + :param sources: List of available sources (from projector) :returns: dict key: (str) PJLink code for source - value: (str) From ProjectorSource, Sources tables or PJLink default code list + value: (str) From Sources table or default PJLink strings """ - # Get manufacturer-defined source text - model_list = self.get_all_objects(Model, Model.name == projector.model) + source_dict = {} + model_list = self.get_all_objects(Model, Model.name == model) if model_list is None or len(model_list) < 1: # No entry for model, so see if there's a default entry - default_list = self.get_object_filtered(Manufacturer, Manufacturer.name == projector.manufacturer) + default_list = self.get_object_filtered(Manufacturer, Manufacturer.name == make) if default_list is None or len(default_list) < 1: # No entry for manufacturer, so can't check for default text - model_list = {} + log.debug('Using default PJLink text for input select') + for source in sources: + log.debug('source = "%s"' % source) + source_dict[source] = '%s %s' % (PJLINK_DEFAULT_SOURCES[source[0]], source[1]) else: - model_list = default_list.models['DEFAULT'] - # Get user-defined source text - local_list = self.get_all_objects(ProjectorSource, ProjectorSource.projector_id == projector.dbid) - if local_list is None or len(local_list) < 1: - local_list = {} - source_dict = {} - for source in projector.source_available: - if source in local_list: - # User defined source text - source_dict[source] = local_list[source] - elif source in model_list: - # Default manufacturer defined source text - source_dict[source] = model_list[source] - else: - # Default PJLink source text - source_dict[source] = PJLINK_DEFAULT_CODES[source] + # We have a manufacturer entry, see if there's a default + # TODO: Finish this section once edit source input is done + pass + else: + # There's at least one model entry, see if there's more than one manufacturer + # TODO: Finish this section once edit source input text is done + pass return source_dict diff --git a/openlp/core/ui/projector/editform.py b/openlp/core/ui/projector/editform.py index d98bf6232..659924161 100644 --- a/openlp/core/ui/projector/editform.py +++ b/openlp/core/ui/projector/editform.py @@ -114,11 +114,13 @@ class Ui_ProjectorEditForm(object): self.dialog_layout.addWidget(self.button_box, 8, 0, 1, 2) def retranslateUi(self, edit_projector_dialog): - if self.new_projector: + if self.projector.port is None: title = translate('OpenLP.ProjectorEditForm', 'Add New Projector') self.projector.port = PJLINK_PORT + self.new_projecor = True else: title = translate('OpenLP.ProjectorEditForm', 'Edit Projector') + self.new_projector = False edit_projector_dialog.setWindowTitle(title) self.ip_label.setText(translate('OpenLP.ProjetorEditForm', 'IP Address')) self.ip_text.setText(self.projector.ip) @@ -157,13 +159,9 @@ class ProjectorEditForm(QDialog, Ui_ProjectorEditForm): 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 + def exec_(self, projector=Projector()): + self.projector = projector + self.new_projector = False self.retranslateUi(self) reply = QDialog.exec_(self) self.projector = None diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index 533437858..91554d84b 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -223,7 +223,9 @@ class SourceSelectTabs(QDialog): :param projector: Projector instance to build source list from """ self.projector = projector - self.source_text = self.projectordb.get_source_list(projector=projector) + self.source_text = self.projectordb.get_source_list(projector.manufacturer, + projector.model, + projector.source_available) 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 = QButtonGroup() From a8cebffec04c91f8f6efd38f7d6bd7ef3c605c7f Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 13:05:35 -0700 Subject: [PATCH 093/115] cleanups --- openlp/core/lib/projector/constants.py | 47 +++++++++++++++ openlp/core/lib/projector/db.py | 60 +++++++++++++------- openlp/core/ui/projector/editform.py | 14 +++-- openlp/core/ui/projector/sourceselectform.py | 4 +- 4 files changed, 96 insertions(+), 29 deletions(-) diff --git a/openlp/core/lib/projector/constants.py b/openlp/core/lib/projector/constants.py index 59bb7d718..d3a1dc3d5 100644 --- a/openlp/core/lib/projector/constants.py +++ b/openlp/core/lib/projector/constants.py @@ -317,3 +317,50 @@ PJLINK_DEFAULT_SOURCES = {'1': translate('OpenLP.DB', 'RGB'), '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 index 46bc15f10..db59154a3 100644 --- a/openlp/core/lib/projector/db.py +++ b/openlp/core/lib/projector/db.py @@ -52,7 +52,7 @@ 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_SOURCES +from openlp.core.lib.projector.constants import PJLINK_DEFAULT_SOURCES, PJLINK_DEFAULT_CODES metadata = MetaData() Base = declarative_base(metadata) @@ -178,6 +178,22 @@ class Projector(CommonBase, Base): sources = Column(String(128)) +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 + """ + code = Column(String(3)) + text = Column(String(20)) + projector_id = Integer(ForeignKey('projector.id')) + + class ProjectorDB(Manager): """ Class to access the projector database. @@ -328,36 +344,40 @@ class ProjectorDB(Manager): log.error('delete_by_id() Entry id="%s" not deleted for some reason' % projector.id) return deleted - def get_source_list(self, make, model, sources): + 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 make: Manufacturer name as retrieved from projector - :param model: Manufacturer model as retrieved from projector - :param sources: List of available sources (from projector) + :param projector: Projector instance :returns: dict key: (str) PJLink code for source - value: (str) From Sources table or default PJLink strings + value: (str) From ProjectorSource, Sources tables or PJLink default code list """ - source_dict = {} - model_list = self.get_all_objects(Model, Model.name == model) + # Get manufacturer-defined source text + model_list = self.get_all_objects(Model, Model.name == projector.model) if model_list is None or len(model_list) < 1: # No entry for model, so see if there's a default entry - default_list = self.get_object_filtered(Manufacturer, Manufacturer.name == make) + default_list = self.get_object_filtered(Manufacturer, Manufacturer.name == projector.manufacturer) if default_list is None or len(default_list) < 1: # No entry for manufacturer, so can't check for default text - log.debug('Using default PJLink text for input select') - for source in sources: - log.debug('source = "%s"' % source) - source_dict[source] = '%s %s' % (PJLINK_DEFAULT_SOURCES[source[0]], source[1]) + model_list = {} else: - # We have a manufacturer entry, see if there's a default - # TODO: Finish this section once edit source input is done - pass - else: - # There's at least one model entry, see if there's more than one manufacturer - # TODO: Finish this section once edit source input text is done - pass + model_list = default_list.models['DEFAULT'] + # Get user-defined source text + local_list = self.get_all_objects(ProjectorSource, ProjectorSource.projector_id == projector.dbid) + if local_list is None or len(local_list) < 1: + local_list = {} + source_dict = {} + for source in projector.source_available: + if source in local_list: + # User defined source text + source_dict[source] = local_list[source] + elif source in model_list: + # Default manufacturer defined source text + source_dict[source] = model_list[source] + else: + # Default PJLink source text + source_dict[source] = PJLINK_DEFAULT_CODES[source] return source_dict diff --git a/openlp/core/ui/projector/editform.py b/openlp/core/ui/projector/editform.py index 659924161..d98bf6232 100644 --- a/openlp/core/ui/projector/editform.py +++ b/openlp/core/ui/projector/editform.py @@ -114,13 +114,11 @@ class Ui_ProjectorEditForm(object): self.dialog_layout.addWidget(self.button_box, 8, 0, 1, 2) def retranslateUi(self, edit_projector_dialog): - if self.projector.port is None: + if self.new_projector: title = translate('OpenLP.ProjectorEditForm', 'Add New Projector') self.projector.port = PJLINK_PORT - self.new_projecor = True else: title = translate('OpenLP.ProjectorEditForm', 'Edit Projector') - self.new_projector = False edit_projector_dialog.setWindowTitle(title) self.ip_label.setText(translate('OpenLP.ProjetorEditForm', 'IP Address')) self.ip_text.setText(self.projector.ip) @@ -159,9 +157,13 @@ class ProjectorEditForm(QDialog, Ui_ProjectorEditForm): self.button_box.helpRequested.connect(self.help_me) self.button_box.rejected.connect(self.cancel_me) - def exec_(self, projector=Projector()): - self.projector = projector - self.new_projector = False + 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 diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index 91554d84b..533437858 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -223,9 +223,7 @@ class SourceSelectTabs(QDialog): :param projector: Projector instance to build source list from """ self.projector = projector - self.source_text = self.projectordb.get_source_list(projector.manufacturer, - projector.model, - projector.source_available) + 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 = QButtonGroup() From 0fe5b9f8fee33404d153c1e837eb083d7e2cd5d3 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 13:11:25 -0700 Subject: [PATCH 094/115] Typo in column define --- openlp/core/lib/projector/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/lib/projector/db.py b/openlp/core/lib/projector/db.py index db59154a3..d52424942 100644 --- a/openlp/core/lib/projector/db.py +++ b/openlp/core/lib/projector/db.py @@ -191,7 +191,7 @@ class ProjectorSource(CommonBase, Base): """ code = Column(String(3)) text = Column(String(20)) - projector_id = Integer(ForeignKey('projector.id')) + projector_id = (Integer, ForeignKey('projector.id')) class ProjectorDB(Manager): From a026633f78fe1d65908fb4bfa7a5b793837e8c48 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 14:24:16 -0700 Subject: [PATCH 095/115] Notes cleanups --- openlp/core/lib/projector/db.py | 6 +-- openlp/core/lib/projector/pjlink1.py | 53 +++++++++----------- openlp/core/ui/projector/sourceselectform.py | 4 +- 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/openlp/core/lib/projector/db.py b/openlp/core/lib/projector/db.py index d52424942..fe4c6b9c6 100644 --- a/openlp/core/lib/projector/db.py +++ b/openlp/core/lib/projector/db.py @@ -45,14 +45,12 @@ import logging log = logging.getLogger(__name__) log.debug('projector.lib.db module loaded') -from os import path - from sqlalchemy import Column, ForeignKey, Integer, MetaData, String 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_SOURCES, PJLINK_DEFAULT_CODES +from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES metadata = MetaData() Base = declarative_base(metadata) @@ -188,6 +186,8 @@ class ProjectorSource(CommonBase, Base): 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 """ code = Column(String(3)) text = Column(String(20)) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index cf3eba2ce..1915ead20 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -51,7 +51,6 @@ __all__ = ['PJLink1'] from codecs import decode -from PyQt4 import QtCore, QtGui from PyQt4.QtCore import pyqtSignal, pyqtSlot from PyQt4.QtNetwork import QAbstractSocket, QTcpSocket @@ -94,6 +93,8 @@ class PJLink1(QTcpSocket): :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 @@ -104,7 +105,6 @@ class PJLink1(QTcpSocket): self.dbid = None self.location = None self.notes = None - # Allowances for Projector Wizard option if 'dbid' in kwargs: self.dbid = kwargs['dbid'] else: @@ -117,10 +117,6 @@ class PJLink1(QTcpSocket): self.notes = kwargs['notes'] else: self.notes = None - if 'wizard' in kwargs: - self.new_wizard = True - else: - self.new_wizard = False if 'poll_time' in kwargs: # Convert seconds to milliseconds self.poll_time = kwargs['poll_time'] * 1000 @@ -139,10 +135,10 @@ class PJLink1(QTcpSocket): self.projector_status = S_NOT_CONNECTED self.error_status = S_OK # Socket information - # Account for self.readLine appending \0 and/or extraneous \r + # Add enough space to input buffer for extraneous \n \r self.maxSize = PJLINK_MAX_PACKET + 2 self.setReadBufferSize(self.maxSize) - # PJLink projector information + # PJLink information self.pjlink_class = '1' # Default class self.reset_information() # Set from ProjectorManager.add_projector() @@ -150,7 +146,8 @@ class PJLink1(QTcpSocket): self.timer = None # Timer that calls the poll_loop self.send_queue = [] self.send_busy = False - self.socket_timer = None # Test for send_busy and brain-dead projectors + # 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, @@ -244,10 +241,12 @@ class PJLink1(QTcpSocket): 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']: - # Changeable information 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: @@ -347,19 +346,20 @@ class PJLink1(QTcpSocket): 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 - if not data.upper().startswith('PJLINK'): - # Invalid response - return self.disconnect_from_host() - # Test for authentication error - if '=' in data: - data_check = data.strip().split('=') - else: - data_check = data.strip().split(' ') - log.debug('(%s) data_check="%s"' % (self.ip, data_check)) # PJLink initial login will be: # 'PJLink 0' - Unauthenticated login - no extra steps required. # 'PJLink 1 XXXXXX' Authenticated login - extra processing required. - # Oops - projector error + 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() @@ -382,10 +382,10 @@ class PJLink1(QTcpSocket): 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() - self.projectorReceivedData.connect(self._send_command) if not self.new_wizard and self.state() == self.ConnectedState: self.timer.setInterval(2000) # Set 2 seconds for initial information self.timer.start() @@ -472,8 +472,8 @@ class PJLink1(QTcpSocket): Add command to output queue if not already in queue. :param cmd: Command to send - :param opts: Optional command option - defaults to '?' (get information) - :param salt: Optional salt for md5 hash for initial authentication + :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: @@ -493,6 +493,8 @@ class PJLink1(QTcpSocket): # 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())) @@ -559,27 +561,22 @@ class PJLink1(QTcpSocket): # Oops - projector error if data.upper() == 'ERRA': # Authentication error - self.send_busy = False 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.send_busy = False self.change_status(E_UNDEFINED, '%s "%s"' % (translate('OpenLP.PJLink1', 'Undefined command:'), cmd)) elif data.upper() == 'ERR2': # Invalid parameter - self.send_busy = False self.change_status(E_PARAMETER) elif data.upper() == 'ERR3': # Projector busy - self.send_busy = False self.change_status(E_UNAVAILABLE) elif data.upper() == 'ERR4': # Projector/display error - self.send_busy = False self.change_status(E_PROJECTOR) self.send_busy = False self.projectorReceivedData.emit() diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index 533437858..6fb8fc914 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -36,8 +36,8 @@ log = logging.getLogger(__name__) log.debug('editform loaded') from PyQt4 import QtCore, QtGui -from PyQt4.QtCore import pyqtSlot, pyqtSignal, QSize -from PyQt4.QtGui import QDialog, QButtonGroup, QDialogButtonBox, QGroupBox, QRadioButton, \ +from PyQt4.QtCore import pyqtSlot, QSize +from PyQt4.QtGui import QDialog, QButtonGroup, QDialogButtonBox, QRadioButton, \ QStyle, QStylePainter, QStyleOptionTab, QTabBar, QTabWidget, QVBoxLayout, QWidget from openlp.core.common import translate, is_macosx From b38921a90b6f19ecf61c225070d3da36284d3ab8 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 14:40:47 -0700 Subject: [PATCH 096/115] Code cleanups --- openlp/core/lib/projector/pjlink1.py | 8 ++++---- tests/interfaces/openlp_core_ui/test_projectormanager.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 1915ead20..9449003cd 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -45,7 +45,7 @@ import logging log = logging.getLogger(__name__) -log.debug('rpjlink1 loaded') +log.debug('pjlink1 loaded') __all__ = ['PJLink1'] @@ -268,11 +268,11 @@ class PJLink1(QTcpSocket): :returns: (Status/Error code, String) """ if status in ERROR_STRING: - return (ERROR_STRING[status], ERROR_MSG[status]) + return ERROR_STRING[status], ERROR_MSG[status] elif status in STATUS_STRING: - return (STATUS_STRING[status], ERROR_MSG[status]) + return STATUS_STRING[status], ERROR_MSG[status] else: - return (status, translate('OpenLP.PJLink1', 'Unknown status')) + return status, translate('OpenLP.PJLink1', 'Unknown status') def change_status(self, status, msg=None): """ diff --git a/tests/interfaces/openlp_core_ui/test_projectormanager.py b/tests/interfaces/openlp_core_ui/test_projectormanager.py index 6ead7c336..4dd79ec7a 100644 --- a/tests/interfaces/openlp_core_ui/test_projectormanager.py +++ b/tests/interfaces/openlp_core_ui/test_projectormanager.py @@ -70,7 +70,7 @@ class TestProjectorManager(TestCase, TestMixin): Delete all the C++ objects at the end so that we don't have a segfault. """ self.projectordb.session.close() - del(self.projector_manager) + del self.projector_manager self.destroy_settings() def bootstrap_initialise_test(self): From 2b5b58379fe19058ffa2ceecd6cf518fb85f2e4c Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 14:50:35 -0700 Subject: [PATCH 097/115] Code cleanups --- openlp/core/ui/projector/editform.py | 4 +-- openlp/core/ui/projector/manager.py | 4 +-- openlp/core/ui/projector/sourceselectform.py | 29 +------------------- openlp/core/ui/projector/tab.py | 2 +- 4 files changed, 6 insertions(+), 33 deletions(-) diff --git a/openlp/core/ui/projector/editform.py b/openlp/core/ui/projector/editform.py index d98bf6232..05b9b280d 100644 --- a/openlp/core/ui/projector/editform.py +++ b/openlp/core/ui/projector/editform.py @@ -49,7 +49,7 @@ from openlp.core.lib.projector.constants import PJLINK_PORT class Ui_ProjectorEditForm(object): """ - The :class:`~opelp.core.lib.ui.projector.editform.Ui_ProjectorEdiForm` class defines + 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): @@ -120,7 +120,7 @@ class Ui_ProjectorEditForm(object): else: title = translate('OpenLP.ProjectorEditForm', 'Edit Projector') edit_projector_dialog.setWindowTitle(title) - self.ip_label.setText(translate('OpenLP.ProjetorEditForm', 'IP Address')) + self.ip_label.setText(translate('OpenLP.ProjectorEditForm', 'IP Address')) self.ip_text.setText(self.projector.ip) self.port_label.setText(translate('OpenLP.ProjectorEditForm', 'Port Number')) self.port_text.setText(str(self.projector.port)) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index a890fa48f..99e5c6229 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -325,7 +325,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, self.socket_timeout = settings.value('socket timeout') self.source_select_dialog_type = settings.value('source dialog type') settings.endGroup() - del(settings) + del settings def context_menu(self, point): """ @@ -922,7 +922,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, """ QtGui.QMessageBox.warning(self, translate('OpenLP.ProjectorManager', '"%s" Authentication Error' % name), - '
There was an authentictaion error while trying to connect.' + '
There was an authentication error while trying to connect.' '

Please verify your PIN setting ' 'for projector item "%s"' % name) diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index 6fb8fc914..e7547a292 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -120,7 +120,7 @@ def Build_Tab(group, source_key, default): group.addButton(itemwidget, int(key)) layout.addWidget(itemwidget) layout.addStretch() - return (widget, button_count, buttonchecked) + return widget, button_count, buttonchecked class FingerTabBarWidget(QTabBar): @@ -317,33 +317,6 @@ class SourceSelectSingle(QDialog): selected = super(SourceSelectSingle, self).exec_() return selected - title = QtGui.QLabel(translate('OpenLP.SourceSelectSingle', 'Select the input source below')) - self.layout.addWidget(title) - self.radio_buttons = [] - source_list = self.projectordb.get_source_list(make=projector.link.manufacturer, - model=projector.link.model, - sources=projector.link.source_available) - sort = [] - for item in source_list.keys(): - sort.append(item) - sort.sort() - current = QtGui.QLabel(translate('OpenLP.SourceSelectSingle', 'Current source is %s' % - source_list[projector.link.source])) - layout.addWidget(current) - for item in sort: - button = self._select_input_widget(parent=self, - selected=projector.link.source, - code=item, - text=source_list[item]) - layout.addWidget(button) - button_box = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | - QtGui.QDialogButtonBox.Cancel) - button_box.accepted.connect(box.accept_me) - button_box.rejected.connect(box.reject_me) - layout.addWidget(button_box) - selected = super(SourceSelectSingle, self).exec_() - return selected - @pyqtSlot() def accept_me(self): """ diff --git a/openlp/core/ui/projector/tab.py b/openlp/core/ui/projector/tab.py index b256aa20b..f569ae073 100644 --- a/openlp/core/ui/projector/tab.py +++ b/openlp/core/ui/projector/tab.py @@ -120,7 +120,7 @@ class ProjectorTab(SettingsTab): def load(self): """ - Load the projetor settings on startup + Load the projector settings on startup """ settings = Settings() settings.beginGroup(self.settings_section) From 04ad28c106e86cb53e7a67f2753b15c7e3df4694 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 15:47:25 -0700 Subject: [PATCH 098/115] Fixed typos in ProjectorSource --- openlp/core/lib/projector/db.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openlp/core/lib/projector/db.py b/openlp/core/lib/projector/db.py index fe4c6b9c6..527507a64 100644 --- a/openlp/core/lib/projector/db.py +++ b/openlp/core/lib/projector/db.py @@ -162,6 +162,8 @@ class Projector(CommonBase, Base): model: Column(String(128)) # From projector (future) other: Column(String(128)) # From projector (future) sources: Column(String(128)) # From projector (future) + + ProjectorSource relates """ ip = Column(String(100)) port = Column(String(8)) @@ -174,6 +176,12 @@ class Projector(CommonBase, Base): 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): @@ -191,7 +199,7 @@ class ProjectorSource(CommonBase, Base): """ code = Column(String(3)) text = Column(String(20)) - projector_id = (Integer, ForeignKey('projector.id')) + projector_id = Column(Integer, ForeignKey('projector.id')) class ProjectorDB(Manager): From 5ae48da58e40f510ff59b3906d8394689010efe4 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 16:45:15 -0700 Subject: [PATCH 099/115] Fix missing poll check --- openlp/core/lib/projector/pjlink1.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 9449003cd..21bf9d605 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -129,6 +129,8 @@ class PJLink1(QTcpSocket): else: # Default is 5 seconds self.socket_timeout = 5000 + # 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 = '' @@ -386,7 +388,7 @@ class PJLink1(QTcpSocket): # Initial data we should know about self.send_command(cmd='CLSS', salt=salt) self.waitForReadyRead() - if not self.new_wizard and self.state() == self.ConnectedState: + if (not self.no_poll) and (self.state() == self.ConnectedState): self.timer.setInterval(2000) # Set 2 seconds for initial information self.timer.start() From 416c37140c3522d1574403db1c88dd73e5c422d3 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 16:51:51 -0700 Subject: [PATCH 100/115] Fix single dialog get_source_list() call --- openlp/core/ui/projector/sourceselectform.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index e7547a292..8d04c95c1 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -295,9 +295,7 @@ class SourceSelectSingle(QDialog): :param projector: Projector instance to build source list from """ self.projector = projector - self.source_text = self.projectordb.get_source_list(projector.manufacturer, - projector.model, - projector.source_available) + self.source_text = self.projectordb.get_source_list(projector=projector) keys = list(self.source_text.keys()) keys.sort() key_count = len(keys) From 2e6556a11b02a442eacb7d66f0f2301ea411f106 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 17:02:37 -0700 Subject: [PATCH 101/115] Fix call to get_source_list in pjlink1 --- openlp/core/lib/projector/pjlink1.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 21bf9d605..c0e13efd0 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -389,6 +389,7 @@ class PJLink1(QTcpSocket): 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() From 6391b17b6d7fb72f50e2b54c44ce18b860f415b1 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Tue, 21 Oct 2014 17:49:21 -0700 Subject: [PATCH 102/115] Fix focus to ip text widget when calling projetor edit --- openlp/core/ui/projector/editform.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openlp/core/ui/projector/editform.py b/openlp/core/ui/projector/editform.py index 05b9b280d..3aaa1cdbd 100644 --- a/openlp/core/ui/projector/editform.py +++ b/openlp/core/ui/projector/editform.py @@ -122,6 +122,7 @@ class Ui_ProjectorEditForm(object): 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')) From 0264d57b61e180b2721123eab3b59563e68aad85 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 22 Oct 2014 19:21:26 -0700 Subject: [PATCH 103/115] Add input source edit text option --- openlp/core/lib/projector/db.py | 93 +++++-- openlp/core/lib/projector/pjlink1.py | 52 +--- openlp/core/ui/projector/manager.py | 85 +++--- openlp/core/ui/projector/sourceselectform.py | 277 ++++++++++++++----- 4 files changed, 336 insertions(+), 171 deletions(-) diff --git a/openlp/core/lib/projector/db.py b/openlp/core/lib/projector/db.py index 527507a64..347e93052 100644 --- a/openlp/core/lib/projector/db.py +++ b/openlp/core/lib/projector/db.py @@ -45,7 +45,7 @@ import logging log = logging.getLogger(__name__) log.debug('projector.lib.db module loaded') -from sqlalchemy import Column, ForeignKey, Integer, MetaData, String +from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, and_ from sqlalchemy.ext.declarative import declarative_base, declared_attr from sqlalchemy.orm import backref, relationship @@ -165,6 +165,15 @@ class Projector(CommonBase, Base): 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)) @@ -197,6 +206,14 @@ class ProjectorSource(CommonBase, Base): 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')) @@ -363,29 +380,57 @@ class ProjectorDB(Manager): key: (str) PJLink code for source value: (str) From ProjectorSource, Sources tables or PJLink default code list """ - # Get manufacturer-defined source text - model_list = self.get_all_objects(Model, Model.name == projector.model) - if model_list is None or len(model_list) < 1: - # No entry for model, so see if there's a default entry - default_list = self.get_object_filtered(Manufacturer, Manufacturer.name == projector.manufacturer) - if default_list is None or len(default_list) < 1: - # No entry for manufacturer, so can't check for default text - model_list = {} - else: - model_list = default_list.models['DEFAULT'] - # Get user-defined source text - local_list = self.get_all_objects(ProjectorSource, ProjectorSource.projector_id == projector.dbid) - if local_list is None or len(local_list) < 1: - local_list = {} source_dict = {} - for source in projector.source_available: - if source in local_list: - # User defined source text - source_dict[source] = local_list[source] - elif source in model_list: - # Default manufacturer defined source text - source_dict[source] = model_list[source] + # 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: - # Default PJLink source text - source_dict[source] = PJLINK_DEFAULT_CODES[source] + 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 index c0e13efd0..132de0dca 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -105,30 +105,13 @@ class PJLink1(QTcpSocket): self.dbid = None self.location = None self.notes = None - if 'dbid' in kwargs: - self.dbid = kwargs['dbid'] - else: - self.dbid = None - if 'location' in kwargs: - self.location = kwargs['location'] - else: - self.location = None - if 'notes' in kwargs: - self.notes = kwargs['notes'] - else: - self.notes = None - if 'poll_time' in kwargs: - # Convert seconds to milliseconds - self.poll_time = kwargs['poll_time'] * 1000 - else: - # Default 20 seconds - self.poll_time = 20000 - if 'socket_timeout' in kwargs: - # Convert seconds to milliseconds - self.socket_timeout = kwargs['socket_timeout'] * 1000 - else: - # Default is 5 seconds - self.socket_timeout = 5000 + 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 @@ -305,18 +288,6 @@ class PJLink1(QTcpSocket): log.debug('(%s) error_status: %s: %s' % (self.ip, status_code, status_message if msg is None else msg)) self.changeStatus.emit(self.ip, status, message) - def check_command(self, cmd): - """ - Verifies command is valid based on PJLink class. - - :param cmd: PJLink command to validate. - :returns: bool - True if command is valid PJLink command - False if command is not a valid PJLink command - """ - return self.pjlink_class in PJLINK_VALID_CMD and \ - cmd in PJLINK_VALID_CMD[self.pjlink_class] - @pyqtSlot() def check_login(self, data=None): """ @@ -401,11 +372,13 @@ class PJLink1(QTcpSocket): 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() @@ -415,16 +388,19 @@ class PJLink1(QTcpSocket): 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('=') @@ -434,11 +410,13 @@ class PJLink1(QTcpSocket): 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.check_command(cmd): + 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) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 99e5c6229..db69d7eb6 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -104,7 +104,7 @@ class Ui_ProjectorManager(object): 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_projector', + 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', @@ -254,6 +254,11 @@ class Ui_ProjectorManager(object): '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'), @@ -348,6 +353,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, 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 @@ -356,6 +362,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, 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) @@ -363,7 +370,10 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, self.menu.projector = real_projector self.menu.exec_(self.projector_list_widget.mapToGlobal(point)) - def on_select_input(self, opt=None): + 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. @@ -374,13 +384,17 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, list_item = self.projector_list_widget.item(self.projector_list_widget.currentRow()) projector = list_item.data(QtCore.Qt.UserRole) # QTabwidget for source select - if self.source_select_dialog_type == DialogSourceStyle.Tabbed: - source_select_form = SourceSelectTabs(parent=self, - projectordb=self.projectordb) - else: - source_select_form = SourceSelectSingle(parent=self, - projectordb=self.projectordb) - source = source_select_form.exec_(projector.link) + 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)) @@ -682,29 +696,14 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, socket_timeout=self.socket_timeout ) - def add_projector(self, opt1, opt2=None): + def add_projector(self, projector, start=False): """ Builds manager list item, projector thread, and timer for projector instance. - If called by add projector wizard: - opt1 = wizard instance - opt2 = item - Otherwise: - opt1 = item - opt2 = None - We are not concerned with the wizard instance, - just the projector item - - :param opt1: See above - :param opt2: See above + :param projector: Projector instance to add + :param start: Start projector if True """ - if opt1 is None: - return - if opt2 is None: - projector = opt1 - else: - projector = opt2 item = ProjectorItem(link=self._add_projector(projector)) item.db_item = projector icon = QtGui.QIcon(QtGui.QPixmap(STATUS_ICONS[S_NOT_CONNECTED])) @@ -714,6 +713,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, 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 @@ -730,8 +730,9 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, 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(5000) # % second timer in case of brain-dead projectors + socket_timer.setInterval(11000) socket_timer.timeout.connect(item.link.socket_abort) item.socket_timer = socket_timer thread.start() @@ -740,10 +741,10 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, item.link.socket_timer = socket_timer item.link.widget = item.widget self.projector_list.append(item) - if self.autostart: + if start: item.link.connect_to_host() - for i in self.projector_list: - log.debug('New projector list - item: (%s) %s' % (i.link.ip, i.link.name)) + 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): @@ -753,19 +754,18 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, :param ip: IP address of new record item to find :param opts: Needed by PyQt4 """ - log.debug('load_projector(ip=%s)' % ip) + 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, opts=None): + def edit_projector_from_wizard(self, projector): """ Update projector from the wizard edit page :param projector: Projector() instance of projector with updated information - :param opts: Needed by PyQt4 """ - + 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 @@ -778,10 +778,10 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, """' Load projectors - only call when initializing """ - log.debug('load_projectors()') + log.debug('_load_projectors()') self.projector_list_widget.clear() for item in self.projectordb.get_projector_all(): - self.add_projector(item) + self.add_projector(projector=item, start=self.autostart) def get_projector_list(self): """ @@ -835,8 +835,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, item = self.one_toolbar.findChild(QtGui.QAction, name) if item == 0: log.debug('No item found with name "%s"' % name) - else: - log.debug('item "%s" updating enabled=%s hidden=%s' % (name, enabled, hidden)) + return item.setVisible(False if hidden else True) item.setEnabled(True if enabled else False) @@ -851,7 +850,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, 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_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') @@ -876,13 +875,13 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, self.get_toolbar_item('show_projector_multiple', hidden=True) if connected: self.get_toolbar_item('view_projector', enabled=True) - self.get_toolbar_item('source_projector', + 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_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) @@ -899,7 +898,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, 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_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) diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index 8d04c95c1..a5040ba5a 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -37,12 +37,13 @@ log.debug('editform loaded') from PyQt4 import QtCore, QtGui from PyQt4.QtCore import pyqtSlot, QSize -from PyQt4.QtGui import QDialog, QButtonGroup, QDialogButtonBox, QRadioButton, \ +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.constants import PJLINK_DEFAULT_SOURCES +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): @@ -53,7 +54,7 @@ def source_group(inputs, source_text): source_text = dict{"key1": "key1-text", "key2": "key2-text", ...} - ex: + return: dict{ key1[0]: { "key11": "key11-text", "key12": "key12-text", "key13": "key13-text", @@ -82,7 +83,7 @@ def source_group(inputs, source_text): return keydict -def Build_Tab(group, source_key, default): +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. @@ -101,25 +102,45 @@ def Build_Tab(group, source_key, default): :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 = QVBoxLayout() + 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) - for key in sourcelist: - itemwidget = QRadioButton(source_key[tempkey][key]) - itemwidget.setAutoExclusive(True) - if default == key: - itemwidget.setChecked(True) - buttonchecked = itemwidget.isChecked() or buttonchecked - group.addButton(itemwidget, int(key)) - layout.addWidget(itemwidget) - layout.addStretch() + 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 @@ -188,16 +209,21 @@ class SourceSelectTabs(QDialog): Class for handling selecting the source for the projector to use. Uses tabbed interface. """ - def __init__(self, parent, projectordb): + 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()') - self.projectordb = projectordb super(SourceSelectTabs, self).__init__(parent) - self.setWindowTitle(translate('OpenLP.SourceSelectForm', 'Select Projector Source')) + 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) @@ -226,48 +252,106 @@ class SourceSelectTabs(QDialog): 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 = QButtonGroup() + self.button_group = [] if self.edit else QButtonGroup() keys = list(self.source_group.keys()) keys.sort() - 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) - thistab = self.tabwidget.addTab(tab, PJLINK_DEFAULT_SOURCES[key]) - if buttonchecked: - self.tabwidget.setCurrentIndex(thistab) - self.button_box = QDialogButtonBox(QtGui.QDialogButtonBox.Ok | + 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.accepted.connect(self.accept_me) - self.button_box.rejected.connect(self.reject_me) + self.button_box.clicked.connect(self.button_clicked) self.layout.addWidget(self.button_box) selected = super(SourceSelectTabs, self).exec_() return selected - @pyqtSlot() + @pyqtSlot(object) + def button_clicked(self, button): + """ + Checks which button was clicked + + :param button: Button that was clicked + :returns: Ok: calls accept_me() + Reset: 100 + Cancel: self.done(0) + """ + if self.button_box.standardButton(button) == self.button_box.Cancel: + self.done(0) + elif self.button_box.standardButton(button) == self.button_box.Reset: + self.done(100) + elif self.button_box.standardButton(button) == self.button_box.Discard: + self.delete_sources() + 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 """ - selected = self.button_group.checkedId() - log.debug('SourceSelectTabs().accepted() Setting source to %s' % selected) + 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) - @pyqtSlot() - def reject_me(self): - """ - Slot to accept 'Cancel' button - """ - log.debug('SourceSelectTabs() - Rejected') - self.done(0) - class SourceSelectSingle(QDialog): """ Class for handling selecting the source for the projector to use. Uses single dialog interface. """ - def __init__(self, parent, projectordb): + def __init__(self, parent, projectordb, edit=False): """ Build the source select dialog. @@ -280,54 +364,113 @@ class SourceSelectSingle(QDialog): self.setObjectName('source_select_single') self.setWindowIcon(build_icon(':/icon/openlp-log-32x32.png')) self.setModal(True) - self.layout = QVBoxLayout() - self.layout.setObjectName('source_select_tabs_layout') - self.layout.setSpacing(10) - self.setLayout(self.layout) - self.setMinimumWidth(350) - self.button_group = QButtonGroup() - self.button_group.setObjectName('source_select_single_buttongroup') + self.edit = edit - def exec_(self, projector): + 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 = [] - for key in keys: - button = QtGui.QRadioButton(self.source_text[key]) - 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.Ok | + 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.accepted.connect(self.accept_me) - self.button_box.rejected.connect(self.reject_me) + self.button_box.clicked.connect(self.button_clicked) self.layout.addWidget(self.button_box) self.setMinimumHeight(key_count*25) 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: calls accept_me() + Reset: 100 + Cancel: self.done(0) + """ + if self.button_box.standardButton(button) == self.button_box.Cancel: + self.done(0) + elif self.button_box.standardButton(button) == self.button_box.Reset: + self.done(100) + elif self.button_box.standardButton(button) == self.button_box.Discard: + self.delete_sources() + 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 """ - selected = self.button_group.checkedId() - log.debug('SourceSelectDialog().accepted() Setting source to %s' % selected) + 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) - - @pyqtSlot() - def reject_me(self): - """ - Slot to accept 'Cancel' button - """ - log.debug('SourceSelectDialog() - Rejected') - self.done(0) From c7d36826aa279408ddb346c471884bda0aa1c19f Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Wed, 22 Oct 2014 19:40:22 -0700 Subject: [PATCH 104/115] Add 1.5 second delay for autoloading projectors --- openlp/core/ui/projector/manager.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index db69d7eb6..cac5de46d 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -313,7 +313,13 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, """ Post-initialize setups. """ - self._load_projectors() + # 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) From 3cf8c0464cd6f4d528bcffc7c0696257b125c52d Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 23 Oct 2014 14:00:06 -0700 Subject: [PATCH 105/115] Fix discard/reset button calls - added button tooltips --- openlp/core/ui/projector/sourceselectform.py | 44 +++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index a5040ba5a..6e7ab7f76 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -144,6 +144,26 @@ def Build_Tab(group, source_key, default, projector, projectordb, edit=False): 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 @@ -283,6 +303,7 @@ class SourceSelectTabs(QDialog): 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 @@ -292,16 +313,17 @@ class SourceSelectTabs(QDialog): Checks which button was clicked :param button: Button that was clicked - :returns: Ok: calls accept_me() - Reset: 100 - Cancel: self.done(0) + :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.done(100) - elif self.button_box.standardButton(button) == self.button_box.Discard: 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: @@ -412,6 +434,7 @@ class SourceSelectSingle(QDialog): 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 @@ -421,16 +444,17 @@ class SourceSelectSingle(QDialog): Checks which button was clicked :param button: Button that was clicked - :returns: Ok: calls accept_me() - Reset: 100 - Cancel: self.done(0) + :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.done(100) - elif self.button_box.standardButton(button) == self.button_box.Discard: 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: From e8e6410d456fa30556ef97e976cc7a6cef4b5f22 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Fri, 24 Oct 2014 12:17:12 -0700 Subject: [PATCH 106/115] Fix test suite change --- tests/interfaces/openlp_core_ui/test_projectormanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/interfaces/openlp_core_ui/test_projectormanager.py b/tests/interfaces/openlp_core_ui/test_projectormanager.py index 4dd79ec7a..a46b0b93c 100644 --- a/tests/interfaces/openlp_core_ui/test_projectormanager.py +++ b/tests/interfaces/openlp_core_ui/test_projectormanager.py @@ -54,7 +54,7 @@ class TestProjectorManager(TestCase, TestMixin): Create the UI and setup necessary options """ self.build_settings() - self.get_application() + self.setup_application() Registry.create() if not hasattr(self, 'projector_manager'): with patch('openlp.core.lib.projector.db.init_url') as mocked_init_url: From 3693e55f5c2673adbfd8698e22cba06b9fa15995 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 27 Oct 2014 11:37:22 +0100 Subject: [PATCH 107/115] Change filename encoding to only apply to local variable. --- openlp/plugins/presentations/lib/pptviewcontroller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/presentations/lib/pptviewcontroller.py b/openlp/plugins/presentations/lib/pptviewcontroller.py index 4aea501b4..8bde0b213 100644 --- a/openlp/plugins/presentations/lib/pptviewcontroller.py +++ b/openlp/plugins/presentations/lib/pptviewcontroller.py @@ -135,11 +135,11 @@ class PptviewDocument(PresentationDocument): self.file_path = os.path.normpath(self.file_path) preview_path = os.path.join(temp_folder, 'slide') # Ensure that the paths are null terminated - self.file_path = self.file_path.encode('utf-16-le') + b'\0' + byte_file_path = self.file_path.encode('utf-16-le') + b'\0' preview_path = preview_path.encode('utf-16-le') + b'\0' if not os.path.isdir(temp_folder): os.makedirs(temp_folder) - self.ppt_id = self.controller.process.OpenPPT(self.file_path, None, rect, preview_path) + self.ppt_id = self.controller.process.OpenPPT(byte_file_path, None, rect, preview_path) if self.ppt_id >= 0: self.create_thumbnails() self.stop_presentation() From 6d2cfedd7e0689cbb20d42a3bf58d336cbde16bf Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 27 Oct 2014 13:52:56 +0100 Subject: [PATCH 108/115] Remove prints --- openlp/plugins/remotes/lib/httprouter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openlp/plugins/remotes/lib/httprouter.py b/openlp/plugins/remotes/lib/httprouter.py index ff28de399..22e2495c0 100644 --- a/openlp/plugins/remotes/lib/httprouter.py +++ b/openlp/plugins/remotes/lib/httprouter.py @@ -526,7 +526,6 @@ class HttpRouter(RegistryProperties): Settings().value('remotes/thumbnails'): # If the file is under our app directory tree send the portion after the match data_path = AppLocation.get_data_path() - print(frame) if frame['image'][0:len(data_path)] == data_path: item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):]) item['text'] = str(frame['title']) @@ -534,7 +533,6 @@ class HttpRouter(RegistryProperties): item['selected'] = (self.live_controller.selected_row == index) if current_item.notes: item['notes'] = item.get('notes', '') + '\n' + current_item.notes - print(item) data.append(item) json_data = {'results': {'slides': data}} if current_item: From 3d387cb296f6b7a009850b1aface780ca5dcd664 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 27 Oct 2014 13:58:59 +0100 Subject: [PATCH 109/115] Fixed a tiny glitch, remove author button became always disabled even if not needed to. Copied from lp:~mahfiaz/openlp/author-delete-button-not-active-in-edit-dialog --- openlp/plugins/songs/forms/editsongform.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index d71cde304..624b409ec 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -625,7 +625,8 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties): """ Remove the author from the list when the delete button is clicked. """ - self.author_remove_button.setEnabled(False) + if self.authors_list_view.count() <= 2: + self.author_remove_button.setEnabled(False) item = self.authors_list_view.currentItem() row = self.authors_list_view.row(item) self.authors_list_view.takeItem(row) From d2f388bb5908e7ae1e73b62bfbd67188225e55fd Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 27 Oct 2014 14:11:55 +0100 Subject: [PATCH 110/115] Fix for another occurrence of bug #1296574. Copied from lp:~erik-lundin/openlp/bug-1296574 Fixes: https://launchpad.net/bugs/1296574 --- openlp/core/ui/slidecontroller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 062a8440f..f706dd929 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -1245,7 +1245,7 @@ class SlideController(DisplayController, RegistryProperties): if event.timerId() == self.timer_id: self.on_slide_selected_next(self.play_slides_loop.isChecked()) - def on_edit_song(self): + def on_edit_song(self, field=None): """ From the preview display requires the service Item to be editied """ @@ -1254,7 +1254,7 @@ class SlideController(DisplayController, RegistryProperties): if new_item: self.add_service_item(new_item) - def on_preview_add_to_service(self): + def on_preview_add_to_service(self, field=None): """ From the preview display request the Item to be added to service """ From e9a151cec0ab8e765377278d2f4451c883e47de9 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 27 Oct 2014 14:14:12 +0100 Subject: [PATCH 111/115] This fixes the adding of images and media to the media manager. Copied from lp:~rafaellerm/openlp/media_import_fix --- openlp/core/lib/listwidgetwithdnd.py | 2 +- openlp/core/lib/treewidgetwithdnd.py | 2 +- openlp/core/ui/media/mediacontroller.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/core/lib/listwidgetwithdnd.py b/openlp/core/lib/listwidgetwithdnd.py index 2618d13c3..cbedbc79c 100644 --- a/openlp/core/lib/listwidgetwithdnd.py +++ b/openlp/core/lib/listwidgetwithdnd.py @@ -109,6 +109,6 @@ class ListWidgetWithDnD(QtGui.QListWidget): listing = os.listdir(local_file) for file in listing: files.append(os.path.join(local_file, file)) - Registry().execute('%s_dnd' % self.mime_data_text, files) + Registry().execute('%s_dnd' % self.mime_data_text, {'files': files, 'target': self.itemAt(event.pos())}) else: event.ignore() diff --git a/openlp/core/lib/treewidgetwithdnd.py b/openlp/core/lib/treewidgetwithdnd.py index cdb6cbbdb..0bdd664e8 100644 --- a/openlp/core/lib/treewidgetwithdnd.py +++ b/openlp/core/lib/treewidgetwithdnd.py @@ -127,7 +127,7 @@ class TreeWidgetWithDnD(QtGui.QTreeWidget): listing = os.listdir(local_file) for file_name in listing: files.append(os.path.join(local_file, file_name)) - Registry().execute('%s_dnd' % self.mime_Data_Text, {'files': files, 'target': self.itemAt(event.pos())}) + Registry().execute('%s_dnd' % self.mime_data_text, {'files': files, 'target': self.itemAt(event.pos())}) elif self.allow_internal_dnd: event.setDropAction(QtCore.Qt.CopyAction) event.accept() diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 4a2d475c1..e3517ba0f 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -146,7 +146,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): if player.is_active: for item in player.video_extensions_list: if item not in self.video_extensions_list: - self.video_extensions_list.extend(item) + self.video_extensions_list.append(item) suffix_list.append(item[2:]) self.service_manager.supported_suffixes(suffix_list) From afde4a25ad26cff0c544f36be884c21dd5735399 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 27 Oct 2014 20:22:57 +0000 Subject: [PATCH 112/115] Fix missing dummy values for debug --- openlp/core/ui/slidecontroller.py | 6 ++--- .../openlp_plugins/remotes/test_router.py | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 062a8440f..1b8b45815 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -1245,7 +1245,7 @@ class SlideController(DisplayController, RegistryProperties): if event.timerId() == self.timer_id: self.on_slide_selected_next(self.play_slides_loop.isChecked()) - def on_edit_song(self): + def on_edit_song(self, field=None): """ From the preview display requires the service Item to be editied """ @@ -1254,7 +1254,7 @@ class SlideController(DisplayController, RegistryProperties): if new_item: self.add_service_item(new_item) - def on_preview_add_to_service(self): + def on_preview_add_to_service(self, field=None): """ From the preview display request the Item to be added to service """ @@ -1351,7 +1351,7 @@ class SlideController(DisplayController, RegistryProperties): seconds %= 60 self.audio_time_label.setText(' %02d:%02d ' % (minutes, seconds)) - def on_track_triggered(self): + def on_track_triggered(self, field=None): """ Start playing a track """ diff --git a/tests/functional/openlp_plugins/remotes/test_router.py b/tests/functional/openlp_plugins/remotes/test_router.py index 8d239a5f0..b1a157574 100644 --- a/tests/functional/openlp_plugins/remotes/test_router.py +++ b/tests/functional/openlp_plugins/remotes/test_router.py @@ -107,6 +107,33 @@ class TestRouter(TestCase, TestMixin): self.assertEqual(mocked_function, function['function'], 'The mocked function should match defined value.') self.assertFalse(function['secure'], 'The mocked function should not require any security.') + def process_secure_http_request_test(self): + """ + Test the router control functionality + """ + # GIVEN: A testing set of Routes + mocked_function = MagicMock() + test_route = [ + (r'^/stage/api/poll$', {'function': mocked_function, 'secure': True}), + ] + self.router.routes = test_route + self.router.settings_section = 'remotes' + Settings().setValue('remotes/authentication enabled', True) + self.router.path = '/stage/api/poll' + self.router.auth = '' + self.router.headers = {'Authorization': None} + self.router.send_response = MagicMock() + self.router.send_header = MagicMock() + self.router.end_headers = MagicMock() + self.router.wfile = MagicMock() + + # WHEN: called with a poll route + self.router.do_post_processor() + + # THEN: the function should have been called only once + self.router.send_response.assert_called_once_with(401) + self.assertEqual(self.router.send_header.call_count, 2, 'The header should have been called twice.') + def get_content_type_test(self): """ Test the get_content_type logic From 11b2b4dc3b58ecc11ed6f6afa7dfe1d5259a14dd Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 27 Oct 2014 21:38:19 +0000 Subject: [PATCH 113/115] Added test for checking extension list creation --- tests/functional/openlp_core_ui/__init__.py | 31 +++++++++ .../openlp_core_ui_media/__init__.py | 31 +++++++++ .../test_mediacontroller.py | 67 +++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 tests/functional/openlp_core_ui_media/__init__.py create mode 100644 tests/functional/openlp_core_ui_media/test_mediacontroller.py diff --git a/tests/functional/openlp_core_ui/__init__.py b/tests/functional/openlp_core_ui/__init__.py index e69de29bb..00a1db85c 100644 --- a/tests/functional/openlp_core_ui/__init__.py +++ b/tests/functional/openlp_core_ui/__init__.py @@ -0,0 +1,31 @@ +# -*- 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 package. +""" diff --git a/tests/functional/openlp_core_ui_media/__init__.py b/tests/functional/openlp_core_ui_media/__init__.py new file mode 100644 index 000000000..c489203a2 --- /dev/null +++ b/tests/functional/openlp_core_ui_media/__init__.py @@ -0,0 +1,31 @@ +# -*- 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.media package. +""" diff --git a/tests/functional/openlp_core_ui_media/test_mediacontroller.py b/tests/functional/openlp_core_ui_media/test_mediacontroller.py new file mode 100644 index 000000000..17fcba954 --- /dev/null +++ b/tests/functional/openlp_core_ui_media/test_mediacontroller.py @@ -0,0 +1,67 @@ +# -*- 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.media package. +""" +from unittest import TestCase + +from openlp.core.ui.media.mediacontroller import MediaController +from openlp.core.ui.media.mediaplayer import MediaPlayer +from openlp.core.common import Registry + +from tests.functional import MagicMock +from tests.helpers.testmixin import TestMixin + + +class TestMediaController(TestCase, TestMixin): + + def setUp(self): + Registry.create() + Registry().register('service_manager', MagicMock()) + + def generate_extensions_lists_test(self): + """ + Test that the extensions are create correctly + """ + # GIVEN: A MediaController and an active player with audio and video extensions + media_controller = MediaController() + media_player = MediaPlayer(None) + media_player.is_active = True + media_player.audio_extensions_list = ['*.mp3', '*.wav', '*.wma', '*.ogg'] + media_player.video_extensions_list = ['*.mp4', '*.mov', '*.avi', '*.ogm'] + media_controller.register_players(media_player) + + # WHEN: calling _generate_extensions_lists + media_controller._generate_extensions_lists() + + # THEN: extensions list should have been copied from the player to the mediacontroller + self.assertListEqual(media_player.video_extensions_list, media_controller.video_extensions_list, + 'Video extensions should be the same') + self.assertListEqual(media_player.audio_extensions_list, media_controller.audio_extensions_list, + 'Audio extensions should be the same') From ae7c86333370769591e5a9b1dbf5dcab3e52991a Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 28 Oct 2014 21:02:06 +0200 Subject: [PATCH 114/115] [fix 1385979] Check if the item is valid first --- openlp/core/ui/settingsform.py | 3 +++ .../openlp_core_ui/test_settingsform.py | 25 ++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/openlp/core/ui/settingsform.py b/openlp/core/ui/settingsform.py index e953da011..e2b51d4fb 100644 --- a/openlp/core/ui/settingsform.py +++ b/openlp/core/ui/settingsform.py @@ -143,6 +143,9 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog, RegistryProperties): """ # Get the item we clicked on list_item = self.setting_list_widget.item(item_index) + # Quick exit to the left if the item doesn't exist (maybe -1?) + if not list_item: + return # Loop through the list of tabs in the stacked layout for tab_index in range(self.stacked_layout.count()): # Get the widget diff --git a/tests/functional/openlp_core_ui/test_settingsform.py b/tests/functional/openlp_core_ui/test_settingsform.py index 5d9b413df..e01f0bde7 100644 --- a/tests/functional/openlp_core_ui/test_settingsform.py +++ b/tests/functional/openlp_core_ui/test_settingsform.py @@ -29,10 +29,10 @@ """ Package to test the openlp.core.ui.settingsform package. """ +from PyQt4 import QtGui from unittest import TestCase from openlp.core.common import Registry -from openlp.core.ui.generaltab import GeneralTab from openlp.core.ui.settingsform import SettingsForm from tests.functional import MagicMock, patch @@ -62,7 +62,7 @@ class TestSettingsForm(TestCase): patch.object(settings_form.setting_list_widget, 'addItem') as mocked_add_item: settings_form.insert_tab(general_tab, is_visible=True) - # THEN: Stuff should happen + # THEN: The general tab should have been inserted into the stacked layout and an item inserted into the list mocked_add_widget.assert_called_with(general_tab) self.assertEqual(1, mocked_add_item.call_count, 'addItem should have been called') @@ -80,6 +80,25 @@ class TestSettingsForm(TestCase): patch.object(settings_form.setting_list_widget, 'addItem') as mocked_add_item: settings_form.insert_tab(general_tab, is_visible=False) - # THEN: Stuff should happen + # THEN: The general tab should have been inserted, but no list item should have been inserted into the list mocked_add_widget.assert_called_with(general_tab) self.assertEqual(0, mocked_add_item.call_count, 'addItem should not have been called') + + def list_item_changed_invalid_item_test(self): + """ + Test that the list_item_changed() slot handles a non-existent item + """ + # GIVEN: A mocked tab inserted into a Settings Form + settings_form = SettingsForm(None) + general_tab = QtGui.QWidget(None) + general_tab.tab_title = 'mock' + general_tab.tab_title_visible = 'Mock' + general_tab.icon_path = ':/icon/openlp-logo-16x16.png' + settings_form.insert_tab(general_tab, is_visible=True) + + with patch.object(settings_form.stacked_layout, 'count') as mocked_count: + # WHEN: The list_item_changed() slot is called with an invalid item index + settings_form.list_item_changed(100) + + # THEN: The rest of the method should not have been called + self.assertEqual(0, mocked_count.call_count, 'The count method of the stacked layout should not be called') From 743dc59d516938e03600bde3ea2f224a7fe58ed7 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 28 Oct 2014 21:40:01 +0200 Subject: [PATCH 115/115] [fix 1386710] Fix traceback after settings form is saved Fixes: https://launchpad.net/bugs/1386710 --- openlp/core/ui/settingsform.py | 18 ++++++++++-- .../openlp_core_ui/test_settingsform.py | 29 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/openlp/core/ui/settingsform.py b/openlp/core/ui/settingsform.py index e953da011..a92c6583c 100644 --- a/openlp/core/ui/settingsform.py +++ b/openlp/core/ui/settingsform.py @@ -96,9 +96,21 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog, RegistryProperties): Process the form saving the settings """ log.debug('Processing settings exit') - for tabIndex in range(self.stacked_layout.count()): - self.stacked_layout.widget(tabIndex).save() - # if the display of image background are changing we need to regenerate the image cache + # We add all the forms into the stacked layout, even if the plugin is inactive, + # but we don't add the item to the list on the side if the plugin is inactive, + # so loop through the list items, and then find the tab for that item. + for item_index in range(self.setting_list_widget.count()): + # Get the list item + list_item = self.setting_list_widget.item(item_index) + if not list_item: + continue + # Now figure out if there's a tab for it, and save the tab. + plugin_name = list_item.data(QtCore.Qt.UserRole) + for tab_index in range(self.stacked_layout.count()): + tab_widget = self.stacked_layout.widget(tab_index) + if tab_widget.tab_title == plugin_name: + tab_widget.save() + # if the image background has been changed we need to regenerate the image cache if 'images_config_updated' in self.processes or 'config_screen_changed' in self.processes: self.register_post_process('images_regenerate') # Now lets process all the post save handlers diff --git a/tests/functional/openlp_core_ui/test_settingsform.py b/tests/functional/openlp_core_ui/test_settingsform.py index 5d9b413df..bc9d0621f 100644 --- a/tests/functional/openlp_core_ui/test_settingsform.py +++ b/tests/functional/openlp_core_ui/test_settingsform.py @@ -29,6 +29,7 @@ """ Package to test the openlp.core.ui.settingsform package. """ +from PyQt4 import QtGui from unittest import TestCase from openlp.core.common import Registry @@ -83,3 +84,31 @@ class TestSettingsForm(TestCase): # THEN: Stuff should happen mocked_add_widget.assert_called_with(general_tab) self.assertEqual(0, mocked_add_item.call_count, 'addItem should not have been called') + + def accept_with_inactive_plugins_test(self): + """ + Test that the accept() method works correctly when some of the plugins are inactive + """ + # GIVEN: A visible general tab and an invisible theme tab in a Settings Form + settings_form = SettingsForm(None) + general_tab = QtGui.QWidget(None) + general_tab.tab_title = 'mock-general' + general_tab.tab_title_visible = 'Mock General' + general_tab.icon_path = ':/icon/openlp-logo-16x16.png' + mocked_general_save = MagicMock() + general_tab.save = mocked_general_save + settings_form.insert_tab(general_tab, is_visible=True) + themes_tab = QtGui.QWidget(None) + themes_tab.tab_title = 'mock-themes' + themes_tab.tab_title_visible = 'Mock Themes' + themes_tab.icon_path = ':/icon/openlp-logo-16x16.png' + mocked_theme_save = MagicMock() + themes_tab.save = mocked_theme_save + settings_form.insert_tab(themes_tab, is_visible=False) + + # WHEN: The accept() method is called + settings_form.accept() + + # THEN: The general tab's save() method should have been called, but not the themes tab + mocked_general_save.assert_called_with() + self.assertEqual(0, mocked_theme_save.call_count, 'The Themes tab\'s save() should not have been called')