diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index efa251eec..84fc6db96 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -107,10 +107,9 @@ class Settings(QtCore.QSettings): __default_settings__ = { 'advanced/add page break': False, 'advanced/alternate rows': not is_win(), + 'advanced/autoscrolling': {'dist': 1, 'pos': 0}, 'advanced/current media plugin': -1, 'advanced/data path': '', - 'advanced/default color': '#ffffff', - 'advanced/default image': ':/graphics/openlp-splash-screen.png', # 7 stands for now, 0 to 6 is Monday to Sunday. 'advanced/default service day': 7, 'advanced/default service enabled': True, @@ -121,7 +120,6 @@ class Settings(QtCore.QSettings): 'advanced/double click live': False, 'advanced/enable exit confirmation': True, 'advanced/expand service item': False, - 'advanced/slide max height': 0, 'advanced/hide mouse': True, 'advanced/is portable': False, 'advanced/max recent files': 20, @@ -131,6 +129,7 @@ class Settings(QtCore.QSettings): 'advanced/recent file count': 4, 'advanced/save current plugin': False, 'advanced/slide limits': SlideLimits.End, + 'advanced/slide max height': 0, 'advanced/single click preview': False, 'advanced/single click service preview': False, 'advanced/x11 bypass wm': X11_BYPASS_DEFAULT, @@ -152,6 +151,9 @@ class Settings(QtCore.QSettings): 'core/save prompt': False, 'core/screen blank': False, 'core/show splash': True, + 'core/logo background color': '#ffffff', + 'core/logo file': ':/graphics/openlp-splash-screen.png', + 'core/logo hide on startup': False, 'core/songselect password': '', 'core/songselect username': '', 'core/update check': True, @@ -180,13 +182,15 @@ class Settings(QtCore.QSettings): 'themes/wrap footer': False, 'user interface/live panel': True, 'user interface/live splitter geometry': QtCore.QByteArray(), - 'user interface/lock panel': False, + 'user interface/lock panel': True, 'user interface/main window geometry': QtCore.QByteArray(), 'user interface/main window position': QtCore.QPoint(0, 0), '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/is preset layout': False, + 'projector/show after wizard': False, 'projector/db type': 'sqlite', 'projector/db username': '', 'projector/db password': '', @@ -207,7 +211,9 @@ class Settings(QtCore.QSettings): # ('general/recent files', 'core/recent files', [(recent_files_conv, None)]), ('songs/search as type', 'advanced/search as type', []), ('media/players', 'media/players_temp', [(media_players_conv, None)]), # Convert phonon to system - ('media/players_temp', 'media/players', []) # Move temp setting from above to correct setting + ('media/players_temp', 'media/players', []), # Move temp setting from above to correct setting + ('advanced/default color', 'core/logo background color', []), # Default image renamed + moved to general > 2.4. + ('advanced/default image', '/core/logo file', []) # Default image renamed + moved to general after 2.4. ] @staticmethod diff --git a/openlp/core/common/uistrings.py b/openlp/core/common/uistrings.py index 33ebb0a5b..f26b41fc5 100644 --- a/openlp/core/common/uistrings.py +++ b/openlp/core/common/uistrings.py @@ -56,6 +56,7 @@ class UiStrings(object): self.AllFiles = translate('OpenLP.Ui', 'All Files') self.Automatic = translate('OpenLP.Ui', 'Automatic') self.BackgroundColor = translate('OpenLP.Ui', 'Background Color') + self.BackgroundColorColon = translate('OpenLP.Ui', 'Background color:') self.Bottom = translate('OpenLP.Ui', 'Bottom') self.Browse = translate('OpenLP.Ui', 'Browse...') self.Cancel = translate('OpenLP.Ui', 'Cancel') diff --git a/openlp/core/lib/projector/db.py b/openlp/core/lib/projector/db.py index 3e050bf22..2b41ea69c 100644 --- a/openlp/core/lib/projector/db.py +++ b/openlp/core/lib/projector/db.py @@ -74,7 +74,7 @@ class Manufacturer(CommonBase, Base): """ Returns a basic representation of a Manufacturer table entry. """ - return '' % self.name + return ''.format(name=self.name) name = Column(String(30)) models = relationship('Model', @@ -101,7 +101,7 @@ class Model(CommonBase, Base): """ Returns a basic representation of a Model table entry. """ - return '' % self.name + return ''.format(name=self.name) manufacturer_id = Column(Integer, ForeignKey('manufacturer.id')) name = Column(String(20)) @@ -131,8 +131,9 @@ class Source(CommonBase, Base): """ Return basic representation of Source table entry. """ - return '' % \ - (self.pjlink_name, self.pjlink_code, self.text) + return ''.format(name=self.pjlink_name, + code=self.pjlink_code, + text=self.text) model_id = Column(Integer, ForeignKey('model.id')) pjlink_name = Column(String(15)) pjlink_code = Column(String(2)) @@ -162,11 +163,22 @@ class Projector(CommonBase, Base): """ 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) + return '< Projector(id="{data}", ip="{ip}", port="{port}", pin="{pin}", name="{name}", ' \ + 'location="{location}", notes="{notes}", pjlink_name="{pjlink_name}", ' \ + 'manufacturer="{manufacturer}", model="{model}", other="{other}", ' \ + 'sources="{sources}", source_list="{source_list}") >'.format(data=self.id, + ip=self.ip, + port=self.port, + pin=self.pin, + name=self.name, + location=self.location, + notes=self.notes, + pjlink_name=self.pjlink_name, + manufacturer=self.manufacturer, + model=self.model, + other=self.other, + sources=self.sources, + source_list=self.source_list) ip = Column(String(100)) port = Column(String(8)) pin = Column(String(20)) @@ -203,10 +215,11 @@ class ProjectorSource(CommonBase, Base): """ Return basic representation of Source table entry. """ - return '' % (self.id, - self.code, - self.text, - self.projector_id) + return ''.format(data=self.id, + code=self.code, + text=self.text, + projector_id=self.projector_id) code = Column(String(3)) text = Column(String(20)) projector_id = Column(Integer, ForeignKey('projector.id')) @@ -217,10 +230,10 @@ class ProjectorDB(Manager): Class to access the projector database. """ def __init__(self, *args, **kwargs): - log.debug('ProjectorDB().__init__(args="%s", kwargs="%s")' % (args, kwargs)) + log.debug('ProjectorDB().__init__(args="{arg}", kwargs="{kwarg}")'.format(arg=args, kwarg=kwargs)) super().__init__(plugin_name='projector', init_schema=self.init_schema) - log.debug('ProjectorDB() Initialized using db url %s' % self.db_url) - log.debug('Session: %s', self.session) + log.debug('ProjectorDB() Initialized using db url {db}'.format(db=self.db_url)) + log.debug('Session: {session}'.format(session=self.session)) def init_schema(self, *args, **kwargs): """ @@ -240,13 +253,14 @@ class ProjectorDB(Manager): :param dbid: DB record id :returns: Projector() instance """ - log.debug('get_projector_by_id(id="%s")' % dbid) + log.debug('get_projector_by_id(id="{data}")'.format(data=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) + log.warn('get_projector_by_id() did not find {data}'.format(data=id)) return None - log.debug('get_projectorby_id() returning 1 entry for "%s" id="%s"' % (dbid, projector.id)) + log.debug('get_projectorby_id() returning 1 entry for "{entry}" id="{data}"'.format(entry=dbid, + data=projector.id)) return projector def get_projector_all(self): @@ -262,7 +276,7 @@ class ProjectorDB(Manager): 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)) + log.debug('get_all() returning {items} item(s)'.format(items=len(return_list))) return return_list def get_projector_by_ip(self, ip): @@ -276,9 +290,10 @@ class ProjectorDB(Manager): 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) + log.warn('get_projector_by_ip() did not find {ip}'.format(ip=ip)) return None - log.debug('get_projectorby_ip() returning 1 entry for "%s" id="%s"' % (ip, projector.id)) + log.debug('get_projectorby_ip() returning 1 entry for "{ip}" id="{data}"'.format(ip=ip, + data=projector.id)) return projector def get_projector_by_name(self, name): @@ -288,13 +303,14 @@ class ProjectorDB(Manager): :param name: Name of projector :returns: Projector() instance """ - log.debug('get_projector_by_name(name="%s")' % name) + log.debug('get_projector_by_name(name="{name}")'.format(name=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) + log.warn('get_projector_by_name() did not find "{name}"'.format(name=name)) return None - log.debug('get_projector_by_name() returning one entry for "%s" id="%s"' % (name, projector.id)) + log.debug('get_projector_by_name() returning one entry for "{name}" id="{data}"'.format(name=name, + data=projector.id)) return projector def add_projector(self, projector): @@ -308,13 +324,13 @@ class ProjectorDB(Manager): """ 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) + log.warn('add_new() skipping entry ip="{ip}" (Already saved)'.format(ip=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) + log.debug('ip="{ip}", name="{name}", location="{location}"'.format(ip=projector.ip, + name=projector.name, + location=projector.location)) + log.debug('notes="{notes}"'.format(notes=projector.notes)) return self.save_object(projector) def update_projector(self, projector=None): @@ -333,7 +349,7 @@ class ProjectorDB(Manager): 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)) + log.debug('({ip}) Updating projector with dbid={dbid}'.format(ip=projector.ip, dbid=projector.id)) old_projector.ip = projector.ip old_projector.name = projector.name old_projector.location = projector.location @@ -357,9 +373,9 @@ class ProjectorDB(Manager): """ deleted = self.delete_object(Projector, projector.id) if deleted: - log.debug('delete_by_id() Removed entry id="%s"' % projector.id) + log.debug('delete_by_id() Removed entry id="{data}"'.format(data=projector.id)) else: - log.error('delete_by_id() Entry id="%s" not deleted for some reason' % projector.id) + log.error('delete_by_id() Entry id="{data}" not deleted for some reason'.format(data=projector.id)) return deleted def get_source_list(self, projector): @@ -395,9 +411,9 @@ class ProjectorDB(Manager): 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) + log.warn('get_source_by_id() did not find "{source}"'.format(source=source)) return None - log.debug('get_source_by_id() returning one entry for "%s""' % (source)) + log.debug('get_source_by_id() returning one entry for "{source}""'.format(source=source)) return source_entry def get_source_by_code(self, code, projector_id): @@ -411,11 +427,14 @@ class ProjectorDB(Manager): 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)) + log.warn('get_source_by_id() not found') + log.warn('code="{code}" projector_id="{data}"'.format(code=code, data=projector_id)) return None - log.debug('get_source_by_id() returning one entry for code="%s" projector_id="%s"' % (code, projector_id)) + log.debug('get_source_by_id() returning one entry') + log.debug('code="{code}" projector_id="{data}"'.format(code=code, data=projector_id)) return source_entry def add_source(self, source): @@ -424,6 +443,6 @@ class ProjectorDB(Manager): :param source: ProjectorSource() instance to add """ - log.debug('Saving ProjectorSource(projector_id="%s" code="%s" text="%s")' % (source.projector_id, - source.code, source.text)) + log.debug('Saving ProjectorSource(projector_id="{data}" ' + 'code="{code}" text="{text}")'.format(data=source.projector_id, code=source.code, text=source.text)) return self.save_object(source) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 4cdd31269..bbf2ccc64 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -91,7 +91,7 @@ class PJLink1(QTcpSocket): :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)) + log.debug('PJlink(args={args} kwargs={kwargs})'.format(args=args, kwargs=kwargs)) self.name = name self.ip = ip self.port = port @@ -147,7 +147,7 @@ class PJLink1(QTcpSocket): """ Reset projector-specific information to default """ - log.debug('(%s) reset_information() connect status is %s' % (self.ip, self.state())) + log.debug('({ip}) reset_information() connect status is {state}'.format(ip=self.ip, state=self.state())) self.power = S_OFF self.pjlink_name = None self.manufacturer = None @@ -170,7 +170,7 @@ class PJLink1(QTcpSocket): """ Connects signals to methods when thread is started. """ - log.debug('(%s) Thread starting' % self.ip) + log.debug('({ip}) Thread starting'.format(ip=self.ip)) self.i_am_running = True self.connected.connect(self.check_login) self.disconnected.connect(self.disconnect_from_host) @@ -180,7 +180,7 @@ class PJLink1(QTcpSocket): """ Cleanups when thread is stopped. """ - log.debug('(%s) Thread stopped' % self.ip) + log.debug('({ip}) Thread stopped'.format(ip=self.ip)) try: self.connected.disconnect(self.check_login) except TypeError: @@ -206,7 +206,7 @@ class PJLink1(QTcpSocket): 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) + log.debug('({ip}) socket_abort() - Killing connection'.format(ip=self.ip)) self.disconnect_from_host(abort=True) def poll_loop(self): @@ -216,7 +216,7 @@ class PJLink1(QTcpSocket): """ if self.state() != self.ConnectedState: return - log.debug('(%s) Updating projector status' % self.ip) + log.debug('({ip}) Updating projector status'.format(ip=self.ip)) # Reset timer in case we were called from a set command if self.timer.interval() < self.poll_time: # Reset timer to 5 seconds @@ -276,11 +276,17 @@ class PJLink1(QTcpSocket): 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)) + log.debug('({ip}) status_connect: {code}: "{message}"'.format(ip=self.ip, + code=status_code, + message=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)) + log.debug('({ip}) projector_status: {code}: "{message}"'.format(ip=self.ip, + code=status_code, + message=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)) + log.debug('({ip}) error_status: {code}: "{message}"'.format(ip=self.ip, + code=status_code, + message=status_message if msg is None else msg)) self.changeStatus.emit(self.ip, status, message) @pyqtSlot() @@ -291,27 +297,27 @@ class PJLink1(QTcpSocket): :param data: Optional data if called from another routine """ - log.debug('(%s) check_login(data="%s")' % (self.ip, data)) + log.debug('({ip}) check_login(data="{data}")'.format(ip=self.ip, data=data)) if data is None: # Reconnected setup? if not self.waitForReadyRead(2000): # Possible timeout issue - log.error('(%s) Socket timeout waiting for login' % self.ip) + log.error('({ip}) Socket timeout waiting for login'.format(ip=self.ip)) self.change_status(E_SOCKET_TIMEOUT) return read = self.readLine(self.maxSize) dontcare = self.readLine(self.maxSize) # Clean out the trailing \r\n if read is None: - log.warn('(%s) read is None - socket error?' % self.ip) + log.warn('({ip}) read is None - socket error?'.format(ip=self.ip)) return elif len(read) < 8: - log.warn('(%s) Not enough data read)' % self.ip) + log.warn('({ip}) Not enough data read)'.format(ip=self.ip)) return data = decode(read, 'ascii') # Possibility of extraneous data on input when reading. # Clean out extraneous characters in buffer. dontcare = self.readLine(self.maxSize) - log.debug('(%s) check_login() read "%s"' % (self.ip, data.strip())) + log.debug('({ip}) check_login() read "{data}"'.format(ip=self.ip, data=data.strip())) # At this point, we should only have the initial login prompt with # possible authentication # PJLink initial login will be: @@ -326,25 +332,25 @@ class PJLink1(QTcpSocket): else: # Process initial connection data_check = data.strip().split(' ') - log.debug('(%s) data_check="%s"' % (self.ip, data_check)) + log.debug('({ip}) data_check="{data}"'.format(ip=self.ip, data=data_check)) # Check for projector reporting an error if data_check[1].upper() == 'ERRA': # Authentication error self.disconnect_from_host() self.change_status(E_AUTHENTICATION) - log.debug('(%s) emitting projectorAuthentication() signal' % self.name) + log.debug('({ip}) emitting projectorAuthentication() signal'.format(ip=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) + log.debug('({ip}) emitting projectorNoAuthentication() signal'.format(ip=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)) + log.debug('({ip}) Setting hash with salt="{data}"'.format(ip=self.ip, data=data_check[2])) + log.debug('({ip}) pin="{data}"'.format(ip=self.ip, data=self.pin)) salt = qmd5_hash(salt=data_check[2].encode('ascii'), data=self.pin.encode('ascii')) else: salt = None @@ -355,7 +361,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) + log.debug('({ip}) Starting timer'.format(ip=self.ip)) self.timer.setInterval(2000) # Set 2 seconds for initial information self.timer.start() @@ -364,15 +370,15 @@ class PJLink1(QTcpSocket): """ Socket interface to retrieve data. """ - log.debug('(%s) get_data(): Reading data' % self.ip) + log.debug('({ip}) get_data(): Reading data'.format(ip=self.ip)) if self.state() != self.ConnectedState: - log.debug('(%s) get_data(): Not connected - returning' % self.ip) + log.debug('({ip}) get_data(): Not connected - returning'.format(ip=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) + log.debug('({ip}) get_data(): No data available (-1)'.format(ip=self.ip)) self.send_busy = False self.projectorReceivedData.emit() return @@ -382,11 +388,11 @@ class PJLink1(QTcpSocket): data = data_in.strip() if len(data) < 7: # Not enough data for a packet - log.debug('(%s) get_data(): Packet length < 7: "%s"' % (self.ip, data)) + log.debug('({ip}) get_data(): Packet length < 7: "{data}"'.format(ip=self.ip, data=data)) self.send_busy = False self.projectorReceivedData.emit() return - log.debug('(%s) get_data(): Checking new data "%s"' % (self.ip, data)) + log.debug('({ip}) get_data(): Checking new data "{data}"'.format(ip=self.ip, data=data)) if data.upper().startswith('PJLINK'): # Reconnected from remote host disconnect ? self.check_login(data) @@ -394,7 +400,7 @@ class PJLink1(QTcpSocket): self.projectorReceivedData.emit() return elif '=' not in data: - log.warn('(%s) get_data(): Invalid packet received' % self.ip) + log.warn('({ip}) get_data(): Invalid packet received'.format(ip=self.ip)) self.send_busy = False self.projectorReceivedData.emit() return @@ -402,15 +408,15 @@ class PJLink1(QTcpSocket): try: (prefix, class_, cmd, data) = (data_split[0][0], data_split[0][1], data_split[0][2:], data_split[1]) except ValueError as e: - log.warn('(%s) get_data(): Invalid packet - expected header + command + data' % self.ip) - log.warn('(%s) get_data(): Received data: "%s"' % (self.ip, read)) + log.warn('({ip}) get_data(): Invalid packet - expected header + command + data'.format(ip=self.ip)) + log.warn('({ip}) get_data(): Received data: "{data}"'.format(ip=self.ip, data=data_in.strip())) self.change_status(E_INVALID_DATA) self.send_busy = False self.projectorReceivedData.emit() return if not (self.pjlink_class in PJLINK_VALID_CMD and cmd in PJLINK_VALID_CMD[self.pjlink_class]): - log.warn('(%s) get_data(): Invalid packet - unknown command "%s"' % (self.ip, cmd)) + log.warn('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd)) self.send_busy = False self.projectorReceivedData.emit() return @@ -424,7 +430,7 @@ class PJLink1(QTcpSocket): :param err: Error code """ - log.debug('(%s) get_error(err=%s): %s' % (self.ip, err, self.errorString())) + log.debug('({ip}) get_error(err={error}): {data}'.format(ip=self.ip, error=err, data=self.errorString())) if err <= 18: # QSocket errors. Redefined in projector.constants so we don't mistake # them for system errors @@ -453,32 +459,35 @@ class PJLink1(QTcpSocket): :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) + log.warn('({ip}) send_command(): Not connected - returning'.format(ip=self.ip)) self.send_queue = [] return self.projectorNetwork.emit(S_NETWORK_SENDING) - log.debug('(%s) send_command(): Building cmd="%s" opts="%s" %s' % (self.ip, - cmd, - opts, - '' if salt is None else 'with hash')) - if salt is None: - out = '%s%s %s%s' % (PJLINK_HEADER, cmd, opts, CR) - else: - out = '%s%s%s %s%s' % (salt, PJLINK_HEADER, cmd, opts, CR) + log.debug('({ip}) send_command(): Building cmd="{command}" opts="{data}"{salt}'.format(ip=self.ip, + command=cmd, + data=opts, + salt='' if salt is None + else ' with hash')) + out = '{salt}{header}{command} {options}{suffix}'.format(salt="" if salt is None else salt, + header=PJLINK_HEADER, + command=cmd, + options=opts, + suffix=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('({ip}) send_command(out="{data}") Already in queue - skipping'.format(ip=self.ip, + data=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())) + log.debug('({ip}) send_command(out="{data}") Sending data'.format(ip=self.ip, data=out.strip())) return self._send_command(data=out) else: - log.debug('(%s) send_command(out="%s") adding to queue' % (self.ip, out.strip())) + log.debug('({ip}) send_command(out="{data}") adding to queue'.format(ip=self.ip, data=out.strip())) self.send_queue.append(out) self.projectorReceivedData.emit() - log.debug('(%s) send_command(): send_busy is %s' % (self.ip, self.send_busy)) + log.debug('({ip}) send_command(): send_busy is {data}'.format(ip=self.ip, data=self.send_busy)) if not self.send_busy: - log.debug('(%s) send_command() calling _send_string()') + log.debug('({ip}) send_command() calling _send_string()'.format(ip=self.ip)) self._send_command() @pyqtSlot() @@ -488,10 +497,10 @@ class PJLink1(QTcpSocket): :param data: Immediate data to send """ - log.debug('(%s) _send_string()' % self.ip) - log.debug('(%s) _send_string(): Connection status: %s' % (self.ip, self.state())) + log.debug('({ip}) _send_string()'.format(ip=self.ip)) + log.debug('({ip}) _send_string(): Connection status: {data}'.format(ip=self.ip, data=self.state())) if self.state() != self.ConnectedState: - log.debug('(%s) _send_string() Not connected - abort' % self.ip) + log.debug('({ip}) _send_string() Not connected - abort'.format(ip=self.ip)) self.send_queue = [] self.send_busy = False return @@ -500,18 +509,18 @@ class PJLink1(QTcpSocket): return if data is not None: out = data - log.debug('(%s) _send_string(data=%s)' % (self.ip, out.strip())) + log.debug('({ip}) _send_string(data="{data}")'.format(ip=self.ip, data=out.strip())) elif len(self.send_queue) != 0: out = self.send_queue.pop(0) - log.debug('(%s) _send_string(queued data=%s)' % (self.ip, out.strip())) + log.debug('({ip}) _send_string(queued data="{data}"%s)'.format(ip=self.ip, data=out.strip())) else: # No data to send - log.debug('(%s) _send_string(): No data to send' % self.ip) + log.debug('({ip}) _send_string(): No data to send'.format(ip=self.ip)) self.send_busy = False return self.send_busy = True - log.debug('(%s) _send_string(): Sending "%s"' % (self.ip, out.strip())) - log.debug('(%s) _send_string(): Queue = %s' % (self.ip, self.send_queue)) + log.debug('({ip}) _send_string(): Sending "{data}"'.format(ip=self.ip, data=out.strip())) + log.debug('({ip}) _send_string(): Queue = {data}'.format(ip=self.ip, data=self.send_queue)) self.socket_timer.start() self.projectorNetwork.emit(S_NETWORK_SENDING) sent = self.write(out.encode('ascii')) @@ -528,19 +537,21 @@ class PJLink1(QTcpSocket): :param cmd: Command to process :param data: Data being processed """ - log.debug('(%s) Processing command "%s"' % (self.ip, cmd)) + log.debug('({ip}) Processing command "{data}"'.format(ip=self.ip, data=cmd)) if data in PJLINK_ERRORS: # Oops - projector error + log.error('({ip}) Projector returned error "{data}"'.format(ip=self.ip, data=data)) if data.upper() == 'ERRA': # Authentication error self.disconnect_from_host() self.change_status(E_AUTHENTICATION) - log.debug('(%s) emitting projectorAuthentication() signal' % self.ip) + log.debug('({ip}) emitting projectorAuthentication() signal'.format(ip=self.ip)) self.projectorAuthentication.emit(self.name) elif data.upper() == 'ERR1': # Undefined command - self.change_status(E_UNDEFINED, '%s "%s"' % - (translate('OpenLP.PJLink1', 'Undefined command:'), cmd)) + self.change_status(E_UNDEFINED, '{error} "{data}"'.format(error=translate('OpenLP.PJLink1', + 'Undefined command:'), + data=cmd)) elif data.upper() == 'ERR2': # Invalid parameter self.change_status(E_PARAMETER) @@ -555,7 +566,7 @@ class PJLink1(QTcpSocket): return # Command succeeded - no extra information elif data.upper() == 'OK': - log.debug('(%s) Command returned OK' % self.ip) + log.debug('({ip}) Command returned OK'.format(ip=self.ip)) # A command returned successfully, recheck data self.send_busy = False self.projectorReceivedData.emit() @@ -564,7 +575,7 @@ class PJLink1(QTcpSocket): if cmd in self.PJLINK1_FUNC: self.PJLINK1_FUNC[cmd](data) else: - log.warn('(%s) Invalid command %s' % (self.ip, cmd)) + log.warn('({ip}) Invalid command {data}'.format(ip=self.ip, data=cmd)) self.send_busy = False self.projectorReceivedData.emit() @@ -583,7 +594,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('({ip}) process_lamp(): Invalid data "{data}"'.format(ip=self.ip, data=data)) return lamps.append(fill) data_dict.pop(0) # Remove lamp hours @@ -610,7 +621,7 @@ class PJLink1(QTcpSocket): self.send_command('INST') else: # Log unknown status response - log.warn('Unknown power response: %s' % data) + log.warn('({ip}) Unknown power response: {data}'.format(ip=self.ip, data=data)) return def process_avmt(self, data): @@ -635,7 +646,7 @@ class PJLink1(QTcpSocket): shutter = True mute = True else: - log.warn('Unknown shutter response: %s' % data) + log.warn('({ip}) Unknown shutter response: {data}'.format(ip=self.ip, data=data)) update_icons = shutter != self.shutter update_icons = update_icons or mute != self.mute self.shutter = shutter @@ -652,6 +663,7 @@ class PJLink1(QTcpSocket): :param data: Currently selected source """ self.source = data + log.info('({ip}) Setting data source to "{data}"'.format(ip=self.ip, data=self.source)) return def process_clss(self, data): @@ -670,7 +682,8 @@ class PJLink1(QTcpSocket): else: clss = data self.pjlink_class = clss - log.debug('(%s) Setting pjlink_class for this projector to "%s"' % (self.ip, self.pjlink_class)) + log.debug('({ip}) Setting pjlink_class for this projector to "{data}"'.format(ip=self.ip, + data=self.pjlink_class)) return def process_name(self, data): @@ -681,6 +694,7 @@ class PJLink1(QTcpSocket): :param data: Projector name """ self.pjlink_name = data + log.debug('({ip}) Setting projector PJLink name to "{data}"'.format(ip=self.ip, data=self.pjlink_name)) return def process_inf1(self, data): @@ -691,6 +705,7 @@ class PJLink1(QTcpSocket): :param data: Projector manufacturer """ self.manufacturer = data + log.debug('({ip}) Setting projector manufacturer data to "{data}"'.format(ip=self.ip, data=self.manufacturer)) return def process_inf2(self, data): @@ -701,6 +716,7 @@ class PJLink1(QTcpSocket): :param data: Model name """ self.model = data + log.debug('({ip}) Setting projector model to "{data}"'.format(ip=self.ip, data=self.model)) return def process_info(self, data): @@ -711,6 +727,7 @@ class PJLink1(QTcpSocket): :param data: Projector other info """ self.other_info = data + log.debug('({ip}) Setting projector other_info to "{data}"'.format(ip=self.ip, data=self.other_info)) return def process_inst(self, data): @@ -727,6 +744,8 @@ class PJLink1(QTcpSocket): sources.sort() self.source_available = sources self.projectorUpdateIcons.emit() + log.debug('({ip}) Setting projector sources_available to "{data}"'.format(ip=self.ip, + data=self.source_available)) return def process_erst(self, data): @@ -776,7 +795,7 @@ class PJLink1(QTcpSocket): Initiate connection to projector. """ if self.state() == self.ConnectedState: - log.warn('(%s) connect_to_host(): Already connected - returning' % self.ip) + log.warn('({ip}) connect_to_host(): Already connected - returning'.format(ip=self.ip)) return self.change_status(S_CONNECTING) self.connectToHost(self.ip, self.port if type(self.port) is int else int(self.port)) @@ -788,9 +807,9 @@ class PJLink1(QTcpSocket): """ if abort or self.state() != self.ConnectedState: if abort: - log.warn('(%s) disconnect_from_host(): Aborting connection' % self.ip) + log.warn('({ip}) disconnect_from_host(): Aborting connection'.format(ip=self.ip)) else: - log.warn('(%s) disconnect_from_host(): Not connected - returning' % self.ip) + log.warn('({ip}) disconnect_from_host(): Not connected - returning'.format(ip=self.ip)) self.reset_information() self.disconnectFromHost() try: @@ -800,8 +819,8 @@ class PJLink1(QTcpSocket): if abort: self.change_status(E_NOT_CONNECTED) else: - log.debug('(%s) disconnect_from_host() Current status %s' % (self.ip, - self._get_status(self.status_connect)[0])) + log.debug('({ip}) disconnect_from_host() ' + 'Current status {data}'.format(ip=self.ip, data=self._get_status(self.status_connect)[0])) if self.status_connect != E_NOT_CONNECTED: self.change_status(S_NOT_CONNECTED) self.reset_information() @@ -811,60 +830,70 @@ class PJLink1(QTcpSocket): """ Send command to retrieve available source inputs. """ + log.debug('({ip}) Sending INST command'.format(ip=self.ip)) return self.send_command(cmd='INST') def get_error_status(self): """ Send command to retrieve currently known errors. """ + log.debug('({ip}) Sending ERST command'.format(ip=self.ip)) return self.send_command(cmd='ERST') def get_input_source(self): """ Send command to retrieve currently selected source input. """ + log.debug('({ip}) Sending INPT command'.format(ip=self.ip)) return self.send_command(cmd='INPT') def get_lamp_status(self): """ Send command to return the lap status. """ + log.debug('({ip}) Sending LAMP command'.format(ip=self.ip)) return self.send_command(cmd='LAMP') def get_manufacturer(self): """ Send command to retrieve manufacturer name. """ + log.debug('({ip}) Sending INF1 command'.format(ip=self.ip)) return self.send_command(cmd='INF1') def get_model(self): """ Send command to retrieve the model name. """ + log.debug('({ip}) Sending INF2 command'.format(ip=self.ip)) return self.send_command(cmd='INF2') def get_name(self): """ Send command to retrieve name as set by end-user (if set). """ + log.debug('({ip}) Sending NAME command'.format(ip=self.ip)) return self.send_command(cmd='NAME') def get_other_info(self): """ Send command to retrieve extra info set by manufacturer. """ + log.debug('({ip}) Sending INFO command'.format(ip=self.ip)) return self.send_command(cmd='INFO') def get_power_status(self): """ Send command to retrieve power status. """ + log.debug('({ip}) Sending POWR command'.format(ip=self.ip)) return self.send_command(cmd='POWR') def get_shutter_status(self): """ Send command to retrieve shutter status. """ + log.debug('({ip}) Sending AVMT command'.format(ip=self.ip)) return self.send_command(cmd='AVMT') def set_input_source(self, src=None): @@ -874,12 +903,12 @@ class PJLink1(QTcpSocket): :param src: Video source to select in projector """ - log.debug('(%s) set_input_source(src=%s)' % (self.ip, src)) + log.debug('({ip}) set_input_source(src="{data}")'.format(ip=self.ip, data=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)) + log.debug('({ip}) Setting input source to "{data}"'.format(ip=self.ip, data=src)) self.send_command(cmd='INPT', opts=src) self.poll_loop() @@ -887,6 +916,7 @@ class PJLink1(QTcpSocket): """ Send command to turn power to on. """ + log.debug('({ip}) Setting POWR to 1 (on)'.format(ip=self.ip)) self.send_command(cmd='POWR', opts='1') self.poll_loop() @@ -894,6 +924,7 @@ class PJLink1(QTcpSocket): """ Send command to turn power to standby. """ + log.debug('({ip}) Setting POWR to 0 (standby)'.format(ip=self.ip)) self.send_command(cmd='POWR', opts='0') self.poll_loop() @@ -901,6 +932,7 @@ class PJLink1(QTcpSocket): """ Send command to set shutter to closed position. """ + log.debug('({ip}) Setting AVMT to 11 (shutter closed)'.format(ip=self.ip)) self.send_command(cmd='AVMT', opts='11') self.poll_loop() @@ -908,5 +940,6 @@ class PJLink1(QTcpSocket): """ Send command to set shutter to open position. """ + log.debug('({ip}) Setting AVMT to "10" (shutter open)'.format(ip=self.ip)) self.send_command(cmd='AVMT', opts='10') self.poll_loop() diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index 8410d4d28..e45aa6e61 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -610,7 +610,7 @@ class ServiceItem(RegistryProperties): str(datetime.timedelta(seconds=self.start_time)) if self.media_length != 0: end = translate('OpenLP.ServiceItem', 'Length: %s') % \ - str(datetime.timedelta(seconds=self.media_length)) + str(datetime.timedelta(seconds=self.media_length // 1000)) if not start and not end: return '' elif start and not end: diff --git a/openlp/core/ui/__init__.py b/openlp/core/ui/__init__.py index 82426539b..c6777c756 100644 --- a/openlp/core/ui/__init__.py +++ b/openlp/core/ui/__init__.py @@ -68,7 +68,6 @@ class DisplayControllerType(object): """ Live = 0 Preview = 1 - Plugin = 2 class SingleColumnTableWidget(QtWidgets.QTableWidget): diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index f32672f58..97e2d3617 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -29,8 +29,8 @@ import sys from PyQt5 import QtCore, QtGui, QtWidgets -from openlp.core.common import AppLocation, Settings, SlideLimits, UiStrings, translate, get_images_filter -from openlp.core.lib import ColorButton, SettingsTab, build_icon +from openlp.core.common import AppLocation, Settings, SlideLimits, UiStrings, translate +from openlp.core.lib import SettingsTab, build_icon from openlp.core.common.languagemanager import format_time log = logging.getLogger(__name__) @@ -45,10 +45,12 @@ class AdvancedTab(SettingsTab): """ Initialise the settings tab """ - self.default_image = ':/graphics/openlp-splash-screen.png' - self.default_color = '#ffffff' self.data_exists = False self.icon_path = ':/system/system_settings.png' + self.autoscroll_map = [None, {'dist': -1, 'pos': 0}, {'dist': -1, 'pos': 1}, {'dist': -1, 'pos': 2}, + {'dist': 0, 'pos': 0}, {'dist': 0, 'pos': 1}, {'dist': 0, 'pos': 2}, + {'dist': 0, 'pos': 3}, {'dist': 1, 'pos': 0}, {'dist': 1, 'pos': 1}, + {'dist': 1, 'pos': 2}, {'dist': 1, 'pos': 3}] advanced_translated = translate('OpenLP.AdvancedTab', 'Advanced') super(AdvancedTab, self).__init__(parent, 'Advanced', advanced_translated) @@ -90,6 +92,13 @@ class AdvancedTab(SettingsTab): self.slide_max_height_spin_box.setRange(0, 1000) self.slide_max_height_spin_box.setSingleStep(20) self.ui_layout.addRow(self.slide_max_height_label, self.slide_max_height_spin_box) + self.autoscroll_label = QtWidgets.QLabel(self.ui_group_box) + self.autoscroll_label.setObjectName('autoscroll_label') + self.autoscroll_combo_box = QtWidgets.QComboBox(self.ui_group_box) + self.autoscroll_combo_box.addItems(['', '', '', '', '', '', '', '', '', '', '', '']) + self.autoscroll_combo_box.setObjectName('autoscroll_combo_box') + self.ui_layout.addRow(self.autoscroll_label) + self.ui_layout.addRow(self.autoscroll_combo_box) self.search_as_type_check_box = QtWidgets.QCheckBox(self.ui_group_box) self.search_as_type_check_box.setObjectName('SearchAsType_check_box') self.ui_layout.addRow(self.search_as_type_check_box) @@ -180,33 +189,6 @@ class AdvancedTab(SettingsTab): self.data_directory_layout.addRow(self.new_data_directory_has_files_label) self.left_layout.addWidget(self.data_directory_group_box) self.left_layout.addStretch() - # Default Image - self.default_image_group_box = QtWidgets.QGroupBox(self.right_column) - self.default_image_group_box.setObjectName('default_image_group_box') - self.default_image_layout = QtWidgets.QFormLayout(self.default_image_group_box) - self.default_image_layout.setObjectName('default_image_layout') - self.default_color_label = QtWidgets.QLabel(self.default_image_group_box) - self.default_color_label.setObjectName('default_color_label') - self.default_color_button = ColorButton(self.default_image_group_box) - self.default_color_button.setObjectName('default_color_button') - self.default_image_layout.addRow(self.default_color_label, self.default_color_button) - self.default_file_label = QtWidgets.QLabel(self.default_image_group_box) - self.default_file_label.setObjectName('default_file_label') - self.default_file_edit = QtWidgets.QLineEdit(self.default_image_group_box) - self.default_file_edit.setObjectName('default_file_edit') - self.default_browse_button = QtWidgets.QToolButton(self.default_image_group_box) - self.default_browse_button.setObjectName('default_browse_button') - self.default_browse_button.setIcon(build_icon(':/general/general_open.png')) - self.default_revert_button = QtWidgets.QToolButton(self.default_image_group_box) - self.default_revert_button.setObjectName('default_revert_button') - self.default_revert_button.setIcon(build_icon(':/general/general_revert.png')) - self.default_file_layout = QtWidgets.QHBoxLayout() - self.default_file_layout.setObjectName('default_file_layout') - self.default_file_layout.addWidget(self.default_file_edit) - self.default_file_layout.addWidget(self.default_browse_button) - self.default_file_layout.addWidget(self.default_revert_button) - self.default_image_layout.addRow(self.default_file_label, self.default_file_layout) - self.right_layout.addWidget(self.default_image_group_box) # Hide mouse self.hide_mouse_group_box = QtWidgets.QGroupBox(self.right_column) self.hide_mouse_group_box.setObjectName('hide_mouse_group_box') @@ -253,9 +235,6 @@ class AdvancedTab(SettingsTab): self.service_name_time.timeChanged.connect(self.update_service_name_example) self.service_name_edit.textChanged.connect(self.update_service_name_example) self.service_name_revert_button.clicked.connect(self.on_service_name_revert_button_clicked) - self.default_color_button.colorChanged.connect(self.on_background_color_changed) - self.default_browse_button.clicked.connect(self.on_default_browse_button_clicked) - self.default_revert_button.clicked.connect(self.on_default_revert_button_clicked) self.alternate_rows_check_box.toggled.connect(self.on_alternate_rows_check_box_toggled) self.data_directory_browse_button.clicked.connect(self.on_data_directory_browse_button_clicked) self.data_directory_default_button.clicked.connect(self.on_data_directory_default_button_clicked) @@ -287,6 +266,31 @@ class AdvancedTab(SettingsTab): self.slide_max_height_label.setText(translate('OpenLP.AdvancedTab', 'Max height for non-text slides\nin slide controller:')) self.slide_max_height_spin_box.setSpecialValueText(translate('OpenLP.AdvancedTab', 'Disabled')) + self.autoscroll_label.setText(translate('OpenLP.AdvancedTab', + 'When changing slides:')) + self.autoscroll_combo_box.setItemText(0, translate('OpenLP.AdvancedTab', 'Do not auto-scroll')) + self.autoscroll_combo_box.setItemText(1, translate('OpenLP.AdvancedTab', + 'Auto-scroll the previous slide into view')) + self.autoscroll_combo_box.setItemText(2, translate('OpenLP.AdvancedTab', + 'Auto-scroll the previous slide to top')) + self.autoscroll_combo_box.setItemText(3, translate('OpenLP.AdvancedTab', + 'Auto-scroll the previous slide to middle')) + self.autoscroll_combo_box.setItemText(4, translate('OpenLP.AdvancedTab', + 'Auto-scroll the current slide into view')) + self.autoscroll_combo_box.setItemText(5, translate('OpenLP.AdvancedTab', + 'Auto-scroll the current slide to top')) + self.autoscroll_combo_box.setItemText(6, translate('OpenLP.AdvancedTab', + 'Auto-scroll the current slide to middle')) + self.autoscroll_combo_box.setItemText(7, translate('OpenLP.AdvancedTab', + 'Auto-scroll the current slide to bottom')) + self.autoscroll_combo_box.setItemText(8, translate('OpenLP.AdvancedTab', + 'Auto-scroll the next slide into view')) + self.autoscroll_combo_box.setItemText(9, translate('OpenLP.AdvancedTab', + 'Auto-scroll the next slide to top')) + self.autoscroll_combo_box.setItemText(10, translate('OpenLP.AdvancedTab', + 'Auto-scroll the next slide to middle')) + self.autoscroll_combo_box.setItemText(11, translate('OpenLP.AdvancedTab', + 'Auto-scroll the next slide to bottom')) self.enable_auto_close_check_box.setText(translate('OpenLP.AdvancedTab', 'Enable application exit confirmation')) self.service_name_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Default Service Name')) @@ -309,11 +313,6 @@ class AdvancedTab(SettingsTab): self.service_name_example_label.setText(translate('OpenLP.AdvancedTab', 'Example:')) self.hide_mouse_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Mouse Cursor')) self.hide_mouse_check_box.setText(translate('OpenLP.AdvancedTab', 'Hide mouse cursor when over display window')) - self.default_image_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Default Image')) - self.default_color_label.setText(translate('OpenLP.AdvancedTab', 'Background color:')) - self.default_file_label.setText(translate('OpenLP.AdvancedTab', 'Image file:')) - self.default_browse_button.setToolTip(translate('OpenLP.AdvancedTab', 'Browse for an image file to display.')) - self.default_revert_button.setToolTip(translate('OpenLP.AdvancedTab', 'Revert to the default OpenLP logo.')) self.data_directory_current_label.setText(translate('OpenLP.AdvancedTab', 'Current path:')) self.data_directory_new_label.setText(translate('OpenLP.AdvancedTab', 'Custom path:')) self.data_directory_browse_button.setToolTip(translate('OpenLP.AdvancedTab', @@ -357,6 +356,10 @@ class AdvancedTab(SettingsTab): self.single_click_service_preview_check_box.setChecked(settings.value('single click service preview')) self.expand_service_item_check_box.setChecked(settings.value('expand service item')) self.slide_max_height_spin_box.setValue(settings.value('slide max height')) + autoscroll_value = settings.value('autoscrolling') + for i in range(0, len(self.autoscroll_map)): + if self.autoscroll_map[i] == autoscroll_value: + self.autoscroll_combo_box.setCurrentIndex(i) self.enable_auto_close_check_box.setChecked(settings.value('enable exit confirmation')) self.hide_mouse_check_box.setChecked(settings.value('hide mouse')) self.service_name_day.setCurrentIndex(settings.value('default service day')) @@ -368,8 +371,6 @@ class AdvancedTab(SettingsTab): self.service_name_check_box.setChecked(default_service_enabled) self.service_name_check_box_toggled(default_service_enabled) self.x11_bypass_check_box.setChecked(settings.value('x11 bypass wm')) - self.default_color = settings.value('default color') - self.default_file_edit.setText(settings.value('default image')) self.slide_limits = settings.value('slide limits') self.is_search_as_you_type_enabled = settings.value('search as type') self.search_as_type_check_box.setChecked(self.is_search_as_you_type_enabled) @@ -411,7 +412,6 @@ class AdvancedTab(SettingsTab): self.current_data_path = AppLocation.get_data_path() log.warning('User requested data path set to default %s' % self.current_data_path) self.data_directory_label.setText(os.path.abspath(self.current_data_path)) - self.default_color_button.color = self.default_color # Don't allow data directory move if running portable. if settings.value('advanced/is portable'): self.data_directory_group_box.hide() @@ -440,11 +440,10 @@ class AdvancedTab(SettingsTab): settings.setValue('single click service preview', self.single_click_service_preview_check_box.isChecked()) settings.setValue('expand service item', self.expand_service_item_check_box.isChecked()) settings.setValue('slide max height', self.slide_max_height_spin_box.value()) + settings.setValue('autoscrolling', self.autoscroll_map[self.autoscroll_combo_box.currentIndex()]) settings.setValue('enable exit confirmation', self.enable_auto_close_check_box.isChecked()) settings.setValue('hide mouse', self.hide_mouse_check_box.isChecked()) settings.setValue('alternate rows', self.alternate_rows_check_box.isChecked()) - settings.setValue('default color', self.default_color) - settings.setValue('default image', self.default_file_edit.text()) settings.setValue('slide limits', self.slide_limits) if self.x11_bypass_check_box.isChecked() != settings.value('x11 bypass wm'): settings.setValue('x11 bypass wm', self.x11_bypass_check_box.isChecked()) @@ -522,24 +521,6 @@ class AdvancedTab(SettingsTab): self.service_name_edit.setText(UiStrings().DefaultServiceName) self.service_name_edit.setFocus() - def on_background_color_changed(self, color): - """ - Select the background colour of the default display screen. - """ - self.default_color = color - - def on_default_browse_button_clicked(self): - """ - Select an image for the default display screen. - """ - file_filters = '%s;;%s (*.*)' % (get_images_filter(), UiStrings().AllFiles) - filename, filter_used = QtWidgets.QFileDialog.getOpenFileName(self, - translate('OpenLP.AdvancedTab', 'Open File'), '', - file_filters) - if filename: - self.default_file_edit.setText(filename) - self.default_file_edit.setFocus() - def on_data_directory_browse_button_clicked(self): """ Browse for a new data directory location. @@ -657,13 +638,6 @@ class AdvancedTab(SettingsTab): self.data_directory_cancel_button.hide() self.new_data_directory_has_files_label.hide() - def on_default_revert_button_clicked(self): - """ - Revert the default screen back to the default settings. - """ - self.default_file_edit.setText(':/graphics/openlp-splash-screen.png') - self.default_file_edit.setFocus() - def on_alternate_rows_check_box_toggled(self, checked): """ Notify user about required restart. diff --git a/openlp/core/ui/firsttimewizard.py b/openlp/core/ui/firsttimewizard.py index 61b82780b..9f740e5cf 100644 --- a/openlp/core/ui/firsttimewizard.py +++ b/openlp/core/ui/firsttimewizard.py @@ -24,7 +24,7 @@ The UI widgets for the first time wizard. """ from PyQt5 import QtCore, QtGui, QtWidgets -from openlp.core.common import translate, is_macosx, clean_button_text +from openlp.core.common import translate, is_macosx, clean_button_text, Settings from openlp.core.lib import build_icon from openlp.core.lib.ui import add_welcome_page @@ -136,6 +136,13 @@ class UiFirstTimeWizard(object): self.alert_check_box.setChecked(True) self.alert_check_box.setObjectName('alert_check_box') self.plugin_layout.addWidget(self.alert_check_box) + self.projectors_check_box = QtWidgets.QCheckBox(self.plugin_page) + # If visibility setting for projector panel is True, check the box. + if Settings().value('projector/show after wizard'): + self.projectors_check_box.setChecked(True) + self.projectors_check_box.setObjectName('projectors_check_box') + self.projectors_check_box.clicked.connect(self.on_projectors_check_box_clicked) + self.plugin_layout.addWidget(self.projectors_check_box) first_time_wizard.setPage(FirstTimePage.Plugins, self.plugin_page) # The song samples page self.songs_page = QtWidgets.QWizardPage() @@ -232,17 +239,28 @@ class UiFirstTimeWizard(object): 'downloaded.')) self.download_label.setText(translate('OpenLP.FirstTimeWizard', 'Please wait while OpenLP downloads the ' 'resource index file...')) - self.plugin_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Activate required Plugins')) - self.plugin_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select the Plugins you wish to use. ')) + self.plugin_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Select parts of the program you wish to use')) + self.plugin_page.setSubTitle(translate('OpenLP.FirstTimeWizard', + 'You can also change these settings after the Wizard.')) self.songs_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Songs')) - self.custom_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Custom Slides')) - self.bible_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Bible')) - self.image_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Images')) - self.presentation_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Presentations')) - self.media_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Media (Audio and Video)')) - self.remote_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Allow remote access')) - self.song_usage_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Monitor Song Usage')) - self.alert_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Allow Alerts')) + self.custom_check_box.setText(translate('OpenLP.FirstTimeWizard', + 'Custom Slides – Easier to manage than songs and they have their own' + ' list of slides')) + self.bible_check_box.setText(translate('OpenLP.FirstTimeWizard', + 'Bibles – Import and show Bibles')) + self.image_check_box.setText(translate('OpenLP.FirstTimeWizard', + 'Images – Show images or replace background with them')) + self.presentation_check_box.setText(translate('OpenLP.FirstTimeWizard', + 'Presentations – Show .ppt, .odp and .pdf files')) + self.media_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Media – Playback of Audio and Video files')) + self.remote_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Remote – Control OpenLP via browser or smart' + 'phone app')) + self.song_usage_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Song Usage Monitor')) + self.alert_check_box.setText(translate('OpenLP.FirstTimeWizard', + 'Alerts – Display informative messages while showing other slides')) + self.projectors_check_box.setText(translate('OpenLP.FirstTimeWizard', + 'Projectors – Control PJLink compatible projects on your network' + ' from OpenLP')) self.no_internet_page.setTitle(translate('OpenLP.FirstTimeWizard', 'No Internet Connection')) self.no_internet_page.setSubTitle( translate('OpenLP.FirstTimeWizard', 'Unable to detect an Internet connection.')) @@ -277,3 +295,10 @@ class UiFirstTimeWizard(object): clean_button_text(first_time_wizard.buttonText(QtWidgets.QWizard.FinishButton))) first_time_wizard.setButtonText(QtWidgets.QWizard.CustomButton2, clean_button_text(first_time_wizard.buttonText(QtWidgets.QWizard.CancelButton))) + + def on_projectors_check_box_clicked(self): + # When clicking projectors_check box, change the visibility setting for Projectors panel. + if Settings().value('projector/show after wizard'): + Settings().setValue('projector/show after wizard', False) + else: + Settings().setValue('projector/show after wizard', True) diff --git a/openlp/core/ui/generaltab.py b/openlp/core/ui/generaltab.py index 8ed8b3edf..be2630b35 100644 --- a/openlp/core/ui/generaltab.py +++ b/openlp/core/ui/generaltab.py @@ -26,8 +26,8 @@ import logging from PyQt5 import QtCore, QtGui, QtWidgets -from openlp.core.common import Registry, Settings, UiStrings, translate -from openlp.core.lib import SettingsTab, ScreenList +from openlp.core.common import Registry, Settings, UiStrings, translate, get_images_filter +from openlp.core.lib import SettingsTab, ScreenList, ColorButton, build_icon log = logging.getLogger(__name__) @@ -40,6 +40,8 @@ class GeneralTab(SettingsTab): """ Initialise the general settings tab """ + self.logo_file = ':/graphics/openlp-splash-screen.png' + self.logo_background_color = '#ffffff' self.screens = ScreenList() self.icon_path = ':/icon/openlp-logo-16x16.png' general_translated = translate('OpenLP.GeneralTab', 'General') @@ -162,6 +164,39 @@ class GeneralTab(SettingsTab): self.check_for_updates_check_box.setVisible(False) self.startup_layout.addWidget(self.check_for_updates_check_box) self.right_layout.addWidget(self.startup_group_box) + # Logo + self.logo_group_box = QtWidgets.QGroupBox(self.right_column) + self.logo_group_box.setObjectName('logo_group_box') + self.logo_layout = QtWidgets.QFormLayout(self.logo_group_box) + self.logo_layout.setObjectName('logo_layout') + self.logo_file_label = QtWidgets.QLabel(self.logo_group_box) + self.logo_file_label.setObjectName('logo_file_label') + self.logo_file_edit = QtWidgets.QLineEdit(self.logo_group_box) + self.logo_file_edit.setObjectName('logo_file_edit') + self.logo_browse_button = QtWidgets.QToolButton(self.logo_group_box) + self.logo_browse_button.setObjectName('logo_browse_button') + self.logo_browse_button.setIcon(build_icon(':/general/general_open.png')) + self.logo_revert_button = QtWidgets.QToolButton(self.logo_group_box) + self.logo_revert_button.setObjectName('logo_revert_button') + self.logo_revert_button.setIcon(build_icon(':/general/general_revert.png')) + self.logo_file_layout = QtWidgets.QHBoxLayout() + self.logo_file_layout.setObjectName('logo_file_layout') + self.logo_file_layout.addWidget(self.logo_file_edit) + self.logo_file_layout.addWidget(self.logo_browse_button) + self.logo_file_layout.addWidget(self.logo_revert_button) + self.logo_layout.addRow(self.logo_file_label, self.logo_file_layout) + self.logo_color_label = QtWidgets.QLabel(self.logo_group_box) + self.logo_color_label.setObjectName('logo_color_label') + self.logo_color_button = ColorButton(self.logo_group_box) + self.logo_color_button.setObjectName('logo_color_button') + self.logo_layout.addRow(self.logo_color_label, self.logo_color_button) + self.logo_hide_on_startup_check_box = QtWidgets.QCheckBox(self.logo_group_box) + self.logo_hide_on_startup_check_box.setObjectName('logo_hide_on_startup_check_box') + self.logo_layout.addRow(self.logo_hide_on_startup_check_box) + self.right_layout.addWidget(self.logo_group_box) + self.logo_color_button.colorChanged.connect(self.on_logo_background_color_changed) + self.logo_browse_button.clicked.connect(self.on_logo_browse_button_clicked) + self.logo_revert_button.clicked.connect(self.on_logo_revert_button_clicked) # Application Settings self.settings_group_box = QtWidgets.QGroupBox(self.right_column) self.settings_group_box.setObjectName('settings_group_box') @@ -212,6 +247,12 @@ class GeneralTab(SettingsTab): self.warning_check_box.setText(translate('OpenLP.GeneralTab', 'Show blank screen warning')) self.auto_open_check_box.setText(translate('OpenLP.GeneralTab', 'Automatically open the last service')) self.show_splash_check_box.setText(translate('OpenLP.GeneralTab', 'Show the splash screen')) + self.logo_group_box.setTitle(translate('OpenLP.GeneralTab', 'Logo')) + self.logo_color_label.setText(UiStrings().BackgroundColorColon) + self.logo_file_label.setText(translate('OpenLP.GeneralTab', 'Logo file:')) + self.logo_browse_button.setToolTip(translate('OpenLP.GeneralTab', 'Browse for an image file to display.')) + self.logo_revert_button.setToolTip(translate('OpenLP.GeneralTab', 'Revert to the default OpenLP logo.')) + self.logo_hide_on_startup_check_box.setText(translate('OpenLP.GeneralTab', 'Don\'t show logo on startup')) self.check_for_updates_check_box.setText(translate('OpenLP.GeneralTab', 'Check for updates to OpenLP')) self.settings_group_box.setTitle(translate('OpenLP.GeneralTab', 'Application Settings')) self.save_check_service_check_box.setText(translate('OpenLP.GeneralTab', @@ -254,6 +295,10 @@ class GeneralTab(SettingsTab): self.warning_check_box.setChecked(settings.value('blank warning')) self.auto_open_check_box.setChecked(settings.value('auto open')) self.show_splash_check_box.setChecked(settings.value('show splash')) + self.logo_background_color = settings.value('logo background color') + self.logo_file_edit.setText(settings.value('logo file')) + self.logo_hide_on_startup_check_box.setChecked(settings.value('logo hide on startup')) + self.logo_color_button.color = self.logo_background_color self.check_for_updates_check_box.setChecked(settings.value('update check')) self.auto_preview_check_box.setChecked(settings.value('auto preview')) self.timeout_spin_box.setValue(settings.value('loop delay')) @@ -284,6 +329,9 @@ class GeneralTab(SettingsTab): settings.setValue('blank warning', self.warning_check_box.isChecked()) settings.setValue('auto open', self.auto_open_check_box.isChecked()) settings.setValue('show splash', self.show_splash_check_box.isChecked()) + settings.setValue('logo background color', self.logo_background_color) + settings.setValue('logo file', self.logo_file_edit.text()) + settings.setValue('logo hide on startup', self.logo_hide_on_startup_check_box.isChecked()) settings.setValue('update check', self.check_for_updates_check_box.isChecked()) settings.setValue('save prompt', self.save_check_service_check_box.isChecked()) settings.setValue('auto unblank', self.auto_unblank_check_box.isChecked()) @@ -346,3 +394,28 @@ class GeneralTab(SettingsTab): Called when the width, height, x position or y position has changed. """ self.display_changed = True + + def on_logo_browse_button_clicked(self): + """ + Select the logo file + """ + file_filters = '%s;;%s (*.*)' % (get_images_filter(), UiStrings().AllFiles) + filename, filter_used = QtWidgets.QFileDialog.getOpenFileName(self, + translate('OpenLP.AdvancedTab', 'Open File'), '', + file_filters) + if filename: + self.logo_file_edit.setText(filename) + self.logo_file_edit.setFocus() + + def on_logo_revert_button_clicked(self): + """ + Revert the logo file back to the default setting. + """ + self.logo_file_edit.setText(':/graphics/openlp-splash-screen.png') + self.logo_file_edit.setFocus() + + def on_logo_background_color_changed(self, color): + """ + Select the background color for logo. + """ + self.logo_background_color = color diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index 68c983d42..88aef818a 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -87,7 +87,7 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): height = self.viewport().width() // self.screen_ratio max_img_row_height = Settings().value('advanced/slide max height') # Adjust for row height cap if in use. - if max_img_row_height > 0 and height > max_img_row_height: + if isinstance(max_img_row_height, int) and max_img_row_height > 0 and height > max_img_row_height: height = max_img_row_height # Apply new height to slides for frame_number in range(len(self.service_item.get_frames())): @@ -98,7 +98,8 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): Will scale non-image slides. """ # Only for non-text slides when row height cap in use - if self.service_item.is_text() or Settings().value('advanced/slide max height') <= 0: + max_img_row_height = Settings().value('advanced/slide max height') + if self.service_item.is_text() or not isinstance(max_img_row_height, int) or max_img_row_height <= 0: return # Get and validate label widget containing slide & adjust max width try: @@ -160,9 +161,9 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): pixmap.setDevicePixelRatio(label.devicePixelRatio()) label.setPixmap(pixmap) slide_height = width // self.screen_ratio - # Setup row height cap if in use. + # Setup and validate row height cap if in use. max_img_row_height = Settings().value('advanced/slide max height') - if max_img_row_height > 0: + if isinstance(max_img_row_height, int) and max_img_row_height > 0: if slide_height > max_img_row_height: slide_height = max_img_row_height label.setMaximumWidth(max_img_row_height * self.screen_ratio) @@ -194,11 +195,22 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): """ Switches to the given row. """ - if slide >= self.slide_count(): - slide = self.slide_count() - 1 - # Scroll to next item if possible. - if slide + 1 < self.slide_count(): - self.scrollToItem(self.item(slide + 1, 0)) + # Retrieve setting + autoscrolling = Settings().value('advanced/autoscrolling') + # Check if auto-scroll disabled (None) and validate value as dict containing 'dist' and 'pos' + # 'dist' represents the slide to scroll to relative to the new slide (-1 = previous, 0 = current, 1 = next) + # 'pos' represents the vert position of of the slide (0 = in view, 1 = top, 2 = middle, 3 = bottom) + if not (isinstance(autoscrolling, dict) and 'dist' in autoscrolling and 'pos' in autoscrolling and + isinstance(autoscrolling['dist'], int) and isinstance(autoscrolling['pos'], int)): + return + # prevent scrolling past list bounds + scroll_to_slide = slide + autoscrolling['dist'] + if scroll_to_slide < 0: + scroll_to_slide = 0 + if scroll_to_slide >= self.slide_count(): + scroll_to_slide = self.slide_count() - 1 + # Scroll to item if possible. + self.scrollToItem(self.item(scroll_to_slide, 0), autoscrolling['pos']) self.selectRow(slide) def current_slide_number(self): diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index d9a9a6468..079235c2d 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -254,10 +254,10 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): if self.is_live: # Build the initial frame. background_color = QtGui.QColor() - background_color.setNamedColor(Settings().value('advanced/default color')) + background_color.setNamedColor(Settings().value('core/logo background color')) if not background_color.isValid(): background_color = QtCore.Qt.white - image_file = Settings().value('advanced/default image') + image_file = Settings().value('core/logo file') splash_image = QtGui.QImage(image_file) self.initial_fame = QtGui.QImage( self.screen['size'].width(), @@ -523,7 +523,9 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): if not Settings().value('core/display on monitor'): return self.frame.evaluateJavaScript('show_blank("show");') - if self.isHidden(): + # Check if setting for hiding logo on startup is enabled. + # If it is, display should remain hidden, otherwise logo is shown. (from def setup) + if self.isHidden() and not Settings().value('core/logo hide on startup'): self.setVisible(True) self.hide_mode = None # Trigger actions when display is active again. diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 228969ad1..ac3c19aa4 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -638,13 +638,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): self.open_cmd_line_files(self.arguments) elif Settings().value(self.general_settings_section + '/auto open'): self.service_manager_contents.load_last_file() + # This will store currently used layout preset so it remains enabled on next startup. + # If any panel is enabled/disabled after preset is set, this setting is not saved. view_mode = Settings().value('%s/view mode' % self.general_settings_section) - if view_mode == 'default': + if view_mode == 'default' and Settings().value('user interface/is preset layout'): self.mode_default_item.setChecked(True) - elif view_mode == 'setup': + elif view_mode == 'setup' and Settings().value('user interface/is preset layout'): self.set_view_mode(True, True, False, True, False, True) self.mode_setup_item.setChecked(True) - elif view_mode == 'live': + elif view_mode == 'live' and Settings().value('user interface/is preset layout'): self.set_view_mode(False, True, False, False, True, True) self.mode_live_item.setChecked(True) @@ -696,6 +698,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): return self.application.set_busy_cursor() self.first_time() + # Check if Projectors panel should be visible or not after wizard. + if Settings().value('projector/show after wizard'): + self.projector_manager_dock.setVisible(True) + else: + self.projector_manager_dock.setVisible(False) for plugin in self.plugin_manager.plugins: self.active_plugin = plugin old_status = self.active_plugin.status @@ -1027,18 +1034,24 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): Put OpenLP into "Default" view mode. """ self.set_view_mode(True, True, True, True, True, True, 'default') + Settings().setValue('user interface/is preset layout', True) + Settings().setValue('projector/show after wizard', True) def on_mode_setup_item_clicked(self): """ Put OpenLP into "Setup" view mode. """ self.set_view_mode(True, True, False, True, False, True, 'setup') + Settings().setValue('user interface/is preset layout', True) + Settings().setValue('projector/show after wizard', True) def on_mode_live_item_clicked(self): """ Put OpenLP into "Live" view mode. """ self.set_view_mode(False, True, False, False, True, True, 'live') + Settings().setValue('user interface/is preset layout', True) + Settings().setValue('projector/show after wizard', True) def set_view_mode(self, media=True, service=True, theme=True, preview=True, live=True, projector=True, mode=''): """ @@ -1176,24 +1189,33 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): Toggle the visibility of the media manager """ self.media_manager_dock.setVisible(not self.media_manager_dock.isVisible()) + Settings().setValue('user interface/is preset layout', False) def toggle_projector_manager(self): """ Toggle visibility of the projector manager """ self.projector_manager_dock.setVisible(not self.projector_manager_dock.isVisible()) + Settings().setValue('user interface/is preset layout', False) + # Check/uncheck checkbox on First time wizard based on visibility of this panel. + if not Settings().value('projector/show after wizard'): + Settings().setValue('projector/show after wizard', True) + else: + Settings().setValue('projector/show after wizard', False) def toggle_service_manager(self): """ Toggle the visibility of the service manager """ self.service_manager_dock.setVisible(not self.service_manager_dock.isVisible()) + Settings().setValue('user interface/is preset layout', False) def toggle_theme_manager(self): """ Toggle the visibility of the theme manager """ self.theme_manager_dock.setVisible(not self.theme_manager_dock.isVisible()) + Settings().setValue('user interface/is preset layout', False) def set_preview_panel_visibility(self, visible): """ @@ -1207,6 +1229,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): self.preview_controller.panel.setVisible(visible) Settings().setValue('user interface/preview panel', visible) self.view_preview_panel.setChecked(visible) + Settings().setValue('user interface/is preset layout', False) def set_lock_panel(self, lock): """ @@ -1217,6 +1240,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): self.service_manager_dock.setFeatures(QtWidgets.QDockWidget.NoDockWidgetFeatures) self.media_manager_dock.setFeatures(QtWidgets.QDockWidget.NoDockWidgetFeatures) self.projector_manager_dock.setFeatures(QtWidgets.QDockWidget.NoDockWidgetFeatures) + self.view_mode_menu.setEnabled(False) self.view_media_manager_item.setEnabled(False) self.view_service_manager_item.setEnabled(False) self.view_theme_manager_item.setEnabled(False) @@ -1228,6 +1252,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): self.service_manager_dock.setFeatures(QtWidgets.QDockWidget.AllDockWidgetFeatures) self.media_manager_dock.setFeatures(QtWidgets.QDockWidget.AllDockWidgetFeatures) self.projector_manager_dock.setFeatures(QtWidgets.QDockWidget.AllDockWidgetFeatures) + self.view_mode_menu.setEnabled(True) self.view_media_manager_item.setEnabled(True) self.view_service_manager_item.setEnabled(True) self.view_theme_manager_item.setEnabled(True) @@ -1248,6 +1273,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): self.live_controller.panel.setVisible(visible) Settings().setValue('user interface/live panel', visible) self.view_live_panel.setChecked(visible) + Settings().setValue('user interface/is preset layout', False) def load_settings(self): """ diff --git a/openlp/core/ui/media/__init__.py b/openlp/core/ui/media/__init__.py index ecd4b98bd..07e2a73fb 100644 --- a/openlp/core/ui/media/__init__.py +++ b/openlp/core/ui/media/__init__.py @@ -60,12 +60,14 @@ class MediaInfo(object): """ file_info = None volume = 100 - is_flash = False is_background = False + can_loop_playback = False length = 0 start_time = 0 end_time = 0 title_track = 0 + is_playing = False + timer = 1000 audio_track = 0 subtitle_track = 0 media_type = MediaType() @@ -104,15 +106,15 @@ def set_media_players(players_list, overridden_player='auto'): Settings().setValue('media/players', players) -def parse_optical_path(input): +def parse_optical_path(input_string): """ Split the optical path info. - :param input: The string to parse + :param input_string: The string to parse :return: The elements extracted from the string: filename, title, audio_track, subtitle_track, start, end """ - log.debug('parse_optical_path, about to parse: "%s"' % input) - clip_info = input.split(sep=':') + log.debug('parse_optical_path, about to parse: "%s"' % input_string) + clip_info = input_string.split(sep=':') title = int(clip_info[1]) audio_track = int(clip_info[2]) subtitle_track = int(clip_info[3]) diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 343ce0dd4..cf116e861 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -33,12 +33,15 @@ from openlp.core.lib import OpenLPToolbar, ItemCapabilities from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players,\ parse_optical_path +from openlp.core.ui.media.vendor.mediainfoWrapper import MediaInfoWrapper from openlp.core.ui.media.mediaplayer import MediaPlayer from openlp.core.common import AppLocation from openlp.core.ui import DisplayControllerType log = logging.getLogger(__name__) +TICK_TIME = 200 + class MediaSlider(QtWidgets.QSlider): """ @@ -51,10 +54,13 @@ class MediaSlider(QtWidgets.QSlider): super(MediaSlider, self).__init__(direction) self.manager = manager self.controller = controller + self.no_matching_player = translate('MediaPlugin.MediaItem', 'File %s not supported using player %s') def mouseMoveEvent(self, event): """ Override event to allow hover time to be displayed. + + :param event: The triggering event """ time_value = QtWidgets.QStyle.sliderValueFromPosition(self.minimum(), self.maximum(), event.x(), self.width()) self.setToolTip('%s' % datetime.timedelta(seconds=int(time_value / 1000))) @@ -63,12 +69,16 @@ class MediaSlider(QtWidgets.QSlider): def mousePressEvent(self, event): """ Mouse Press event no new functionality + + :param event: The triggering event """ QtWidgets.QSlider.mousePressEvent(self, event) def mouseReleaseEvent(self, event): """ Set the slider position when the mouse is clicked and released on the slider. + + :param event: The triggering event """ self.setValue(QtWidgets.QStyle.sliderValueFromPosition(self.minimum(), self.maximum(), event.x(), self.width())) QtWidgets.QSlider.mouseReleaseEvent(self, event) @@ -96,13 +106,17 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): self.display_controllers = {} self.current_media_players = {} # Timer for video state - self.timer = QtCore.QTimer() - self.timer.setInterval(200) + self.live_timer = QtCore.QTimer() + self.live_timer.setInterval(TICK_TIME) + self.preview_timer = QtCore.QTimer() + self.preview_timer.setInterval(TICK_TIME) # Signals - self.timer.timeout.connect(self.media_state) + self.live_timer.timeout.connect(self.media_state_live) + self.preview_timer.timeout.connect(self.media_state_preview) Registry().register_function('playbackPlay', self.media_play_msg) Registry().register_function('playbackPause', self.media_pause_msg) Registry().register_function('playbackStop', self.media_stop_msg) + Registry().register_function('playbackLoop', self.media_loop_msg) Registry().register_function('seek_slider', self.media_seek_msg) Registry().register_function('volume_slider', self.media_volume_msg) Registry().register_function('media_hide', self.media_hide) @@ -172,8 +186,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): log.warning('Failed to import %s on path %s', module_name, path) player_classes = MediaPlayer.__subclasses__() for player_class in player_classes: - player = player_class(self) - self.register_players(player) + self.register_players(player_class(self)) if not self.media_players: return False saved_players, overridden_player = get_media_players() @@ -188,31 +201,39 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): self._generate_extensions_lists() return True - def media_state(self): + def media_state_live(self): """ - Check if there is a running media Player and do updating stuff (e.g. update the UI) + Check if there is a running Live media Player and do updating stuff (e.g. update the UI) """ - if not list(self.current_media_players.keys()): - self.timer.stop() + display = self._define_display(self.display_controllers[DisplayControllerType.Live]) + if DisplayControllerType.Live in self.current_media_players: + self.current_media_players[DisplayControllerType.Live].resize(display) + self.current_media_players[DisplayControllerType.Live].update_ui(display) + self.tick(self.display_controllers[DisplayControllerType.Live]) + if self.current_media_players[DisplayControllerType.Live].get_live_state() is not MediaState.Playing: + self.live_timer.stop() else: - any_active = False - for source in list(self.current_media_players.keys()): - display = self._define_display(self.display_controllers[source]) - self.current_media_players[source].resize(display) - self.current_media_players[source].update_ui(display) - if self.current_media_players[source].state == MediaState.Playing: - any_active = True - # There are still any active players - no need to stop timer. - if any_active: - return - # no players are active anymore - for source in list(self.current_media_players.keys()): - if self.current_media_players[source].state != MediaState.Paused: - display = self._define_display(self.display_controllers[source]) - display.controller.seek_slider.setSliderPosition(0) - display.controller.mediabar.actions['playbackPlay'].setVisible(True) - display.controller.mediabar.actions['playbackPause'].setVisible(False) - self.timer.stop() + self.live_timer.stop() + self.media_stop(self.display_controllers[DisplayControllerType.Live]) + if self.display_controllers[DisplayControllerType.Live].media_info.can_loop_playback: + self.media_play(self.display_controllers[DisplayControllerType.Live], True) + + def media_state_preview(self): + """ + Check if there is a running Preview media Player and do updating stuff (e.g. update the UI) + """ + display = self._define_display(self.display_controllers[DisplayControllerType.Preview]) + if DisplayControllerType.Preview in self.current_media_players: + self.current_media_players[DisplayControllerType.Preview].resize(display) + self.current_media_players[DisplayControllerType.Preview].update_ui(display) + self.tick(self.display_controllers[DisplayControllerType.Preview]) + if self.current_media_players[DisplayControllerType.Preview].get_preview_state() is not MediaState.Playing: + self.preview_timer.stop() + else: + self.preview_timer.stop() + self.media_stop(self.display_controllers[DisplayControllerType.Preview]) + if self.display_controllers[DisplayControllerType.Preview].media_info.can_loop_playback: + self.media_play(self.display_controllers[DisplayControllerType.Preview], True) def get_media_display_css(self): """ @@ -274,6 +295,15 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): icon=':/slides/media_playback_stop.png', tooltip=translate('OpenLP.SlideController', 'Stop playing media.'), triggers=controller.send_to_plugins) + controller.mediabar.add_toolbar_action('playbackLoop', text='media_playback_loop', + icon=':/slides/media_playback_stop.png', checked=False, + tooltip=translate('OpenLP.SlideController', 'Loop playing media.'), + triggers=controller.send_to_plugins) + controller.position_label = QtWidgets.QLabel() + controller.position_label.setText(' 00:00 / 00:00') + controller.position_label.setToolTip(translate('OpenLP.SlideController', 'Video timer.')) + controller.position_label.setObjectName('position_label') + controller.mediabar.add_toolbar_widget(controller.position_label) # Build the seek_slider. controller.seek_slider = MediaSlider(QtCore.Qt.Horizontal, self, controller) controller.seek_slider.setMaximum(1000) @@ -297,6 +327,8 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): controller.mediabar.add_toolbar_widget(controller.volume_slider) controller.controller_layout.addWidget(controller.mediabar) controller.mediabar.setVisible(False) + if not controller.is_live: + controller.volume_slider.setEnabled(False) # Signals controller.seek_slider.valueChanged.connect(controller.send_to_plugins) controller.volume_slider.valueChanged.connect(controller.send_to_plugins) @@ -335,7 +367,8 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): if self.current_media_players[controller.controller_type] != self.media_players['webkit']: controller.display.set_transparency(False) - def resize(self, display, player): + @staticmethod + def resize(display, player): """ After Mainwindow changes or Splitter moved all related media widgets have to be resized @@ -353,7 +386,6 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): :param hidden: The player which is doing the playing :param video_behind_text: Is the video to be played behind text. """ - log.debug('video') is_valid = False controller = self.display_controllers[source] # stop running videos @@ -361,6 +393,8 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): controller.media_info = MediaInfo() controller.media_info.volume = controller.volume_slider.value() controller.media_info.is_background = video_behind_text + # background will always loop video. + controller.media_info.can_loop_playback = video_behind_text controller.media_info.file_info = QtCore.QFileInfo(service_item.get_frame_path()) display = self._define_display(controller) if controller.is_live: @@ -373,6 +407,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): controller) else: log.debug('video is not optical and live') + controller.media_info.length = service_item.media_length is_valid = self._check_file_type(controller, display, service_item) display.override['theme'] = '' display.override['video'] = True @@ -392,6 +427,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): controller) else: log.debug('video is not optical and preview') + controller.media_info.length = service_item.media_length is_valid = self._check_file_type(controller, display, service_item) if not is_valid: # Media could not be loaded correctly @@ -428,26 +464,22 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): :param service_item: The ServiceItem containing the details to be played. """ - controller = self.display_controllers[DisplayControllerType.Plugin] - log.debug('media_length') - # stop running videos - self.media_reset(controller) - controller.media_info = MediaInfo() - controller.media_info.volume = 0 - controller.media_info.file_info = QtCore.QFileInfo(service_item.get_frame_path()) - display = controller.preview_display - if not self._check_file_type(controller, display, service_item): + media_info = MediaInfo() + media_info.volume = 0 + media_info.file_info = QtCore.QFileInfo(service_item.get_frame_path()) + # display = controller.preview_display + suffix = '*.%s' % media_info.file_info.suffix().lower() + used_players = get_media_players()[0] + player = self.media_players[used_players[0]] + if suffix not in player.video_extensions_list and suffix not in player.audio_extensions_list: # Media could not be loaded correctly - critical_error_message_box(translate('MediaPlugin.MediaItem', 'Unsupported File'), - translate('MediaPlugin.MediaItem', 'Unsupported File')) + critical_error_message_box(translate('MediaPlugin.MediaItem', 'Unsupported Media File'), + translate('MediaPlugin.MediaItem', 'File %s not supported using player %s') % + (service_item.get_frame_path(), used_players[0])) return False - if not self.media_play(controller): - critical_error_message_box(translate('MediaPlugin.MediaItem', 'Unsupported File'), - translate('MediaPlugin.MediaItem', 'Unsupported File')) - return False - service_item.set_media_length(controller.media_info.length) - self.media_stop(controller) - log.debug('use %s controller' % self.current_media_players[controller.controller_type]) + media_data = MediaInfoWrapper.parse(service_item.get_frame_path()) + # duration returns in milli seconds + service_item.set_media_length(media_data.tracks[0].duration) return True def media_setup_optical(self, filename, title, audio_track, subtitle_track, start, end, display, controller): @@ -458,13 +490,12 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): :param title: The main/title track to play. :param audio_track: The audio track to play. :param subtitle_track: The subtitle track to play. - :param start: Start position in miliseconds. - :param end: End position in miliseconds. + :param start: Start position in milliseconds. + :param end: End position in milliseconds. :param display: The display to play the media. - :param controller: The media contraoller. - :return: True if setup succeded else False. + :param controller: The media controller. + :return: True if setup succeeded else False. """ - log.debug('media_setup_optical') if controller is None: controller = self.display_controllers[DisplayControllerType.Plugin] # stop running videos @@ -476,9 +507,9 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): controller.media_info.media_type = MediaType.CD else: controller.media_info.media_type = MediaType.DVD - controller.media_info.start_time = start / 1000 - controller.media_info.end_time = end / 1000 - controller.media_info.length = (end - start) / 1000 + controller.media_info.start_time = start // 1000 + controller.media_info.end_time = end // 1000 + controller.media_info.length = (end - start) // 1000 controller.media_info.title_track = title controller.media_info.audio_track = audio_track controller.media_info.subtitle_track = subtitle_track @@ -506,13 +537,13 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): controller.media_info.media_type = MediaType.DVD return True - def _check_file_type(self, controller, display, service_item): + @staticmethod + def _get_used_players(service_item): """ - Select the correct media Player type from the prioritized Player list + Find the player for a given service item - :param controller: First element is the controller which should be used - :param display: Which display to use - :param service_item: The ServiceItem containing the details to be played. + :param service_item: where the information is about the media and required player + :return: player description """ used_players = get_media_players()[0] # If no player, we can't play @@ -525,6 +556,17 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): used_players = default_player else: used_players = [service_item.processor.lower()] + return used_players + + def _check_file_type(self, controller, display, service_item): + """ + Select the correct media Player type from the prioritized Player list + + :param controller: First element is the controller which should be used + :param display: Which display to use + :param service_item: The ServiceItem containing the details to be played. + """ + used_players = self._get_used_players(service_item) if controller.media_info.file_info.isFile(): suffix = '*.%s' % controller.media_info.file_info.suffix().lower() for title in used_players: @@ -573,17 +615,15 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): :param msg: First element is the controller which should be used :param status: """ - log.debug('media_play_msg') self.media_play(msg[0], status) - def media_play(self, controller, status=True): + def media_play(self, controller, first_time=True): """ Responds to the request to play a loaded video :param controller: The controller to be played - :param status: + :param first_time: """ - log.debug('media_play') controller.seek_slider.blockSignals(True) controller.volume_slider.blockSignals(True) display = self._define_display(controller) @@ -595,35 +635,60 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): self.media_volume(controller, 0) else: self.media_volume(controller, controller.media_info.volume) - if status: + if first_time: if not controller.media_info.is_background: display.frame.evaluateJavaScript('show_blank("desktop");') self.current_media_players[controller.controller_type].set_visible(display, True) - # Flash needs to be played and will not AutoPlay - if controller.media_info.is_flash: - controller.mediabar.actions['playbackPlay'].setVisible(True) - controller.mediabar.actions['playbackPause'].setVisible(False) - else: - controller.mediabar.actions['playbackPlay'].setVisible(False) - controller.mediabar.actions['playbackPause'].setVisible(True) + controller.mediabar.actions['playbackPlay'].setVisible(False) + controller.mediabar.actions['playbackPause'].setVisible(True) controller.mediabar.actions['playbackStop'].setDisabled(False) - if controller.is_live: - if controller.hide_menu.defaultAction().isChecked() and not controller.media_info.is_background: - controller.hide_menu.defaultAction().trigger() - # Start Timer for ui updates - if not self.timer.isActive(): - self.timer.start() + if controller.is_live: + if controller.hide_menu.defaultAction().isChecked() and not controller.media_info.is_background: + controller.hide_menu.defaultAction().trigger() + # Start Timer for ui updates + if not self.live_timer.isActive(): + self.live_timer.start() + else: + # Start Timer for ui updates + if not self.preview_timer.isActive(): + self.preview_timer.start() controller.seek_slider.blockSignals(False) controller.volume_slider.blockSignals(False) + controller.media_info.is_playing = True + display = self._define_display(controller) + display.setVisible(True) return True + def tick(self, controller): + """ + Add a tick while the media is playing but only count if not paused + + :param controller: The Controller to be processed + """ + start_again = False + if controller.media_info.is_playing and controller.media_info.length > 0: + if controller.media_info.timer > controller.media_info.length: + self.media_stop(controller, True) + if controller.media_info.can_loop_playback: + start_again = True + controller.media_info.timer += TICK_TIME + seconds = controller.media_info.timer // 1000 + minutes = seconds // 60 + seconds %= 60 + total_seconds = controller.media_info.length // 1000 + total_minutes = total_seconds // 60 + total_seconds %= 60 + controller.position_label.setText(' %02d:%02d / %02d:%02d' % + (minutes, seconds, total_minutes, total_seconds)) + if start_again: + self.media_play(controller, True) + def media_pause_msg(self, msg): """ Responds to the request to pause a loaded video :param msg: First element is the controller which should be used """ - log.debug('media_pause_msg') self.media_pause(msg[0]) def media_pause(self, controller): @@ -632,12 +697,31 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): :param controller: The Controller to be paused """ - log.debug('media_pause') display = self._define_display(controller) - self.current_media_players[controller.controller_type].pause(display) - controller.mediabar.actions['playbackPlay'].setVisible(True) - controller.mediabar.actions['playbackStop'].setDisabled(False) - controller.mediabar.actions['playbackPause'].setVisible(False) + if controller.controller_type in self.current_media_players: + self.current_media_players[controller.controller_type].pause(display) + controller.mediabar.actions['playbackPlay'].setVisible(True) + controller.mediabar.actions['playbackStop'].setDisabled(False) + controller.mediabar.actions['playbackPause'].setVisible(False) + controller.media_info.is_playing = False + + def media_loop_msg(self, msg): + """ + Responds to the request to loop a loaded video + + :param msg: First element is the controller which should be used + """ + self.media_loop(msg[0]) + + @staticmethod + def media_loop(controller): + """ + Responds to the request to loop a loaded video + + :param controller: The controller that needs to be stopped + """ + controller.media_info.can_loop_playback = not controller.media_info.can_loop_playback + controller.mediabar.actions['playbackLoop'].setChecked(controller.media_info.can_loop_playback) def media_stop_msg(self, msg): """ @@ -645,25 +729,28 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): :param msg: First element is the controller which should be used """ - log.debug('media_stop_msg') self.media_stop(msg[0]) - def media_stop(self, controller): + def media_stop(self, controller, looping_background=False): """ Responds to the request to stop a loaded video :param controller: The controller that needs to be stopped + :param looping_background: The background is looping so do not blank. """ - log.debug('media_stop') display = self._define_display(controller) if controller.controller_type in self.current_media_players: - display.frame.evaluateJavaScript('show_blank("black");') + if not looping_background: + display.frame.evaluateJavaScript('show_blank("black");') self.current_media_players[controller.controller_type].stop(display) self.current_media_players[controller.controller_type].set_visible(display, False) controller.seek_slider.setSliderPosition(0) controller.mediabar.actions['playbackPlay'].setVisible(True) controller.mediabar.actions['playbackStop'].setDisabled(True) controller.mediabar.actions['playbackPause'].setVisible(False) + controller.media_info.is_playing = False + controller.media_info.timer = 1000 + controller.media_timer = 0 def media_volume_msg(self, msg): """ @@ -694,7 +781,6 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): :param msg: First element is the controller which should be used Second element is a list with the seek value as first element """ - log.debug('media_seek') controller = msg[0] seek_value = msg[1][0] self.media_seek(controller, seek_value) @@ -706,15 +792,15 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): :param controller: The controller to use. :param seek_value: The value to set. """ - log.debug('media_seek') display = self._define_display(controller) self.current_media_players[controller.controller_type].seek(display, seek_value) + controller.media_info.timer = seek_value def media_reset(self, controller): """ Responds to the request to reset a loaded video + :param controller: The controller to use. """ - log.debug('media_reset') self.set_controls_visible(controller, False) display = self._define_display(controller) if controller.controller_type in self.current_media_players: @@ -735,7 +821,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): return display = self._define_display(self.live_controller) if self.live_controller.controller_type in self.current_media_players and \ - self.current_media_players[self.live_controller.controller_type].state == MediaState.Playing: + self.current_media_players[self.live_controller.controller_type].get_live_state() == MediaState.Playing: self.current_media_players[self.live_controller.controller_type].pause(display) self.current_media_players[self.live_controller.controller_type].set_visible(display, False) @@ -753,7 +839,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): Registry().execute('live_display_hide', hide_mode) display = self._define_display(self.live_controller) if self.live_controller.controller_type in self.current_media_players and \ - self.current_media_players[self.live_controller.controller_type].state == MediaState.Playing: + self.current_media_players[self.live_controller.controller_type].get_live_state() == MediaState.Playing: self.current_media_players[self.live_controller.controller_type].pause(display) self.current_media_players[self.live_controller.controller_type].set_visible(display, False) @@ -770,22 +856,25 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): return display = self._define_display(self.live_controller) if self.live_controller.controller_type in self.current_media_players and \ - self.current_media_players[self.live_controller.controller_type].state != MediaState.Playing: + self.current_media_players[self.live_controller.controller_type].get_live_state() != \ + MediaState.Playing: if self.current_media_players[self.live_controller.controller_type].play(display): self.current_media_players[self.live_controller.controller_type].set_visible(display, True) # Start Timer for ui updates - if not self.timer.isActive(): - self.timer.start() + if not self.live_timer.isActive(): + self.live_timer.start() def finalise(self): """ Reset all the media controllers when OpenLP shuts down """ - self.timer.stop() + self.live_timer.stop() + self.preview_timer.stop() for controller in self.display_controllers: self.media_reset(self.display_controllers[controller]) - def _define_display(self, controller): + @staticmethod + def _define_display(controller): """ Extract the correct display for a given controller diff --git a/openlp/core/ui/media/mediaplayer.py b/openlp/core/ui/media/mediaplayer.py index b25916372..d9c7ad321 100644 --- a/openlp/core/ui/media/mediaplayer.py +++ b/openlp/core/ui/media/mediaplayer.py @@ -41,7 +41,7 @@ class MediaPlayer(RegistryProperties): self.is_active = False self.can_background = False self.can_folder = False - self.state = MediaState.Off + self.state = {0: MediaState.Off, 1: MediaState.Off} self.has_own_widget = False self.audio_extensions_list = [] self.video_extensions_list = [] @@ -55,12 +55,16 @@ class MediaPlayer(RegistryProperties): def setup(self, display): """ Create the related widgets for the current display + + :param display: The display to be updated. """ pass def load(self, display): """ Load a new media file and check if it is valid + + :param display: The display to be updated. """ return True @@ -68,54 +72,75 @@ class MediaPlayer(RegistryProperties): """ If the main display size or position is changed, the media widgets should also resized + + :param display: The display to be updated. """ pass def play(self, display): """ Starts playing of current Media File + + :param display: The display to be updated. """ pass def pause(self, display): """ Pause of current Media File + + :param display: The display to be updated. """ pass def stop(self, display): """ Stop playing of current Media File + + :param display: The display to be updated. """ pass - def volume(self, display, vol): + def volume(self, display, volume): """ Change volume of current Media File + + :param display: The display to be updated. + :param volume: The volume to set. """ pass def seek(self, display, seek_value): """ Change playing position of current Media File + + :param display: The display to be updated. + :param seek_value: The where to seek to. """ pass def reset(self, display): """ Remove the current loaded video + + :param display: The display to be updated. """ pass def set_visible(self, display, status): """ Show/Hide the media widgets + + :param display: The display to be updated. + :param status: The status to be set. """ pass def update_ui(self, display): """ Do some ui related stuff (e.g. update the seek slider) + + :param display: The display to be updated. """ pass @@ -142,3 +167,45 @@ class MediaPlayer(RegistryProperties): Returns Information about the player """ return '' + + def get_live_state(self): + """ + Get the state of the live player + :return: Live state + """ + return self.state[0] + + def set_live_state(self, state): + """ + Set the State of the Live player + :param state: State to be set + :return: None + """ + self.state[0] = state + + def get_preview_state(self): + """ + Get the state of the preview player + :return: Preview State + """ + return self.state[1] + + def set_preview_state(self, state): + """ + Set the state of the Preview Player + :param state: State to be set + :return: None + """ + self.state[1] = state + + def set_state(self, state, display): + """ + Set the State based on the display being processed + :param state: State to be set + :param display: Identify the Display type + :return: None + """ + if display.controller.is_live: + self.set_live_state(state) + else: + self.set_preview_state(state) diff --git a/openlp/core/ui/media/playertab.py b/openlp/core/ui/media/playertab.py index 3db5e06b4..ed34993ca 100644 --- a/openlp/core/ui/media/playertab.py +++ b/openlp/core/ui/media/playertab.py @@ -133,12 +133,16 @@ class PlayerTab(SettingsTab): def on_background_color_changed(self, color): """ Set the background color + + :param color: The color to be set. """ self.background_color = color def on_player_check_box_changed(self, check_state): """ Add or remove players depending on their status + + :param check_state: The requested status. """ player = self.sender().player_name if check_state == QtCore.Qt.Checked: diff --git a/openlp/core/ui/media/systemplayer.py b/openlp/core/ui/media/systemplayer.py index 79069f9c9..ad1907044 100644 --- a/openlp/core/ui/media/systemplayer.py +++ b/openlp/core/ui/media/systemplayer.py @@ -4,14 +4,7 @@ ############################################################################### # 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 # +# Copyright (c) 2008-2016 OpenLP Developers # # --------------------------------------------------------------------------- # # 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 # @@ -124,7 +117,8 @@ class SystemPlayer(MediaPlayer): def load(self, display): """ Load a video into the display - :param display: + + :param display: The display where the media is """ log.debug('load vid in System Controller') controller = display.controller @@ -141,93 +135,122 @@ class SystemPlayer(MediaPlayer): def resize(self, display): """ Resize the display - :param display: + + :param display: The display where the media is """ display.video_widget.resize(display.size()) def play(self, display): """ Play the current media item - :param display: + + :param display: The display where the media is """ log.info('Play the current item') controller = display.controller start_time = 0 - if display.media_player.state() != QtMultimedia.QMediaPlayer.PausedState and \ - controller.media_info.start_time > 0: - start_time = controller.media_info.start_time + if display.controller.is_live: + if self.get_live_state() != QtMultimedia.QMediaPlayer.PausedState and controller.media_info.start_time > 0: + start_time = controller.media_info.start_time + else: + if self.get_preview_state() != QtMultimedia.QMediaPlayer.PausedState and \ + controller.media_info.start_time > 0: + start_time = controller.media_info.start_time display.media_player.play() if start_time > 0: self.seek(display, controller.media_info.start_time * 1000) self.volume(display, controller.media_info.volume) display.media_player.durationChanged.connect(functools.partial(self.set_duration, controller)) - self.state = MediaState.Playing + self.set_state(MediaState.Playing, display) display.video_widget.raise_() return True def pause(self, display): """ Pause the current media item + + :param display: The display where the media is """ display.media_player.pause() - if display.media_player.state() == QtMultimedia.QMediaPlayer.PausedState: - self.state = MediaState.Paused + if display.controller.is_live: + if self.get_live_state() == QtMultimedia.QMediaPlayer.PausedState: + self.set_state(MediaState.Paused, display) + else: + if self.get_preview_state() == QtMultimedia.QMediaPlayer.PausedState: + self.set_state(MediaState.Paused, display) def stop(self, display): """ Stop the current media item + + :param display: The display where the media is """ - display.media_player.blockSignals(True) - display.media_player.durationChanged.disconnect() - display.media_player.blockSignals(False) display.media_player.stop() self.set_visible(display, False) - self.state = MediaState.Stopped + self.set_state(MediaState.Stopped, display) - def volume(self, display, vol): + def volume(self, display, volume): """ Set the volume + + :param display: The display where the media is + :param volume: The volume to be set """ if display.has_audio: - display.media_player.setVolume(vol) + display.media_player.setVolume(volume) def seek(self, display, seek_value): """ Go to a particular point in the current media item + + :param display: The display where the media is + :param seek_value: The where to seek to """ display.media_player.setPosition(seek_value) def reset(self, display): """ Reset the media player + + :param display: The display where the media is """ display.media_player.stop() display.media_player.setMedia(QtMultimedia.QMediaContent()) self.set_visible(display, False) display.video_widget.setVisible(False) - self.state = MediaState.Off + self.set_state(MediaState.Off, display) def set_visible(self, display, status): """ Set the visibility of the widget + + :param display: The display where the media is + :param status: The visibility status to be set """ if self.has_own_widget: display.video_widget.setVisible(status) @staticmethod def set_duration(controller, duration): - controller.media_info.length = int(duration / 1000) - controller.seek_slider.setMaximum(controller.media_info.length * 1000) + """ + + :param controller: the controller displaying the media + :param duration: how long is the media + :return: + """ + controller.seek_slider.setMaximum(controller.media_info.length) def update_ui(self, display): """ Update the UI + + :param display: The display where the media is """ if display.media_player.state() == QtMultimedia.QMediaPlayer.PausedState and self.state != MediaState.Paused: self.stop(display) controller = display.controller if controller.media_info.end_time > 0: - if display.media_player.position() > controller.media_info.end_time * 1000: + if display.media_player.position() > controller.media_info.end_time: self.stop(display) self.set_visible(display, False) if not controller.seek_slider.isSliderDown(): diff --git a/openlp/core/ui/media/vendor/mediainfoWrapper.py b/openlp/core/ui/media/vendor/mediainfoWrapper.py new file mode 100644 index 000000000..35f16667d --- /dev/null +++ b/openlp/core/ui/media/vendor/mediainfoWrapper.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# 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:`~openlp.core.ui.media.mediainfo` module contains code to run mediainfo on a media file and obtain +information related to the rwquested media. +""" +import json +import os +from subprocess import Popen +from tempfile import mkstemp + +import six +from bs4 import BeautifulSoup, NavigableString + +ENV_DICT = os.environ + + +class Track(object): + + def __getattribute__(self, name): + try: + return object.__getattribute__(self, name) + except: + pass + return None + + def __init__(self, xml_dom_fragment): + self.xml_dom_fragment = xml_dom_fragment + self.track_type = xml_dom_fragment.attrs['type'] + for el in self.xml_dom_fragment.children: + if not isinstance(el, NavigableString): + node_name = el.name.lower().strip().strip('_') + if node_name == 'id': + node_name = 'track_id' + node_value = el.string + other_node_name = "other_%s" % node_name + if getattr(self, node_name) is None: + setattr(self, node_name, node_value) + else: + if getattr(self, other_node_name) is None: + setattr(self, other_node_name, [node_value, ]) + else: + getattr(self, other_node_name).append(node_value) + + for o in [d for d in self.__dict__.keys() if d.startswith('other_')]: + try: + primary = o.replace('other_', '') + setattr(self, primary, int(getattr(self, primary))) + except: + for v in getattr(self, o): + try: + current = getattr(self, primary) + setattr(self, primary, int(v)) + getattr(self, o).append(current) + break + except: + pass + + def __repr__(self): + return "".format(self.track_id, self.track_type) + + def to_data(self): + data = {} + for k, v in six.iteritems(self.__dict__): + if k != 'xml_dom_fragment': + data[k] = v + return data + + +class MediaInfoWrapper(object): + + def __init__(self, xml): + self.xml_dom = xml + xml_types = (str,) # no unicode type in python3 + if isinstance(xml, xml_types): + self.xml_dom = MediaInfoWrapper.parse_xml_data_into_dom(xml) + + @staticmethod + def parse_xml_data_into_dom(xml_data): + return BeautifulSoup(xml_data, "xml") + + @staticmethod + def parse(filename, environment=ENV_DICT): + command = ["mediainfo", "-f", "--Output=XML", filename] + fileno_out, fname_out = mkstemp(suffix=".xml", prefix="media-") + fileno_err, fname_err = mkstemp(suffix=".err", prefix="media-") + fp_out = os.fdopen(fileno_out, 'r+b') + fp_err = os.fdopen(fileno_err, 'r+b') + p = Popen(command, stdout=fp_out, stderr=fp_err, env=environment) + p.wait() + fp_out.seek(0) + + xml_dom = MediaInfoWrapper.parse_xml_data_into_dom(fp_out.read()) + fp_out.close() + fp_err.close() + os.unlink(fname_out) + os.unlink(fname_err) + return MediaInfoWrapper(xml_dom) + + def _populate_tracks(self): + if self.xml_dom is None: + return + for xml_track in self.xml_dom.Mediainfo.File.find_all("track"): + self._tracks.append(Track(xml_track)) + + @property + def tracks(self): + if not hasattr(self, "_tracks"): + self._tracks = [] + if len(self._tracks) == 0: + self._populate_tracks() + return self._tracks + + def to_data(self): + data = {'tracks': []} + for track in self.tracks: + data['tracks'].append(track.to_data()) + return data + + def to_json(self): + return json.dumps(self.to_data()) diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index 99f8c37fd..9c2110e22 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -144,6 +144,9 @@ class VlcPlayer(MediaPlayer): def setup(self, display): """ Set up the media player + + :param display: The display where the media is + :return: """ vlc = get_vlc() display.vlc_widget = QtWidgets.QFrame(display) @@ -186,6 +189,9 @@ class VlcPlayer(MediaPlayer): def load(self, display): """ Load a video into VLC + + :param display: The display where the media is + :return: """ vlc = get_vlc() log.debug('load vid in Vlc Controller') @@ -214,18 +220,16 @@ class VlcPlayer(MediaPlayer): # parse the metadata of the file display.vlc_media.parse() self.volume(display, volume) - # We need to set media_info.length during load because we want - # to avoid start and stop the video twice. Once for real playback - # and once to just get media length. - # - # Media plugin depends on knowing media length before playback. - controller.media_info.length = int(display.vlc_media_player.get_media().get_duration() / 1000) return True def media_state_wait(self, display, media_state): """ Wait for the video to change its state Wait no longer than 60 seconds. (loading an iso file needs a long time) + + :param media_state: The state of the playing media + :param display: The display where the media is + :return: """ vlc = get_vlc() start = datetime.now() @@ -240,25 +244,40 @@ class VlcPlayer(MediaPlayer): def resize(self, display): """ Resize the player + + :param display: The display where the media is + :return: """ display.vlc_widget.resize(display.size()) def play(self, display): """ Play the current item + + :param display: The display where the media is + :return: """ vlc = get_vlc() controller = display.controller start_time = 0 log.debug('vlc play') - if self.state != MediaState.Paused and controller.media_info.start_time > 0: - start_time = controller.media_info.start_time + if display.controller.is_live: + if self.get_live_state() != MediaState.Paused and controller.media_info.start_time > 0: + start_time = controller.media_info.start_time + else: + if self.get_preview_state() != MediaState.Paused and controller.media_info.start_time > 0: + start_time = controller.media_info.start_time threading.Thread(target=display.vlc_media_player.play).start() if not self.media_state_wait(display, vlc.State.Playing): return False - if self.state != MediaState.Paused and controller.media_info.start_time > 0: - log.debug('vlc play, starttime set') - start_time = controller.media_info.start_time + if display.controller.is_live: + if self.get_live_state() != MediaState.Paused and controller.media_info.start_time > 0: + log.debug('vlc play, start time set') + start_time = controller.media_info.start_time + else: + if self.get_preview_state() != MediaState.Paused and controller.media_info.start_time > 0: + log.debug('vlc play, start time set') + start_time = controller.media_info.start_time log.debug('mediatype: ' + str(controller.media_info.media_type)) # Set tracks for the optical device if controller.media_info.media_type == MediaType.DVD: @@ -279,37 +298,45 @@ class VlcPlayer(MediaPlayer): log.debug('vlc play, starttime set: ' + str(controller.media_info.start_time)) start_time = controller.media_info.start_time controller.media_info.length = controller.media_info.end_time - controller.media_info.start_time - else: - controller.media_info.length = int(display.vlc_media_player.get_media().get_duration() / 1000) self.volume(display, controller.media_info.volume) if start_time > 0 and display.vlc_media_player.is_seekable(): - display.vlc_media_player.set_time(int(start_time * 1000)) - controller.seek_slider.setMaximum(controller.media_info.length * 1000) - self.state = MediaState.Playing + display.vlc_media_player.set_time(int(start_time)) + controller.seek_slider.setMaximum(controller.media_info.length) + self.set_state(MediaState.Playing, display) display.vlc_widget.raise_() return True def pause(self, display): """ Pause the current item + + :param display: The display where the media is + :return: """ vlc = get_vlc() if display.vlc_media.get_state() != vlc.State.Playing: return display.vlc_media_player.pause() if self.media_state_wait(display, vlc.State.Paused): - self.state = MediaState.Paused + self.set_state(MediaState.Paused, display) def stop(self, display): """ Stop the current item + + :param display: The display where the media is + :return: """ threading.Thread(target=display.vlc_media_player.stop).start() - self.state = MediaState.Stopped + self.set_state(MediaState.Stopped, display) def volume(self, display, vol): """ Set the volume + + :param vol: The volume to be sets + :param display: The display where the media is + :return: """ if display.has_audio: display.vlc_media_player.audio_set_volume(vol) @@ -317,6 +344,9 @@ class VlcPlayer(MediaPlayer): def seek(self, display, seek_value): """ Go to a particular position + + :param seek_value: The position of where a seek goes to + :param display: The display where the media is """ if display.controller.media_info.media_type == MediaType.CD \ or display.controller.media_info.media_type == MediaType.DVD: @@ -327,14 +357,19 @@ class VlcPlayer(MediaPlayer): def reset(self, display): """ Reset the player + + :param display: The display where the media is """ display.vlc_media_player.stop() display.vlc_widget.setVisible(False) - self.state = MediaState.Off + self.set_state(MediaState.Off, display) def set_visible(self, display, status): """ Set the visibility + + :param display: The display where the media is + :param status: The visibility status """ if self.has_own_widget: display.vlc_widget.setVisible(status) @@ -342,6 +377,8 @@ class VlcPlayer(MediaPlayer): def update_ui(self, display): """ Update the UI + + :param display: The display where the media is """ vlc = get_vlc() # Stop video if playback is finished. diff --git a/openlp/core/ui/media/webkitplayer.py b/openlp/core/ui/media/webkitplayer.py index 0a35fe085..cc8c7f55c 100644 --- a/openlp/core/ui/media/webkitplayer.py +++ b/openlp/core/ui/media/webkitplayer.py @@ -99,74 +99,6 @@ VIDEO_HTML = """ """ -FLASH_CSS = """ -#flash { - z-index:5; -} -""" - -FLASH_JS = """ - function getFlashMovieObject(movieName) - { - if (window.document[movieName]){ - return window.document[movieName]; - } - if (document.embeds && document.embeds[movieName]){ - return document.embeds[movieName]; - } - } - - function show_flash(state, path, volume, variable_value){ - var text = document.getElementById('flash'); - var flashMovie = getFlashMovieObject("OpenLPFlashMovie"); - var src = "src = 'file:///" + path + "'"; - var view_parm = " wmode='opaque'" + " width='100%%'" + " height='100%%'"; - var swf_parm = " name='OpenLPFlashMovie'" + " autostart='true' loop='false' play='true'" + - " hidden='false' swliveconnect='true' allowscriptaccess='always'" + " volume='" + volume + "'"; - - switch(state){ - case 'load': - text.innerHTML = ""; - flashMovie = getFlashMovieObject("OpenLPFlashMovie"); - flashMovie.Play(); - break; - case 'play': - flashMovie.Play(); - break; - case 'pause': - flashMovie.StopPlay(); - break; - case 'stop': - flashMovie.StopPlay(); - tempHtml = text.innerHTML; - text.innerHTML = ''; - text.innerHTML = tempHtml; - break; - case 'close': - flashMovie.StopPlay(); - text.innerHTML = ''; - break; - case 'length': - return flashMovie.TotalFrames(); - case 'current_time': - return flashMovie.CurrentFrame(); - case 'seek': -// flashMovie.GotoFrame(variable_value); - break; - case 'isEnded': - //TODO check flash end - return false; - case 'setVisible': - text.style.visibility = variable_value; - break; - } - } -""" - -FLASH_HTML = """ - -""" - VIDEO_EXT = ['*.3gp', '*.3gpp', '*.3g2', '*.3gpp2', '*.aac', '*.flv', '*.f4a', '*.f4b', '*.f4p', '*.f4v', '*.mov', '*.m4a', '*.m4b', '*.m4p', '*.m4v', '*.mkv', '*.mp4', '*.ogv', '*.webm', '*.mpg', '*.wmv', '*.mpeg', '*.avi', '*.swf'] @@ -198,23 +130,25 @@ class WebkitPlayer(MediaPlayer): """ background = QtGui.QColor(Settings().value('players/background color')).name() css = VIDEO_CSS % {'bgcolor': background} - return css + FLASH_CSS + return css def get_media_display_javascript(self): """ Add javascript functions to htmlbuilder """ - return VIDEO_JS + FLASH_JS + return VIDEO_JS def get_media_display_html(self): """ Add html code to htmlbuilder """ - return VIDEO_HTML + FLASH_HTML + return VIDEO_HTML def setup(self, display): """ Set up the player + + :param display: The display to be updated. """ display.web_view.resize(display.size()) display.web_view.raise_() @@ -235,6 +169,8 @@ class WebkitPlayer(MediaPlayer): def load(self, display): """ Load a video + + :param display: The display to be updated. """ log.debug('load vid in Webkit Controller') controller = display.controller @@ -249,132 +185,120 @@ class WebkitPlayer(MediaPlayer): else: loop = 'false' display.web_view.setVisible(True) - if controller.media_info.file_info.suffix() == 'swf': - controller.media_info.is_flash = True - js = 'show_flash("load","%s");' % (path.replace('\\', '\\\\')) - else: - js = 'show_video("load", "%s", %s, %s);' % (path.replace('\\', '\\\\'), str(vol), loop) + js = 'show_video("load", "%s", %s, %s);' % (path.replace('\\', '\\\\'), str(vol), loop) display.frame.evaluateJavaScript(js) return True def resize(self, display): """ Resize the player + + :param display: The display to be updated. """ display.web_view.resize(display.size()) def play(self, display): """ Play a video + + :param display: The display to be updated. """ controller = display.controller display.web_loaded = True - length = 0 start_time = 0 - if self.state != MediaState.Paused and controller.media_info.start_time > 0: - start_time = controller.media_info.start_time - self.set_visible(display, True) - if controller.media_info.is_flash: - display.frame.evaluateJavaScript('show_flash("play");') + if display.controller.is_live: + if self.get_live_state() != MediaState.Paused and controller.media_info.start_time > 0: + start_time = controller.media_info.start_time else: - display.frame.evaluateJavaScript('show_video("play");') + if self.get_preview_state() != MediaState.Paused and controller.media_info.start_time > 0: + start_time = controller.media_info.start_time + self.set_visible(display, True) + display.frame.evaluateJavaScript('show_video("play");') if start_time > 0: self.seek(display, controller.media_info.start_time * 1000) - # TODO add playing check and get the correct media length - controller.media_info.length = length - self.state = MediaState.Playing + self.set_state(MediaState.Playing, display) display.web_view.raise_() return True def pause(self, display): """ Pause a video + + :param display: The display to be updated. """ - controller = display.controller - if controller.media_info.is_flash: - display.frame.evaluateJavaScript('show_flash("pause");') - else: - display.frame.evaluateJavaScript('show_video("pause");') - self.state = MediaState.Paused + display.frame.evaluateJavaScript('show_video("pause");') + self.set_state(MediaState.Paused, display) def stop(self, display): """ Stop a video + + :param display: The display to be updated. """ - controller = display.controller - if controller.media_info.is_flash: - display.frame.evaluateJavaScript('show_flash("stop");') - else: - display.frame.evaluateJavaScript('show_video("stop");') - self.state = MediaState.Stopped + display.frame.evaluateJavaScript('show_video("stop");') + self.set_state(MediaState.Stopped, display) def volume(self, display, volume): """ Set the volume + + :param display: The display to be updated. + :param volume: The volume to be set. """ - controller = display.controller # 1.0 is the highest value if display.has_audio: vol = float(volume) / float(100) - if not controller.media_info.is_flash: - display.frame.evaluateJavaScript('show_video(null, null, %s);' % str(vol)) + display.frame.evaluateJavaScript('show_video(null, null, %s);' % str(vol)) def seek(self, display, seek_value): """ Go to a position in the video + + :param display: The display to be updated. + :param seek_value: The value to be set. """ - controller = display.controller - if controller.media_info.is_flash: - seek = seek_value - display.frame.evaluateJavaScript('show_flash("seek", null, null, "%s");' % seek) - else: - seek = float(seek_value) / 1000 - display.frame.evaluateJavaScript('show_video("seek", null, null, null, "%f");' % seek) + seek = float(seek_value) / 1000 + display.frame.evaluateJavaScript('show_video("seek", null, null, null, "%f");' % seek) def reset(self, display): """ Reset the player - """ - controller = display.controller - if controller.media_info.is_flash: - display.frame.evaluateJavaScript('show_flash("close");') - else: - display.frame.evaluateJavaScript('show_video("close");') - self.state = MediaState.Off - def set_visible(self, display, status): + :param display: The display to be updated. + """ + display.frame.evaluateJavaScript('show_video("close");') + self.set_state(MediaState.Off, display) + + def set_visible(self, display, visibility): """ Set the visibility + + :param display: The display to be updated. + :param visibility: The visibility to be set. """ - controller = display.controller - if status: + if visibility: is_visible = "visible" else: is_visible = "hidden" - if controller.media_info.is_flash: - display.frame.evaluateJavaScript('show_flash("setVisible", null, null, "%s");' % is_visible) - else: - display.frame.evaluateJavaScript('show_video("setVisible", null, null, null, "%s");' % is_visible) + display.frame.evaluateJavaScript('show_video("setVisible", null, null, null, "%s");' % is_visible) def update_ui(self, display): """ Update the UI + + :param display: The display to be updated. """ controller = display.controller - if controller.media_info.is_flash: - current_time = display.frame.evaluateJavaScript('show_flash("current_time");') - length = display.frame.evaluateJavaScript('show_flash("length");') - else: - if display.frame.evaluateJavaScript('show_video("isEnded");'): - self.stop(display) - current_time = display.frame.evaluateJavaScript('show_video("current_time");') - # check if conversion was ok and value is not 'NaN' - if current_time and current_time != float('inf'): - current_time = int(current_time * 1000) - length = display.frame.evaluateJavaScript('show_video("length");') - # check if conversion was ok and value is not 'NaN' - if length and length != float('inf'): - length = int(length * 1000) + if display.frame.evaluateJavaScript('show_video("isEnded");'): + self.stop(display) + current_time = display.frame.evaluateJavaScript('show_video("current_time");') + # check if conversion was ok and value is not 'NaN' + if current_time and current_time != float('inf'): + current_time = int(current_time * 1000) + length = display.frame.evaluateJavaScript('show_video("length");') + # check if conversion was ok and value is not 'NaN' + if length and length != float('inf'): + length = int(length * 1000) if current_time and length: controller.media_info.length = length controller.seek_slider.setMaximum(length) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index fd88e67ee..96ce82868 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -84,7 +84,7 @@ class DisplayController(QtWidgets.QWidget): super(DisplayController, self).__init__(parent) self.is_live = False self.display = None - self.controller_type = DisplayControllerType.Plugin + self.controller_type = None def send_to_plugins(self, *args): """ diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index d620a0f79..20143ddaa 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -147,6 +147,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): def update_lines_text(self, lines): """ Updates the lines on a page on the wizard + :param lines: then number of lines to be displayed """ self.main_line_count_label.setText( translate('OpenLP.ThemeForm', '(approximately %d lines per slide)') % int(lines)) @@ -186,6 +187,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): def on_current_id_changed(self, page_id): """ Detects Page changes and updates as appropriate. + :param page_id: current page number """ enabled = self.page(page_id) == self.area_position_page self.setOption(QtWidgets.QWizard.HaveCustomButton1, enabled) diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index 4a35f909c..3835056fb 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -111,6 +111,7 @@ class OpenLPWizard(QtWidgets.QWizard, RegistryProperties): def setupUi(self, image): """ Set up the wizard UI. + :param image: path to start up image """ self.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) self.setModal(True) @@ -210,6 +211,7 @@ class OpenLPWizard(QtWidgets.QWizard, RegistryProperties): def on_current_id_changed(self, page_id): """ Perform necessary functions depending on which wizard page is active. + :param page_id: current page number """ if self.with_progress_page and self.page(page_id) == self.progress_page: self.pre_wizard() @@ -221,6 +223,7 @@ class OpenLPWizard(QtWidgets.QWizard, RegistryProperties): def custom_page_changed(self, page_id): """ Called when changing to a page other than the progress page + :param page_id: current page number """ pass diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index eec4b310a..507196395 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -29,8 +29,8 @@ from openlp.core.common import Registry, RegistryProperties, AppLocation, Settin translate from openlp.core.lib import ItemCapabilities, MediaManagerItem, MediaType, ServiceItem, ServiceItemContext, \ build_icon, check_item_selected -from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box -from openlp.core.ui import DisplayController, Display, DisplayControllerType +from openlp.core.lib.ui import create_widget_action, critical_error_message_box, create_horizontal_adjusting_combo_box +from openlp.core.ui import DisplayControllerType from openlp.core.ui.media import get_media_players, set_media_players, parse_optical_path, format_milliseconds from openlp.core.common.languagemanager import get_locale_key from openlp.core.ui.media.vlcplayer import get_vlc @@ -78,19 +78,9 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): self.single_service_item = False self.has_search = True self.media_object = None - self.display_controller = DisplayController(self.parent()) - self.display_controller.controller_layout = QtWidgets.QVBoxLayout() - self.media_controller.register_controller(self.display_controller) - self.media_controller.set_controls_visible(self.display_controller, False) - self.display_controller.preview_display = Display(self.display_controller) - self.display_controller.preview_display.hide() - self.display_controller.preview_display.setGeometry(QtCore.QRect(0, 0, 300, 300)) - self.display_controller.preview_display.screen = {'size': self.display_controller.preview_display.geometry()} - self.display_controller.preview_display.setup() - self.media_controller.setup_display(self.display_controller.preview_display, False) + # self.display_controller = DisplayController(self.parent()) Registry().register_function('video_background_replaced', self.video_background_replaced) Registry().register_function('mediaitem_media_rebuild', self.rebuild_players) - Registry().register_function('config_screen_changed', self.display_setup) # Allow DnD from the desktop self.list_view.activateDnD() @@ -101,12 +91,17 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): """ self.on_new_prompt = translate('MediaPlugin.MediaItem', 'Select Media') self.replace_action.setText(UiStrings().ReplaceBG) + self.replace_action_context.setText(UiStrings().ReplaceBG) if 'webkit' in get_media_players()[0]: self.replace_action.setToolTip(UiStrings().ReplaceLiveBG) + self.replace_action_context.setToolTip(UiStrings().ReplaceLiveBG) else: self.replace_action.setToolTip(UiStrings().ReplaceLiveBGDisabled) + self.replace_action_context.setToolTip(UiStrings().ReplaceLiveBGDisabled) self.reset_action.setText(UiStrings().ResetBG) self.reset_action.setToolTip(UiStrings().ResetLiveBG) + self.reset_action_context.setText(UiStrings().ResetBG) + self.reset_action_context.setToolTip(UiStrings().ResetLiveBG) self.automatic = UiStrings().Automatic self.display_type_label.setText(translate('MediaPlugin.MediaItem', 'Use Player:')) @@ -155,6 +150,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): triggers=self.on_replace_click) if 'webkit' not in get_media_players()[0]: self.replace_action.setDisabled(True) + self.replace_action_context.setDisabled(True) self.reset_action = self.toolbar.add_toolbar_action('reset_action', icon=':/system/system_close.png', visible=False, triggers=self.on_reset_click) self.media_widget = QtWidgets.QWidget(self) @@ -173,7 +169,17 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): self.page_layout.addWidget(self.media_widget) self.display_type_combo_box.currentIndexChanged.connect(self.override_player_changed) - def override_player_changed(self, index): + def add_custom_context_actions(self): + create_widget_action(self.list_view, separator=True) + self.replace_action_context = create_widget_action( + self.list_view, text=UiStrings().ReplaceBG, icon=':/slides/slide_blank.png', + triggers=self.on_replace_click) + self.reset_action_context = create_widget_action( + self.list_view, text=UiStrings().ReplaceLiveBG, icon=':/system/system_close.png', + visible=False, triggers=self.on_reset_click) + + @staticmethod + def override_player_changed(index): """ The Player has been overridden @@ -191,12 +197,14 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): """ self.media_controller.media_reset(self.live_controller) self.reset_action.setVisible(False) + self.reset_action_context.setVisible(False) def video_background_replaced(self): """ Triggered by main display on change of serviceitem. """ self.reset_action.setVisible(False) + self.reset_action_context.setVisible(False) def on_replace_click(self): """ @@ -215,6 +223,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): service_item.add_from_command(path, name, CLAPPERBOARD) if self.media_controller.video(DisplayControllerType.Live, service_item, video_behind_text=True): self.reset_action.setVisible(True) + self.reset_action_context.setVisible(True) else: critical_error_message_box(UiStrings().LiveBGError, translate('MediaPlugin.MediaItem', @@ -273,16 +282,14 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): service_item.processor = self.display_type_combo_box.currentText() service_item.add_from_command(path, name, CLAPPERBOARD) # Only get start and end times if going to a service - if context == ServiceItemContext.Service: - # Start media and obtain the length - if not self.media_controller.media_length(service_item): - return False + if not self.media_controller.media_length(service_item): + return False service_item.add_capability(ItemCapabilities.CanAutoStartForLive) service_item.add_capability(ItemCapabilities.CanEditTitle) service_item.add_capability(ItemCapabilities.RequiresMedia) if Settings().value(self.settings_section + '/media auto start') == QtCore.Qt.Checked: service_item.will_auto_start = True - # force a non-existent theme + # force a non-existent theme service_item.theme = -1 return True @@ -305,12 +312,6 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): ' '.join(self.media_controller.video_extensions_list), ' '.join(self.media_controller.audio_extensions_list), UiStrings().AllFiles) - def display_setup(self): - """ - Setup media controller display. - """ - self.media_controller.setup_display(self.display_controller.preview_display, False) - def populate_display_types(self): """ Load the combobox with the enabled media players, allowing user to select a specific player if settings allow. @@ -385,16 +386,16 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): if item_name: self.list_view.addItem(item_name) - def get_list(self, type=MediaType.Audio): + def get_list(self, media_type=MediaType.Audio): """ Get the list of media, optional select media type. - :param type: Type to get, defaults to audio. + :param media_type: Type to get, defaults to audio. :return: The media list """ media = Settings().value(self.settings_section + '/media files') media.sort(key=lambda filename: get_locale_key(os.path.split(str(filename))[1])) - if type == MediaType.Audio: + if media_type == MediaType.Audio: extension = self.media_controller.audio_extensions_list else: extension = self.media_controller.video_extensions_list diff --git a/openlp/plugins/media/mediaplugin.py b/openlp/plugins/media/mediaplugin.py index a1e54a132..daeb4dc2c 100644 --- a/openlp/plugins/media/mediaplugin.py +++ b/openlp/plugins/media/mediaplugin.py @@ -60,13 +60,6 @@ class MediaPlugin(Plugin): """ Override the inherited initialise() method in order to upgrade the media before trying to load it """ - # FIXME: Remove after 2.2 release. - # This is needed to load the list of media from the config saved before the settings rewrite. - if self.media_item_class is not None: - loaded_list = Settings().get_files_from_config(self) - # Now save the list to the config using our Settings class. - if loaded_list: - Settings().setValue('%s/%s files' % (self.settings_section, self.name), loaded_list) super().initialise() def app_startup(self): diff --git a/openlp/plugins/songs/forms/songselectform.py b/openlp/plugins/songs/forms/songselectform.py index c5398fc0d..84ced5383 100755 --- a/openlp/plugins/songs/forms/songselectform.py +++ b/openlp/plugins/songs/forms/songselectform.py @@ -299,6 +299,7 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog): # Set up UI components self.view_button.setEnabled(False) self.search_button.setEnabled(False) + self.search_combobox.setEnabled(False) self.search_progress_bar.setMinimum(0) self.search_progress_bar.setMaximum(0) self.search_progress_bar.setValue(0) @@ -354,6 +355,7 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog): self.application.process_events() self.set_progress_visible(False) self.search_button.setEnabled(True) + self.search_combobox.setEnabled(True) self.application.process_events() def on_search_results_widget_selection_changed(self): diff --git a/resources/images/media_repeat.png b/resources/images/media_repeat.png new file mode 100644 index 000000000..78330449a Binary files /dev/null and b/resources/images/media_repeat.png differ diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index effe7321d..370473673 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -147,6 +147,7 @@ media_audio.png media_video.png media_optical.png + media_repeat.png slidecontroller_multimedia.png auto-start_active.png auto-start_inactive.png diff --git a/tests/functional/openlp_core_lib/test_projector_pjlink1.py b/tests/functional/openlp_core_lib/test_projector_pjlink1.py index 04198b717..5d0d26ceb 100644 --- a/tests/functional/openlp_core_lib/test_projector_pjlink1.py +++ b/tests/functional/openlp_core_lib/test_projector_pjlink1.py @@ -108,6 +108,23 @@ class TestPJLink(TestCase): mock_process_inpt.called_with('31', "process_inpt should have been called with 31") + @patch.object(pjlink_test, 'projectorReceivedData') + def projector_process_lamp_test(self, mock_projectorReceivedData): + """ + Test setting lamp on/off and hours + """ + # GIVEN: Test object + pjlink = pjlink_test + + # WHEN: Call process_command with lamp data + pjlink.process_command('LAMP', '22222 1') + + # THEN: Lamp should have been set with status=ON and hours=22222 + self.assertEquals(pjlink.lamp[0]['On'], True, + 'Lamp power status should have been set to TRUE') + self.assertEquals(pjlink.lamp[0]['Hours'], 22222, + 'Lamp hours should have been set to 22222') + @patch.object(pjlink_test, 'projectorReceivedData') def projector_process_multiple_lamp_test(self, mock_projectorReceivedData): """ diff --git a/tests/functional/openlp_core_ui/test_listpreviewwidget.py b/tests/functional/openlp_core_ui/test_listpreviewwidget.py index a222189e6..e9f99a8eb 100644 --- a/tests/functional/openlp_core_ui/test_listpreviewwidget.py +++ b/tests/functional/openlp_core_ui/test_listpreviewwidget.py @@ -130,12 +130,14 @@ class TestListPreviewWidget(TestCase): # WHEN: __recalculate_layout() is called (via resizeEvent) list_preview_widget.resizeEvent(None) + self.mocked_Settings_obj.value.return_value = None + list_preview_widget.resizeEvent(None) # THEN: resizeRowsToContents() should not be called, while setRowHeight() should be called # twice for each slide. self.assertEquals(mocked_resizeRowsToContents.call_count, 0, 'Should not be called') - self.assertEquals(mocked_setRowHeight.call_count, 4, 'Should be called twice for each slide') - calls = [call(0, 200), call(1, 200), call(0, 400), call(1, 400)] + self.assertEquals(mocked_setRowHeight.call_count, 6, 'Should be called 3 times for each slide') + calls = [call(0, 200), call(1, 200), call(0, 400), call(1, 400), call(0, 400), call(1, 400)] mocked_setRowHeight.assert_has_calls(calls) @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') @@ -236,6 +238,8 @@ class TestListPreviewWidget(TestCase): # WHEN: row_resized() is called list_preview_widget.row_resized(0, 100, 150) + self.mocked_Settings_obj.value.return_value = None + list_preview_widget.row_resized(0, 100, 150) # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should not be called self.assertEquals(mocked_cellWidget_child.setMaximumWidth.call_count, 0, 'Should not be called') @@ -273,3 +277,101 @@ class TestListPreviewWidget(TestCase): # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should be called mocked_cellWidget_child.setMaximumWidth.assert_called_once_with(150) + + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.selectRow') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.scrollToItem') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.item') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.slide_count') + def autoscroll_test_setting_invalid(self, mocked_slide_count, mocked_item, mocked_scrollToItem, mocked_selectRow): + """ + Test if 'advanced/autoscrolling' setting None or invalid, that no autoscrolling occurs on change_slide(). + """ + # GIVEN: A setting for autoscrolling and a ListPreviewWidget. + # Mock Settings().value('advanced/autoscrolling') + self.mocked_Settings_obj.value.return_value = None + # Mocked returns + mocked_slide_count.return_value = 1 + mocked_item.return_value = None + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + + # WHEN: change_slide() is called + list_preview_widget.change_slide(0) + self.mocked_Settings_obj.value.return_value = 1 + list_preview_widget.change_slide(0) + self.mocked_Settings_obj.value.return_value = {'fail': 1} + list_preview_widget.change_slide(0) + self.mocked_Settings_obj.value.return_value = {'dist': 1, 'fail': 1} + list_preview_widget.change_slide(0) + self.mocked_Settings_obj.value.return_value = {'dist': 'fail', 'pos': 1} + list_preview_widget.change_slide(0) + self.mocked_Settings_obj.value.return_value = {'dist': 1, 'pos': 'fail'} + list_preview_widget.change_slide(0) + + # THEN: no further functions should be called + self.assertEquals(mocked_slide_count.call_count, 0, 'Should not be called') + self.assertEquals(mocked_scrollToItem.call_count, 0, 'Should not be called') + self.assertEquals(mocked_selectRow.call_count, 0, 'Should not be called') + self.assertEquals(mocked_item.call_count, 0, 'Should not be called') + + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.selectRow') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.scrollToItem') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.item') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.slide_count') + def autoscroll_test_dist_bounds(self, mocked_slide_count, mocked_item, mocked_scrollToItem, mocked_selectRow): + """ + Test if 'advanced/autoscrolling' setting asks to scroll beyond list bounds, that it does not beyond. + """ + # GIVEN: A setting for autoscrolling and a ListPreviewWidget. + # Mock Settings().value('advanced/autoscrolling') + self.mocked_Settings_obj.value.return_value = {'dist': -1, 'pos': 1} + # Mocked returns + mocked_slide_count.return_value = 1 + mocked_item.return_value = None + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + + # WHEN: change_slide() is called + list_preview_widget.change_slide(0) + self.mocked_Settings_obj.value.return_value = {'dist': 1, 'pos': 1} + list_preview_widget.change_slide(0) + + # THEN: no further functions should be called + self.assertEquals(mocked_slide_count.call_count, 3, 'Should be called') + self.assertEquals(mocked_scrollToItem.call_count, 2, 'Should be called') + self.assertEquals(mocked_selectRow.call_count, 2, 'Should be called') + self.assertEquals(mocked_item.call_count, 2, 'Should be called') + calls = [call(0, 0), call(0, 0)] + mocked_item.assert_has_calls(calls) + + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.selectRow') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.scrollToItem') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.item') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.slide_count') + def autoscroll_test_normal(self, mocked_slide_count, mocked_item, mocked_scrollToItem, mocked_selectRow): + """ + Test if 'advanced/autoscrolling' setting valid, autoscrolling called as expected. + """ + # GIVEN: A setting for autoscrolling and a ListPreviewWidget. + # Mock Settings().value('advanced/autoscrolling') + self.mocked_Settings_obj.value.return_value = {'dist': -1, 'pos': 1} + # Mocked returns + mocked_slide_count.return_value = 3 + mocked_item.return_value = None + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + + # WHEN: change_slide() is called + list_preview_widget.change_slide(1) + self.mocked_Settings_obj.value.return_value = {'dist': 0, 'pos': 1} + list_preview_widget.change_slide(1) + self.mocked_Settings_obj.value.return_value = {'dist': 1, 'pos': 1} + list_preview_widget.change_slide(1) + + # THEN: no further functions should be called + self.assertEquals(mocked_slide_count.call_count, 3, 'Should be called') + self.assertEquals(mocked_scrollToItem.call_count, 3, 'Should be called') + self.assertEquals(mocked_selectRow.call_count, 3, 'Should be called') + self.assertEquals(mocked_item.call_count, 3, 'Should be called') + calls = [call(0, 0), call(1, 0), call(2, 0)] + mocked_item.assert_has_calls(calls) diff --git a/tests/functional/openlp_core_ui/test_maindisplay.py b/tests/functional/openlp_core_ui/test_maindisplay.py index 9c80740f2..fc7ae4910 100644 --- a/tests/functional/openlp_core_ui/test_maindisplay.py +++ b/tests/functional/openlp_core_ui/test_maindisplay.py @@ -26,7 +26,7 @@ from unittest import TestCase, skipUnless from PyQt5 import QtCore -from openlp.core.common import Registry, is_macosx +from openlp.core.common import Registry, is_macosx, Settings from openlp.core.lib import ScreenList from openlp.core.ui import MainDisplay from openlp.core.ui.maindisplay import TRANSPARENT_STYLESHEET, OPAQUE_STYLESHEET @@ -183,3 +183,43 @@ class TestMainDisplay(TestCase, TestMixin): 'Window level should be NSMainMenuWindowLevel + 2') self.assertEqual(pyobjc_nsview.window().collectionBehavior(), NSWindowCollectionBehaviorManaged, 'Window collection behavior should be NSWindowCollectionBehaviorManaged') + + @patch(u'openlp.core.ui.maindisplay.Settings') + def show_display_startup_logo_test(self, MockedSettings): + # GIVEN: Mocked show_display, setting for logo visibility + display = MagicMock() + main_display = MainDisplay(display) + main_display.frame = MagicMock() + main_display.isHidden = MagicMock() + main_display.isHidden.return_value = True + main_display.setVisible = MagicMock() + mocked_settings = MagicMock() + mocked_settings.value.return_value = False + MockedSettings.return_value = mocked_settings + main_display.shake_web_view = MagicMock() + + # WHEN: show_display is called. + main_display.show_display() + + # THEN: setVisible should had been called with "True" + main_display.setVisible.assert_called_once_with(True) + + @patch(u'openlp.core.ui.maindisplay.Settings') + def show_display_hide_startup_logo_test(self, MockedSettings): + # GIVEN: Mocked show_display, setting for logo visibility + display = MagicMock() + main_display = MainDisplay(display) + main_display.frame = MagicMock() + main_display.isHidden = MagicMock() + main_display.isHidden.return_value = False + main_display.setVisible = MagicMock() + mocked_settings = MagicMock() + mocked_settings.value.return_value = False + MockedSettings.return_value = mocked_settings + main_display.shake_web_view = MagicMock() + + # WHEN: show_display is called. + main_display.show_display() + + # THEN: setVisible should had not been called + main_display.setVisible.assert_not_called() diff --git a/tests/functional/openlp_core_ui/test_mainwindow.py b/tests/functional/openlp_core_ui/test_mainwindow.py index a49ded25c..8a8b2516c 100644 --- a/tests/functional/openlp_core_ui/test_mainwindow.py +++ b/tests/functional/openlp_core_ui/test_mainwindow.py @@ -26,6 +26,8 @@ import os from unittest import TestCase +from PyQt5 import QtWidgets + from openlp.core.ui.mainwindow import MainWindow from openlp.core.lib.ui import UiStrings from openlp.core.common.registry import Registry @@ -148,7 +150,7 @@ class TestMainWindow(TestCase, TestMixin): # THEN: the following registry functions should have been registered self.assertEqual(len(self.registry.service_list), 6, 'The registry should have 6 services.') - self.assertEqual(len(self.registry.functions_list), 16, 'The registry should have 16 functions') + self.assertEqual(len(self.registry.functions_list), 17, 'The registry should have 17 functions') self.assertTrue('application' in self.registry.service_list, 'The application should have been registered.') self.assertTrue('main_window' in self.registry.service_list, 'The main_window should have been registered.') self.assertTrue('media_controller' in self.registry.service_list, 'The media_controller should have been ' @@ -189,3 +191,57 @@ class TestMainWindow(TestCase, TestMixin): # THEN: The media manager dock is made visible self.assertEqual(0, mocked_media_manager_dock.setVisible.call_count) mocked_widget.on_focus.assert_called_with() + + @patch('openlp.core.ui.mainwindow.MainWindow.plugin_manager') + @patch('openlp.core.ui.mainwindow.MainWindow.first_time') + @patch('openlp.core.ui.mainwindow.MainWindow.application') + @patch('openlp.core.ui.mainwindow.FirstTimeForm') + @patch('openlp.core.ui.mainwindow.QtWidgets.QMessageBox.warning') + @patch('openlp.core.ui.mainwindow.Settings') + def on_first_time_wizard_clicked_show_projectors_after_test(self, mocked_Settings, mocked_warning, + mocked_FirstTimeForm, mocked_application, + mocked_first_time, + mocked_plugin_manager): + # GIVEN: Main_window, patched things, patched "Yes" as confirmation to re-run wizard, settings to True. + mocked_Settings_obj = MagicMock() + mocked_Settings_obj.value.return_value = True + mocked_Settings.return_value = mocked_Settings_obj + mocked_warning.return_value = QtWidgets.QMessageBox.Yes + mocked_FirstTimeForm_obj = MagicMock() + mocked_FirstTimeForm_obj.was_cancelled = False + mocked_FirstTimeForm.return_value = mocked_FirstTimeForm_obj + mocked_plugin_manager.plugins = [] + self.main_window.projector_manager_dock = MagicMock() + + # WHEN: on_first_time_wizard_clicked is called + self.main_window.on_first_time_wizard_clicked() + + # THEN: projector_manager_dock.setVisible should had been called once + self.main_window.projector_manager_dock.setVisible.assert_called_once_with(True) + + @patch('openlp.core.ui.mainwindow.MainWindow.plugin_manager') + @patch('openlp.core.ui.mainwindow.MainWindow.first_time') + @patch('openlp.core.ui.mainwindow.MainWindow.application') + @patch('openlp.core.ui.mainwindow.FirstTimeForm') + @patch('openlp.core.ui.mainwindow.QtWidgets.QMessageBox.warning') + @patch('openlp.core.ui.mainwindow.Settings') + def on_first_time_wizard_clicked_hide_projectors_after_test(self, mocked_Settings, mocked_warning, + mocked_FirstTimeForm, mocked_application, + mocked_first_time, + mocked_plugin_manager): + # GIVEN: Main_window, patched things, patched "Yes" as confirmation to re-run wizard, settings to False. + mocked_Settings_obj = MagicMock() + mocked_Settings_obj.value.return_value = False + mocked_Settings.return_value = mocked_Settings_obj + mocked_warning.return_value = QtWidgets.QMessageBox.Yes + mocked_FirstTimeForm_obj = MagicMock() + mocked_FirstTimeForm_obj.was_cancelled = False + mocked_FirstTimeForm.return_value = mocked_FirstTimeForm_obj + mocked_plugin_manager.plugins = [] + self.main_window.projector_manager_dock = MagicMock() + + # WHEN: on_first_time_wizard_clicked is called + self.main_window.on_first_time_wizard_clicked() + + # THEN: projector_manager_dock.setVisible should had been called once + self.main_window.projector_manager_dock.setVisible.assert_called_once_with(False) diff --git a/tests/functional/openlp_core_ui_media/test_mediacontroller.py b/tests/functional/openlp_core_ui_media/test_mediacontroller.py index 567ee9847..a37961b7a 100644 --- a/tests/functional/openlp_core_ui_media/test_mediacontroller.py +++ b/tests/functional/openlp_core_ui_media/test_mediacontroller.py @@ -78,10 +78,11 @@ class TestMediaController(TestCase, TestMixin): """ Test that we don't try to play media when no players available """ - # GIVEN: A mocked UiStrings, get_media_players, controller, display and service_item - with patch('openlp.core.ui.media.mediacontroller.get_media_players') as mocked_get_media_players,\ + # GIVEN: A mocked UiStrings, get_used_players, controller, display and service_item + with patch('openlp.core.ui.media.mediacontroller.MediaController._get_used_players') as \ + mocked_get_used_players,\ patch('openlp.core.ui.media.mediacontroller.UiStrings') as mocked_uistrings: - mocked_get_media_players.return_value = ([], '') + mocked_get_used_players.return_value = ([]) mocked_ret_uistrings = MagicMock() mocked_ret_uistrings.Automatic = 1 mocked_uistrings.return_value = mocked_ret_uistrings @@ -97,14 +98,14 @@ class TestMediaController(TestCase, TestMixin): # THEN: it should return False self.assertFalse(ret, '_check_file_type should return False when no mediaplayers are available.') - @patch('openlp.core.ui.media.mediacontroller.get_media_players') + @patch('openlp.core.ui.media.mediacontroller.MediaController._get_used_players') @patch('openlp.core.ui.media.mediacontroller.UiStrings') - def check_file_type_no_processor_test(self, mocked_uistrings, mocked_get_media_players): + def check_file_type_no_processor_test(self, mocked_uistrings, mocked_get_used_players): """ Test that we don't try to play media when the processor for the service item is None """ # GIVEN: A mocked UiStrings, get_media_players, controller, display and service_item - mocked_get_media_players.return_value = ([], '') + mocked_get_used_players.return_value = ([], '') mocked_ret_uistrings = MagicMock() mocked_ret_uistrings.Automatic = 1 mocked_uistrings.return_value = mocked_ret_uistrings @@ -120,14 +121,14 @@ class TestMediaController(TestCase, TestMixin): # THEN: it should return False self.assertFalse(ret, '_check_file_type should return False when the processor for service_item is None.') - @patch('openlp.core.ui.media.mediacontroller.get_media_players') + @patch('openlp.core.ui.media.mediacontroller.MediaController._get_used_players') @patch('openlp.core.ui.media.mediacontroller.UiStrings') - def check_file_type_automatic_processor_test(self, mocked_uistrings, mocked_get_media_players): + def check_file_type_automatic_processor_test(self, mocked_uistrings, mocked_get_used_players): """ Test that we can play media when players are available and we have a automatic processor from the service item """ # GIVEN: A mocked UiStrings, get_media_players, controller, display and service_item - mocked_get_media_players.return_value = (['vlc', 'webkit'], '') + mocked_get_used_players.return_value = (['vlc', 'webkit']) mocked_ret_uistrings = MagicMock() mocked_ret_uistrings.Automatic = 1 mocked_uistrings.return_value = mocked_ret_uistrings @@ -150,21 +151,21 @@ class TestMediaController(TestCase, TestMixin): self.assertTrue(ret, '_check_file_type should return True when mediaplayers are available and ' 'the service item has an automatic processor.') - @patch('openlp.core.ui.media.mediacontroller.get_media_players') + @patch('openlp.core.ui.media.mediacontroller.MediaController._get_used_players') @patch('openlp.core.ui.media.mediacontroller.UiStrings') - def check_file_type_processor_different_from_available_test(self, mocked_uistrings, mocked_get_media_players): + def check_file_type_processor_different_from_available_test(self, mocked_uistrings, mocked_get_used_players): """ Test that we can play media when players available are different from the processor from the service item """ # GIVEN: A mocked UiStrings, get_media_players, controller, display and service_item - mocked_get_media_players.return_value = (['phonon'], '') + mocked_get_used_players.return_value = (['system']) mocked_ret_uistrings = MagicMock() mocked_ret_uistrings.Automatic = 'automatic' mocked_uistrings.return_value = mocked_ret_uistrings media_controller = MediaController() mocked_phonon = MagicMock() mocked_phonon.video_extensions_list = ['*.mp4'] - media_controller.media_players = {'phonon': mocked_phonon} + media_controller.media_players = {'system': mocked_phonon} mocked_controller = MagicMock() mocked_suffix = MagicMock() mocked_suffix.return_value = 'mp4' diff --git a/tests/functional/openlp_core_ui_media/test_vlcplayer.py b/tests/functional/openlp_core_ui_media/test_vlcplayer.py index 98ab16ca7..8e49fe4d9 100644 --- a/tests/functional/openlp_core_ui_media/test_vlcplayer.py +++ b/tests/functional/openlp_core_ui_media/test_vlcplayer.py @@ -380,7 +380,6 @@ class TestVLCPlayer(TestCase, TestMixin): mocked_display.vlc_media_player.set_media.assert_called_with(mocked_vlc_media) mocked_vlc_media.parse.assert_called_with() mocked_volume.assert_called_with(mocked_display, 100) - self.assertEqual(10, mocked_controller.media_info.length) self.assertTrue(result) @patch('openlp.core.ui.media.vlcplayer.is_win') @@ -426,7 +425,6 @@ class TestVLCPlayer(TestCase, TestMixin): mocked_display.vlc_media_player.set_media.assert_called_with(mocked_vlc_media) mocked_vlc_media.parse.assert_called_with() mocked_volume.assert_called_with(mocked_display, 100) - self.assertEqual(10, mocked_controller.media_info.length) self.assertTrue(result) @patch('openlp.core.ui.media.vlcplayer.is_win') @@ -472,7 +470,6 @@ class TestVLCPlayer(TestCase, TestMixin): mocked_display.vlc_media_player.set_media.assert_called_with(mocked_vlc_media) mocked_vlc_media.parse.assert_called_with() mocked_volume.assert_called_with(mocked_display, 100) - self.assertEqual(10, mocked_controller.media_info.length) self.assertTrue(result) @patch('openlp.core.ui.media.vlcplayer.is_win') @@ -628,7 +625,7 @@ class TestVLCPlayer(TestCase, TestMixin): mocked_display.controller = mocked_controller mocked_display.vlc_media_player.get_media.return_value = mocked_media vlc_player = VlcPlayer(None) - vlc_player.state = MediaState.Paused + vlc_player.set_state(MediaState.Paused, mocked_display) # WHEN: play() is called with patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait, \ @@ -638,10 +635,8 @@ class TestVLCPlayer(TestCase, TestMixin): # THEN: A bunch of things should happen to play the media mocked_thread.start.assert_called_with() - self.assertEqual(50, mocked_controller.media_info.length) mocked_volume.assert_called_with(mocked_display, 100) - mocked_controller.seek_slider.setMaximum.assert_called_with(50000) - self.assertEqual(MediaState.Playing, vlc_player.state) + self.assertEqual(MediaState.Playing, vlc_player.get_live_state()) mocked_display.vlc_widget.raise_.assert_called_with() self.assertTrue(result, 'The value returned from play() should be True') @@ -661,7 +656,7 @@ class TestVLCPlayer(TestCase, TestMixin): mocked_display = MagicMock() mocked_display.controller = mocked_controller vlc_player = VlcPlayer(None) - vlc_player.state = MediaState.Paused + vlc_player.set_state(MediaState.Paused, mocked_display) # WHEN: play() is called with patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait, \ @@ -695,7 +690,7 @@ class TestVLCPlayer(TestCase, TestMixin): mocked_display = MagicMock() mocked_display.controller = mocked_controller vlc_player = VlcPlayer(None) - vlc_player.state = MediaState.Paused + vlc_player.set_state(MediaState.Paused, mocked_display) # WHEN: play() is called with patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait, \ @@ -709,10 +704,8 @@ class TestVLCPlayer(TestCase, TestMixin): mocked_display.vlc_media_player.play.assert_called_with() mocked_display.vlc_media_player.audio_set_track.assert_called_with(1) mocked_display.vlc_media_player.video_set_spu.assert_called_with(1) - self.assertEqual(50, mocked_controller.media_info.length) mocked_volume.assert_called_with(mocked_display, 100) - mocked_controller.seek_slider.setMaximum.assert_called_with(50000) - self.assertEqual(MediaState.Playing, vlc_player.state) + self.assertEqual(MediaState.Playing, vlc_player.get_live_state()) mocked_display.vlc_widget.raise_.assert_called_with() self.assertTrue(result, 'The value returned from play() should be True') @@ -739,7 +732,7 @@ class TestVLCPlayer(TestCase, TestMixin): mocked_display.vlc_media.get_state.assert_called_with() mocked_display.vlc_media_player.pause.assert_called_with() mocked_media_state_wait.assert_called_with(mocked_display, 2) - self.assertEqual(MediaState.Paused, vlc_player.state) + self.assertEqual(MediaState.Paused, vlc_player.get_live_state()) @patch('openlp.core.ui.media.vlcplayer.get_vlc') def pause_not_playing_test(self, mocked_get_vlc): @@ -805,7 +798,7 @@ class TestVLCPlayer(TestCase, TestMixin): # THEN: A thread should have been started to stop VLC mocked_threading.Thread.assert_called_with(target=mocked_stop) mocked_thread.start.assert_called_with() - self.assertEqual(MediaState.Stopped, vlc_player.state) + self.assertEqual(MediaState.Stopped, vlc_player.get_live_state()) def volume_test(self): """ @@ -900,10 +893,10 @@ class TestVLCPlayer(TestCase, TestMixin): # WHEN: reset() is called vlc_player.reset(mocked_display) - # THEN: The media should be stopped and invsibile + # THEN: The media should be stopped and invisible mocked_display.vlc_media_player.stop.assert_called_with() mocked_display.vlc_widget.setVisible.assert_called_with(False) - self.assertEqual(MediaState.Off, vlc_player.state) + self.assertEqual(MediaState.Off, vlc_player.get_live_state()) def set_visible_has_own_widget_test(self): """ diff --git a/tests/functional/openlp_plugins/media/test_mediaplugin.py b/tests/functional/openlp_plugins/media/test_mediaplugin.py index 9dbab37cf..1e11de4fa 100644 --- a/tests/functional/openlp_plugins/media/test_mediaplugin.py +++ b/tests/functional/openlp_plugins/media/test_mediaplugin.py @@ -54,8 +54,6 @@ class MediaPluginTest(TestCase, TestMixin): media_plugin.initialise() # THEN: The settings should be upgraded and the base initialise() method should be called - mocked_settings.get_files_from_config.assert_called_with(media_plugin) - mocked_settings.setValue.assert_called_with('media/media files', True) mocked_initialise.assert_called_with() def test_about_text(self): diff --git a/tests/functional/openlp_plugins/songs/test_songselect.py b/tests/functional/openlp_plugins/songs/test_songselect.py index 5a94ee1ac..18ada0338 100644 --- a/tests/functional/openlp_plugins/songs/test_songselect.py +++ b/tests/functional/openlp_plugins/songs/test_songselect.py @@ -716,8 +716,43 @@ class TestSongSelectForm(TestCase, TestMixin): # WHEN: The stop button is clicked ssform.on_stop_button_clicked() - # THEN: The view button should be enabled + # THEN: The view button, search box and search button should be enabled mocked_song_select_importer.stop.assert_called_with() + self.assertTrue(ssform.search_button.isEnabled()) + self.assertTrue(ssform.search_combobox.isEnabled()) + + @patch('openlp.plugins.songs.forms.songselectform.Settings') + @patch('openlp.plugins.songs.forms.songselectform.QtCore.QThread') + @patch('openlp.plugins.songs.forms.songselectform.SearchWorker') + def on_search_button_clicked_test(self, MockedSearchWorker, MockedQtThread, MockedSettings): + """ + Test that search fields are disabled when search button is clicked. + """ + # GIVEN: A mocked SongSelect form + ssform = SongSelectForm(None, MagicMock(), MagicMock()) + ssform.initialise() + + # WHEN: The search button is clicked + ssform.on_search_button_clicked() + + # THEN: The search box and search button should be disabled + self.assertFalse(ssform.search_button.isEnabled()) + self.assertFalse(ssform.search_combobox.isEnabled()) + + def on_search_finished_test(self): + """ + Test that search fields are enabled when search is finished. + """ + # GIVEN: A mocked SongSelect form + ssform = SongSelectForm(None, MagicMock(), MagicMock()) + ssform.initialise() + + # WHEN: The search is finished + ssform.on_search_finished() + + # THEN: The search box and search button should be enabled + self.assertTrue(ssform.search_button.isEnabled()) + self.assertTrue(ssform.search_combobox.isEnabled()) class TestSongSelectFileImport(SongImportTestHelper): diff --git a/tests/interfaces/openlp_core_ul_media_vendor/__init__.py b/tests/interfaces/openlp_core_ul_media_vendor/__init__.py new file mode 100644 index 000000000..02bded5b0 --- /dev/null +++ b/tests/interfaces/openlp_core_ul_media_vendor/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# 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 # +############################################################################### diff --git a/tests/interfaces/openlp_core_ul_media_vendor/test_mediainfoWrapper.py b/tests/interfaces/openlp_core_ul_media_vendor/test_mediainfoWrapper.py new file mode 100644 index 000000000..acf17f581 --- /dev/null +++ b/tests/interfaces/openlp_core_ul_media_vendor/test_mediainfoWrapper.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# 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. +""" + +import os +from unittest import TestCase + +from openlp.core.ui.media.vendor.mediainfoWrapper import MediaInfoWrapper + +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources', 'media')) + +TEST_MEDIA = [['avi_file.avi', 61495], ['mp3_file.mp3', 134426], ['mpg_file.mpg', 9404], ['mp4_file.mp4', 188336]] + + +class TestMediainfoWrapper(TestCase): + + def media_length_test(self): + """ + Test the Media Info basic functionality + """ + for test_data in TEST_MEDIA: + # GIVEN: a media file + full_path = os.path.normpath(os.path.join(TEST_PATH, test_data[0])) + + # WHEN the media data is retrieved + results = MediaInfoWrapper.parse(full_path) + + # THEN you can determine the run time + self.assertEqual(results.tracks[0].duration, test_data[1], 'The correct duration is returned for ' + + test_data[0]) diff --git a/tests/resources/media/avi_file.avi b/tests/resources/media/avi_file.avi new file mode 100644 index 000000000..9a89932fe Binary files /dev/null and b/tests/resources/media/avi_file.avi differ diff --git a/tests/resources/media/mp3_file.mp3 b/tests/resources/media/mp3_file.mp3 new file mode 100644 index 000000000..2b5cc44f7 Binary files /dev/null and b/tests/resources/media/mp3_file.mp3 differ diff --git a/tests/resources/media/mp4_file.mp4 b/tests/resources/media/mp4_file.mp4 new file mode 100644 index 000000000..73abf10a0 Binary files /dev/null and b/tests/resources/media/mp4_file.mp4 differ diff --git a/tests/resources/media/mpg_file.mpg b/tests/resources/media/mpg_file.mpg new file mode 100644 index 000000000..af768c542 Binary files /dev/null and b/tests/resources/media/mpg_file.mpg differ