diff --git a/.bzrignore b/.bzrignore index b7dffe4fb..97af7bea6 100644 --- a/.bzrignore +++ b/.bzrignore @@ -45,3 +45,4 @@ cover *.kdev4 coverage tags +output diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index b8a1a4d2e..f3076a86f 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -24,6 +24,7 @@ The :mod:`common` module contains most of the components and libraries that make OpenLP work. """ import hashlib + import logging import os import re @@ -31,6 +32,7 @@ import sys import traceback from ipaddress import IPv4Address, IPv6Address, AddressValueError from shutil import which +from subprocess import check_output, CalledProcessError, STDOUT from PyQt5 import QtCore, QtGui from PyQt5.QtCore import QCryptographicHash as QHash @@ -247,6 +249,9 @@ from .applocation import AppLocation from .actions import ActionList from .languagemanager import LanguageManager +if is_win(): + from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW + def add_actions(target, actions): """ @@ -371,3 +376,28 @@ def clean_filename(filename): if not isinstance(filename, str): filename = str(filename, 'utf-8') return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename)) + + +def check_binary_exists(program_path): + """ + Function that checks whether a binary exists. + + :param program_path:The full path to the binary to check. + :return: program output to be parsed + """ + log.debug('testing program_path: %s', program_path) + try: + # Setup startupinfo options for check_output to avoid console popping up on windows + if is_win(): + startupinfo = STARTUPINFO() + startupinfo.dwFlags |= STARTF_USESHOWWINDOW + else: + startupinfo = None + runlog = check_output([program_path, '--help'], stderr=STDOUT, startupinfo=startupinfo) + except CalledProcessError as e: + runlog = e.output + except Exception: + trace_error_handler(log) + runlog = '' + log.debug('check_output returned: %s' % runlog) + return runlog 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 283cd7251..307daf170 100644 --- a/openlp/core/common/uistrings.py +++ b/openlp/core/common/uistrings.py @@ -57,6 +57,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/__init__.py b/openlp/core/lib/__init__.py index 6e62bbf9c..e56cb0d61 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -312,12 +312,9 @@ def create_separated_list(string_list): return translate('OpenLP.core.lib', '%s, %s', 'Locale list separator: start') % (string_list[0], merged) -from .colorbutton import ColorButton from .exceptions import ValidationError from .filedialog import FileDialog from .screen import ScreenList -from .listwidgetwithdnd import ListWidgetWithDnD -from .treewidgetwithdnd import TreeWidgetWithDnD from .formattingtags import FormattingTags from .spelltextedit import SpellTextEdit from .plugin import PluginStatus, StringContent, Plugin @@ -325,8 +322,6 @@ from .pluginmanager import PluginManager from .settingstab import SettingsTab from .serviceitem import ServiceItem, ServiceItemType, ItemCapabilities from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css -from .toolbar import OpenLPToolbar -from .dockwidget import OpenLPDockWidget from .imagemanager import ImageManager from .renderer import Renderer from .mediamanageritem import MediaManagerItem diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index 04df1d38a..5af90c1b7 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -29,10 +29,11 @@ import re from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate -from openlp.core.lib import FileDialog, OpenLPToolbar, ServiceItem, StringContent, ListWidgetWithDnD, \ - ServiceItemContext +from openlp.core.lib import FileDialog, ServiceItem, StringContent, ServiceItemContext from openlp.core.lib.searchedit import SearchEdit from openlp.core.lib.ui import create_widget_action, critical_error_message_box +from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD +from openlp.core.ui.lib.toolbar import OpenLPToolbar log = logging.getLogger(__name__) 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..599efd8e7 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): @@ -114,7 +113,6 @@ from .settingsform import SettingsForm from .formattingtagform import FormattingTagForm from .formattingtagcontroller import FormattingTagController from .shortcutlistform import ShortcutListForm -from .mediadockmanager import MediaDockManager from .servicemanager import ServiceManager from .thememanager import ThemeManager from .projector.manager import ProjectorManager @@ -122,7 +120,7 @@ from .projector.tab import ProjectorTab from .projector.editform import ProjectorEditForm __all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', 'ThemeForm', - 'ThemeManager', 'MediaDockManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', + 'ThemeManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', 'Display', 'ServiceNoteForm', 'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay', 'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm', 'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'SingleColumnTableWidget', 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 cbec14a40..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,27 +239,39 @@ 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.')) self.no_internet_text = translate('OpenLP.FirstTimeWizard', 'No Internet connection was found. The First Time Wizard needs an Internet ' 'connection in order to be able to download sample songs, Bibles and themes.' - ' Click the Finish button now to start OpenLP with initial settings and ' + ' Click the %s button now to start OpenLP with initial settings and ' 'no sample data.\n\nTo re-run the First Time Wizard and import this sample ' 'data at a later time, check your Internet connection and re-run this ' - 'wizard by selecting "Tools/Re-run First Time Wizard" from OpenLP.') + 'wizard by selecting "Tools/Re-run First Time Wizard" from OpenLP.') % \ + clean_button_text(first_time_wizard.buttonText(QtWidgets.QWizard.FinishButton)) self.cancel_wizard_text = translate('OpenLP.FirstTimeWizard', '\n\nTo cancel the First Time Wizard completely (and not start OpenLP), ' 'click the %s button now.') % \ @@ -272,5 +291,14 @@ class UiFirstTimeWizard(object): self.progress_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Please wait while resources are downloaded ' 'and OpenLP is configured.')) self.progress_label.setText(translate('OpenLP.FirstTimeWizard', 'Starting configuration process...')) - first_time_wizard.setButtonText(QtWidgets.QWizard.CustomButton1, translate('OpenLP.FirstTimeWizard', 'Finish')) - first_time_wizard.setButtonText(QtWidgets.QWizard.CustomButton2, translate('OpenLP.FirstTimeWizard', 'Cancel')) + first_time_wizard.setButtonText(QtWidgets.QWizard.CustomButton1, + 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..816e947ba 100644 --- a/openlp/core/ui/generaltab.py +++ b/openlp/core/ui/generaltab.py @@ -26,8 +26,9 @@ 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, build_icon +from openlp.core.ui.lib.colorbutton import ColorButton log = logging.getLogger(__name__) @@ -40,6 +41,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 +165,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 +248,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 +296,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 +330,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 +395,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/lib/__init__.py b/openlp/core/ui/lib/__init__.py index 02bded5b0..6cdeac8a6 100644 --- a/openlp/core/ui/lib/__init__.py +++ b/openlp/core/ui/lib/__init__.py @@ -19,3 +19,15 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### + +from .colorbutton import ColorButton +from .listwidgetwithdnd import ListWidgetWithDnD +from .treewidgetwithdnd import TreeWidgetWithDnD +from .toolbar import OpenLPToolbar +from .dockwidget import OpenLPDockWidget +from .wizard import OpenLPWizard, WizardStrings +from .mediadockmanager import MediaDockManager +from .listpreviewwidget import ListPreviewWidget + +__all__ = ['ColorButton', 'ListPreviewWidget', 'ListWidgetWithDnD', 'OpenLPToolbar', 'OpenLPDockWidget', + 'OpenLPWizard', 'WizardStrings', 'MediaDockManager', 'ListPreviewWidget'] diff --git a/openlp/core/lib/colorbutton.py b/openlp/core/ui/lib/colorbutton.py similarity index 100% rename from openlp/core/lib/colorbutton.py rename to openlp/core/ui/lib/colorbutton.py diff --git a/openlp/core/lib/dockwidget.py b/openlp/core/ui/lib/dockwidget.py similarity index 100% rename from openlp/core/lib/dockwidget.py rename to openlp/core/ui/lib/dockwidget.py diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/lib/listpreviewwidget.py similarity index 85% rename from openlp/core/ui/listpreviewwidget.py rename to openlp/core/ui/lib/listpreviewwidget.py index 68c983d42..88aef818a 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/lib/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/lib/listwidgetwithdnd.py b/openlp/core/ui/lib/listwidgetwithdnd.py similarity index 100% rename from openlp/core/lib/listwidgetwithdnd.py rename to openlp/core/ui/lib/listwidgetwithdnd.py diff --git a/openlp/core/ui/mediadockmanager.py b/openlp/core/ui/lib/mediadockmanager.py similarity index 100% rename from openlp/core/ui/mediadockmanager.py rename to openlp/core/ui/lib/mediadockmanager.py diff --git a/openlp/core/lib/toolbar.py b/openlp/core/ui/lib/toolbar.py similarity index 100% rename from openlp/core/lib/toolbar.py rename to openlp/core/ui/lib/toolbar.py diff --git a/openlp/core/lib/treewidgetwithdnd.py b/openlp/core/ui/lib/treewidgetwithdnd.py similarity index 100% rename from openlp/core/lib/treewidgetwithdnd.py rename to openlp/core/ui/lib/treewidgetwithdnd.py diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/lib/wizard.py similarity index 98% rename from openlp/core/ui/wizard.py rename to openlp/core/ui/lib/wizard.py index 4a35f909c..5f2321f48 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/lib/wizard.py @@ -45,6 +45,7 @@ class WizardStrings(object): OS = 'OpenSong' OSIS = 'OSIS' ZEF = 'Zefania' + SWORD = 'Sword' # These strings should need a good reason to be retranslated elsewhere. FinishedImport = translate('OpenLP.Ui', 'Finished import.') FormatLabel = translate('OpenLP.Ui', 'Format:') @@ -111,8 +112,9 @@ 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.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) self.setModal(True) self.setOptions(QtWidgets.QWizard.IndependentPages | QtWidgets.QWizard.NoBackButtonOnStartPage | QtWidgets.QWizard.NoBackButtonOnLastPage) @@ -210,6 +212,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 +224,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/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..39e0ac518 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -38,15 +38,17 @@ from openlp.core.common import Registry, RegistryProperties, AppLocation, Langua check_directory_exists, translate, is_win, is_macosx, add_actions from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.versionchecker import get_application_version -from openlp.core.lib import Renderer, OpenLPDockWidget, PluginManager, ImageManager, PluginStatus, ScreenList, \ - build_icon +from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, ScreenList, build_icon from openlp.core.lib.ui import UiStrings, create_action from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \ - MediaDockManager, ShortcutListForm, FormattingTagForm, PreviewController + ShortcutListForm, FormattingTagForm, PreviewController from openlp.core.ui.firsttimeform import FirstTimeForm from openlp.core.ui.media import MediaController from openlp.core.ui.printserviceform import PrintServiceForm from openlp.core.ui.projector.manager import ProjectorManager +from openlp.core.ui.lib.toolbar import OpenLPToolbar +from openlp.core.ui.lib.dockwidget import OpenLPDockWidget +from openlp.core.ui.lib.mediadockmanager import MediaDockManager log = logging.getLogger(__name__) @@ -638,13 +640,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 +700,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 +1036,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 +1191,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 +1231,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 +1242,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 +1254,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 +1275,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..021ea5281 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -29,16 +29,21 @@ import datetime from PyQt5 import QtCore, QtWidgets from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, translate -from openlp.core.lib import OpenLPToolbar, ItemCapabilities +from openlp.core.lib import 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.mediaplayer import MediaPlayer from openlp.core.common import AppLocation from openlp.core.ui import DisplayControllerType +from openlp.core.ui.media.vendor.mediainfoWrapper import MediaInfoWrapper +from openlp.core.ui.media.mediaplayer import MediaPlayer +from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players,\ + parse_optical_path +from openlp.core.ui.lib.toolbar import OpenLPToolbar +from openlp.core.ui.lib.dockwidget import OpenLPDockWidget log = logging.getLogger(__name__) +TICK_TIME = 200 + class MediaSlider(QtWidgets.QSlider): """ @@ -51,10 +56,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 +71,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 +108,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 +188,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 +203,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 +297,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=':/media/media_repeat.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 +329,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 +369,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 +388,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 +395,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 +409,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 +429,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 +466,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 +492,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 +509,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 +539,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 +558,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 +617,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 +637,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 +699,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 +731,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 +783,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 +794,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 +823,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 +841,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 +858,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..1fca21450 100644 --- a/openlp/core/ui/media/playertab.py +++ b/openlp/core/ui/media/playertab.py @@ -26,9 +26,10 @@ import platform from PyQt5 import QtCore, QtWidgets from openlp.core.common import Registry, Settings, UiStrings, translate -from openlp.core.lib import ColorButton, SettingsTab +from openlp.core.lib import SettingsTab from openlp.core.lib.ui import create_button from openlp.core.ui.media import get_media_players, set_media_players +from openlp.core.ui.lib.colorbutton import ColorButton class MediaQCheckBox(QtWidgets.QCheckBox): @@ -133,12 +134,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/projector/editform.py b/openlp/core/ui/projector/editform.py index 4b06f486f..4996cc75f 100644 --- a/openlp/core/ui/projector/editform.py +++ b/openlp/core/ui/projector/editform.py @@ -182,9 +182,10 @@ class ProjectorEditForm(QDialog, Ui_ProjectorEditForm): QtWidgets.QMessageBox.warning(self, translate('OpenLP.ProjectorEdit', 'Duplicate Name'), translate('OpenLP.ProjectorEdit', - 'There is already an entry with name "%s" in ' - 'the database as ID "%s".
' - 'Please enter a different name.' % (name, record.id))) + 'There is already an entry with name "{name}" in ' + 'the database as ID "{record}".
' + 'Please enter a different name.'.format(name=name, + record=record.id))) valid = False return adx = self.ip_text.text() @@ -198,17 +199,17 @@ class ProjectorEditForm(QDialog, Ui_ProjectorEditForm): QtWidgets.QMessageBox.warning(self, translate('OpenLP.ProjectorWizard', 'Duplicate IP Address'), translate('OpenLP.ProjectorWizard', - 'IP address "%s"
is already in the database as ID %s.' - '

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

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

Please enter a valid IP address.' % adx)) + 'IP address "{ip}"
is not a valid IP address.' + '

Please enter a valid IP address.'.format(ip=adx))) valid = False return port = int(self.port_text.text()) @@ -219,8 +220,8 @@ class ProjectorEditForm(QDialog, Ui_ProjectorEditForm): 'Port numbers below 1000 are reserved for admin use only, ' '
and port numbers above 32767 are not currently usable.' '

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

Default PJLink port is %s' % PJLINK_PORT)) + '1000 and 32767.

' + 'Default PJLink port is {port}'.format(port=PJLINK_PORT))) valid = False if valid: self.projector.ip = self.ip_text.text() diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index fc40ee386..7c56c2916 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -35,7 +35,7 @@ from PyQt5.QtWidgets import QWidget from openlp.core.common import RegistryProperties, Settings, OpenLPMixin, \ RegistryMixin, translate -from openlp.core.lib import OpenLPToolbar +from openlp.core.ui.lib import OpenLPToolbar from openlp.core.lib.ui import create_widget_action from openlp.core.lib.projector import DialogSourceStyle from openlp.core.lib.projector.constants import * @@ -344,7 +344,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, real_projector = item.data(QtCore.Qt.UserRole) projector_name = str(item.text()) visible = real_projector.link.status_connect >= S_CONNECTED - log.debug('(%s) Building menu - visible = %s' % (projector_name, visible)) + log.debug('({name}) Building menu - visible = {visible}'.format(name=projector_name, visible=visible)) self.delete_action.setVisible(True) self.edit_action.setVisible(True) self.connect_action.setVisible(not visible) @@ -394,7 +394,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, projectordb=self.projectordb, edit=edit) source = source_select_form.exec(projector.link) - log.debug('(%s) source_select_form() returned %s' % (projector.link.ip, source)) + log.debug('({ip}) source_select_form() returned {data}'.format(ip=projector.link.ip, data=source)) if source is not None and source > 0: projector.link.set_input_source(str(source)) return @@ -473,8 +473,9 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, return projector = list_item.data(QtCore.Qt.UserRole) msg = QtWidgets.QMessageBox() - msg.setText(translate('OpenLP.ProjectorManager', 'Delete projector (%s) %s?') % (projector.link.ip, - projector.link.name)) + msg.setText(translate('OpenLP.ProjectorManager', + 'Delete projector ({ip}) {name}?'.format(ip=projector.link.ip, + name=projector.link.name))) msg.setInformativeText(translate('OpenLP.ProjectorManager', 'Are you sure you want to delete this projector?')) msg.setStandardButtons(msg.Cancel | msg.Ok) msg.setDefaultButton(msg.Cancel) @@ -522,7 +523,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, list_item = None deleted = self.projectordb.delete_projector(projector.db_item) for item in self.projector_list: - log.debug('New projector list - item: %s %s' % (item.link.ip, item.link.name)) + log.debug('New projector list - item: {ip} {name}'.format(ip=item.link.ip, name=item.link.name)) def on_disconnect_projector(self, opt=None): """ @@ -627,53 +628,58 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, """ lwi = self.projector_list_widget.item(self.projector_list_widget.currentRow()) projector = lwi.data(QtCore.Qt.UserRole) - message = '%s: %s
' % (translate('OpenLP.ProjectorManager', 'Name'), - projector.link.name) - message = '%s%s: %s
' % (message, translate('OpenLP.ProjectorManager', 'IP'), - projector.link.ip) - message = '%s%s: %s
' % (message, translate('OpenLP.ProjectorManager', 'Port'), - projector.link.port) - message = '%s%s: %s
' % (message, translate('OpenLP.ProjectorManager', 'Notes'), - projector.link.notes) - message = '%s

' % message + message = '{title}: {data}
'.format(title=translate('OpenLP.ProjectorManager', 'Name'), + data=projector.link.name) + message += '{title}: {data}
'.format(title=translate('OpenLP.ProjectorManager', 'IP'), + data=projector.link.ip) + message += '{title}: {data}
'.format(title=translate('OpenLP.ProjectorManager', 'Port'), + data=projector.link.port) + message += '{title}: {data}
'.format(title=translate('OpenLP.ProjectorManager', 'Notes'), + data=projector.link.notes) + message += '

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

' % (message, translate('OpenLP.ProjectorManager', 'Other info'), - projector.link.other_info) - message = '%s%s: %s
' % (message, translate('OpenLP.ProjectorManager', 'Power status'), - ERROR_MSG[projector.link.power]) - message = '%s%s: %s
' % (message, translate('OpenLP.ProjectorManager', 'Shutter is'), - translate('OpenLP.ProjectorManager', 'Closed') - if projector.link.shutter else translate('OpenLP', 'Open')) + message += '{title}: {data}
'.format(title=translate('OpenLP.ProjectorManager', + 'Projector Name'), + data=projector.link.pjlink_name) + message += '{title}: {data}
'.format(title=translate('OpenLP.ProjectorManager', 'Manufacturer'), + data=projector.link.manufacturer) + message += '{title}: {data}
'.format(title=translate('OpenLP.ProjectorManager', 'Model'), + data=projector.link.model) + message += '{title}: {data}

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

' % message + message += '{title} {count} {status} '.format(title=translate('OpenLP.ProjectorManager', + 'Lamp'), + count=count, + status=translate('OpenLP.ProjectorManager', + ' is on') + if item['On'] + else translate('OpenLP.ProjectorManager', + 'is off')) + + message += '{title}: {hours}
'.format(title=translate('OpenLP.ProjectorManager', 'Hours'), + hours=item['Hours']) + count += 1 + message += '

' if projector.link.projector_errors is None: - message = '%s%s' % (message, translate('OpenLP.ProjectorManager', 'No current errors or warnings')) + message += translate('OpenLP.ProjectorManager', 'No current errors or warnings') else: - message = '%s%s' % (message, translate('OpenLP.ProjectorManager', 'Current errors/warnings')) + message += '{data}'.format(data=translate('OpenLP.ProjectorManager', 'Current errors/warnings')) for (key, val) in projector.link.projector_errors.items(): - message = '%s%s: %s
' % (message, key, ERROR_MSG[val]) + message += '{key}: {data}
'.format(key=key, data=ERROR_MSG[val]) QtWidgets.QMessageBox.information(self, translate('OpenLP.ProjectorManager', 'Projector Information'), message) def _add_projector(self, projector): @@ -743,7 +749,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, if start: item.link.connect_to_host() for item in self.projector_list: - log.debug('New projector list - item: (%s) %s' % (item.link.ip, item.link.name)) + log.debug('New projector list - item: ({ip}) {name}'.format(ip=item.link.ip, name=item.link.name)) @pyqtSlot(str) def add_projector_from_wizard(self, ip, opts=None): @@ -753,7 +759,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, :param ip: IP address of new record item to find :param opts: Needed by PyQt5 """ - log.debug('add_projector_from_wizard(ip=%s)' % ip) + log.debug('add_projector_from_wizard(ip={ip})'.format(ip=ip)) item = self.projectordb.get_projector_by_ip(ip) self.add_projector(item) @@ -764,7 +770,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, :param projector: Projector() instance of projector with updated information """ - log.debug('edit_projector_from_wizard(ip=%s)' % projector.ip) + log.debug('edit_projector_from_wizard(ip={ip})'.format(ip=projector.ip)) self.old_projector.link.name = projector.name self.old_projector.link.ip = projector.ip self.old_projector.link.pin = None if projector.pin == '' else projector.pin @@ -816,7 +822,9 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, else: status_code = status message = ERROR_MSG[status] if msg is None else msg - log.debug('(%s) updateStatus(status=%s) message: "%s"' % (item.link.name, status_code, message)) + log.debug('({name}) updateStatus(status={status}) message: "{message}"'.format(name=item.link.name, + status=status_code, + message=message)) if status in STATUS_ICONS: if item.status == status: return @@ -826,14 +834,14 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, status_code = ERROR_STRING[status] elif status in STATUS_STRING: status_code = STATUS_STRING[status] - log.debug('(%s) Updating icon with %s' % (item.link.name, status_code)) + log.debug('({name}) Updating icon with {code}'.format(name=item.link.name, code=status_code)) item.widget.setIcon(item.icon) self.update_icons() def get_toolbar_item(self, name, enabled=False, hidden=False): item = self.one_toolbar.findChild(QtWidgets.QAction, name) if item == 0: - log.debug('No item found with name "%s"' % name) + log.debug('No item found with name "{name}"'.format(name=name)) return item.setVisible(False if hidden else True) item.setEnabled(True if enabled else False) @@ -918,11 +926,12 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, :param name: Name from QListWidgetItem """ - QtWidgets.QMessageBox.warning(self, translate('OpenLP.ProjectorManager', - '"%s" Authentication Error' % name), + title = '"{name} {message}" '.format(name=name, + message=translate('OpenLP.ProjectorManager', 'Authentication Error')) + QtWidgets.QMessageBox.warning(self, title, '
There was an authentication error while trying to connect.' '

Please verify your PIN setting ' - 'for projector item "%s"' % name) + 'for projector item "{name}"'.format(name=name)) @pyqtSlot(str) def no_authentication_error(self, name): @@ -932,11 +941,12 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, :param name: Name from QListWidgetItem """ - QtWidgets.QMessageBox.warning(self, translate('OpenLP.ProjectorManager', - '"%s" No Authentication Error' % name), + title = '"{name} {message}" '.format(name=name, + message=translate('OpenLP.ProjectorManager', 'No Authentication Error')) + QtWidgets.QMessageBox.warning(self, title, '
PIN is set and projector does not require authentication.' '

Please verify your PIN setting ' - 'for projector item "%s"' % name) + 'for projector item "{name}"'.format(name=name)) class ProjectorItem(QObject): @@ -972,5 +982,5 @@ def not_implemented(function): QtWidgets.QMessageBox.information(None, translate('OpenLP.ProjectorManager', 'Not Implemented Yet'), translate('OpenLP.ProjectorManager', - 'Function "%s"
has not been implemented yet.' - '
Please check back again later.' % function)) + 'Function "{function}"
has not been implemented yet.' + '
Please check back again later.'.format(function=function))) diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index 11efcdb08..7d73f6a5a 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -115,7 +115,7 @@ def Build_Tab(group, source_key, default, projector, projectordb, edit=False): if edit: for key in sourcelist: item = QLineEdit() - item.setObjectName('source_key_%s' % key) + item.setObjectName('source_key_{key}'.format(key=key)) source_item = projectordb.get_source_by_code(code=key, projector_id=projector.db_item.id) if source_item is None: item.setText(PJLINK_DEFAULT_CODES[key]) @@ -161,7 +161,7 @@ def set_button_tooltip(bar): button.setToolTip(translate('OpenLP.SourceSelectForm', 'Save changes and return to OpenLP')) else: - log.debug('No tooltip for button {}'.format(button.text())) + log.debug('No tooltip for button {text}'.format(text=button.text())) class FingerTabBarWidget(QTabBar): @@ -359,16 +359,20 @@ class SourceSelectTabs(QDialog): continue item = self.projectordb.get_source_by_code(code=code, projector_id=projector.id) if item is None: - log.debug("(%s) Adding new source text %s: %s" % (projector.ip, code, text)) + log.debug("({ip}) Adding new source text {code}: {text}".format(ip=projector.ip, + code=code, + text=text)) item = ProjectorSource(projector_id=projector.id, code=code, text=text) else: item.text = text - log.debug('(%s) Updating source code %s with text="%s"' % (projector.ip, item.code, item.text)) + log.debug('({ip}) Updating source code {code} with text="{text}"'.format(ip=projector.ip, + code=item.code, + text=item.text)) self.projectordb.add_source(item) selected = 0 else: selected = self.button_group.checkedId() - log.debug('SourceSelectTabs().accepted() Setting source to %s' % selected) + log.debug('SourceSelectTabs().accepted() Setting source to {selected}'.format(selected=selected)) self.done(selected) @@ -417,7 +421,7 @@ class SourceSelectSingle(QDialog): if self.edit: for key in keys: item = QLineEdit() - item.setObjectName('source_key_%s' % key) + item.setObjectName('source_key_{key}'.format(key=key)) source_item = self.projectordb.get_source_by_code(code=key, projector_id=self.projector.db_item.id) if source_item is None: item.setText(PJLINK_DEFAULT_CODES[key]) @@ -498,14 +502,18 @@ class SourceSelectSingle(QDialog): continue item = self.projectordb.get_source_by_code(code=code, projector_id=projector.id) if item is None: - log.debug("(%s) Adding new source text %s: %s" % (projector.ip, code, text)) + log.debug("({ip}) Adding new source text {code}: {text}".format(ip=projector.ip, + code=code, + text=text)) item = ProjectorSource(projector_id=projector.id, code=code, text=text) else: item.text = text - log.debug('(%s) Updating source code %s with text="%s"' % (projector.ip, item.code, item.text)) + log.debug('({ip}) Updating source code {code} with text="{text}"'.format(ip=projector.ip, + code=item.code, + text=item.text)) self.projectordb.add_source(item) selected = 0 else: selected = self.button_group.checkedId() - log.debug('SourceSelectDialog().accepted() Setting source to %s' % selected) + log.debug('SourceSelectDialog().accepted() Setting source to {selected}'.format(selected=selected)) self.done(selected) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 66cbdf1b7..82b489344 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -35,9 +35,10 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, ThemeLevel, OpenLPMixin, \ RegistryMixin, check_directory_exists, UiStrings, translate, split_filename, delete_file from openlp.core.common.actions import ActionList, CategoryOrder -from openlp.core.lib import OpenLPToolbar, ServiceItem, ItemCapabilities, PluginStatus, build_icon +from openlp.core.lib import ServiceItem, ItemCapabilities, PluginStatus, build_icon from openlp.core.lib.ui import critical_error_message_box, create_widget_action, find_and_set_in_combo_box from openlp.core.ui import ServiceNoteForm, ServiceItemEditForm, StartTimeForm +from openlp.core.ui.lib import OpenLPToolbar from openlp.core.common.languagemanager import format_time diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index fd88e67ee..ea2abe5fb 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -33,11 +33,14 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, RegistryProperties, Settings, SlideLimits, UiStrings, translate, \ RegistryMixin, OpenLPMixin from openlp.core.common.actions import ActionList, CategoryOrder -from openlp.core.lib import OpenLPToolbar, ItemCapabilities, ServiceItem, ImageSource, ServiceItemAction, \ - ScreenList, build_icon, build_html +from openlp.core.lib import ItemCapabilities, ServiceItem, ImageSource, ServiceItemAction, ScreenList, build_icon, \ + build_html from openlp.core.lib.ui import create_action +from openlp.core.ui.lib.toolbar import OpenLPToolbar +from openlp.core.ui.lib.dockwidget import OpenLPDockWidget +from openlp.core.ui.lib.listpreviewwidget import ListPreviewWidget from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType -from openlp.core.ui.listpreviewwidget import ListPreviewWidget + # Threshold which has to be trespassed to toggle. HIDE_MENU_THRESHOLD = 27 @@ -84,7 +87,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..fc231a859 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -31,6 +31,7 @@ from openlp.core.common import Registry, RegistryProperties, UiStrings, translat from openlp.core.lib.theme import BackgroundType, BackgroundGradientType from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui import ThemeLayoutForm +from openlp.core.ui.lib.colorbutton import ColorButton from .themewizard import Ui_ThemeWizard log = logging.getLogger(__name__) @@ -147,6 +148,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 +188,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/thememanager.py b/openlp/core/ui/thememanager.py index a80640150..32975e9aa 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -31,11 +31,12 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, OpenLPMixin, RegistryMixin, \ check_directory_exists, UiStrings, translate, is_win, get_filesystem_encoding, delete_file -from openlp.core.lib import FileDialog, ImageSource, OpenLPToolbar, ValidationError, get_text_file_string, build_icon, \ +from openlp.core.lib import FileDialog, ImageSource, ValidationError, get_text_file_string, build_icon, \ check_item_selected, create_thumb, validate_thumb from openlp.core.lib.theme import ThemeXML, BackgroundType from openlp.core.lib.ui import critical_error_message_box, create_widget_action from openlp.core.ui import FileRenameForm, ThemeForm +from openlp.core.ui.lib import OpenLPToolbar from openlp.core.common.languagemanager import get_locale_key @@ -760,8 +761,8 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage used_count = plugin.uses_theme(theme) if used_count: plugin_usage = "%s%s" % (plugin_usage, (translate('OpenLP.ThemeManager', - '%s time(s) by %s') % - (used_count, plugin.name))) + '%(count)s time(s) by %(plugin)s') % + {'count': used_count, 'plugin': plugin.name})) plugin_usage = "%s\n" % plugin_usage if plugin_usage: critical_error_message_box(translate('OpenLP.ThemeManager', 'Unable to delete theme'), diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index b041a0905..ab8854ef2 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -25,9 +25,10 @@ The Create/Edit theme wizard from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import UiStrings, translate, is_macosx -from openlp.core.lib import build_icon, ColorButton +from openlp.core.lib import build_icon from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets +from openlp.core.ui.lib.colorbutton import ColorButton class Ui_ThemeWizard(object): diff --git a/openlp/plugins/alerts/lib/alertstab.py b/openlp/plugins/alerts/lib/alertstab.py index 2875493b6..2859a71ce 100644 --- a/openlp/plugins/alerts/lib/alertstab.py +++ b/openlp/plugins/alerts/lib/alertstab.py @@ -23,8 +23,9 @@ from PyQt5 import QtGui, QtWidgets from openlp.core.common import Settings, UiStrings, translate -from openlp.core.lib import ColorButton, SettingsTab +from openlp.core.lib import SettingsTab from openlp.core.lib.ui import create_valign_selection_widgets +from openlp.core.ui.lib.colorbutton import ColorButton class AlertsTab(SettingsTab): diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index 27dbea963..66ba252fc 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -27,14 +27,19 @@ import os import urllib.error from PyQt5 import QtWidgets +try: + from pysword import modules + PYSWORD_AVAILABLE = True +except: + PYSWORD_AVAILABLE = False from openlp.core.common import AppLocation, Settings, UiStrings, translate, clean_filename from openlp.core.lib.db import delete_database from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.wizard import OpenLPWizard, WizardStrings +from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings from openlp.core.common.languagemanager import get_locale_key from openlp.plugins.bibles.lib.manager import BibleFormat -from openlp.plugins.bibles.lib.db import BiblesResourcesDB, clean_filename +from openlp.plugins.bibles.lib.db import clean_filename from openlp.plugins.bibles.lib.http import CWExtract, BGExtract, BSExtract log = logging.getLogger(__name__) @@ -94,6 +99,19 @@ class BibleImportForm(OpenLPWizard): self.manager.set_process_dialog(self) self.restart() self.select_stack.setCurrentIndex(0) + if PYSWORD_AVAILABLE: + self.pysword_folder_modules = modules.SwordModules() + try: + self.pysword_folder_modules_json = self.pysword_folder_modules.parse_modules() + except FileNotFoundError: + log.debug('No installed SWORD modules found in the default location') + self.sword_bible_combo_box.clear() + return + bible_keys = self.pysword_folder_modules_json.keys() + for key in bible_keys: + self.sword_bible_combo_box.addItem(self.pysword_folder_modules_json[key]['description'], key) + else: + self.sword_tab_widget.setDisabled(True) def custom_signals(self): """ @@ -106,6 +124,8 @@ class BibleImportForm(OpenLPWizard): self.open_song_browse_button.clicked.connect(self.on_open_song_browse_button_clicked) self.zefania_browse_button.clicked.connect(self.on_zefania_browse_button_clicked) self.web_update_button.clicked.connect(self.on_web_update_button_clicked) + self.sword_browse_button.clicked.connect(self.on_sword_browse_button_clicked) + self.sword_zipbrowse_button.clicked.connect(self.on_sword_zipbrowse_button_clicked) def add_custom_pages(self): """ @@ -121,7 +141,7 @@ class BibleImportForm(OpenLPWizard): self.format_label = QtWidgets.QLabel(self.select_page) self.format_label.setObjectName('FormatLabel') self.format_combo_box = QtWidgets.QComboBox(self.select_page) - self.format_combo_box.addItems(['', '', '', '', '']) + self.format_combo_box.addItems(['', '', '', '', '', '']) self.format_combo_box.setObjectName('FormatComboBox') self.format_layout.addRow(self.format_label, self.format_combo_box) self.spacer = QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) @@ -275,6 +295,64 @@ class BibleImportForm(OpenLPWizard): self.zefania_layout.addRow(self.zefania_file_label, self.zefania_file_layout) self.zefania_layout.setItem(5, QtWidgets.QFormLayout.LabelRole, self.spacer) self.select_stack.addWidget(self.zefania_widget) + self.sword_widget = QtWidgets.QWidget(self.select_page) + self.sword_widget.setObjectName('SwordWidget') + self.sword_layout = QtWidgets.QVBoxLayout(self.sword_widget) + self.sword_layout.setObjectName('SwordLayout') + self.sword_tab_widget = QtWidgets.QTabWidget(self.sword_widget) + self.sword_tab_widget.setObjectName('SwordTabWidget') + self.sword_folder_tab = QtWidgets.QWidget(self.sword_tab_widget) + self.sword_folder_tab.setObjectName('SwordFolderTab') + self.sword_folder_tab_layout = QtWidgets.QGridLayout(self.sword_folder_tab) + self.sword_folder_tab_layout.setObjectName('SwordTabFolderLayout') + self.sword_folder_label = QtWidgets.QLabel(self.sword_folder_tab) + self.sword_folder_label.setObjectName('SwordSourceLabel') + self.sword_folder_tab_layout.addWidget(self.sword_folder_label, 0, 0) + self.sword_folder_label.setObjectName('SwordFolderLabel') + self.sword_folder_edit = QtWidgets.QLineEdit(self.sword_folder_tab) + self.sword_folder_edit.setObjectName('SwordFolderEdit') + self.sword_browse_button = QtWidgets.QToolButton(self.sword_folder_tab) + self.sword_browse_button.setIcon(self.open_icon) + self.sword_browse_button.setObjectName('SwordBrowseButton') + self.sword_folder_tab_layout.addWidget(self.sword_folder_edit, 0, 1) + self.sword_folder_tab_layout.addWidget(self.sword_browse_button, 0, 2) + self.sword_bible_label = QtWidgets.QLabel(self.sword_folder_tab) + self.sword_bible_label.setObjectName('SwordBibleLabel') + self.sword_folder_tab_layout.addWidget(self.sword_bible_label, 1, 0) + self.sword_bible_combo_box = QtWidgets.QComboBox(self.sword_folder_tab) + self.sword_bible_combo_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) + self.sword_bible_combo_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically) + self.sword_bible_combo_box.setObjectName('SwordBibleComboBox') + self.sword_folder_tab_layout.addWidget(self.sword_bible_combo_box, 1, 1) + self.sword_tab_widget.addTab(self.sword_folder_tab, '') + self.sword_zip_tab = QtWidgets.QWidget(self.sword_tab_widget) + self.sword_zip_tab.setObjectName('SwordZipTab') + self.sword_zip_layout = QtWidgets.QGridLayout(self.sword_zip_tab) + self.sword_zip_layout.setObjectName('SwordZipLayout') + self.sword_zipfile_label = QtWidgets.QLabel(self.sword_zip_tab) + self.sword_zipfile_label.setObjectName('SwordZipFileLabel') + self.sword_zipfile_edit = QtWidgets.QLineEdit(self.sword_zip_tab) + self.sword_zipfile_edit.setObjectName('SwordZipFileEdit') + self.sword_zipbrowse_button = QtWidgets.QToolButton(self.sword_zip_tab) + self.sword_zipbrowse_button.setIcon(self.open_icon) + self.sword_zipbrowse_button.setObjectName('SwordZipBrowseButton') + self.sword_zipbible_label = QtWidgets.QLabel(self.sword_folder_tab) + self.sword_zipbible_label.setObjectName('SwordZipBibleLabel') + self.sword_zipbible_combo_box = QtWidgets.QComboBox(self.sword_zip_tab) + self.sword_zipbible_combo_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) + self.sword_zipbible_combo_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically) + self.sword_zipbible_combo_box.setObjectName('SwordZipBibleComboBox') + self.sword_zip_layout.addWidget(self.sword_zipfile_label, 0, 0) + self.sword_zip_layout.addWidget(self.sword_zipfile_edit, 0, 1) + self.sword_zip_layout.addWidget(self.sword_zipbrowse_button, 0, 2) + self.sword_zip_layout.addWidget(self.sword_zipbible_label, 1, 0) + self.sword_zip_layout.addWidget(self.sword_zipbible_combo_box, 1, 1) + self.sword_tab_widget.addTab(self.sword_zip_tab, '') + self.sword_layout.addWidget(self.sword_tab_widget) + self.sword_disabled_label = QtWidgets.QLabel(self.sword_widget) + self.sword_disabled_label.setObjectName('SwordDisabledLabel') + self.sword_layout.addWidget(self.sword_disabled_label) + self.select_stack.addWidget(self.sword_widget) self.select_page_layout.addLayout(self.select_stack) self.addPage(self.select_page) # License Page @@ -323,6 +401,7 @@ class BibleImportForm(OpenLPWizard): self.format_combo_box.setItemText(BibleFormat.WebDownload, translate('BiblesPlugin.ImportWizardForm', 'Web Download')) self.format_combo_box.setItemText(BibleFormat.Zefania, WizardStrings.ZEF) + self.format_combo_box.setItemText(BibleFormat.SWORD, WizardStrings.SWORD) self.osis_file_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bible file:')) self.csv_books_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Books file:')) self.csv_verses_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Verses file:')) @@ -346,6 +425,22 @@ class BibleImportForm(OpenLPWizard): self.web_tab_widget.setTabText( self.web_tab_widget.indexOf(self.web_proxy_tab), translate('BiblesPlugin.ImportWizardForm', 'Proxy Server (Optional)')) + self.sword_bible_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bibles:')) + self.sword_folder_label.setText(translate('BiblesPlugin.ImportWizardForm', 'SWORD data folder:')) + self.sword_zipfile_label.setText(translate('BiblesPlugin.ImportWizardForm', 'SWORD zip-file:')) + self.sword_folder_edit.setPlaceholderText(translate('BiblesPlugin.ImportWizardForm', + 'Defaults to the standard SWORD data folder')) + self.sword_zipbible_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bibles:')) + self.sword_tab_widget.setTabText(self.sword_tab_widget.indexOf(self.sword_folder_tab), + translate('BiblesPlugin.ImportWizardForm', 'Import from folder')) + self.sword_tab_widget.setTabText(self.sword_tab_widget.indexOf(self.sword_zip_tab), + translate('BiblesPlugin.ImportWizardForm', 'Import from Zip-file')) + if PYSWORD_AVAILABLE: + self.sword_disabled_label.setText('') + else: + self.sword_disabled_label.setText(translate('BiblesPlugin.ImportWizardForm', + 'To import SWORD bibles the pysword python module must be ' + 'installed. Please read the manual for instructions.')) self.license_details_page.setTitle( translate('BiblesPlugin.ImportWizardForm', 'License Details')) self.license_details_page.setSubTitle(translate('BiblesPlugin.ImportWizardForm', @@ -374,6 +469,9 @@ class BibleImportForm(OpenLPWizard): if self.currentPage() == self.welcome_page: return True elif self.currentPage() == self.select_page: + self.version_name_edit.clear() + self.permissions_edit.clear() + self.copyright_edit.clear() if self.field('source_format') == BibleFormat.OSIS: if not self.field('osis_location'): critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.OSIS) @@ -410,6 +508,31 @@ class BibleImportForm(OpenLPWizard): return False else: self.version_name_edit.setText(self.web_translation_combo_box.currentText()) + elif self.field('source_format') == BibleFormat.SWORD: + # Test the SWORD tab that is currently active + if self.sword_tab_widget.currentIndex() == self.sword_tab_widget.indexOf(self.sword_folder_tab): + if not self.field('sword_folder_path') and self.sword_bible_combo_box.count() == 0: + critical_error_message_box(UiStrings().NFSs, + WizardStrings.YouSpecifyFolder % WizardStrings.SWORD) + self.sword_folder_edit.setFocus() + return False + key = self.sword_bible_combo_box.itemData(self.sword_bible_combo_box.currentIndex()) + if 'description' in self.pysword_folder_modules_json[key]: + self.version_name_edit.setText(self.pysword_folder_modules_json[key]['description']) + if 'distributionlicense' in self.pysword_folder_modules_json[key]: + self.permissions_edit.setText(self.pysword_folder_modules_json[key]['distributionlicense']) + if 'copyright' in self.pysword_folder_modules_json[key]: + self.copyright_edit.setText(self.pysword_folder_modules_json[key]['copyright']) + elif self.sword_tab_widget.currentIndex() == self.sword_tab_widget.indexOf(self.sword_zip_tab): + if not self.field('sword_zip_path'): + critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.SWORD) + self.sword_zipfile_edit.setFocus() + return False + key = self.sword_zipbible_combo_box.itemData(self.sword_zipbible_combo_box.currentIndex()) + if 'description' in self.pysword_zip_modules_json[key]: + self.version_name_edit.setText(self.pysword_zip_modules_json[key]['description']) + if 'distributionlicense' in self.pysword_zip_modules_json[key]: + self.permissions_edit.setText(self.pysword_zip_modules_json[key]['distributionlicense']) return True elif self.currentPage() == self.license_details_page: license_version = self.field('license_version') @@ -531,6 +654,40 @@ class BibleImportForm(OpenLPWizard): self.web_update_button.setEnabled(True) self.web_progress_bar.setVisible(False) + def on_sword_browse_button_clicked(self): + """ + Show the file open dialog for the SWORD folder. + """ + self.get_folder(WizardStrings.OpenTypeFolder % WizardStrings.SWORD, self.sword_folder_edit, + 'last directory import') + if self.sword_folder_edit.text(): + try: + self.pysword_folder_modules = modules.SwordModules(self.sword_folder_edit.text()) + self.pysword_folder_modules_json = self.pysword_folder_modules.parse_modules() + bible_keys = self.pysword_folder_modules_json.keys() + self.sword_bible_combo_box.clear() + for key in bible_keys: + self.sword_bible_combo_box.addItem(self.pysword_folder_modules_json[key]['description'], key) + except: + self.sword_bible_combo_box.clear() + + def on_sword_zipbrowse_button_clicked(self): + """ + Show the file open dialog for a SWORD zip-file. + """ + self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.SWORD, self.sword_zipfile_edit, + 'last directory import') + if self.sword_zipfile_edit.text(): + try: + self.pysword_zip_modules = modules.SwordModules(self.sword_zipfile_edit.text()) + self.pysword_zip_modules_json = self.pysword_zip_modules.parse_modules() + bible_keys = self.pysword_zip_modules_json.keys() + self.sword_zipbible_combo_box.clear() + for key in bible_keys: + self.sword_zipbible_combo_box.addItem(self.pysword_zip_modules_json[key]['description'], key) + except: + self.sword_zipbible_combo_box.clear() + def register_fields(self): """ Register the bible import wizard fields. @@ -543,6 +700,8 @@ class BibleImportForm(OpenLPWizard): self.select_page.registerField('zefania_file', self.zefania_file_edit) self.select_page.registerField('web_location', self.web_source_combo_box) self.select_page.registerField('web_biblename', self.web_translation_combo_box) + self.select_page.registerField('sword_folder_path', self.sword_folder_edit) + self.select_page.registerField('sword_zip_path', self.sword_zipfile_edit) self.select_page.registerField('proxy_server', self.web_server_edit) self.select_page.registerField('proxy_username', self.web_user_edit) self.select_page.registerField('proxy_password', self.web_password_edit) @@ -565,6 +724,8 @@ class BibleImportForm(OpenLPWizard): self.setField('csv_versefile', '') self.setField('opensong_file', '') self.setField('zefania_file', '') + self.setField('sword_folder_path', '') + self.setField('sword_zip_path', '') self.setField('web_location', WebDownload.Crosswalk) self.setField('web_biblename', self.web_translation_combo_box.currentIndex()) self.setField('proxy_server', settings.value('proxy address')) @@ -626,9 +787,21 @@ class BibleImportForm(OpenLPWizard): language_id=language_id ) elif bible_type == BibleFormat.Zefania: - # Import an Zefania bible. + # Import a Zefania bible. importer = self.manager.import_bible(BibleFormat.Zefania, name=license_version, filename=self.field('zefania_file')) + elif bible_type == BibleFormat.SWORD: + # Import a SWORD bible. + if self.sword_tab_widget.currentIndex() == self.sword_tab_widget.indexOf(self.sword_folder_tab): + importer = self.manager.import_bible(BibleFormat.SWORD, name=license_version, + sword_path=self.field('sword_folder_path'), + sword_key=self.sword_bible_combo_box.itemData( + self.sword_bible_combo_box.currentIndex())) + else: + importer = self.manager.import_bible(BibleFormat.SWORD, name=license_version, + sword_path=self.field('sword_zip_path'), + sword_key=self.sword_zipbible_combo_box.itemData( + self.sword_zipbible_combo_box.currentIndex())) if importer.do_import(license_version): self.manager.save_meta_data(license_version, license_version, license_copyright, license_permissions) self.manager.reload_bibles() diff --git a/openlp/plugins/bibles/forms/bibleupgradeform.py b/openlp/plugins/bibles/forms/bibleupgradeform.py index 611e6ead3..11771e9aa 100644 --- a/openlp/plugins/bibles/forms/bibleupgradeform.py +++ b/openlp/plugins/bibles/forms/bibleupgradeform.py @@ -32,7 +32,7 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common import Registry, AppLocation, UiStrings, Settings, check_directory_exists, translate, \ delete_file from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.wizard import OpenLPWizard, WizardStrings +from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings from openlp.plugins.bibles.lib.db import BibleDB, BibleMeta, OldBibleDB, BiblesResourcesDB from openlp.plugins.bibles.lib.http import BSExtract, BGExtract, CWExtract diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index c81e65575..4398688ee 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -504,7 +504,7 @@ class CWExtract(RegistryProperties): soup = get_soup_for_bible_ref(chapter_url) if not soup: return None - content = soup.find_all(('h4', {'class': 'small-header'})) + content = soup.find_all('h4', {'class': 'small-header'}) if not content: log.error('No books found in the Crosswalk response.') send_error_message('parse') diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index 83f6b26bd..b0a429737 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -31,7 +31,10 @@ from .http import HTTPBible from .opensong import OpenSongBible from .osis import OSISBible from .zefania import ZefaniaBible - +try: + from .sword import SwordBible +except: + pass log = logging.getLogger(__name__) @@ -46,6 +49,7 @@ class BibleFormat(object): OpenSong = 2 WebDownload = 3 Zefania = 4 + SWORD = 5 @staticmethod def get_class(bible_format): @@ -64,6 +68,8 @@ class BibleFormat(object): return HTTPBible elif bible_format == BibleFormat.Zefania: return ZefaniaBible + elif bible_format == BibleFormat.SWORD: + return SwordBible else: return None @@ -78,6 +84,7 @@ class BibleFormat(object): BibleFormat.OpenSong, BibleFormat.WebDownload, BibleFormat.Zefania, + BibleFormat.SWORD ] diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index 8942643c0..39df59b30 100644 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -881,6 +881,9 @@ class BibleMediaItem(MediaManagerItem): except IndexError: log.exception('The second_search_results does not have as many verses as the search_results.') break + except TypeError: + log.exception('The second_search_results does not have this book.') + break bible_text = '%s %d%s%d (%s, %s)' % (book, verse.chapter, verse_separator, verse.verse, version, second_version) else: diff --git a/openlp/plugins/bibles/lib/sword.py b/openlp/plugins/bibles/lib/sword.py new file mode 100644 index 000000000..6f91803a6 --- /dev/null +++ b/openlp/plugins/bibles/lib/sword.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-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 # +############################################################################### + +import logging +from pysword import modules + +from openlp.core.common import translate +from openlp.core.lib.ui import critical_error_message_box +from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB + + +log = logging.getLogger(__name__) + + +class SwordBible(BibleDB): + """ + SWORD Bible format importer class. + """ + def __init__(self, parent, **kwargs): + """ + Constructor to create and set up an instance of the SwordBible class. This class is used to import Bibles + from SWORD bible modules. + """ + log.debug(self.__class__.__name__) + BibleDB.__init__(self, parent, **kwargs) + self.sword_key = kwargs['sword_key'] + self.sword_path = kwargs['sword_path'] + if self.sword_path == '': + self.sword_path = None + + def do_import(self, bible_name=None): + """ + Loads a Bible from SWORD module. + """ + log.debug('Starting SWORD import from "%s"' % self.sword_key) + success = True + try: + pysword_modules = modules.SwordModules(self.sword_path) + pysword_module_json = pysword_modules.parse_modules()[self.sword_key] + bible = pysword_modules.get_bible_from_module(self.sword_key) + language = pysword_module_json['lang'] + language = language[language.find('.') + 1:] + language_id = BiblesResourcesDB.get_language(language)['id'] + self.save_meta('language_id', language_id) + books = bible.get_structure().get_books() + # Count number of books + num_books = 0 + if 'ot' in books: + num_books += len(books['ot']) + if 'nt' in books: + num_books += len(books['nt']) + self.wizard.progress_bar.setMaximum(num_books) + # Import the bible + for testament in books.keys(): + for book in books[testament]: + book_ref_id = self.get_book_ref_id_by_name(book.name, num_books, language_id) + book_details = BiblesResourcesDB.get_book_by_id(book_ref_id) + db_book = self.create_book(book_details['name'], book_ref_id, book_details['testament_id']) + for chapter_number in range(1, book.num_chapters + 1): + if self.stop_import_flag: + break + verses = bible.get_iter(book.name, chapter_number) + verse_number = 0 + for verse in verses: + verse_number += 1 + self.create_verse(db_book.id, chapter_number, verse_number, verse) + self.wizard.increment_progress_bar( + translate('BiblesPlugin.Sword', 'Importing %s...') % db_book.name) + self.session.commit() + self.application.process_events() + except Exception as e: + critical_error_message_box( + message=translate('BiblesPlugin.SwordImport', 'An unexpected error happened while importing the SWORD ' + 'bible, please report this to the OpenLP developers.\n' + '%s' % e)) + log.exception(str(e)) + success = False + if self.stop_import_flag: + return False + else: + return success diff --git a/openlp/plugins/bibles/lib/zefania.py b/openlp/plugins/bibles/lib/zefania.py index 6e9f0b956..9b44cdf26 100644 --- a/openlp/plugins/bibles/lib/zefania.py +++ b/openlp/plugins/bibles/lib/zefania.py @@ -70,7 +70,8 @@ class ZefaniaBible(BibleDB): log.error('Importing books from "%s" failed' % self.filename) return False self.save_meta('language_id', language_id) - num_books = int(zefania_bible_tree.xpath("count(//BIBLEBOOK)")) + num_books = int(zefania_bible_tree.xpath('count(//BIBLEBOOK)')) + self.wizard.progress_bar.setMaximum(int(zefania_bible_tree.xpath('count(//CHAPTER)'))) # Strip tags we don't use - keep content etree.strip_tags(zefania_bible_tree, ('STYLE', 'GRAM', 'NOTE', 'SUP', 'XREF')) # Strip tags we don't use - remove content diff --git a/openlp/plugins/images/lib/imagetab.py b/openlp/plugins/images/lib/imagetab.py index 80578dc56..2cc6776b1 100644 --- a/openlp/plugins/images/lib/imagetab.py +++ b/openlp/plugins/images/lib/imagetab.py @@ -23,7 +23,8 @@ from PyQt5 import QtWidgets from openlp.core.common import Settings, UiStrings, translate -from openlp.core.lib import ColorButton, SettingsTab +from openlp.core.lib import SettingsTab +from openlp.core.ui.lib.colorbutton import ColorButton class ImageTab(SettingsTab): diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index f35fd48c7..d127fba4b 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -27,9 +27,10 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, AppLocation, Settings, UiStrings, check_directory_exists, translate, \ delete_file, get_images_filter -from openlp.core.lib import ItemCapabilities, MediaManagerItem, ServiceItemContext, StringContent, TreeWidgetWithDnD,\ - build_icon, check_item_selected, create_thumb, validate_thumb +from openlp.core.lib import ItemCapabilities, MediaManagerItem, ServiceItemContext, StringContent, build_icon, \ + check_item_selected, create_thumb, validate_thumb from openlp.core.lib.ui import create_widget_action, critical_error_message_box +from openlp.core.ui.lib.treewidgetwithdnd import TreeWidgetWithDnD from openlp.core.common.languagemanager import get_locale_key from openlp.plugins.images.forms import AddGroupForm, ChooseGroupForm from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups @@ -195,7 +196,7 @@ class ImageMediaItem(MediaManagerItem): Add custom buttons to the end of the toolbar """ self.replace_action = self.toolbar.add_toolbar_action('replace_action', - icon=':/slides/slide_blank.png', + icon=':/slides/slide_theme.png', triggers=self.on_replace_click) self.reset_action = self.toolbar.add_toolbar_action('reset_action', icon=':/system/system_close.png', diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index dfe6f1fa4..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:')) @@ -151,10 +146,11 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): Adds buttons to the end of the header bar. """ # Replace backgrounds do not work at present so remove functionality. - self.replace_action = self.toolbar.add_toolbar_action('replace_action', icon=':/slides/slide_blank.png', + self.replace_action = self.toolbar.add_toolbar_action('replace_action', icon=':/slides/slide_theme.png', 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..1d5529084 100644 --- a/openlp/plugins/media/mediaplugin.py +++ b/openlp/plugins/media/mediaplugin.py @@ -24,10 +24,13 @@ The Media plugin """ import logging +import os +import re +from shutil import which from PyQt5 import QtCore -from openlp.core.common import Settings, translate +from openlp.core.common import AppLocation, Settings, translate, check_binary_exists, is_win from openlp.core.lib import Plugin, StringContent, build_icon from openlp.plugins.media.lib import MediaMediaItem, MediaTab @@ -60,15 +63,17 @@ 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 check_pre_conditions(self): + """ + Check it we have a valid environment. + :return: true or false + """ + log.debug('check_installed Mediainfo') + # Use the user defined program if given + return process_check_binary('mediainfo') + def app_startup(self): """ Override app_startup() in order to do nothing @@ -144,3 +149,21 @@ class MediaPlugin(Plugin): Add html code to htmlbuilder. """ return self.media_controller.get_media_display_html() + + +def process_check_binary(program_path): + """ + Function that checks whether a binary MediaInfo is present + + :param program_path:The full path to the binary to check. + :return: If exists or not + """ + program_type = None + runlog = check_binary_exists(program_path) + print(runlog, type(runlog)) + # Analyse the output to see it the program is mediainfo + for line in runlog.splitlines(): + decoded_line = line.decode() + if re.search('MediaInfo Command line', decoded_line, re.IGNORECASE): + return True + return False diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index dbea84327..48150a9f2 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -22,13 +22,12 @@ import os import logging -from tempfile import NamedTemporaryFile import re from shutil import which -from subprocess import check_output, CalledProcessError, STDOUT +from subprocess import check_output, CalledProcessError -from openlp.core.common import AppLocation -from openlp.core.common import Settings, is_win, trace_error_handler +from openlp.core.common import AppLocation, check_binary_exists +from openlp.core.common import Settings, is_win from openlp.core.lib import ScreenList from .presentationcontroller import PresentationController, PresentationDocument @@ -61,7 +60,7 @@ class PdfController(PresentationController): self.check_installed() @staticmethod - def check_binary(program_path): + def process_check_binary(program_path): """ Function that checks whether a binary is either ghostscript or mudraw or neither. Is also used from presentationtab.py @@ -70,22 +69,7 @@ class PdfController(PresentationController): :return: Type of the binary, 'gs' if ghostscript, 'mudraw' if mudraw, None if invalid. """ program_type = None - runlog = '' - log.debug('testing program_path: %s', program_path) - try: - # Setup startupinfo options for check_output to avoid console popping up on windows - if is_win(): - startupinfo = STARTUPINFO() - startupinfo.dwFlags |= STARTF_USESHOWWINDOW - else: - startupinfo = None - runlog = check_output([program_path, '--help'], stderr=STDOUT, startupinfo=startupinfo) - except CalledProcessError as e: - runlog = e.output - except Exception: - trace_error_handler(log) - runlog = '' - log.debug('check_output returned: %s' % runlog) + runlog = check_binary_exists(program_path) # Analyse the output to see it the program is mudraw, ghostscript or neither for line in runlog.splitlines(): decoded_line = line.decode() @@ -122,7 +106,7 @@ class PdfController(PresentationController): # Use the user defined program if given if Settings().value('presentations/enable_pdf_program'): pdf_program = Settings().value('presentations/pdf_program') - program_type = self.check_binary(pdf_program) + program_type = self.process_check_binary(pdf_program) if program_type == 'gs': self.gsbin = pdf_program elif program_type == 'mudraw': diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 76ee65571..af64e401f 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -144,18 +144,33 @@ class RemoteTab(SettingsTab): self.android_app_group_box = QtWidgets.QGroupBox(self.right_column) self.android_app_group_box.setObjectName('android_app_group_box') self.right_layout.addWidget(self.android_app_group_box) - self.qr_layout = QtWidgets.QVBoxLayout(self.android_app_group_box) - self.qr_layout.setObjectName('qr_layout') - self.qr_code_label = QtWidgets.QLabel(self.android_app_group_box) - self.qr_code_label.setPixmap(QtGui.QPixmap(':/remotes/android_app_qr.png')) - self.qr_code_label.setAlignment(QtCore.Qt.AlignCenter) - self.qr_code_label.setObjectName('qr_code_label') - self.qr_layout.addWidget(self.qr_code_label) - self.qr_description_label = QtWidgets.QLabel(self.android_app_group_box) - self.qr_description_label.setObjectName('qr_description_label') - self.qr_description_label.setOpenExternalLinks(True) - self.qr_description_label.setWordWrap(True) - self.qr_layout.addWidget(self.qr_description_label) + self.android_qr_layout = QtWidgets.QVBoxLayout(self.android_app_group_box) + self.android_qr_layout.setObjectName('android_qr_layout') + self.android_qr_code_label = QtWidgets.QLabel(self.android_app_group_box) + self.android_qr_code_label.setPixmap(QtGui.QPixmap(':/remotes/android_app_qr.png')) + self.android_qr_code_label.setAlignment(QtCore.Qt.AlignCenter) + self.android_qr_code_label.setObjectName('android_qr_code_label') + self.android_qr_layout.addWidget(self.android_qr_code_label) + self.android_qr_description_label = QtWidgets.QLabel(self.android_app_group_box) + self.android_qr_description_label.setObjectName('android_qr_description_label') + self.android_qr_description_label.setOpenExternalLinks(True) + self.android_qr_description_label.setWordWrap(True) + self.android_qr_layout.addWidget(self.android_qr_description_label) + self.ios_app_group_box = QtWidgets.QGroupBox(self.right_column) + self.ios_app_group_box.setObjectName('ios_app_group_box') + self.right_layout.addWidget(self.ios_app_group_box) + self.ios_qr_layout = QtWidgets.QVBoxLayout(self.ios_app_group_box) + self.ios_qr_layout.setObjectName('ios_qr_layout') + self.ios_qr_code_label = QtWidgets.QLabel(self.ios_app_group_box) + self.ios_qr_code_label.setPixmap(QtGui.QPixmap(':/remotes/ios_app_qr.png')) + self.ios_qr_code_label.setAlignment(QtCore.Qt.AlignCenter) + self.ios_qr_code_label.setObjectName('ios_qr_code_label') + self.ios_qr_layout.addWidget(self.ios_qr_code_label) + self.ios_qr_description_label = QtWidgets.QLabel(self.ios_app_group_box) + self.ios_qr_description_label.setObjectName('ios_qr_description_label') + self.ios_qr_description_label.setOpenExternalLinks(True) + self.ios_qr_description_label.setWordWrap(True) + self.ios_qr_layout.addWidget(self.ios_qr_description_label) self.left_layout.addStretch() self.right_layout.addStretch() self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed) @@ -176,10 +191,15 @@ class RemoteTab(SettingsTab): self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab', 'Show thumbnails of non-text slides in remote and stage view.')) self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App')) - self.qr_description_label.setText( + self.android_qr_description_label.setText( translate('RemotePlugin.RemoteTab', 'Scan the QR code or click download to install the ' 'Android app from Google Play.') % 'https://play.google.com/store/apps/details?id=org.openlp.android2') + self.ios_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'iOS App')) + self.ios_qr_description_label.setText( + translate('RemotePlugin.RemoteTab', 'Scan the QR code or click download to install the ' + 'iOS app from the App Store.') % + 'https://itunes.apple.com/app/id1096218725') self.https_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'HTTPS Server')) self.https_error_label.setText( translate('RemotePlugin.RemoteTab', 'Could not find an SSL certificate. The HTTPS server will not be ' diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 0fa4ee670..26de9507f 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -30,7 +30,7 @@ import os from PyQt5 import QtCore, QtWidgets from openlp.core.common import Registry, RegistryProperties, translate -from openlp.core.ui.wizard import OpenLPWizard, WizardStrings +from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings from openlp.plugins.songs.lib import delete_song from openlp.plugins.songs.lib.db import Song, MediaFile from openlp.plugins.songs.forms.songreviewwidget import SongReviewWidget diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index 678169e64..b33788a4c 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -34,6 +34,7 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common import Registry, RegistryProperties, AppLocation, UiStrings, check_directory_exists, translate from openlp.core.lib import FileDialog, PluginStatus, MediaType, create_separated_list from openlp.core.lib.ui import set_case_insensitive_completer, critical_error_message_box, find_and_set_in_combo_box +from openlp.core.common.languagemanager import get_natural_key from openlp.plugins.songs.lib import VerseType, clean_song from openlp.plugins.songs.lib.db import Book, Song, Author, AuthorType, Topic, MediaFile, SongBookEntry from openlp.plugins.songs.lib.ui import SongStrings @@ -110,7 +111,12 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): """ Generically load a set of objects into a cache and a combobox. """ - objects = self.manager.get_all_objects(cls, order_by_ref=cls.name) + def get_key(obj): + """Get the key to sort by""" + return get_natural_key(obj.name) + + objects = self.manager.get_all_objects(cls) + objects.sort(key=get_key) combo.clear() combo.addItem('') for obj in objects: @@ -343,7 +349,12 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): """ Load the authors from the database into the combobox. """ - authors = self.manager.get_all_objects(Author, order_by_ref=Author.display_name) + def get_author_key(author): + """Get the key to sort by""" + return get_natural_key(author.display_name) + + authors = self.manager.get_all_objects(Author) + authors.sort(key=get_author_key) self.authors_combo_box.clear() self.authors_combo_box.addItem('') self.authors = [] @@ -378,9 +389,14 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): """ Load the themes into a combobox. """ + def get_theme_key(theme): + """Get the key to sort by""" + return get_natural_key(theme) + self.theme_combo_box.clear() self.theme_combo_box.addItem('') self.themes = theme_list + self.themes.sort(key=get_theme_key) self.theme_combo_box.addItems(theme_list) set_case_insensitive_completer(self.themes, self.theme_combo_box) diff --git a/openlp/plugins/songs/forms/songexportform.py b/openlp/plugins/songs/forms/songexportform.py index ee35ea7e5..e8a559c44 100644 --- a/openlp/plugins/songs/forms/songexportform.py +++ b/openlp/plugins/songs/forms/songexportform.py @@ -30,7 +30,7 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common import Registry, UiStrings, translate from openlp.core.lib import create_separated_list, build_icon from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.wizard import OpenLPWizard, WizardStrings +from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings from openlp.plugins.songs.lib.db import Song from openlp.plugins.songs.lib.openlyricsexport import OpenLyricsExport @@ -203,6 +203,10 @@ class SongExportForm(OpenLPWizard): """ Set default form values for the song export wizard. """ + def get_song_key(song): + """Get the key to sort by""" + return song.sort_key + self.restart() self.finish_button.setVisible(False) self.cancel_button.setVisible(True) @@ -213,7 +217,7 @@ class SongExportForm(OpenLPWizard): # Load the list of songs. self.application.set_busy_cursor() songs = self.plugin.manager.get_all_objects(Song) - songs.sort(key=lambda song: song.sort_key) + songs.sort(key=get_song_key) for song in songs: # No need to export temporary songs. if song.temporary: diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py index 9058324fc..7a6af3981 100644 --- a/openlp/plugins/songs/forms/songimportform.py +++ b/openlp/plugins/songs/forms/songimportform.py @@ -31,7 +31,7 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common import RegistryProperties, Settings, UiStrings, translate from openlp.core.lib import FileDialog from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.wizard import OpenLPWizard, WizardStrings +from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings from openlp.plugins.songs.lib.importer import SongFormat, SongFormatSelect log = logging.getLogger(__name__) diff --git a/openlp/plugins/songs/forms/songmaintenanceform.py b/openlp/plugins/songs/forms/songmaintenanceform.py index 1fdfb74d4..74462e6d0 100644 --- a/openlp/plugins/songs/forms/songmaintenanceform.py +++ b/openlp/plugins/songs/forms/songmaintenanceform.py @@ -27,6 +27,7 @@ from sqlalchemy.sql import and_ from openlp.core.common import Registry, RegistryProperties, UiStrings, translate from openlp.core.lib.ui import critical_error_message_box +from openlp.core.common.languagemanager import get_natural_key from openlp.plugins.songs.forms.authorsform import AuthorsForm from openlp.plugins.songs.forms.topicsform import TopicsForm from openlp.plugins.songs.forms.songbookform import SongBookForm @@ -120,8 +121,13 @@ class SongMaintenanceForm(QtWidgets.QDialog, Ui_SongMaintenanceDialog, RegistryP """ Reloads the Authors list. """ + def get_author_key(author): + """Get the key to sort by""" + return get_natural_key(author.display_name) + self.authors_list_widget.clear() - authors = self.manager.get_all_objects(Author, order_by_ref=Author.display_name) + authors = self.manager.get_all_objects(Author) + authors.sort(key=get_author_key) for author in authors: if author.display_name: author_name = QtWidgets.QListWidgetItem(author.display_name) @@ -134,8 +140,13 @@ class SongMaintenanceForm(QtWidgets.QDialog, Ui_SongMaintenanceDialog, RegistryP """ Reloads the Topics list. """ + def get_topic_key(topic): + """Get the key to sort by""" + return get_natural_key(topic.name) + self.topics_list_widget.clear() - topics = self.manager.get_all_objects(Topic, order_by_ref=Topic.name) + topics = self.manager.get_all_objects(Topic) + topics.sort(key=get_topic_key) for topic in topics: topic_name = QtWidgets.QListWidgetItem(topic.name) topic_name.setData(QtCore.Qt.UserRole, topic.id) @@ -145,8 +156,13 @@ class SongMaintenanceForm(QtWidgets.QDialog, Ui_SongMaintenanceDialog, RegistryP """ Reloads the Books list. """ + def get_book_key(book): + """Get the key to sort by""" + return get_natural_key(book.name) + self.song_books_list_widget.clear() - books = self.manager.get_all_objects(Book, order_by_ref=Book.name) + books = self.manager.get_all_objects(Book) + books.sort(key=get_book_key) for book in books: book_name = QtWidgets.QListWidgetItem('%s (%s)' % (book.name, book.publisher)) book_name.setData(QtCore.Qt.UserRole, book.id) 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/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py index 1d45f52b2..0e03bfe7c 100644 --- a/openlp/plugins/songs/lib/__init__.py +++ b/openlp/plugins/songs/lib/__init__.py @@ -255,6 +255,7 @@ class VerseType(object): for num, translation in enumerate(VerseType.translated_names): if verse_name == translation.lower(): return num + return None @staticmethod def from_loose_input(verse_name, default=Other): @@ -270,7 +271,7 @@ class VerseType(object): if verse_index is None: verse_index = VerseType.from_string(verse_name, default) elif len(verse_name) == 1: - verse_index = VerseType.from_translated_tag(verse_name, default) + verse_index = VerseType.from_translated_tag(verse_name, None) if verse_index is None: verse_index = VerseType.from_tag(verse_name, default) else: diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index 5ea35d6b6..3026915e4 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -383,7 +383,7 @@ def init_schema(url): # Use lazy='joined' to always load authors when the song is fetched from the database (bug 1366198) 'authors': relation(Author, secondary=authors_songs_table, viewonly=True, lazy='joined'), 'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight), - 'songbook_entries': relation(SongBookEntry, backref='song', cascade="all, delete-orphan"), + 'songbook_entries': relation(SongBookEntry, backref='song', cascade='all, delete-orphan'), 'topics': relation(Topic, backref='songs', secondary=songs_topics_table) }) mapper(Topic, topics_table) diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index 47f6edb46..7b9101306 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -26,7 +26,7 @@ import os import logging from openlp.core.common import translate, UiStrings, is_win -from openlp.core.ui.wizard import WizardStrings +from openlp.core.ui.lib.wizard import WizardStrings from .importers.opensong import OpenSongImport from .importers.easyslides import EasySlidesImport from .importers.openlp import OpenLPSongImport diff --git a/openlp/plugins/songs/lib/importers/foilpresenter.py b/openlp/plugins/songs/lib/importers/foilpresenter.py index b1b12960a..061f50a9f 100644 --- a/openlp/plugins/songs/lib/importers/foilpresenter.py +++ b/openlp/plugins/songs/lib/importers/foilpresenter.py @@ -90,7 +90,7 @@ import os from lxml import etree, objectify from openlp.core.lib import translate -from openlp.core.ui.wizard import WizardStrings +from openlp.core.ui.lib.wizard import WizardStrings from openlp.plugins.songs.lib import clean_song, VerseType from openlp.plugins.songs.lib.importers.songimport import SongImport from openlp.plugins.songs.lib.db import Author, Book, Song, Topic diff --git a/openlp/plugins/songs/lib/importers/openlp.py b/openlp/plugins/songs/lib/importers/openlp.py index b914ed1e1..20c603e28 100644 --- a/openlp/plugins/songs/lib/importers/openlp.py +++ b/openlp/plugins/songs/lib/importers/openlp.py @@ -31,7 +31,7 @@ from sqlalchemy.orm.exc import UnmappedClassError from openlp.core.common import translate from openlp.core.lib.db import BaseModel -from openlp.core.ui.wizard import WizardStrings +from openlp.core.ui.lib.wizard import WizardStrings from openlp.plugins.songs.lib import clean_song from openlp.plugins.songs.lib.db import Author, Book, Song, Topic, MediaFile from .songimport import SongImport @@ -51,7 +51,7 @@ class OpenLPSongImport(SongImport): :param manager: The song manager for the running OpenLP installation. :param kwargs: The database providing the data to import. """ - SongImport.__init__(self, manager, **kwargs) + super(OpenLPSongImport, self).__init__(manager, **kwargs) self.source_session = None def do_import(self, progress_dialog=None): @@ -63,49 +63,61 @@ class OpenLPSongImport(SongImport): class OldAuthor(BaseModel): """ - Author model + Maps to the authors table """ pass class OldBook(BaseModel): """ - Book model + Maps to the songbooks table """ pass class OldMediaFile(BaseModel): """ - MediaFile model + Maps to the media_files table """ pass class OldSong(BaseModel): """ - Song model + Maps to the songs table """ pass class OldTopic(BaseModel): """ - Topic model + Maps to the topics table + """ + pass + + class OldSongBookEntry(BaseModel): + """ + Maps to the songs_songbooks table """ pass # Check the file type - if not self.import_source.endswith('.sqlite'): + if not isinstance(self.import_source, str) or not self.import_source.endswith('.sqlite'): self.log_error(self.import_source, translate('SongsPlugin.OpenLPSongImport', 'Not a valid OpenLP 2 song database.')) return self.import_source = 'sqlite:///%s' % self.import_source - # Load the db file + # Load the db file and reflect it engine = create_engine(self.import_source) source_meta = MetaData() source_meta.reflect(engine) self.source_session = scoped_session(sessionmaker(bind=engine)) + # Run some checks to see which version of the database we have if 'media_files' in list(source_meta.tables.keys()): has_media_files = True else: has_media_files = False + if 'songs_songbooks' in list(source_meta.tables.keys()): + has_songs_books = True + else: + has_songs_books = False + # Load up the tabls and map them out source_authors_table = source_meta.tables['authors'] source_song_books_table = source_meta.tables['song_books'] source_songs_table = source_meta.tables['songs'] @@ -113,6 +125,7 @@ class OpenLPSongImport(SongImport): source_authors_songs_table = source_meta.tables['authors_songs'] source_songs_topics_table = source_meta.tables['songs_topics'] source_media_files_songs_table = None + # Set up media_files relations if has_media_files: source_media_files_table = source_meta.tables['media_files'] source_media_files_songs_table = source_meta.tables.get('media_files_songs') @@ -120,9 +133,15 @@ class OpenLPSongImport(SongImport): class_mapper(OldMediaFile) except UnmappedClassError: mapper(OldMediaFile, source_media_files_table) + if has_songs_books: + source_songs_songbooks_table = source_meta.tables['songs_songbooks'] + try: + class_mapper(OldSongBookEntry) + except UnmappedClassError: + mapper(OldSongBookEntry, source_songs_songbooks_table, properties={'songbook': relation(OldBook)}) + # Set up the songs relationships song_props = { 'authors': relation(OldAuthor, backref='songs', secondary=source_authors_songs_table), - 'book': relation(OldBook, backref='songs'), 'topics': relation(OldTopic, backref='songs', secondary=source_songs_topics_table) } if has_media_files: @@ -134,6 +153,11 @@ class OpenLPSongImport(SongImport): relation(OldMediaFile, backref='songs', foreign_keys=[source_media_files_table.c.song_id], primaryjoin=source_songs_table.c.id == source_media_files_table.c.song_id) + if has_songs_books: + song_props['songbook_entries'] = relation(OldSongBookEntry, backref='song', cascade='all, delete-orphan') + else: + song_props['book'] = relation(OldBook, backref='songs') + # Map the rest of the tables try: class_mapper(OldAuthor) except UnmappedClassError: @@ -163,44 +187,54 @@ class OpenLPSongImport(SongImport): old_titles = song.search_title.split('@') if len(old_titles) > 1: new_song.alternate_title = old_titles[1] - # Values will be set when cleaning the song. + # Transfer the values to the new song object new_song.search_title = '' new_song.search_lyrics = '' - new_song.song_number = song.song_number new_song.lyrics = song.lyrics new_song.verse_order = song.verse_order new_song.copyright = song.copyright new_song.comments = song.comments new_song.theme_name = song.theme_name new_song.ccli_number = song.ccli_number + if hasattr(song, 'song_number') and song.song_number: + new_song.song_number = song.song_number + # Find or create all the authors and add them to the new song object for author in song.authors: existing_author = self.manager.get_object_filtered(Author, Author.display_name == author.display_name) - if existing_author is None: + if not existing_author: existing_author = Author.populate( first_name=author.first_name, last_name=author.last_name, display_name=author.display_name) new_song.add_author(existing_author) - if song.book: - existing_song_book = self.manager.get_object_filtered(Book, Book.name == song.book.name) - if existing_song_book is None: - existing_song_book = Book.populate(name=song.book.name, publisher=song.book.publisher) - new_song.book = existing_song_book + # Find or create all the topics and add them to the new song object if song.topics: for topic in song.topics: existing_topic = self.manager.get_object_filtered(Topic, Topic.name == topic.name) - if existing_topic is None: + if not existing_topic: existing_topic = Topic.populate(name=topic.name) new_song.topics.append(existing_topic) - if has_media_files: - if song.media_files: - for media_file in song.media_files: - existing_media_file = self.manager.get_object_filtered( - MediaFile, MediaFile.file_name == media_file.file_name) - if existing_media_file: - new_song.media_files.append(existing_media_file) - else: - new_song.media_files.append(MediaFile.populate(file_name=media_file.file_name)) + # Find or create all the songbooks and add them to the new song object + if has_songs_books and song.songbook_entries: + for entry in song.songbook_entries: + existing_book = self.manager.get_object_filtered(Book, Book.name == entry.songbook.name) + if not existing_book: + existing_book = Book.populate(name=entry.songbook.name, publisher=entry.songbook.publisher) + new_song.add_songbook_entry(existing_book, entry.entry) + elif song.book: + existing_book = self.manager.get_object_filtered(Book, Book.name == song.book.name) + if not existing_book: + existing_book = Book.populate(name=song.book.name, publisher=song.book.publisher) + new_song.add_songbook_entry(existing_book, '') + # Find or create all the media files and add them to the new song object + if has_media_files and song.media_files: + for media_file in song.media_files: + existing_media_file = self.manager.get_object_filtered( + MediaFile, MediaFile.file_name == media_file.file_name) + if existing_media_file: + new_song.media_files.append(existing_media_file) + else: + new_song.media_files.append(MediaFile.populate(file_name=media_file.file_name)) clean_song(self.manager, new_song) self.manager.save_object(new_song) if progress_dialog: diff --git a/openlp/plugins/songs/lib/importers/openlyrics.py b/openlp/plugins/songs/lib/importers/openlyrics.py index c7bde403a..f60023cdf 100644 --- a/openlp/plugins/songs/lib/importers/openlyrics.py +++ b/openlp/plugins/songs/lib/importers/openlyrics.py @@ -29,7 +29,7 @@ import os from lxml import etree -from openlp.core.ui.wizard import WizardStrings +from openlp.core.ui.lib.wizard import WizardStrings from openlp.plugins.songs.lib.importers.songimport import SongImport from openlp.plugins.songs.lib.ui import SongStrings from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics, OpenLyricsError diff --git a/openlp/plugins/songs/lib/importers/powerpraise.py b/openlp/plugins/songs/lib/importers/powerpraise.py index b93eed0fe..93a360542 100644 --- a/openlp/plugins/songs/lib/importers/powerpraise.py +++ b/openlp/plugins/songs/lib/importers/powerpraise.py @@ -27,7 +27,7 @@ Powerpraise song files into the current database. import os from lxml import objectify -from openlp.core.ui.wizard import WizardStrings +from openlp.core.ui.lib.wizard import WizardStrings from .songimport import SongImport diff --git a/openlp/plugins/songs/lib/importers/presentationmanager.py b/openlp/plugins/songs/lib/importers/presentationmanager.py index da31ce953..c26f11312 100644 --- a/openlp/plugins/songs/lib/importers/presentationmanager.py +++ b/openlp/plugins/songs/lib/importers/presentationmanager.py @@ -26,10 +26,11 @@ Presentationmanager song files into the current database. import os import re + import chardet from lxml import objectify, etree -from openlp.core.ui.wizard import WizardStrings +from openlp.core.ui.lib.wizard import WizardStrings from .songimport import SongImport diff --git a/openlp/plugins/songs/lib/importers/propresenter.py b/openlp/plugins/songs/lib/importers/propresenter.py index cddf0e52b..55e05a08f 100644 --- a/openlp/plugins/songs/lib/importers/propresenter.py +++ b/openlp/plugins/songs/lib/importers/propresenter.py @@ -29,7 +29,7 @@ import base64 import logging from lxml import objectify -from openlp.core.ui.wizard import WizardStrings +from openlp.core.ui.lib.wizard import WizardStrings from openlp.plugins.songs.lib import strip_rtf from .songimport import SongImport diff --git a/openlp/plugins/songs/lib/importers/songimport.py b/openlp/plugins/songs/lib/importers/songimport.py index 54c82da29..835386b26 100644 --- a/openlp/plugins/songs/lib/importers/songimport.py +++ b/openlp/plugins/songs/lib/importers/songimport.py @@ -28,7 +28,7 @@ import os from PyQt5 import QtCore from openlp.core.common import Registry, AppLocation, check_directory_exists, translate -from openlp.core.ui.wizard import WizardStrings +from openlp.core.ui.lib.wizard import WizardStrings from openlp.plugins.songs.lib import clean_song, VerseType from openlp.plugins.songs.lib.db import Song, Author, Topic, Book, MediaFile from openlp.plugins.songs.lib.ui import SongStrings diff --git a/openlp/plugins/songs/lib/importers/songshowplus.py b/openlp/plugins/songs/lib/importers/songshowplus.py index 4851894ab..d9a205e22 100644 --- a/openlp/plugins/songs/lib/importers/songshowplus.py +++ b/openlp/plugins/songs/lib/importers/songshowplus.py @@ -29,7 +29,7 @@ import logging import re import struct -from openlp.core.ui.wizard import WizardStrings +from openlp.core.ui.lib.wizard import WizardStrings from openlp.plugins.songs.lib import VerseType, retrieve_windows_encoding from openlp.plugins.songs.lib.importers.songimport import SongImport diff --git a/openlp/plugins/songs/lib/importers/wordsofworship.py b/openlp/plugins/songs/lib/importers/wordsofworship.py index 8b87ee214..6135ae2b8 100644 --- a/openlp/plugins/songs/lib/importers/wordsofworship.py +++ b/openlp/plugins/songs/lib/importers/wordsofworship.py @@ -107,9 +107,9 @@ class WordsOfWorshipImport(SongImport): song_data = open(source, 'rb') if song_data.read(19).decode() != 'WoW File\nSong Words': self.log_error(source, - str(translate('SongsPlugin.WordsofWorshipSongImport', - 'Invalid Words of Worship song file. Missing "%s" header.' - % 'WoW File\\nSong Words'))) + translate('SongsPlugin.WordsofWorshipSongImport', + 'Invalid Words of Worship song file. Missing "%s" header.') + % 'WoW File\\nSong Words') continue # Seek to byte which stores number of blocks in the song song_data.seek(56) @@ -117,9 +117,9 @@ class WordsOfWorshipImport(SongImport): song_data.seek(66) if song_data.read(16).decode() != 'CSongDoc::CBlock': self.log_error(source, - str(translate('SongsPlugin.WordsofWorshipSongImport', - 'Invalid Words of Worship song file. Missing "%s" ' - 'string.' % 'CSongDoc::CBlock'))) + translate('SongsPlugin.WordsofWorshipSongImport', + 'Invalid Words of Worship song file. Missing "%s" string.') + % 'CSongDoc::CBlock') continue # Seek to the beginning of the first block song_data.seek(82) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index d724bfaf2..8edac2877 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -194,13 +194,13 @@ class SongMediaItem(MediaManagerItem): log.debug('Authors Search') search_string = '%' + search_keywords + '%' search_results = self.plugin.manager.get_all_objects( - Author, Author.display_name.like(search_string), Author.display_name.asc()) + Author, Author.display_name.like(search_string)) self.display_results_author(search_results) elif search_type == SongSearch.Topics: log.debug('Topics Search') search_string = '%' + search_keywords + '%' search_results = self.plugin.manager.get_all_objects( - Topic, Topic.name.like(search_string), Topic.name.asc()) + Topic, Topic.name.like(search_string)) self.display_results_topic(search_results) elif search_type == SongSearch.Books: log.debug('Songbook Search') @@ -215,7 +215,7 @@ class SongMediaItem(MediaManagerItem): log.debug('Theme Search') search_string = '%' + search_keywords + '%' search_results = self.plugin.manager.get_all_objects( - Song, Song.theme_name.like(search_string), Song.theme_name.asc()) + Song, Song.theme_name.like(search_string)) self.display_results_themes(search_results) elif search_type == SongSearch.Copyright: log.debug('Copyright Search') @@ -258,10 +258,14 @@ class SongMediaItem(MediaManagerItem): :param search_results: A list of db Song objects :return: None """ + def get_song_key(song): + """Get the key to sort by""" + return song.sort_key + log.debug('display results Song') self.save_auto_select_id() self.list_view.clear() - search_results.sort(key=lambda song: song.sort_key) + search_results.sort(key=get_song_key) for song in search_results: # Do not display temporary songs if song.temporary: @@ -283,12 +287,20 @@ class SongMediaItem(MediaManagerItem): :param search_results: A list of db Author objects :return: None """ + def get_author_key(author): + """Get the key to sort by""" + return get_natural_key(author.display_name) + + def get_song_key(song): + """Get the key to sort by""" + return song.sort_key + log.debug('display results Author') self.list_view.clear() - search_results = sorted(search_results, key=lambda author: get_natural_key(author.display_name)) + search_results.sort(key=get_author_key) for author in search_results: - songs = sorted(author.songs, key=lambda song: song.sort_key) - for song in songs: + author.songs.sort(key=get_song_key) + for song in author.songs: # Do not display temporary songs if song.temporary: continue @@ -304,11 +316,15 @@ class SongMediaItem(MediaManagerItem): :param search_results: A list of db SongBookEntry objects :return: None """ + def get_songbook_key(songbook_entry): + """Get the key to sort by""" + return (get_natural_key(songbook_entry.songbook.name), get_natural_key(songbook_entry.entry)) + log.debug('display results Book') self.list_view.clear() - search_results = sorted(search_results, key=lambda songbook_entry: - (get_natural_key(songbook_entry.songbook.name), get_natural_key(songbook_entry.entry))) + search_results.sort(key=get_songbook_key) for songbook_entry in search_results: + # Do not display temporary songs if songbook_entry.song.temporary: continue song_detail = '%s #%s: %s' % (songbook_entry.songbook.name, songbook_entry.entry, songbook_entry.song.title) @@ -323,12 +339,20 @@ class SongMediaItem(MediaManagerItem): :param search_results: A list of db Topic objects :return: None """ + def get_topic_key(topic): + """Get the key to sort by""" + return get_natural_key(topic.name) + + def get_song_key(song): + """Get the key to sort by""" + return song.sort_key + log.debug('display results Topic') self.list_view.clear() - search_results = sorted(search_results, key=lambda topic: get_natural_key(topic.name)) + search_results.sort(key=get_topic_key) for topic in search_results: - songs = sorted(topic.songs, key=lambda song: song.sort_key) - for song in songs: + topic.songs.sort(key=get_song_key) + for song in topic.songs: # Do not display temporary songs if song.temporary: continue @@ -344,10 +368,13 @@ class SongMediaItem(MediaManagerItem): :param search_results: A list of db Song objects :return: None """ + def get_theme_key(song): + """Get the key to sort by""" + return (get_natural_key(song.theme_name), song.sort_key) + log.debug('display results Themes') self.list_view.clear() - search_results = sorted(search_results, key=lambda song: (get_natural_key(song.theme_name), - song.sort_key)) + search_results.sort(key=get_theme_key) for song in search_results: # Do not display temporary songs if song.temporary: @@ -364,11 +391,14 @@ class SongMediaItem(MediaManagerItem): :param search_results: A list of db Song objects :return: None """ + def get_cclinumber_key(song): + """Get the key to sort by""" + return (get_natural_key(song.ccli_number), song.sort_key) + log.debug('display results CCLI number') self.list_view.clear() - songs = sorted(search_results, key=lambda song: (get_natural_key(song.ccli_number), - song.sort_key)) - for song in songs: + search_results.sort(key=get_cclinumber_key) + for song in search_results: # Do not display temporary songs if song.temporary: continue diff --git a/resources/images/general_preview.png b/resources/images/general_preview.png index d196792bb..2d6b7b631 100644 Binary files a/resources/images/general_preview.png and b/resources/images/general_preview.png differ diff --git a/resources/images/ios_app_qr.png b/resources/images/ios_app_qr.png new file mode 100644 index 000000000..c7244fc33 Binary files /dev/null and b/resources/images/ios_app_qr.png differ 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 68a6ec488..c73b02c48 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -148,6 +148,7 @@ media_audio.png media_video.png media_optical.png + media_repeat.png slidecontroller_multimedia.png auto-start_active.png auto-start_inactive.png @@ -206,5 +207,6 @@ android_app_qr.png + ios_app_qr.png diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index a37ba5374..e6f7d2c37 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -102,6 +102,7 @@ OPTIONAL_MODULES = [ ('nose', '(testing framework)', True), ('mock', '(testing module)', sys.version_info[1] < 3), ('jenkins', '(access jenkins api - package name: jenkins-webapi)', True), + ('pysword', '(import SWORD bibles)', True), ] w = sys.stdout.write diff --git a/tests/functional/__init__.py b/tests/functional/__init__.py index d30361d63..33a4ee96b 100644 --- a/tests/functional/__init__.py +++ b/tests/functional/__init__.py @@ -26,12 +26,12 @@ import sys from PyQt5 import QtWidgets if sys.version_info[1] >= 3: - from unittest.mock import ANY, MagicMock, patch, mock_open, call + from unittest.mock import ANY, MagicMock, patch, mock_open, call, PropertyMock else: - from mock import ANY, MagicMock, patch, mock_open, call + from mock import ANY, MagicMock, patch, mock_open, call, PropertyMock # Only one QApplication can be created. Use QtWidgets.QApplication.instance() when you need to "create" a QApplication. application = QtWidgets.QApplication([]) application.setApplicationName('OpenLP') -__all__ = ['ANY', 'MagicMock', 'patch', 'mock_open', 'call', 'application'] +__all__ = ['ANY', 'MagicMock', 'patch', 'mock_open', 'call', 'application', 'PropertyMock'] diff --git a/tests/functional/openlp_core_common/test_projector_utilities.py b/tests/functional/openlp_core_common/test_projector_utilities.py index d29267de0..aebdd7509 100644 --- a/tests/functional/openlp_core_common/test_projector_utilities.py +++ b/tests/functional/openlp_core_common/test_projector_utilities.py @@ -23,13 +23,12 @@ Package to test the openlp.core.ui.projector.networkutils package. """ -import os - from unittest import TestCase from openlp.core.common import verify_ip_address, md5_hash, qmd5_hash from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_HASH + salt = TEST_SALT pin = TEST_PIN test_hash = TEST_HASH diff --git a/tests/functional/openlp_core_lib/test_htmlbuilder.py b/tests/functional/openlp_core_lib/test_htmlbuilder.py index 8ca98060d..58841eb90 100644 --- a/tests/functional/openlp_core_lib/test_htmlbuilder.py +++ b/tests/functional/openlp_core_lib/test_htmlbuilder.py @@ -197,6 +197,7 @@ FOOTER_CSS_BASE = """ """ FOOTER_CSS = FOOTER_CSS_BASE % ('nowrap') FOOTER_CSS_WRAP = FOOTER_CSS_BASE % ('normal') +FOOTER_CSS_INVALID = '' class Htmbuilder(TestCase, TestMixin): @@ -359,6 +360,27 @@ class Htmbuilder(TestCase, TestMixin): # THEN: Footer should wrap self.assertEqual(FOOTER_CSS_WRAP, css, 'The footer strings should be equal.') + def build_footer_invalid_test(self): + """ + Test the build_footer_css() function + """ + # GIVEN: Create a theme. + css = [] + item = MagicMock() + item.theme_data = None + item.footer = 'FAIL' + height = 1024 + + # WHEN: Settings say that footer should wrap + css.append(build_footer_css(item, height)) + item.theme_data = 'TEST' + item.footer = None + css.append(build_footer_css(item, height)) + + # THEN: Footer should wrap + self.assertEqual(FOOTER_CSS_INVALID, css[0], 'The footer strings should be blank.') + self.assertEqual(FOOTER_CSS_INVALID, css[1], 'The footer strings should be blank.') + def webkit_version_test(self): """ Test the webkit_version() function diff --git a/tests/functional/openlp_core_lib/test_projector_pjlink1.py b/tests/functional/openlp_core_lib/test_projector_pjlink1.py index a3d99e884..5d0d26ceb 100644 --- a/tests/functional/openlp_core_lib/test_projector_pjlink1.py +++ b/tests/functional/openlp_core_lib/test_projector_pjlink1.py @@ -107,3 +107,47 @@ class TestPJLink(TestCase): # THEN: process_inpt method should have been called with 31 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): + """ + Test setting multiple lamp on/off and hours + """ + # GIVEN: Test object + pjlink = pjlink_test + + # WHEN: Call process_command with lamp data + pjlink.process_command('LAMP', '11111 1 22222 0 33333 1') + + # THEN: Lamp should have been set with proper lamp status + self.assertEquals(len(pjlink.lamp), 3, + 'Projector should have 3 lamps specified') + self.assertEquals(pjlink.lamp[0]['On'], True, + 'Lamp 1 power status should have been set to TRUE') + self.assertEquals(pjlink.lamp[0]['Hours'], 11111, + 'Lamp 1 hours should have been set to 11111') + self.assertEquals(pjlink.lamp[1]['On'], False, + 'Lamp 2 power status should have been set to FALSE') + self.assertEquals(pjlink.lamp[1]['Hours'], 22222, + 'Lamp 2 hours should have been set to 22222') + self.assertEquals(pjlink.lamp[2]['On'], True, + 'Lamp 3 power status should have been set to TRUE') + self.assertEquals(pjlink.lamp[2]['Hours'], 33333, + 'Lamp 3 hours should have been set to 33333') 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_lib/__init__.py b/tests/functional/openlp_core_ui_lib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/functional/openlp_core_lib/test_color_button.py b/tests/functional/openlp_core_ui_lib/test_color_button.py similarity index 90% rename from tests/functional/openlp_core_lib/test_color_button.py rename to tests/functional/openlp_core_ui_lib/test_color_button.py index ea71b3bf9..b65b81448 100644 --- a/tests/functional/openlp_core_lib/test_color_button.py +++ b/tests/functional/openlp_core_ui_lib/test_color_button.py @@ -24,7 +24,7 @@ This module contains tests for the openlp.core.lib.filedialog module """ from unittest import TestCase -from openlp.core.lib.colorbutton import ColorButton +from openlp.core.ui.lib.colorbutton import ColorButton from tests.functional import MagicMock, call, patch @@ -33,11 +33,11 @@ class TestColorDialog(TestCase): Test the :class:`~openlp.core.lib.colorbutton.ColorButton` class """ def setUp(self): - self.change_color_patcher = patch('openlp.core.lib.colorbutton.ColorButton.change_color') - self.clicked_patcher = patch('openlp.core.lib.colorbutton.ColorButton.clicked') - self.color_changed_patcher = patch('openlp.core.lib.colorbutton.ColorButton.colorChanged') - self.qt_gui_patcher = patch('openlp.core.lib.colorbutton.QtWidgets') - self.translate_patcher = patch('openlp.core.lib.colorbutton.translate', **{'return_value': 'Tool Tip Text'}) + self.change_color_patcher = patch('openlp.core.ui.lib.colorbutton.ColorButton.change_color') + self.clicked_patcher = patch('openlp.core.ui.lib.colorbutton.ColorButton.clicked') + self.color_changed_patcher = patch('openlp.core.ui.lib.colorbutton.ColorButton.colorChanged') + self.qt_gui_patcher = patch('openlp.core.ui.lib.colorbutton.QtWidgets') + self.translate_patcher = patch('openlp.core.ui.lib.colorbutton.translate', **{'return_value': 'Tool Tip Text'}) self.addCleanup(self.change_color_patcher.stop) self.addCleanup(self.clicked_patcher.stop) self.addCleanup(self.color_changed_patcher.stop) @@ -55,7 +55,7 @@ class TestColorDialog(TestCase): """ # GIVEN: The ColorButton class, a mocked change_color, setToolTip methods and clicked signal - with patch('openlp.core.lib.colorbutton.ColorButton.setToolTip') as mocked_set_tool_tip: + with patch('openlp.core.ui.lib.colorbutton.ColorButton.setToolTip') as mocked_set_tool_tip: # WHEN: The ColorButton object is instantiated widget = ColorButton() @@ -74,7 +74,7 @@ class TestColorDialog(TestCase): self.change_color_patcher.stop() # GIVEN: An instance of the ColorButton object, and a mocked out setStyleSheet - with patch('openlp.core.lib.colorbutton.ColorButton.setStyleSheet') as mocked_set_style_sheet: + with patch('openlp.core.ui.lib.colorbutton.ColorButton.setStyleSheet') as mocked_set_style_sheet: widget = ColorButton() # WHEN: Changing the color @@ -123,7 +123,7 @@ class TestColorDialog(TestCase): """ # GIVEN: An instance of ColorButton, with a mocked __init__ - with patch('openlp.core.lib.colorbutton.ColorButton.__init__', **{'return_value': None}): + with patch('openlp.core.ui.lib.colorbutton.ColorButton.__init__', **{'return_value': None}): widget = ColorButton() # WHEN: Setting the color property diff --git a/tests/functional/openlp_core_ui/test_listpreviewwidget.py b/tests/functional/openlp_core_ui_lib/test_listpreviewwidget.py similarity index 64% rename from tests/functional/openlp_core_ui/test_listpreviewwidget.py rename to tests/functional/openlp_core_ui_lib/test_listpreviewwidget.py index a222189e6..0ed88cc88 100644 --- a/tests/functional/openlp_core_ui/test_listpreviewwidget.py +++ b/tests/functional/openlp_core_ui_lib/test_listpreviewwidget.py @@ -20,12 +20,12 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -Package to test the openlp.core.ui.listpreviewwidget package. +Package to test the openlp.core.ui.lib.listpreviewwidget package. """ from unittest import TestCase from openlp.core.common import Settings -from openlp.core.ui.listpreviewwidget import ListPreviewWidget +from openlp.core.ui.lib.listpreviewwidget import ListPreviewWidget from openlp.core.lib import ServiceItem from tests.functional import MagicMock, patch, call @@ -38,13 +38,13 @@ class TestListPreviewWidget(TestCase): Mock out stuff for all the tests """ # Mock self.parent().width() - self.parent_patcher = patch('openlp.core.ui.listpreviewwidget.ListPreviewWidget.parent') + self.parent_patcher = patch('openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.parent') self.mocked_parent = self.parent_patcher.start() self.mocked_parent.width.return_value = 100 self.addCleanup(self.parent_patcher.stop) # Mock Settings().value() - self.Settings_patcher = patch('openlp.core.ui.listpreviewwidget.Settings') + self.Settings_patcher = patch('openlp.core.ui.lib.listpreviewwidget.Settings') self.mocked_Settings = self.Settings_patcher.start() self.mocked_Settings_obj = MagicMock() self.mocked_Settings_obj.value.return_value = None @@ -52,7 +52,7 @@ class TestListPreviewWidget(TestCase): self.addCleanup(self.Settings_patcher.stop) # Mock self.viewport().width() - self.viewport_patcher = patch('openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') + self.viewport_patcher = patch('openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.viewport') self.mocked_viewport = self.viewport_patcher.start() self.mocked_viewport_obj = MagicMock() self.mocked_viewport_obj.width.return_value = 200 @@ -72,8 +72,8 @@ class TestListPreviewWidget(TestCase): self.assertIsNotNone(list_preview_widget, 'The ListPreviewWidget object should not be None') self.assertEquals(list_preview_widget.screen_ratio, 1, 'Should not be called') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') def replace_recalculate_layout_test_text(self, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled, txt slides unchanged in replace_service_item & __recalc... @@ -104,8 +104,8 @@ class TestListPreviewWidget(TestCase): self.assertEquals(mocked_resizeRowsToContents.call_count, 2, 'Should be called') self.assertEquals(mocked_setRowHeight.call_count, 0, 'Should not be called') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') def replace_recalculate_layout_test_img(self, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." disabled, img slides unchanged in replace_service_item & __recalc... @@ -130,16 +130,18 @@ 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') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') def replace_recalculate_layout_test_img_max(self, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled, img slides resized in replace_service_item & __recalc... @@ -172,9 +174,9 @@ class TestListPreviewWidget(TestCase): calls = [call(0, 100), call(1, 100), call(0, 100), call(1, 100)] mocked_setRowHeight.assert_has_calls(calls) - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.cellWidget') def row_resized_test_text(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled, text-based slides not affected in row_resized. @@ -206,9 +208,9 @@ class TestListPreviewWidget(TestCase): # 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') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.cellWidget') def row_resized_test_img(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." disabled, image-based slides not affected in row_resized. @@ -236,13 +238,15 @@ 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') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.cellWidget') def row_resized_test_img_max(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled, image-based slides are scaled in row_resized. @@ -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.lib.listpreviewwidget.ListPreviewWidget.selectRow') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.scrollToItem') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.item') + @patch(u'openlp.core.ui.lib.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.lib.listpreviewwidget.ListPreviewWidget.selectRow') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.scrollToItem') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.item') + @patch(u'openlp.core.ui.lib.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.lib.listpreviewwidget.ListPreviewWidget.selectRow') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.scrollToItem') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.item') + @patch(u'openlp.core.ui.lib.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_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/bibles/test_mediaitem.py b/tests/functional/openlp_plugins/bibles/test_mediaitem.py index 112448d73..c585ac35a 100644 --- a/tests/functional/openlp_plugins/bibles/test_mediaitem.py +++ b/tests/functional/openlp_plugins/bibles/test_mediaitem.py @@ -114,49 +114,3 @@ class TestMediaItem(TestCase, TestMixin): mocked_list_view.selectAll.assert_called_once_with() self.assertEqual(self.media_item.search_results, {}) self.assertEqual(self.media_item.second_search_results, {}) - - def on_quick_reference_search_test(self): - """ - BOOM BOOM BANANAS - """ - - # GIVEN: A mocked build_display_results which returns an empty list - self.media_item.quickVersionComboBox = MagicMock() - self.media_item.quickSecondComboBox = MagicMock() - self.media_item.quick_search_edit = MagicMock() - - #mocked_text = self.media_item() - #mocked_text.text.return_value = 'Gen. 1' - #self.media_item.text = mocked_text - #self.media_item.text.return_value = 'Gen. 1' - # self.mocked_main_window.information_message = MagicMock() - - self.media_item.search_results = MagicMock() - self.media_item.advancedSearchButton = MagicMock() - self.media_item.advancedSearchButton.setEnabled = MagicMock() - - # WHEN: Calling display_results with a single bible version - self.media_item.banana() - - # THEN: No items should be added to the list, and select all should have been called. - # self.assertEqual(0, self.mocked_main_window.information_message, 'lama') - # mocked_media_item.assert_called_with(mocked_main_window.information_message) - # self.mocked_text.text.assert_called_with('Gen. 1') - # mocked_process_item.assert_called_once_with(mocked_item, 7) - self.media_item.advancedSearchButton.setEnabled.assert_called_once_with(True) - - - """ - def on_quick_reference_search_test(self): - - Test the display_results method a large number of results (> 100) are returned - - - # GIVEN: A mocked build_display_results which returns a large list of results - media_item = BibleMediaItem(MagicMock) - - # WHEN: Calling display_results - #self.media_item.on_quick_reference_search() - - # THEN: addItem should have been called 100 times, and the lsit items should not be selected. - """ \ No newline at end of file diff --git a/tests/functional/openlp_plugins/bibles/test_swordimport.py b/tests/functional/openlp_plugins/bibles/test_swordimport.py new file mode 100644 index 000000000..ae4d9cdf9 --- /dev/null +++ b/tests/functional/openlp_plugins/bibles/test_swordimport.py @@ -0,0 +1,109 @@ +# -*- 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 # +############################################################################### +""" +This module contains tests for the SWORD Bible importer. +""" + +import os +import json +from unittest import TestCase, SkipTest + +from tests.functional import MagicMock, patch +try: + from openlp.plugins.bibles.lib.sword import SwordBible +except ImportError: + raise SkipTest('PySword is not installed, skipping SWORD test.') +from openlp.plugins.bibles.lib.db import BibleDB + +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + '..', '..', '..', 'resources', 'bibles')) + + +class TestSwordImport(TestCase): + """ + Test the functions in the :mod:`swordimport` module. + """ + + def setUp(self): + self.registry_patcher = patch('openlp.plugins.bibles.lib.db.Registry') + self.registry_patcher.start() + self.manager_patcher = patch('openlp.plugins.bibles.lib.db.Manager') + self.manager_patcher.start() + + def tearDown(self): + self.registry_patcher.stop() + self.manager_patcher.stop() + + def create_importer_test(self): + """ + Test creating an instance of the Sword file importer + """ + # GIVEN: A mocked out "manager" + mocked_manager = MagicMock() + + # WHEN: An importer object is created + importer = SwordBible(mocked_manager, path='.', name='.', filename='', sword_key='', sword_path='') + + # THEN: The importer should be an instance of BibleDB + self.assertIsInstance(importer, BibleDB) + + @patch('openlp.plugins.bibles.lib.sword.SwordBible.application') + @patch('openlp.plugins.bibles.lib.sword.modules') + @patch('openlp.plugins.bibles.lib.db.BiblesResourcesDB') + def simple_import_test(self, mocked_bible_res_db, mocked_pysword_modules, mocked_application): + """ + Test that a simple SWORD import works + """ + # GIVEN: Test files with a mocked out "manager", "import_wizard", and mocked functions + # get_book_ref_id_by_name, create_verse, create_book, session and get_language. + # Also mocked pysword structures + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = SwordBible(mocked_manager, path='.', name='.', filename='', sword_key='', sword_path='') + result_file = open(os.path.join(TEST_PATH, 'dk1933.json'), 'rb') + test_data = json.loads(result_file.read().decode()) + importer.wizard = mocked_import_wizard + importer.get_book_ref_id_by_name = MagicMock() + importer.create_verse = MagicMock() + importer.create_book = MagicMock() + importer.session = MagicMock() + mocked_bible_res_db.get_language.return_value = 'Danish' + mocked_bible = MagicMock() + mocked_genesis = MagicMock() + mocked_genesis.name = 'Genesis' + mocked_genesis.num_chapters = 1 + books = {'ot': [mocked_genesis]} + mocked_structure = MagicMock() + mocked_structure.get_books.return_value = books + mocked_bible.get_structure.return_value = mocked_structure + mocked_bible.get_iter.return_value = [verse[1] for verse in test_data['verses']] + mocked_module = MagicMock() + mocked_module.get_bible_from_module.return_value = mocked_bible + mocked_pysword_modules.SwordModules.return_value = mocked_module + + # WHEN: Importing bible file + importer.do_import() + + # THEN: The create_verse() method should have been called with each verse in the file. + self.assertTrue(importer.create_verse.called) + for verse_tag, verse_text in test_data['verses']: + importer.create_verse.assert_any_call(importer.create_book().id, 1, int(verse_tag), verse_text) diff --git a/tests/functional/openlp_plugins/media/test_mediaplugin.py b/tests/functional/openlp_plugins/media/test_mediaplugin.py index 9dbab37cf..c49cdbaa4 100644 --- a/tests/functional/openlp_plugins/media/test_mediaplugin.py +++ b/tests/functional/openlp_plugins/media/test_mediaplugin.py @@ -25,7 +25,7 @@ Test the media plugin from unittest import TestCase from openlp.core import Registry -from openlp.plugins.media.mediaplugin import MediaPlugin +from openlp.plugins.media.mediaplugin import MediaPlugin, process_check_binary from tests.functional import MagicMock, patch from tests.helpers.testmixin import TestMixin @@ -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): @@ -65,3 +63,29 @@ class MediaPluginTest(TestCase, TestMixin): self.assertIsInstance(MediaPlugin.about(), str) # THEN: about() should return a non-empty string self.assertNotEquals(len(MediaPlugin.about()), 0) + + @patch('openlp.plugins.media.mediaplugin.check_binary_exists') + def process_check_binary_pass_test(self, mocked_checked_binary_exists): + """ + Test that the Process check returns true if found + """ + # GIVEN: A media plugin instance + # WHEN: function is called with the correct name + mocked_checked_binary_exists.return_value = str.encode('MediaInfo Command line') + result = process_check_binary('MediaInfo') + + # THEN: The the result should be True + self.assertTrue(result, 'Mediainfo should have been found') + + @patch('openlp.plugins.media.mediaplugin.check_binary_exists') + def process_check_binary_fail_test(self, mocked_checked_binary_exists): + """ + Test that the Process check returns false if not found + """ + # GIVEN: A media plugin instance + # WHEN: function is called with the wrong name + mocked_checked_binary_exists.return_value = str.encode('MediaInfo1 Command line') + result = process_check_binary("MediaInfo1") + + # THEN: The the result should be True + self.assertFalse(result, "Mediainfo should not have been found") diff --git a/tests/functional/openlp_plugins/songs/test_lib.py b/tests/functional/openlp_plugins/songs/test_lib.py index 8cca502b0..77da4f543 100644 --- a/tests/functional/openlp_plugins/songs/test_lib.py +++ b/tests/functional/openlp_plugins/songs/test_lib.py @@ -26,7 +26,7 @@ from unittest import TestCase from openlp.plugins.songs.lib import VerseType, clean_string, clean_title, strip_rtf from openlp.plugins.songs.lib.songcompare import songs_probably_equal, _remove_typos, _op_length -from tests.functional import patch, MagicMock +from tests.functional import patch, MagicMock, PropertyMock class TestLib(TestCase): @@ -477,3 +477,27 @@ class TestVerseType(TestCase): # THEN: The result should be None self.assertIsNone(result, 'The result should be None, but was "%s"' % result) + + @patch('openlp.plugins.songs.lib.VerseType.translated_tags', new_callable=PropertyMock, return_value=['x']) + def from_loose_input_with_invalid_input_test(self, mocked_translated_tags): + """ + Test that the from_loose_input() method returns a sane default when passed an invalid tag and None as default. + """ + # GIVEN: A mocked VerseType.translated_tags + # WHEN: We run the from_loose_input() method with an invalid verse type, we get the specified default back + result = VerseType.from_loose_input('m', None) + + # THEN: The result should be None + self.assertIsNone(result, 'The result should be None, but was "%s"' % result) + + @patch('openlp.plugins.songs.lib.VerseType.translated_tags', new_callable=PropertyMock, return_value=['x']) + def from_loose_input_with_valid_input_test(self, mocked_translated_tags): + """ + Test that the from_loose_input() method returns valid output on valid input. + """ + # GIVEN: A mocked VerseType.translated_tags + # WHEN: We run the from_loose_input() method with a valid verse type, we get the expected VerseType back + result = VerseType.from_loose_input('v') + + # THEN: The result should be a Verse + self.assertEqual(result, VerseType.Verse, 'The result should be a verse, but was "%s"' % result) diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 3cd5f97ba..4b9fd50ee 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -53,6 +53,7 @@ class TestMediaItem(TestCase, TestMixin): self.media_item.list_view.save_auto_select_id = MagicMock() self.media_item.list_view.clear = MagicMock() self.media_item.list_view.addItem = MagicMock() + self.media_item.list_view.setCurrentItem = MagicMock() self.media_item.auto_select_id = -1 self.media_item.display_songbook = False self.media_item.display_copyright_symbol = False @@ -79,13 +80,22 @@ class TestMediaItem(TestCase, TestMixin): mock_song.title = 'My Song' mock_song.sort_key = 'My Song' mock_song.authors = [] + mock_song_temp = MagicMock() + mock_song_temp.id = 2 + mock_song_temp.title = 'My Temporary' + mock_song_temp.sort_key = 'My Temporary' + mock_song_temp.authors = [] mock_author = MagicMock() mock_author.display_name = 'My Author' mock_song.authors.append(mock_author) + mock_song_temp.authors.append(mock_author) mock_song.temporary = False + mock_song_temp.temporary = True mock_search_results.append(mock_song) + mock_search_results.append(mock_song_temp) mock_qlist_widget = MagicMock() MockedQListWidgetItem.return_value = mock_qlist_widget + self.media_item.auto_select_id = 1 # WHEN: I display song search results self.media_item.display_results_song(mock_search_results) @@ -93,9 +103,10 @@ class TestMediaItem(TestCase, TestMixin): # THEN: The current list view is cleared, the widget is created, and the relevant attributes set self.media_item.list_view.clear.assert_called_with() self.media_item.save_auto_select_id.assert_called_with() - MockedQListWidgetItem.assert_called_with('My Song (My Author)') - mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id) - self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) + MockedQListWidgetItem.assert_called_once_with('My Song (My Author)') + mock_qlist_widget.setData.assert_called_once_with(MockedUserRole, mock_song.id) + self.media_item.list_view.addItem.assert_called_once_with(mock_qlist_widget) + self.media_item.list_view.setCurrentItem.assert_called_with(mock_qlist_widget) def display_results_author_test(self): """ @@ -107,13 +118,19 @@ class TestMediaItem(TestCase, TestMixin): mock_search_results = [] mock_author = MagicMock() mock_song = MagicMock() + mock_song_temp = MagicMock() mock_author.display_name = 'My Author' mock_author.songs = [] mock_song.id = 1 mock_song.title = 'My Song' mock_song.sort_key = 'My Song' mock_song.temporary = False + mock_song_temp.id = 2 + mock_song_temp.title = 'My Temporary' + mock_song_temp.sort_key = 'My Temporary' + mock_song_temp.temporary = True mock_author.songs.append(mock_song) + mock_author.songs.append(mock_song_temp) mock_search_results.append(mock_author) mock_qlist_widget = MagicMock() MockedQListWidgetItem.return_value = mock_qlist_widget @@ -123,9 +140,9 @@ class TestMediaItem(TestCase, TestMixin): # THEN: The current list view is cleared, the widget is created, and the relevant attributes set self.media_item.list_view.clear.assert_called_with() - MockedQListWidgetItem.assert_called_with('My Author (My Song)') - mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id) - self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) + MockedQListWidgetItem.assert_called_once_with('My Author (My Song)') + mock_qlist_widget.setData.assert_called_once_with(MockedUserRole, mock_song.id) + self.media_item.list_view.addItem.assert_called_once_with(mock_qlist_widget) def display_results_book_test(self): """ @@ -136,17 +153,27 @@ class TestMediaItem(TestCase, TestMixin): patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole: mock_search_results = [] mock_songbook_entry = MagicMock() + mock_songbook_entry_temp = MagicMock() mock_songbook = MagicMock() mock_song = MagicMock() + mock_song_temp = MagicMock() mock_songbook_entry.entry = '1' + mock_songbook_entry_temp.entry = '2' mock_songbook.name = 'My Book' mock_song.id = 1 mock_song.title = 'My Song' mock_song.sort_key = 'My Song' mock_song.temporary = False + mock_song_temp.id = 2 + mock_song_temp.title = 'My Temporary' + mock_song_temp.sort_key = 'My Temporary' + mock_song_temp.temporary = True mock_songbook_entry.song = mock_song mock_songbook_entry.songbook = mock_songbook + mock_songbook_entry_temp.song = mock_song_temp + mock_songbook_entry_temp.songbook = mock_songbook mock_search_results.append(mock_songbook_entry) + mock_search_results.append(mock_songbook_entry_temp) mock_qlist_widget = MagicMock() MockedQListWidgetItem.return_value = mock_qlist_widget @@ -155,9 +182,9 @@ class TestMediaItem(TestCase, TestMixin): # THEN: The current list view is cleared, the widget is created, and the relevant attributes set self.media_item.list_view.clear.assert_called_with() - MockedQListWidgetItem.assert_called_with('My Book #1: My Song') - mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_songbook_entry.song.id) - self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) + MockedQListWidgetItem.assert_called_once_with('My Book #1: My Song') + mock_qlist_widget.setData.assert_called_once_with(MockedUserRole, mock_songbook_entry.song.id) + self.media_item.list_view.addItem.assert_called_once_with(mock_qlist_widget) def display_results_topic_test(self): """ @@ -169,13 +196,19 @@ class TestMediaItem(TestCase, TestMixin): mock_search_results = [] mock_topic = MagicMock() mock_song = MagicMock() + mock_song_temp = MagicMock() mock_topic.name = 'My Topic' mock_topic.songs = [] mock_song.id = 1 mock_song.title = 'My Song' mock_song.sort_key = 'My Song' mock_song.temporary = False + mock_song_temp.id = 2 + mock_song_temp.title = 'My Temporary' + mock_song_temp.sort_key = 'My Temporary' + mock_song_temp.temporary = True mock_topic.songs.append(mock_song) + mock_topic.songs.append(mock_song_temp) mock_search_results.append(mock_topic) mock_qlist_widget = MagicMock() MockedQListWidgetItem.return_value = mock_qlist_widget @@ -185,9 +218,9 @@ class TestMediaItem(TestCase, TestMixin): # THEN: The current list view is cleared, the widget is created, and the relevant attributes set self.media_item.list_view.clear.assert_called_with() - MockedQListWidgetItem.assert_called_with('My Topic (My Song)') - mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id) - self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) + MockedQListWidgetItem.assert_called_once_with('My Topic (My Song)') + mock_qlist_widget.setData.assert_called_once_with(MockedUserRole, mock_song.id) + self.media_item.list_view.addItem.assert_called_once_with(mock_qlist_widget) def display_results_themes_test(self): """ @@ -198,12 +231,19 @@ class TestMediaItem(TestCase, TestMixin): patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole: mock_search_results = [] mock_song = MagicMock() + mock_song_temp = MagicMock() mock_song.id = 1 mock_song.title = 'My Song' mock_song.sort_key = 'My Song' mock_song.theme_name = 'My Theme' mock_song.temporary = False + mock_song_temp.id = 2 + mock_song_temp.title = 'My Temporary' + mock_song_temp.sort_key = 'My Temporary' + mock_song_temp.theme_name = 'My Theme' + mock_song_temp.temporary = True mock_search_results.append(mock_song) + mock_search_results.append(mock_song_temp) mock_qlist_widget = MagicMock() MockedQListWidgetItem.return_value = mock_qlist_widget @@ -212,9 +252,9 @@ class TestMediaItem(TestCase, TestMixin): # THEN: The current list view is cleared, the widget is created, and the relevant attributes set self.media_item.list_view.clear.assert_called_with() - MockedQListWidgetItem.assert_called_with('My Theme (My Song)') - mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id) - self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) + MockedQListWidgetItem.assert_called_once_with('My Theme (My Song)') + mock_qlist_widget.setData.assert_called_once_with(MockedUserRole, mock_song.id) + self.media_item.list_view.addItem.assert_called_once_with(mock_qlist_widget) def display_results_cclinumber_test(self): """ @@ -225,12 +265,19 @@ class TestMediaItem(TestCase, TestMixin): patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole: mock_search_results = [] mock_song = MagicMock() + mock_song_temp = MagicMock() mock_song.id = 1 mock_song.title = 'My Song' mock_song.sort_key = 'My Song' mock_song.ccli_number = '12345' mock_song.temporary = False + mock_song_temp.id = 2 + mock_song_temp.title = 'My Temporary' + mock_song_temp.sort_key = 'My Temporary' + mock_song_temp.ccli_number = '12346' + mock_song_temp.temporary = True mock_search_results.append(mock_song) + mock_search_results.append(mock_song_temp) mock_qlist_widget = MagicMock() MockedQListWidgetItem.return_value = mock_qlist_widget @@ -239,9 +286,9 @@ class TestMediaItem(TestCase, TestMixin): # THEN: The current list view is cleared, the widget is created, and the relevant attributes set self.media_item.list_view.clear.assert_called_with() - MockedQListWidgetItem.assert_called_with('12345 (My Song)') - mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id) - self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) + MockedQListWidgetItem.assert_called_once_with('12345 (My Song)') + mock_qlist_widget.setData.assert_called_once_with(MockedUserRole, mock_song.id) + self.media_item.list_view.addItem.assert_called_once_with(mock_qlist_widget) def build_song_footer_one_author_test(self): """ diff --git a/tests/functional/openlp_plugins/songs/test_openlpimporter.py b/tests/functional/openlp_plugins/songs/test_openlpimporter.py new file mode 100644 index 000000000..113db16e0 --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_openlpimporter.py @@ -0,0 +1,76 @@ +# -*- 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 # +############################################################################### +""" +This module contains tests for the OpenLP song importer. +""" +from unittest import TestCase + +from openlp.plugins.songs.lib.importers.openlp import OpenLPSongImport +from openlp.core.common import Registry +from tests.functional import patch, MagicMock + + +class TestOpenLPImport(TestCase): + """ + Test the functions in the :mod:`openlp` importer module. + """ + def setUp(self): + """ + Create the registry + """ + Registry.create() + + def create_importer_test(self): + """ + Test creating an instance of the OpenLP database importer + """ + # GIVEN: A mocked out SongImport class, and a mocked out "manager" + with patch('openlp.plugins.songs.lib.importers.openlp.SongImport'): + mocked_manager = MagicMock() + + # WHEN: An importer object is created + importer = OpenLPSongImport(mocked_manager, filenames=[]) + + # THEN: The importer object should not be None + self.assertIsNotNone(importer, 'Import should not be none') + + def invalid_import_source_test(self): + """ + Test OpenLPSongImport.do_import handles different invalid import_source values + """ + # GIVEN: A mocked out SongImport class, and a mocked out "manager" + with patch('openlp.plugins.songs.lib.importers.openlp.SongImport'): + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = OpenLPSongImport(mocked_manager, filenames=[]) + importer.import_wizard = mocked_import_wizard + importer.stop_import_flag = True + + # WHEN: Import source is not a list + for source in ['not a list', 0]: + importer.import_source = source + + # THEN: do_import should return none and the progress bar maximum should not be set. + self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is not a list') + self.assertEqual(mocked_import_wizard.progress_bar.setMaximum.called, False, + 'setMaximum on import_wizard.progress_bar should not have been called') + 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_ui/test_listpreviewwidget.py b/tests/interfaces/openlp_core_ui_lib/test_listpreviewwidget.py similarity index 95% rename from tests/interfaces/openlp_core_ui/test_listpreviewwidget.py rename to tests/interfaces/openlp_core_ui_lib/test_listpreviewwidget.py index b4e5fdbf0..3e0e48e8b 100644 --- a/tests/interfaces/openlp_core_ui/test_listpreviewwidget.py +++ b/tests/interfaces/openlp_core_ui_lib/test_listpreviewwidget.py @@ -20,7 +20,7 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ - Package to test the openlp.core.ui.listpreviewwidget. + Package to test the openlp.core.ui.lib.listpreviewwidget. """ from unittest import TestCase @@ -29,7 +29,7 @@ from PyQt5 import QtGui, QtWidgets from openlp.core.common import Registry from openlp.core.lib import ServiceItem -from openlp.core.ui import listpreviewwidget +from openlp.core.ui.lib import ListWidgetWithDnD, ListPreviewWidget from tests.interfaces import MagicMock, patch from tests.utils.osdinteraction import read_service_from_file from tests.helpers.testmixin import TestMixin @@ -48,7 +48,7 @@ class TestListPreviewWidget(TestCase, TestMixin): self.image_manager = MagicMock() self.image_manager.get_image.return_value = self.image Registry().register('image_manager', self.image_manager) - self.preview_widget = listpreviewwidget.ListPreviewWidget(self.main_window, 2) + self.preview_widget = ListPreviewWidget(self.main_window, 2) def tearDown(self): """ 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/interfaces/openlp_plugins/bibles/forms/test_bibleimportform.py b/tests/interfaces/openlp_plugins/bibles/forms/test_bibleimportform.py index 76d0195f5..25dcb9d45 100644 --- a/tests/interfaces/openlp_plugins/bibles/forms/test_bibleimportform.py +++ b/tests/interfaces/openlp_plugins/bibles/forms/test_bibleimportform.py @@ -27,7 +27,7 @@ from unittest import TestCase from PyQt5 import QtWidgets from openlp.core.common import Registry -from openlp.plugins.bibles.forms.bibleimportform import BibleImportForm, WebDownload +import openlp.plugins.bibles.forms.bibleimportform as bibleimportform from tests.helpers.testmixin import TestMixin from tests.functional import MagicMock, patch @@ -46,7 +46,8 @@ class TestBibleImportForm(TestCase, TestMixin): self.setup_application() self.main_window = QtWidgets.QMainWindow() Registry().register('main_window', self.main_window) - self.form = BibleImportForm(self.main_window, MagicMock(), MagicMock()) + bibleimportform.PYSWORD_AVAILABLE = False + self.form = bibleimportform.BibleImportForm(self.main_window, MagicMock(), MagicMock()) def tearDown(self): """ @@ -76,3 +77,16 @@ class TestBibleImportForm(TestCase, TestMixin): # THEN: The webbible list should still be empty self.assertEqual(self.form.web_bible_list, {}, 'The webbible list should be empty') + + def custom_init_test(self): + """ + Test that custom_init works as expected if pysword is unavailable + """ + # GIVEN: A mocked sword_tab_widget + self.form.sword_tab_widget = MagicMock() + + # WHEN: Running custom_init + self.form.custom_init() + + # THEN: sword_tab_widget.setDisabled(True) should have been called + self.form.sword_tab_widget.setDisabled.assert_called_with(True) 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