This commit is contained in:
Philip Ridout 2017-05-30 19:50:39 +01:00
commit a4ffbce6c3
40 changed files with 1086 additions and 552 deletions

View File

@ -24,7 +24,7 @@ The :mod:`common` module contains most of the components and libraries that make
OpenLP work.
"""
import hashlib
import importlib
import logging
import os
import re
@ -32,6 +32,7 @@ import sys
import traceback
from chardet.universaldetector import UniversalDetector
from ipaddress import IPv4Address, IPv6Address, AddressValueError
from pathlib import Path
from shutil import which
from subprocess import check_output, CalledProcessError, STDOUT
@ -79,6 +80,49 @@ def check_directory_exists(directory, do_not_log=False):
log.exception('failed to check if directory exists or create directory')
def extension_loader(glob_pattern, excluded_files=[]):
"""
A utility function to find and load OpenLP extensions, such as plugins, presentation and media controllers and
importers.
:param glob_pattern: A glob pattern used to find the extension(s) to be imported. Should be relative to the
application directory. i.e. openlp/plugins/*/*plugin.py
:type glob_pattern: str
:param excluded_files: A list of file names to exclude that the glob pattern may find.
:type excluded_files: list of strings
:return: None
:rtype: None
"""
app_dir = Path(AppLocation.get_directory(AppLocation.AppDir)).parent
for extension_path in app_dir.glob(glob_pattern):
extension_path = extension_path.relative_to(app_dir)
if extension_path.name in excluded_files:
continue
module_name = path_to_module(extension_path)
try:
importlib.import_module(module_name)
except (ImportError, OSError):
# On some platforms importing vlc.py might cause OSError exceptions. (e.g. Mac OS X)
log.warning('Failed to import {module_name} on path {extension_path}'
.format(module_name=module_name, extension_path=str(extension_path)))
def path_to_module(path):
"""
Convert a path to a module name (i.e openlp.core.common)
:param path: The path to convert to a module name.
:type path: Path
:return: The module name.
:rtype: str
"""
module_path = path.with_suffix('')
return '.'.join(module_path.parts)
def get_frozen_path(frozen_option, non_frozen_option):
"""
Return a path based on the system status.
@ -398,7 +442,7 @@ def check_binary_exists(program_path):
"""
Function that checks whether a binary exists.
:param program_path:The full path to the binary to check.
:param program_path: The full path to the binary to check.
:return: program output to be parsed
"""
log.debug('testing program_path: {text}'.format(text=program_path))

View File

@ -621,5 +621,5 @@ from .imagemanager import ImageManager
from .renderer import Renderer
from .mediamanageritem import MediaManagerItem
from .projector.db import ProjectorDB, Projector
from .projector.pjlink1 import PJLink1
from .projector.pjlink1 import PJLink
from .projector.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING

View File

@ -23,10 +23,9 @@
Provide plugin management
"""
import os
import imp
from openlp.core.lib import Plugin, PluginStatus
from openlp.core.common import AppLocation, RegistryProperties, OpenLPMixin, RegistryMixin
from openlp.core.common import AppLocation, RegistryProperties, OpenLPMixin, RegistryMixin, extension_loader
class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
@ -70,32 +69,8 @@ class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
"""
Scan a directory for objects inheriting from the ``Plugin`` class.
"""
start_depth = len(os.path.abspath(self.base_path).split(os.sep))
present_plugin_dir = os.path.join(self.base_path, 'presentations')
self.log_debug('finding plugins in {path} at depth {depth:d}'.format(path=self.base_path, depth=start_depth))
for root, dirs, files in os.walk(self.base_path):
for name in files:
if name.endswith('.py') and not name.startswith('__'):
path = os.path.abspath(os.path.join(root, name))
this_depth = len(path.split(os.sep))
if this_depth - start_depth > 2:
# skip anything lower down
break
module_name = name[:-3]
# import the modules
self.log_debug('Importing {name} from {root}. Depth {depth:d}'.format(name=module_name,
root=root,
depth=this_depth))
try:
# Use the "imp" library to try to get around a problem with the PyUNO library which
# monkey-patches the __import__ function to do some magic. This causes issues with our tests.
# First, try to find the module we want to import, searching the directory in root
fp, path_name, description = imp.find_module(module_name, [root])
# Then load the module (do the actual import) using the details from find_module()
imp.load_module(module_name, fp, path_name, description)
except ImportError as e:
self.log_exception('Failed to import module {name} on path {path}: '
'{args}'.format(name=module_name, path=path, args=e.args[0]))
glob_pattern = os.path.join('openlp', 'plugins', '*', '*plugin.py')
extension_loader(glob_pattern)
plugin_classes = Plugin.__subclasses__()
plugin_objects = []
for p in plugin_classes:

View File

@ -150,11 +150,15 @@ class Projector(CommonBase, Base):
name: Column(String(20))
location: Column(String(30))
notes: Column(String(200))
pjlink_name: Column(String(128)) # From projector (future)
manufacturer: Column(String(128)) # From projector (future)
model: Column(String(128)) # From projector (future)
other: Column(String(128)) # From projector (future)
sources: Column(String(128)) # From projector (future)
pjlink_name: Column(String(128)) # From projector
manufacturer: Column(String(128)) # From projector
model: Column(String(128)) # From projector
other: Column(String(128)) # From projector
sources: Column(String(128)) # From projector
serial_no: Column(String(30)) # From projector (Class 2)
sw_version: Column(String(30)) # From projector (Class 2)
model_filter: Column(String(30)) # From projector (Class 2)
model_lamp: Column(String(30)) # From projector (Class 2)
ProjectorSource relates
"""
@ -164,20 +168,25 @@ class Projector(CommonBase, Base):
"""
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)
'manufacturer="{manufacturer}", model="{model}", serial_no="{serial}", other="{other}", ' \
'sources="{sources}", source_list="{source_list}", model_filter="{mfilter}", ' \
'model_lamp="{mlamp}", sw_version="{sw_ver}") >'.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,
serial=self.serial_no,
mfilter=self.model_filter,
mlamp=self.model_lamp,
sw_ver=self.sw_version)
ip = Column(String(100))
port = Column(String(8))
pin = Column(String(20))
@ -189,6 +198,10 @@ class Projector(CommonBase, Base):
model = Column(String(128))
other = Column(String(128))
sources = Column(String(128))
serial_no = Column(String(30))
sw_version = Column(String(30))
model_filter = Column(String(30))
model_lamp = Column(String(30))
source_list = relationship('ProjectorSource',
order_by='ProjectorSource.code',
backref='projector',
@ -359,6 +372,10 @@ class ProjectorDB(Manager):
old_projector.model = projector.model
old_projector.other = projector.other
old_projector.sources = projector.sources
old_projector.serial_no = projector.serial_no
old_projector.sw_version = projector.sw_version
old_projector.model_filter = projector.model_filter
old_projector.model_lamp = projector.model_lamp
return self.save_object(old_projector)
def delete_projector(self, projector):

View File

@ -42,7 +42,7 @@ log = logging.getLogger(__name__)
log.debug('pjlink1 loaded')
__all__ = ['PJLink1']
__all__ = ['PJLink']
from codecs import decode
@ -68,7 +68,7 @@ PJLINK_HEADER = '{prefix}{{linkclass}}'.format(prefix=PJLINK_PREFIX)
PJLINK_SUFFIX = CR
class PJLink1(QtNetwork.QTcpSocket):
class PJLink(QtNetwork.QTcpSocket):
"""
Socket service for connecting to a PJLink-capable projector.
"""
@ -129,7 +129,7 @@ class PJLink1(QtNetwork.QTcpSocket):
self.ip = ip
self.port = port
self.pin = pin
super(PJLink1, self).__init__()
super(PJLink, self).__init__()
self.dbid = None
self.location = None
self.notes = None
@ -162,7 +162,7 @@ class PJLink1(QtNetwork.QTcpSocket):
# Socket timer for some possible brain-dead projectors or network cable pulled
self.socket_timer = None
# Map command to function
self.pjlink1_functions = {
self.pjlink_functions = {
'AVMT': self.process_avmt,
'CLSS': self.process_clss,
'ERST': self.process_erst,
@ -286,7 +286,7 @@ class PJLink1(QtNetwork.QTcpSocket):
elif status in STATUS_STRING:
return STATUS_STRING[status], ERROR_MSG[status]
else:
return status, translate('OpenLP.PJLink1', 'Unknown status')
return status, translate('OpenLP.PJLink', 'Unknown status')
def change_status(self, status, msg=None):
"""
@ -296,7 +296,7 @@ class PJLink1(QtNetwork.QTcpSocket):
:param status: Status code
:param msg: Optional message
"""
message = translate('OpenLP.PJLink1', 'No message') if msg is None else msg
message = translate('OpenLP.PJLink', 'No message') if msg is None else msg
(code, message) = self._get_status(status)
if msg is not None:
message = msg
@ -576,7 +576,7 @@ class PJLink1(QtNetwork.QTcpSocket):
if sent == -1:
# Network error?
self.change_status(E_NETWORK,
translate('OpenLP.PJLink1', 'Error while sending data to projector'))
translate('OpenLP.PJLink', 'Error while sending data to projector'))
def process_command(self, cmd, data):
"""
@ -625,8 +625,9 @@ class PJLink1(QtNetwork.QTcpSocket):
self.projectorReceivedData.emit()
return
if cmd in self.pjlink1_functions:
self.pjlink1_functions[cmd](data)
if cmd in self.pjlink_functions:
log.debug('({ip}) Calling function for {cmd}'.format(ip=self.ip, cmd=cmd))
self.pjlink_functions[cmd](data)
else:
log.warning('({ip}) Invalid command {data}'.format(ip=self.ip, data=cmd))
self.send_busy = False
@ -662,6 +663,7 @@ class PJLink1(QtNetwork.QTcpSocket):
:param data: Power status
"""
log.debug('({ip}: Processing POWR command'.format(ip=self.ip))
if data in PJLINK_POWR_STATUS:
power = PJLINK_POWR_STATUS[data]
update_icons = self.power != power

View File

@ -164,7 +164,7 @@ class ThemeXML(object):
jsn = get_text_file_string(json_file)
jsn = json.loads(jsn)
self.expand_json(jsn)
self.background_filename = None
self.background_filename = ''
def expand_json(self, var, prev=None):
"""

View File

@ -25,13 +25,13 @@ The :mod:`advancedtab` provides an advanced settings facility.
from datetime import datetime, timedelta
import logging
import os
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
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
from openlp.core.lib import SettingsTab, build_icon
from openlp.core.ui.lib import PathEdit, PathType
log = logging.getLogger(__name__)
@ -153,32 +153,17 @@ class AdvancedTab(SettingsTab):
self.data_directory_group_box.setObjectName('data_directory_group_box')
self.data_directory_layout = QtWidgets.QFormLayout(self.data_directory_group_box)
self.data_directory_layout.setObjectName('data_directory_layout')
self.data_directory_current_label = QtWidgets.QLabel(self.data_directory_group_box)
self.data_directory_current_label.setObjectName('data_directory_current_label')
self.data_directory_label = QtWidgets.QLabel(self.data_directory_group_box)
self.data_directory_label.setObjectName('data_directory_label')
self.data_directory_new_label = QtWidgets.QLabel(self.data_directory_group_box)
self.data_directory_new_label.setObjectName('data_directory_current_label')
self.new_data_directory_edit = QtWidgets.QLineEdit(self.data_directory_group_box)
self.new_data_directory_edit.setObjectName('new_data_directory_edit')
self.new_data_directory_edit.setReadOnly(True)
self.data_directory_path_edit = PathEdit(self.data_directory_group_box, path_type=PathType.Directories,
default_path=AppLocation.get_directory(AppLocation.DataDir))
self.data_directory_layout.addRow(self.data_directory_new_label, self.data_directory_path_edit)
self.new_data_directory_has_files_label = QtWidgets.QLabel(self.data_directory_group_box)
self.new_data_directory_has_files_label.setObjectName('new_data_directory_has_files_label')
self.new_data_directory_has_files_label.setWordWrap(True)
self.data_directory_browse_button = QtWidgets.QToolButton(self.data_directory_group_box)
self.data_directory_browse_button.setObjectName('data_directory_browse_button')
self.data_directory_browse_button.setIcon(build_icon(':/general/general_open.png'))
self.data_directory_default_button = QtWidgets.QToolButton(self.data_directory_group_box)
self.data_directory_default_button.setObjectName('data_directory_default_button')
self.data_directory_default_button.setIcon(build_icon(':/general/general_revert.png'))
self.data_directory_cancel_button = QtWidgets.QToolButton(self.data_directory_group_box)
self.data_directory_cancel_button.setObjectName('data_directory_cancel_button')
self.data_directory_cancel_button.setIcon(build_icon(':/general/general_delete.png'))
self.new_data_directory_label_layout = QtWidgets.QHBoxLayout()
self.new_data_directory_label_layout.setObjectName('new_data_directory_label_layout')
self.new_data_directory_label_layout.addWidget(self.new_data_directory_edit)
self.new_data_directory_label_layout.addWidget(self.data_directory_browse_button)
self.new_data_directory_label_layout.addWidget(self.data_directory_default_button)
self.data_directory_copy_check_layout = QtWidgets.QHBoxLayout()
self.data_directory_copy_check_layout.setObjectName('data_directory_copy_check_layout')
self.data_directory_copy_check_box = QtWidgets.QCheckBox(self.data_directory_group_box)
@ -186,8 +171,6 @@ class AdvancedTab(SettingsTab):
self.data_directory_copy_check_layout.addWidget(self.data_directory_copy_check_box)
self.data_directory_copy_check_layout.addStretch()
self.data_directory_copy_check_layout.addWidget(self.data_directory_cancel_button)
self.data_directory_layout.addRow(self.data_directory_current_label, self.data_directory_label)
self.data_directory_layout.addRow(self.data_directory_new_label, self.new_data_directory_label_layout)
self.data_directory_layout.addRow(self.data_directory_copy_check_layout)
self.data_directory_layout.addRow(self.new_data_directory_has_files_label)
self.left_layout.addWidget(self.data_directory_group_box)
@ -239,8 +222,7 @@ class AdvancedTab(SettingsTab):
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.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)
self.data_directory_path_edit.pathChanged.connect(self.on_data_directory_path_edit_path_changed)
self.data_directory_cancel_button.clicked.connect(self.on_data_directory_cancel_button_clicked)
self.data_directory_copy_check_box.toggled.connect(self.on_data_directory_copy_check_box_toggled)
self.end_slide_radio_button.clicked.connect(self.on_end_slide_button_clicked)
@ -317,12 +299,7 @@ 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.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',
'Browse for new data file location.'))
self.data_directory_default_button.setToolTip(
translate('OpenLP.AdvancedTab', 'Set the data location to the default.'))
self.data_directory_new_label.setText(translate('OpenLP.AdvancedTab', 'Path:'))
self.data_directory_cancel_button.setText(translate('OpenLP.AdvancedTab', 'Cancel'))
self.data_directory_cancel_button.setToolTip(
translate('OpenLP.AdvancedTab', 'Cancel OpenLP data directory location change.'))
@ -396,8 +373,7 @@ class AdvancedTab(SettingsTab):
self.new_data_directory_has_files_label.hide()
self.data_directory_cancel_button.hide()
# Since data location can be changed, make sure the path is present.
self.current_data_path = AppLocation.get_data_path()
self.data_directory_label.setText(os.path.abspath(self.current_data_path))
self.data_directory_path_edit.path = AppLocation.get_data_path()
# Don't allow data directory move if running portable.
if settings.value('advanced/is portable'):
self.data_directory_group_box.hide()
@ -509,24 +485,10 @@ class AdvancedTab(SettingsTab):
self.service_name_edit.setText(UiStrings().DefaultServiceName)
self.service_name_edit.setFocus()
def on_data_directory_browse_button_clicked(self):
def on_data_directory_path_edit_path_changed(self, new_data_path):
"""
Browse for a new data directory location.
"""
old_root_path = str(self.data_directory_label.text())
# Get the new directory location.
new_data_path = QtWidgets.QFileDialog.getExistingDirectory(self, translate('OpenLP.AdvancedTab',
'Select Data Directory Location'),
old_root_path,
options=QtWidgets.QFileDialog.ShowDirsOnly)
# Set the new data path.
if new_data_path:
new_data_path = os.path.normpath(new_data_path)
if self.current_data_path.lower() == new_data_path.lower():
self.on_data_directory_cancel_button_clicked()
return
else:
return
# Make sure they want to change the data.
answer = QtWidgets.QMessageBox.question(self, translate('OpenLP.AdvancedTab', 'Confirm Data Directory Change'),
translate('OpenLP.AdvancedTab', 'Are you sure you want to change the '
@ -535,40 +497,14 @@ class AdvancedTab(SettingsTab):
'closed.').format(path=new_data_path),
defaultButton=QtWidgets.QMessageBox.No)
if answer != QtWidgets.QMessageBox.Yes:
self.data_directory_path_edit.path = AppLocation.get_data_path()
return
# Check if data already exists here.
self.check_data_overwrite(new_data_path)
# Save the new location.
self.main_window.set_new_data_path(new_data_path)
self.new_data_directory_edit.setText(new_data_path)
self.data_directory_cancel_button.show()
def on_data_directory_default_button_clicked(self):
"""
Re-set the data directory location to the 'default' location.
"""
new_data_path = AppLocation.get_directory(AppLocation.DataDir)
if self.current_data_path.lower() != new_data_path.lower():
# Make sure they want to change the data location back to the
# default.
answer = QtWidgets.QMessageBox.question(self, translate('OpenLP.AdvancedTab', 'Reset Data Directory'),
translate('OpenLP.AdvancedTab', 'Are you sure you want to change '
'the location of the OpenLP data '
'directory to the default location?'
'\n\nThis location will be used '
'after OpenLP is closed.'),
defaultButton=QtWidgets.QMessageBox.No)
if answer != QtWidgets.QMessageBox.Yes:
return
self.check_data_overwrite(new_data_path)
# Save the new location.
self.main_window.set_new_data_path(new_data_path)
self.new_data_directory_edit.setText(os.path.abspath(new_data_path))
self.data_directory_cancel_button.show()
else:
# We cancel the change in case user changed their mind.
self.on_data_directory_cancel_button_clicked()
def on_data_directory_copy_check_box_toggled(self):
"""
Copy existing data when you change your data directory.
@ -585,7 +521,6 @@ class AdvancedTab(SettingsTab):
Check if there's already data in the target directory.
"""
test_path = os.path.join(data_path, 'songs')
self.data_directory_copy_check_box.show()
if os.path.exists(test_path):
self.data_exists = True
# Check is they want to replace existing data.
@ -599,6 +534,7 @@ class AdvancedTab(SettingsTab):
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
QtWidgets.QMessageBox.No),
QtWidgets.QMessageBox.No)
self.data_directory_copy_check_box.show()
if answer == QtWidgets.QMessageBox.Yes:
self.data_directory_copy_check_box.setChecked(True)
self.new_data_directory_has_files_label.show()
@ -614,7 +550,7 @@ class AdvancedTab(SettingsTab):
"""
Cancel the data directory location change
"""
self.new_data_directory_edit.clear()
self.data_directory_path_edit.path = AppLocation.get_data_path()
self.data_directory_copy_check_box.setChecked(False)
self.main_window.set_new_data_path(None)
self.main_window.set_copy_data(False)

View File

@ -27,8 +27,8 @@ import logging
from PyQt5 import QtCore, QtGui, QtWidgets
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
from openlp.core.lib import SettingsTab, ScreenList
from openlp.core.ui.lib import ColorButton, PathEdit
log = logging.getLogger(__name__)
@ -172,20 +172,8 @@ class GeneralTab(SettingsTab):
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_file_path_edit = PathEdit(self.logo_group_box, default_path=':/graphics/openlp-splash-screen.png')
self.logo_layout.addRow(self.logo_file_label, self.logo_file_path_edit)
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)
@ -196,8 +184,6 @@ class GeneralTab(SettingsTab):
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')
@ -254,8 +240,6 @@ class GeneralTab(SettingsTab):
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'))
@ -282,6 +266,9 @@ class GeneralTab(SettingsTab):
self.audio_group_box.setTitle(translate('OpenLP.GeneralTab', 'Background Audio'))
self.start_paused_check_box.setText(translate('OpenLP.GeneralTab', 'Start background audio paused'))
self.repeat_list_check_box.setText(translate('OpenLP.GeneralTab', 'Repeat track list'))
self.logo_file_path_edit.dialog_caption = dialog_caption = translate('OpenLP.AdvancedTab', 'Select Logo File')
self.logo_file_path_edit.filters = '{text};;{names} (*)'.format(
text=get_images_filter(), names=UiStrings().AllFiles)
def load(self):
"""
@ -304,7 +291,7 @@ class GeneralTab(SettingsTab):
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_file_path_edit.path = 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'))
@ -338,7 +325,7 @@ class GeneralTab(SettingsTab):
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 file', self.logo_file_path_edit.path)
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())
@ -404,25 +391,6 @@ class GeneralTab(SettingsTab):
"""
self.display_changed = True
def on_logo_browse_button_clicked(self):
"""
Select the logo file
"""
file_filters = '{text};;{names} (*.*)'.format(text=get_images_filter(), names=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.

View File

@ -21,14 +21,16 @@
###############################################################################
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
from .listwidgetwithdnd import ListWidgetWithDnD
from .mediadockmanager import MediaDockManager
from .dockwidget import OpenLPDockWidget
from .toolbar import OpenLPToolbar
from .wizard import OpenLPWizard, WizardStrings
from .pathedit import PathEdit, PathType
from .spelltextedit import SpellTextEdit
from .treewidgetwithdnd import TreeWidgetWithDnD
__all__ = ['ColorButton', 'ListPreviewWidget', 'ListWidgetWithDnD', 'OpenLPToolbar', 'OpenLPDockWidget',
'OpenLPWizard', 'WizardStrings', 'MediaDockManager', 'ListPreviewWidget', 'SpellTextEdit']
__all__ = ['ColorButton', 'ListPreviewWidget', 'ListWidgetWithDnD', 'MediaDockManager', 'OpenLPDockWidget',
'OpenLPToolbar', 'OpenLPWizard', 'PathEdit', 'PathType', 'SpellTextEdit', 'TreeWidgetWithDnD',
'WizardStrings']

View File

@ -39,7 +39,7 @@ class ColorButton(QtWidgets.QPushButton):
"""
Initialise the ColorButton
"""
super(ColorButton, self).__init__()
super().__init__(parent)
self.parent = parent
self.change_color('#ffffff')
self.setToolTip(translate('OpenLP.ColorButton', 'Click to select a color.'))

205
openlp/core/ui/lib/pathedit.py Executable file
View File

@ -0,0 +1,205 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 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 #
###############################################################################
from enum import Enum
import os.path
from PyQt5 import QtCore, QtWidgets
from openlp.core.common import UiStrings, translate
from openlp.core.lib import build_icon
class PathType(Enum):
Files = 1
Directories = 2
class PathEdit(QtWidgets.QWidget):
"""
The :class:`~openlp.core.ui.lib.pathedit.PathEdit` class subclasses QWidget to create a custom widget for use when
a file or directory needs to be selected.
"""
pathChanged = QtCore.pyqtSignal(str)
def __init__(self, parent=None, path_type=PathType.Files, default_path=None, dialog_caption=None, show_revert=True):
"""
Initalise the PathEdit widget
:param parent: The parent of the widget. This is just passed to the super method.
:type parent: QWidget or None
:param dialog_caption: Used to customise the caption in the QFileDialog.
:param dialog_caption: str
:param default_path: The default path. This is set as the path when the revert button is clicked
:type default_path: str
:param show_revert: Used to determin if the 'revert button' should be visible.
:type show_revert: bool
:return: None
:rtype: None
"""
super().__init__(parent)
self.default_path = default_path
self.dialog_caption = dialog_caption
self._path_type = path_type
self._path = None
self.filters = '{all_files} (*)'.format(all_files=UiStrings().AllFiles)
self._setup(show_revert)
def _setup(self, show_revert):
"""
Set up the widget
:param show_revert: Show or hide the revert button
:type show_revert: bool
:return: None
:rtype: None
"""
widget_layout = QtWidgets.QHBoxLayout()
widget_layout.setContentsMargins(0, 0, 0, 0)
self.line_edit = QtWidgets.QLineEdit(self)
self.line_edit.setText(self._path)
widget_layout.addWidget(self.line_edit)
self.browse_button = QtWidgets.QToolButton(self)
self.browse_button.setIcon(build_icon(':/general/general_open.png'))
widget_layout.addWidget(self.browse_button)
self.revert_button = QtWidgets.QToolButton(self)
self.revert_button.setIcon(build_icon(':/general/general_revert.png'))
self.revert_button.setVisible(show_revert)
widget_layout.addWidget(self.revert_button)
self.setLayout(widget_layout)
# Signals and Slots
self.browse_button.clicked.connect(self.on_browse_button_clicked)
self.revert_button.clicked.connect(self.on_revert_button_clicked)
self.line_edit.editingFinished.connect(self.on_line_edit_editing_finished)
self.update_button_tool_tips()
@property
def path(self):
"""
A property getter method to return the selected path.
:return: The selected path
:rtype: str
"""
return self._path
@path.setter
def path(self, path):
"""
A Property setter method to set the selected path
:param path: The path to set the widget to
:type path: str
"""
self._path = path
self.line_edit.setText(path)
self.line_edit.setToolTip(path)
@property
def path_type(self):
"""
A property getter method to return the path_type. Path type allows you to sepecify if the user is restricted to
selecting a file or directory.
:return: The type selected
:rtype: Enum of PathEdit
"""
return self._path_type
@path_type.setter
def path_type(self, path_type):
"""
A Property setter method to set the path type
:param path: The type of path to select
:type path: Enum of PathEdit
"""
self._path_type = path_type
self.update_button_tool_tips()
def update_button_tool_tips(self):
"""
Called to update the tooltips on the buttons. This is changing path types, and when the widget is initalised
:return: None
"""
if self._path_type == PathType.Directories:
self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for directory.'))
self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default directory.'))
else:
self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for file.'))
self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default file.'))
def on_browse_button_clicked(self):
"""
A handler to handle a click on the browse button.
Show the QFileDialog and process the input from the user
:return: None
"""
caption = self.dialog_caption
path = ''
if self._path_type == PathType.Directories:
if not caption:
caption = translate('OpenLP.PathEdit', 'Select Directory')
path = QtWidgets.QFileDialog.getExistingDirectory(self, caption,
self._path, QtWidgets.QFileDialog.ShowDirsOnly)
elif self._path_type == PathType.Files:
if not caption:
caption = self.dialog_caption = translate('OpenLP.PathEdit', 'Select File')
path, filter_used = QtWidgets.QFileDialog.getOpenFileName(self, caption, self._path, self.filters)
if path:
path = os.path.normpath(path)
self.on_new_path(path)
def on_revert_button_clicked(self):
"""
A handler to handle a click on the revert button.
Set the new path to the value of the default_path instance variable.
:return: None
"""
self.on_new_path(self.default_path)
def on_line_edit_editing_finished(self):
"""
A handler to handle when the line edit has finished being edited.
:return: None
"""
self.on_new_path(self.line_edit.text())
def on_new_path(self, path):
"""
A method called to validate and set a new path.
Emits the pathChanged Signal
:param path: The new path
:type path: str
:return: None
"""
if self._path != path:
self.path = path
self.pathChanged.emit(path)

View File

@ -28,7 +28,8 @@ import os
import datetime
from PyQt5 import QtCore, QtWidgets
from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, translate
from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, \
extension_loader, translate
from openlp.core.lib import ItemCapabilities
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.common import AppLocation
@ -39,6 +40,7 @@ from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_pla
parse_optical_path
from openlp.core.ui.lib.toolbar import OpenLPToolbar
log = logging.getLogger(__name__)
TICK_TIME = 200
@ -172,19 +174,9 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
Check to see if we have any media Player's available.
"""
log.debug('_check_available_media_players')
controller_dir = os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'core', 'ui', 'media')
for filename in os.listdir(controller_dir):
if filename.endswith('player.py') and filename != 'mediaplayer.py':
path = os.path.join(controller_dir, filename)
if os.path.isfile(path):
module_name = 'openlp.core.ui.media.' + os.path.splitext(filename)[0]
log.debug('Importing controller %s', module_name)
try:
__import__(module_name, globals(), locals(), [])
# On some platforms importing vlc.py might cause
# also OSError exceptions. (e.g. Mac OS X)
except (ImportError, OSError):
log.warning('Failed to import %s on path %s', module_name, path)
controller_dir = os.path.join('openlp', 'core', 'ui', 'media')
glob_pattern = os.path.join(controller_dir, '*player.py')
extension_loader(glob_pattern, ['mediaplayer.py'])
player_classes = MediaPlayer.__subclasses__()
for player_class in player_classes:
self.register_players(player_class(self))

View File

@ -38,7 +38,7 @@ from openlp.core.lib.projector.constants import ERROR_MSG, ERROR_STRING, E_AUTHE
E_NETWORK, E_NOT_CONNECTED, E_UNKNOWN_SOCKET_ERROR, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_COOLDOWN, \
S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP
from openlp.core.lib.projector.db import ProjectorDB
from openlp.core.lib.projector.pjlink1 import PJLink1
from openlp.core.lib.projector.pjlink1 import PJLink
from openlp.core.ui.projector.editform import ProjectorEditForm
from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle
@ -690,19 +690,19 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
Helper app to build a projector instance
:param projector: Dict of projector database information
:returns: PJLink1() instance
:returns: PJLink() instance
"""
log.debug('_add_projector()')
return PJLink1(dbid=projector.id,
ip=projector.ip,
port=int(projector.port),
name=projector.name,
location=projector.location,
notes=projector.notes,
pin=None if projector.pin == '' else projector.pin,
poll_time=self.poll_time,
socket_timeout=self.socket_timeout
)
return PJLink(dbid=projector.id,
ip=projector.ip,
port=int(projector.port),
name=projector.name,
location=projector.location,
notes=projector.notes,
pin=None if projector.pin == '' else projector.pin,
poll_time=self.poll_time,
socket_timeout=self.socket_timeout
)
def add_projector(self, projector, start=False):
"""
@ -961,7 +961,7 @@ class ProjectorItem(QtCore.QObject):
"""
Initialization for ProjectorItem instance
:param link: PJLink1 instance for QListWidgetItem
:param link: PJLink instance for QListWidgetItem
"""
self.link = link
self.thread = None

View File

@ -69,10 +69,16 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
self.video_color_button.colorChanged.connect(self.on_video_color_changed)
self.gradient_start_button.colorChanged.connect(self.on_gradient_start_color_changed)
self.gradient_end_button.colorChanged.connect(self.on_gradient_end_color_changed)
self.image_browse_button.clicked.connect(self.on_image_browse_button_clicked)
self.image_file_edit.editingFinished.connect(self.on_image_file_edit_editing_finished)
self.video_browse_button.clicked.connect(self.on_video_browse_button_clicked)
self.video_file_edit.editingFinished.connect(self.on_video_file_edit_editing_finished)
self.image_path_edit.filters = \
'{name};;{text} (*)'.format(name=get_images_filter(), text=UiStrings().AllFiles)
self.image_path_edit.pathChanged.connect(self.on_image_path_edit_path_changed)
# TODO: Should work
visible_formats = '({name})'.format(name='; '.join(VIDEO_EXT))
actual_formats = '({name})'.format(name=' '.join(VIDEO_EXT))
video_filter = '{trans} {visible} {actual}'.format(trans=translate('OpenLP', 'Video Files'),
visible=visible_formats, actual=actual_formats)
self.video_path_edit.filters = '{video};;{ui} (*)'.format(video=video_filter, ui=UiStrings().AllFiles)
self.video_path_edit.pathChanged.connect(self.on_video_path_edit_path_changed)
self.main_color_button.colorChanged.connect(self.on_main_color_changed)
self.outline_color_button.colorChanged.connect(self.on_outline_color_changed)
self.shadow_color_button.colorChanged.connect(self.on_shadow_color_changed)
@ -112,7 +118,8 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
self.background_page.registerField('color', self.color_button)
self.background_page.registerField('gradient_start', self.gradient_start_button)
self.background_page.registerField('gradient_end', self.gradient_end_button)
self.background_page.registerField('background_image', self.image_file_edit)
self.background_page.registerField('background_image', self.image_path_edit,
'path', self.image_path_edit.pathChanged)
self.background_page.registerField('gradient', self.gradient_combo_box)
self.main_area_page.registerField('main_color_button', self.main_color_button)
self.main_area_page.registerField('main_size_spin_box', self.main_size_spin_box)
@ -309,11 +316,11 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
self.setField('background_type', 1)
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image):
self.image_color_button.color = self.theme.background_border_color
self.image_file_edit.setText(self.theme.background_filename)
self.image_path_edit.path = self.theme.background_filename
self.setField('background_type', 2)
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
self.video_color_button.color = self.theme.background_border_color
self.video_file_edit.setText(self.theme.background_filename)
self.video_path_edit.path = self.theme.background_filename
self.setField('background_type', 4)
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Transparent):
self.setField('background_type', 3)
@ -441,47 +448,20 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
"""
self.theme.background_end_color = color
def on_image_browse_button_clicked(self):
def on_image_path_edit_path_changed(self, filename):
"""
Background Image button pushed.
"""
images_filter = get_images_filter()
images_filter = '{name};;{text} (*.*)'.format(name=images_filter, text=UiStrings().AllFiles)
filename, filter_used = QtWidgets.QFileDialog.getOpenFileName(
self, translate('OpenLP.ThemeWizard', 'Select Image'),
self.image_file_edit.text(), images_filter)
if filename:
self.theme.background_filename = filename
self.theme.background_filename = filename
self.set_background_page_values()
def on_image_file_edit_editing_finished(self):
"""
Background image path edited
"""
self.theme.background_filename = str(self.image_file_edit.text())
def on_video_browse_button_clicked(self):
def on_video_path_edit_path_changed(self, filename):
"""
Background video button pushed.
"""
visible_formats = '({name})'.format(name='; '.join(VIDEO_EXT))
actual_formats = '({name})'.format(name=' '.join(VIDEO_EXT))
video_filter = '{trans} {visible} {actual}'.format(trans=translate('OpenLP', 'Video Files'),
visible=visible_formats, actual=actual_formats)
video_filter = '{video};;{ui} (*.*)'.format(video=video_filter, ui=UiStrings().AllFiles)
filename, filter_used = QtWidgets.QFileDialog.getOpenFileName(
self, translate('OpenLP.ThemeWizard', 'Select Video'),
self.video_file_edit.text(), video_filter)
if filename:
self.theme.background_filename = filename
self.theme.background_filename = filename
self.set_background_page_values()
def on_video_file_edit_editing_finished(self):
"""
Background video path edited
"""
self.theme.background_filename = str(self.image_file_edit.text())
def on_main_color_changed(self, color):
"""
Set the main colour value

View File

@ -28,7 +28,7 @@ from openlp.core.common import UiStrings, translate, is_macosx
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
from openlp.core.ui.lib import ColorButton, PathEdit
class Ui_ThemeWizard(object):
@ -116,16 +116,10 @@ class Ui_ThemeWizard(object):
self.image_layout.addRow(self.image_color_label, self.image_color_button)
self.image_label = QtWidgets.QLabel(self.image_widget)
self.image_label.setObjectName('image_label')
self.image_file_layout = QtWidgets.QHBoxLayout()
self.image_file_layout.setObjectName('image_file_layout')
self.image_file_edit = QtWidgets.QLineEdit(self.image_widget)
self.image_file_edit.setObjectName('image_file_edit')
self.image_file_layout.addWidget(self.image_file_edit)
self.image_browse_button = QtWidgets.QToolButton(self.image_widget)
self.image_browse_button.setObjectName('image_browse_button')
self.image_browse_button.setIcon(build_icon(':/general/general_open.png'))
self.image_file_layout.addWidget(self.image_browse_button)
self.image_layout.addRow(self.image_label, self.image_file_layout)
self.image_path_edit = PathEdit(self.image_widget,
dialog_caption=translate('OpenLP.ThemeWizard', 'Select Image'),
show_revert=False)
self.image_layout.addRow(self.image_label, self.image_path_edit)
self.image_layout.setItem(2, QtWidgets.QFormLayout.LabelRole, self.spacer)
self.background_stack.addWidget(self.image_widget)
self.transparent_widget = QtWidgets.QWidget(self.background_page)
@ -147,16 +141,10 @@ class Ui_ThemeWizard(object):
self.video_layout.addRow(self.video_color_label, self.video_color_button)
self.video_label = QtWidgets.QLabel(self.video_widget)
self.video_label.setObjectName('video_label')
self.video_file_layout = QtWidgets.QHBoxLayout()
self.video_file_layout.setObjectName('video_file_layout')
self.video_file_edit = QtWidgets.QLineEdit(self.video_widget)
self.video_file_edit.setObjectName('video_file_edit')
self.video_file_layout.addWidget(self.video_file_edit)
self.video_browse_button = QtWidgets.QToolButton(self.video_widget)
self.video_browse_button.setObjectName('video_browse_button')
self.video_browse_button.setIcon(build_icon(':/general/general_open.png'))
self.video_file_layout.addWidget(self.video_browse_button)
self.video_layout.addRow(self.video_label, self.video_file_layout)
self.video_path_edit = PathEdit(self.video_widget,
dialog_caption=translate('OpenLP.ThemeWizard', 'Select Video'),
show_revert=False)
self.video_layout.addRow(self.video_label, self.video_path_edit)
self.video_layout.setItem(2, QtWidgets.QFormLayout.LabelRole, self.spacer)
self.background_stack.addWidget(self.video_widget)
theme_wizard.addPage(self.background_page)

View File

@ -135,7 +135,6 @@ class BibleImportForm(OpenLPWizard):
Add the bible import specific wizard pages.
"""
# Select Page
self.spacers = []
self.select_page = QtWidgets.QWizardPage()
self.select_page.setObjectName('SelectPage')
self.select_page_layout = QtWidgets.QVBoxLayout(self.select_page)
@ -148,8 +147,8 @@ class BibleImportForm(OpenLPWizard):
self.format_combo_box.addItems(['', '', '', '', '', '', ''])
self.format_combo_box.setObjectName('FormatComboBox')
self.format_layout.addRow(self.format_label, self.format_combo_box)
self.spacers.append(QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum))
self.format_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacers[-1])
self.spacer = QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum)
self.format_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacer)
self.select_page_layout.addLayout(self.format_layout)
self.select_stack = QtWidgets.QStackedLayout()
self.select_stack.setObjectName('SelectStack')
@ -171,8 +170,7 @@ class BibleImportForm(OpenLPWizard):
self.osis_browse_button.setObjectName('OsisBrowseButton')
self.osis_file_layout.addWidget(self.osis_browse_button)
self.osis_layout.addRow(self.osis_file_label, self.osis_file_layout)
self.spacers.append(QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum))
self.osis_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacers[-1])
self.osis_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacer)
self.select_stack.addWidget(self.osis_widget)
self.csv_widget = QtWidgets.QWidget(self.select_page)
self.csv_widget.setObjectName('CsvWidget')
@ -205,8 +203,7 @@ class BibleImportForm(OpenLPWizard):
self.csv_verses_button.setObjectName('CsvVersesButton')
self.csv_verses_layout.addWidget(self.csv_verses_button)
self.csv_layout.addRow(self.csv_verses_label, self.csv_verses_layout)
self.spacers.append(QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum))
self.csv_layout.setItem(2, QtWidgets.QFormLayout.LabelRole, self.spacers[-1])
self.csv_layout.setItem(3, QtWidgets.QFormLayout.LabelRole, self.spacer)
self.select_stack.addWidget(self.csv_widget)
self.open_song_widget = QtWidgets.QWidget(self.select_page)
self.open_song_widget.setObjectName('OpenSongWidget')
@ -226,8 +223,7 @@ class BibleImportForm(OpenLPWizard):
self.open_song_browse_button.setObjectName('OpenSongBrowseButton')
self.open_song_file_layout.addWidget(self.open_song_browse_button)
self.open_song_layout.addRow(self.open_song_file_label, self.open_song_file_layout)
self.spacers.append(QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum))
self.open_song_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacers[-1])
self.open_song_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacer)
self.select_stack.addWidget(self.open_song_widget)
self.web_tab_widget = QtWidgets.QTabWidget(self.select_page)
self.web_tab_widget.setObjectName('WebTabWidget')
@ -304,8 +300,7 @@ class BibleImportForm(OpenLPWizard):
self.zefania_browse_button.setObjectName('ZefaniaBrowseButton')
self.zefania_file_layout.addWidget(self.zefania_browse_button)
self.zefania_layout.addRow(self.zefania_file_label, self.zefania_file_layout)
self.spacers.append(QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum))
self.zefania_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacers[-1])
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')
@ -386,8 +381,7 @@ class BibleImportForm(OpenLPWizard):
self.wordproject_browse_button.setObjectName('WordProjectBrowseButton')
self.wordproject_file_layout.addWidget(self.wordproject_browse_button)
self.wordproject_layout.addRow(self.wordproject_file_label, self.wordproject_file_layout)
self.spacers.append(QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum))
self.wordproject_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacers[-1])
self.wordproject_layout.setItem(5, QtWidgets.QFormLayout.LabelRole, self.spacer)
self.select_stack.addWidget(self.wordproject_widget)
self.select_page_layout.addLayout(self.select_stack)
self.addPage(self.select_page)
@ -505,8 +499,7 @@ class BibleImportForm(OpenLPWizard):
self.csv_verses_label.minimumSizeHint().width(),
self.open_song_file_label.minimumSizeHint().width(),
self.zefania_file_label.minimumSizeHint().width())
for spacer in self.spacers:
spacer.changeSize(label_width, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
self.spacer.changeSize(label_width, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
def validateCurrentPage(self):
"""

View File

@ -58,7 +58,8 @@ from PyQt5 import QtCore
from openlp.core.lib import ScreenList
from openlp.core.common import get_uno_command, get_uno_instance
from .presentationcontroller import PresentationController, PresentationDocument, TextType
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \
TextType
log = logging.getLogger(__name__)

View File

@ -29,7 +29,7 @@ from subprocess import check_output, CalledProcessError
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
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
if is_win():
from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW

View File

@ -43,7 +43,7 @@ if is_win():
from openlp.core.lib import ScreenList
from openlp.core.lib.ui import UiStrings, critical_error_message_box, translate
from openlp.core.common import trace_error_handler, Registry
from .presentationcontroller import PresentationController, PresentationDocument
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
log = logging.getLogger(__name__)

View File

@ -35,7 +35,7 @@ if is_win():
from openlp.core.common import AppLocation
from openlp.core.lib import ScreenList
from .presentationcontroller import PresentationController, PresentationDocument
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
log = logging.getLogger(__name__)

View File

@ -25,7 +25,8 @@ from PyQt5 import QtGui, QtWidgets
from openlp.core.common import Settings, UiStrings, translate
from openlp.core.lib import SettingsTab, build_icon
from openlp.core.lib.ui import critical_error_message_box
from .pdfcontroller import PdfController
from openlp.core.ui.lib import PathEdit
from openlp.plugins.presentations.lib.pdfcontroller import PdfController
class PresentationTab(SettingsTab):
@ -88,26 +89,15 @@ class PresentationTab(SettingsTab):
self.pdf_program_check_box = QtWidgets.QCheckBox(self.pdf_group_box)
self.pdf_program_check_box.setObjectName('pdf_program_check_box')
self.pdf_layout.addRow(self.pdf_program_check_box)
self.pdf_program_path_layout = QtWidgets.QHBoxLayout()
self.pdf_program_path_layout.setObjectName('pdf_program_path_layout')
self.pdf_program_path = QtWidgets.QLineEdit(self.pdf_group_box)
self.pdf_program_path.setObjectName('pdf_program_path')
self.pdf_program_path.setReadOnly(True)
self.pdf_program_path.setPalette(self.get_grey_text_palette(True))
self.pdf_program_path_layout.addWidget(self.pdf_program_path)
self.pdf_program_browse_button = QtWidgets.QToolButton(self.pdf_group_box)
self.pdf_program_browse_button.setObjectName('pdf_program_browse_button')
self.pdf_program_browse_button.setIcon(build_icon(':/general/general_open.png'))
self.pdf_program_browse_button.setEnabled(False)
self.pdf_program_path_layout.addWidget(self.pdf_program_browse_button)
self.pdf_layout.addRow(self.pdf_program_path_layout)
self.program_path_edit = PathEdit(self.pdf_group_box)
self.pdf_layout.addRow(self.program_path_edit)
self.left_layout.addWidget(self.pdf_group_box)
self.left_layout.addStretch()
self.right_column.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
self.right_layout.addStretch()
# Signals and slots
self.pdf_program_browse_button.clicked.connect(self.on_pdf_program_browse_button_clicked)
self.pdf_program_check_box.clicked.connect(self.on_pdf_program_check_box_clicked)
self.program_path_edit.pathChanged.connect(self.on_program_path_edit_path_changed)
self.pdf_program_check_box.clicked.connect(self.program_path_edit.setEnabled)
def retranslateUi(self):
"""
@ -132,6 +122,8 @@ class PresentationTab(SettingsTab):
'(This may fix PowerPoint scaling issues in Windows 8 and 10)'))
self.pdf_program_check_box.setText(
translate('PresentationPlugin.PresentationTab', 'Use given full path for mudraw or ghostscript binary:'))
self.program_path_edit.dialog_caption = translate('PresentationPlugin.PresentationTab',
'Select mudraw or ghostscript binary')
def set_controller_text(self, checkbox, controller):
if checkbox.isEnabled():
@ -161,11 +153,10 @@ class PresentationTab(SettingsTab):
# load pdf-program settings
enable_pdf_program = Settings().value(self.settings_section + '/enable_pdf_program')
self.pdf_program_check_box.setChecked(enable_pdf_program)
self.pdf_program_path.setPalette(self.get_grey_text_palette(not enable_pdf_program))
self.pdf_program_browse_button.setEnabled(enable_pdf_program)
self.program_path_edit.setEnabled(enable_pdf_program)
pdf_program = Settings().value(self.settings_section + '/pdf_program')
if pdf_program:
self.pdf_program_path.setText(pdf_program)
self.program_path_edit.path = pdf_program
def save(self):
"""
@ -201,7 +192,7 @@ class PresentationTab(SettingsTab):
Settings().setValue(setting_key, self.ppt_window_check_box.checkState())
changed = True
# Save pdf-settings
pdf_program = self.pdf_program_path.text()
pdf_program = self.program_path_edit.path
enable_pdf_program = self.pdf_program_check_box.checkState()
# If the given program is blank disable using the program
if pdf_program == '':
@ -228,42 +219,12 @@ class PresentationTab(SettingsTab):
checkbox.setEnabled(controller.is_available())
self.set_controller_text(checkbox, controller)
def on_pdf_program_browse_button_clicked(self):
def on_program_path_edit_path_changed(self, filename):
"""
Select the mudraw or ghostscript binary that should be used.
"""
filename, filter_used = QtWidgets.QFileDialog.getOpenFileName(
self, translate('PresentationPlugin.PresentationTab', 'Select mudraw or ghostscript binary.'),
self.pdf_program_path.text())
if filename:
program_type = PdfController.process_check_binary(filename)
if not program_type:
if not PdfController.process_check_binary(filename):
critical_error_message_box(UiStrings().Error,
translate('PresentationPlugin.PresentationTab',
'The program is not ghostscript or mudraw which is required.'))
else:
self.pdf_program_path.setText(filename)
def on_pdf_program_check_box_clicked(self, checked):
"""
When checkbox for manual entering pdf-program is clicked,
enable or disable the textbox for the programpath and the browse-button.
:param checked: If the box is checked or not.
"""
self.pdf_program_path.setPalette(self.get_grey_text_palette(not checked))
self.pdf_program_browse_button.setEnabled(checked)
def get_grey_text_palette(self, greyed):
"""
Returns a QPalette with greyed out text as used for placeholderText.
:param greyed: Determines whether the palette should be grayed.
:return: The created palette.
"""
palette = QtGui.QPalette()
color = self.palette().color(QtGui.QPalette.Active, QtGui.QPalette.Text)
if greyed:
color.setAlpha(128)
palette.setColor(QtGui.QPalette.Active, QtGui.QPalette.Text, color)
return palette

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
@ -20,19 +20,18 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`presentationplugin` module provides the ability for OpenLP to display presentations from a variety of document
formats.
The :mod:`openlp.plugins.presentations.presentationplugin` module provides the ability for OpenLP to display
presentations from a variety of document formats.
"""
import os
import logging
from PyQt5 import QtCore
from openlp.core.common import AppLocation, translate
from openlp.core.common import AppLocation, extension_loader, translate
from openlp.core.lib import Plugin, StringContent, build_icon
from openlp.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab
log = logging.getLogger(__name__)
@ -122,17 +121,9 @@ class PresentationPlugin(Plugin):
Check to see if we have any presentation software available. If not do not install the plugin.
"""
log.debug('check_pre_conditions')
controller_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), 'presentations', 'lib')
for filename in os.listdir(controller_dir):
if filename.endswith('controller.py') and filename != 'presentationcontroller.py':
path = os.path.join(controller_dir, filename)
if os.path.isfile(path):
module_name = 'openlp.plugins.presentations.lib.' + os.path.splitext(filename)[0]
log.debug('Importing controller {name}'.format(name=module_name))
try:
__import__(module_name, globals(), locals(), [])
except ImportError:
log.warning('Failed to import {name} on path {path}'.format(name=module_name, path=path))
controller_dir = os.path.join('openlp', 'plugins', 'presentations', 'lib')
glob_pattern = os.path.join(controller_dir, '*controller.py')
extension_loader(glob_pattern, ['presentationcontroller.py'])
controller_classes = PresentationController.__subclasses__()
for controller_class in controller_classes:
controller = controller_class(self)

View File

@ -512,10 +512,13 @@ def strip_rtf(text, default_encoding=None):
elif not ignorable:
ebytes.append(int(hex_, 16))
elif tchar:
if curskip > 0:
curskip -= 1
elif not ignorable:
if not ignorable:
ebytes += tchar.encode()
if len(ebytes) >= curskip:
ebytes = ebytes[curskip:]
else:
curskip -= len(ebytes)
ebytes = ""
text = ''.join(out)
return text, default_encoding

View File

@ -160,26 +160,27 @@ class SongFormat(object):
DreamBeam = 5
EasySlides = 6
EasyWorshipDB = 7
EasyWorshipService = 8
FoilPresenter = 9
Lyrix = 10
MediaShout = 11
OpenSong = 12
OPSPro = 13
PowerPraise = 14
PowerSong = 15
PresentationManager = 16
ProPresenter = 17
SongBeamer = 18
SongPro = 19
SongShowPlus = 20
SongsOfFellowship = 21
SundayPlus = 22
VideoPsalm = 23
WordsOfWorship = 24
WorshipAssistant = 25
WorshipCenterPro = 26
ZionWorx = 27
EasyWorshipSqliteDB = 8
EasyWorshipService = 9
FoilPresenter = 10
Lyrix = 11
MediaShout = 12
OpenSong = 13
OPSPro = 14
PowerPraise = 15
PowerSong = 16
PresentationManager = 17
ProPresenter = 18
SongBeamer = 19
SongPro = 20
SongShowPlus = 21
SongsOfFellowship = 22
SundayPlus = 23
VideoPsalm = 24
WordsOfWorship = 25
WorshipAssistant = 26
WorshipCenterPro = 27
ZionWorx = 28
# Set optional attribute defaults
__defaults__ = {
@ -251,9 +252,17 @@ class SongFormat(object):
'name': 'EasyWorship Song Database',
'prefix': 'ew',
'selectMode': SongFormatSelect.SingleFile,
'filter': '{text} (*.db)'.format(text=translate('SongsPlugin.ImportWizardForm',
'filter': '{text} (*.DB)'.format(text=translate('SongsPlugin.ImportWizardForm',
'EasyWorship Song Database'))
},
EasyWorshipSqliteDB: {
'class': EasyWorshipSongImport,
'name': 'EasyWorship 6 Song Database',
'prefix': 'ew',
'selectMode': SongFormatSelect.SingleFolder,
'filter': '{text} (*.db)'.format(text=translate('SongsPlugin.ImportWizardForm',
'EasyWorship 6 Song Data Directory'))
},
EasyWorshipService: {
'class': EasyWorshipSongImport,
'name': 'EasyWorship Service',
@ -440,6 +449,7 @@ class SongFormat(object):
SongFormat.DreamBeam,
SongFormat.EasySlides,
SongFormat.EasyWorshipDB,
SongFormat.EasyWorshipSqliteDB,
SongFormat.EasyWorshipService,
SongFormat.FoilPresenter,
SongFormat.Lyrix,

View File

@ -28,6 +28,7 @@ import struct
import re
import zlib
import logging
import sqlite3
from openlp.core.lib import translate
from openlp.plugins.songs.lib import VerseType
@ -77,8 +78,10 @@ class EasyWorshipSongImport(SongImport):
"""
if self.import_source.lower().endswith('ews'):
self.import_ews()
else:
elif self.import_source.endswith('DB'):
self.import_db()
else:
self.import_sqlite_db()
def import_ews(self):
"""
@ -125,8 +128,8 @@ class EasyWorshipSongImport(SongImport):
else:
log.debug('Given ews file is of unknown version.')
return
entry_count = self.get_i32(file_pos)
entry_length = self.get_i16(file_pos + 4)
entry_count = self.ews_get_i32(file_pos)
entry_length = self.ews_get_i16(file_pos + 4)
file_pos += 6
self.import_wizard.progress_bar.setMaximum(entry_count)
# Loop over songs
@ -144,13 +147,13 @@ class EasyWorshipSongImport(SongImport):
# 0x08 = Audio, 0x09 = Web
# 1410 Song number cstring 10
self.set_defaults()
self.title = self.get_string(file_pos + 0, 50)
authors = self.get_string(file_pos + 307, 50)
copyright = self.get_string(file_pos + 358, 100)
admin = self.get_string(file_pos + 459, 50)
cont_ptr = self.get_i32(file_pos + 800)
cont_type = self.get_i32(file_pos + 820)
self.ccli_number = self.get_string(file_pos + 1410, 10)
self.title = self.ews_get_string(file_pos + 0, 50)
authors = self.ews_get_string(file_pos + 307, 50)
copyright = self.ews_get_string(file_pos + 358, 100)
admin = self.ews_get_string(file_pos + 459, 50)
cont_ptr = self.ews_get_i32(file_pos + 800)
cont_type = self.ews_get_i32(file_pos + 820)
self.ccli_number = self.ews_get_string(file_pos + 1410, 10)
# Only handle content type 1 (songs)
if cont_type != 1:
file_pos += entry_length
@ -164,9 +167,9 @@ class EasyWorshipSongImport(SongImport):
# Checksum int32be 4 Alder-32 checksum.
# (unknown) 4 0x51 0x4b 0x03 0x04
# Content length int32le 4 Length of content after decompression
content_length = self.get_i32(cont_ptr)
deflated_content = self.get_bytes(cont_ptr + 4, content_length - 10)
deflated_length = self.get_i32(cont_ptr + 4 + content_length - 6)
content_length = self.ews_get_i32(cont_ptr)
deflated_content = self.ews_get_bytes(cont_ptr + 4, content_length - 10)
deflated_length = self.ews_get_i32(cont_ptr + 4 + content_length - 6)
inflated_content = zlib.decompress(deflated_content, 15, deflated_length)
if copyright:
self.copyright = copyright
@ -196,7 +199,7 @@ class EasyWorshipSongImport(SongImport):
Import the songs from the database
"""
# Open the DB and MB files if they exist
import_source_mb = self.import_source.replace('.DB', '.MB').replace('.db', '.mb')
import_source_mb = self.import_source.replace('.DB', '.MB')
if not os.path.isfile(self.import_source):
self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport',
'This file does not exist.'))
@ -260,16 +263,16 @@ class EasyWorshipSongImport(SongImport):
for i, field_name in enumerate(field_names):
field_type, field_size = struct.unpack_from('BB', field_info, i * 2)
field_descriptions.append(FieldDescEntry(field_name, field_type, field_size))
self.set_record_struct(field_descriptions)
self.db_set_record_struct(field_descriptions)
# Pick out the field description indexes we will need
try:
success = True
fi_title = self.find_field(b'Title')
fi_author = self.find_field(b'Author')
fi_copy = self.find_field(b'Copyright')
fi_admin = self.find_field(b'Administrator')
fi_words = self.find_field(b'Words')
fi_ccli = self.find_field(b'Song Number')
fi_title = self.db_find_field(b'Title')
fi_author = self.db_find_field(b'Author')
fi_copy = self.db_find_field(b'Copyright')
fi_admin = self.db_find_field(b'Administrator')
fi_words = self.db_find_field(b'Words')
fi_ccli = self.db_find_field(b'Song Number')
except IndexError:
# This is the wrong table
success = False
@ -297,13 +300,13 @@ class EasyWorshipSongImport(SongImport):
raw_record = db_file.read(record_size)
self.fields = self.record_structure.unpack(raw_record)
self.set_defaults()
self.title = self.get_field(fi_title).decode(self.encoding)
self.title = self.db_get_field(fi_title).decode(self.encoding)
# Get remaining fields.
copy = self.get_field(fi_copy)
admin = self.get_field(fi_admin)
ccli = self.get_field(fi_ccli)
authors = self.get_field(fi_author)
words = self.get_field(fi_words)
copy = self.db_get_field(fi_copy)
admin = self.db_get_field(fi_admin)
ccli = self.db_get_field(fi_ccli)
authors = self.db_get_field(fi_author)
words = self.db_get_field(fi_words)
if copy:
self.copyright = copy.decode(self.encoding)
if admin:
@ -337,6 +340,82 @@ class EasyWorshipSongImport(SongImport):
db_file.close()
self.memo_file.close()
def _find_file(self, base_path, path_list):
"""
Find the specified file, with the option of the file being at any level in the specified directory structure.
:param base_path: the location search in
:param path_list: the targeted file, preceded by directories that may be their parents relative to the base_path
:return: path for targeted file
"""
target_file = ''
while len(path_list) > 0:
target_file = os.path.join(path_list[-1], target_file)
path_list = path_list[:len(path_list) - 1]
full_path = os.path.join(base_path, target_file)
full_path = full_path[:len(full_path) - 1]
if os.path.isfile(full_path):
return full_path
return ''
def import_sqlite_db(self):
"""
Import the songs from an EasyWorship 6 SQLite database
"""
songs_db_path = self._find_file(self.import_source, ["Databases", "Data", "Songs.db"])
song_words_db_path = self._find_file(self.import_source, ["Databases", "Data", "SongWords.db"])
invalid_dir_msg = 'This does not appear to be a valid Easy Worship 6 database directory.'
# check to see if needed files are there
if not os.path.isfile(songs_db_path):
self.log_error(songs_db_path, translate('SongsPlugin.EasyWorshipSongImport', invalid_dir_msg))
return
if not os.path.isfile(song_words_db_path):
self.log_error(song_words_db_path, translate('SongsPlugin.EasyWorshipSongImport', invalid_dir_msg))
return
# get database handles
songs_conn = sqlite3.connect(songs_db_path)
words_conn = sqlite3.connect(song_words_db_path)
if songs_conn is None or words_conn is None:
self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport',
'This is not a valid Easy Worship 6 database.'))
songs_conn.close()
words_conn.close()
return
songs_db = songs_conn.cursor()
words_db = words_conn.cursor()
if songs_conn is None or words_conn is None:
self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport',
'This is not a valid Easy Worship 6 database.'))
songs_conn.close()
words_conn.close()
return
# Take a stab at how text is encoded
self.encoding = 'cp1252'
self.encoding = retrieve_windows_encoding(self.encoding)
if not self.encoding:
log.debug('No encoding set.')
return
# import songs
songs = songs_db.execute('SELECT rowid,title,author,copyright,vendor_id FROM song;')
for song in songs:
song_id = song[0]
# keep extra copy of title for error message because error check clears it
self.title = title = song[1]
self.author = song[2]
self.copyright = song[3]
self.ccli_number = song[4]
words = words_db.execute('SELECT words FROM word WHERE song_id = ?;', (song_id,))
self.set_song_import_object(self.author, words.fetchone()[0].encode())
if not self.finish():
self.log_error(self.import_source,
translate('SongsPlugin.EasyWorshipSongImport',
'"{title}" could not be imported. {entry}').
format(title=title, entry=self.entry_error_log))
# close database handles
songs_conn.close()
words_conn.close()
return
def set_song_import_object(self, authors, words):
"""
Set the SongImport object members.
@ -409,7 +488,7 @@ class EasyWorshipSongImport(SongImport):
self.comments += str(translate('SongsPlugin.EasyWorshipSongImport',
'\n[above are Song Tags with notes imported from EasyWorship]'))
def find_field(self, field_name):
def db_find_field(self, field_name):
"""
Find a field in the descriptions
@ -417,7 +496,7 @@ class EasyWorshipSongImport(SongImport):
"""
return [i for i, x in enumerate(self.field_descriptions) if x.name == field_name][0]
def set_record_struct(self, field_descriptions):
def db_set_record_struct(self, field_descriptions):
"""
Save the record structure
@ -445,7 +524,7 @@ class EasyWorshipSongImport(SongImport):
self.record_structure = struct.Struct(''.join(fsl))
self.field_descriptions = field_descriptions
def get_field(self, field_desc_index):
def db_get_field(self, field_desc_index):
"""
Extract the field
@ -489,7 +568,7 @@ class EasyWorshipSongImport(SongImport):
else:
return 0
def get_bytes(self, pos, length):
def ews_get_bytes(self, pos, length):
"""
Get bytes from ews_file
@ -500,7 +579,7 @@ class EasyWorshipSongImport(SongImport):
self.ews_file.seek(pos)
return self.ews_file.read(length)
def get_string(self, pos, length):
def ews_get_string(self, pos, length):
"""
Get string from ews_file
@ -508,12 +587,12 @@ class EasyWorshipSongImport(SongImport):
:param length: Characters to read
:return: String read
"""
bytes = self.get_bytes(pos, length)
bytes = self.ews_get_bytes(pos, length)
mask = '<' + str(length) + 's'
byte_str, = struct.unpack(mask, bytes)
return byte_str.decode(self.encoding).replace('\0', '').strip()
def get_i16(self, pos):
def ews_get_i16(self, pos):
"""
Get short int from ews_file
@ -521,19 +600,19 @@ class EasyWorshipSongImport(SongImport):
:return: Short integer read
"""
bytes = self.get_bytes(pos, 2)
bytes = self.ews_get_bytes(pos, 2)
mask = '<h'
number, = struct.unpack(mask, bytes)
return number
def get_i32(self, pos):
def ews_get_i32(self, pos):
"""
Get long int from ews_file
:param pos: Position to read from
:return: Long integer read
"""
bytes = self.get_bytes(pos, 4)
bytes = self.ews_get_bytes(pos, 4)
mask = '<i'
number, = struct.unpack(mask, bytes)
return number

View File

@ -25,6 +25,7 @@ from PyQt5 import QtCore, QtWidgets
from openlp.core.common import translate
from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box
from openlp.core.ui.lib import PathEdit, PathType
class Ui_SongUsageDetailDialog(object):
@ -68,20 +69,13 @@ class Ui_SongUsageDetailDialog(object):
self.file_horizontal_layout.setSpacing(8)
self.file_horizontal_layout.setContentsMargins(8, 8, 8, 8)
self.file_horizontal_layout.setObjectName('file_horizontal_layout')
self.file_line_edit = QtWidgets.QLineEdit(self.file_group_box)
self.file_line_edit.setObjectName('file_line_edit')
self.file_line_edit.setReadOnly(True)
self.file_horizontal_layout.addWidget(self.file_line_edit)
self.save_file_push_button = QtWidgets.QPushButton(self.file_group_box)
self.save_file_push_button.setMaximumWidth(self.save_file_push_button.size().height())
self.save_file_push_button.setIcon(build_icon(':/general/general_open.png'))
self.save_file_push_button.setObjectName('save_file_push_button')
self.file_horizontal_layout.addWidget(self.save_file_push_button)
self.report_path_edit = PathEdit(self.file_group_box, path_type = PathType.Directories, show_revert=False)
self.file_horizontal_layout.addWidget(self.report_path_edit)
self.vertical_layout.addWidget(self.file_group_box)
self.button_box = create_button_box(song_usage_detail_dialog, 'button_box', ['cancel', 'ok'])
self.vertical_layout.addWidget(self.button_box)
self.retranslateUi(song_usage_detail_dialog)
self.save_file_push_button.clicked.connect(song_usage_detail_dialog.define_output_location)
self.report_path_edit.pathChanged.connect(song_usage_detail_dialog.on_report_path_edit_path_changed)
def retranslateUi(self, song_usage_detail_dialog):
"""

View File

@ -54,25 +54,20 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP
"""
self.from_date_calendar.setSelectedDate(Settings().value(self.plugin.settings_section + '/from date'))
self.to_date_calendar.setSelectedDate(Settings().value(self.plugin.settings_section + '/to date'))
self.file_line_edit.setText(Settings().value(self.plugin.settings_section + '/last directory export'))
self.report_path_edit.path = Settings().value(self.plugin.settings_section + '/last directory export')
def define_output_location(self):
def on_report_path_edit_path_changed(self, file_path):
"""
Triggered when the Directory selection button is clicked
"""
path = QtWidgets.QFileDialog.getExistingDirectory(
self, translate('SongUsagePlugin.SongUsageDetailForm', 'Output File Location'),
Settings().value(self.plugin.settings_section + '/last directory export'))
if path:
Settings().setValue(self.plugin.settings_section + '/last directory export', path)
self.file_line_edit.setText(path)
Settings().setValue(self.plugin.settings_section + '/last directory export', file_path)
def accept(self):
"""
Ok was triggered so lets save the data and run the report
"""
log.debug('accept')
path = self.file_line_edit.text()
path = self.report_path_edit.path
if not path:
self.main_window.error_message(
translate('SongUsagePlugin.SongUsageDetailForm', 'Output Path Not Selected'),

View File

@ -22,11 +22,13 @@
"""
Functional tests to test the AppLocation class and related methods.
"""
from pathlib import Path
from unittest import TestCase
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock, call, patch
from openlp.core.common import check_directory_exists, de_hump, trace_error_handler, translate, is_win, is_macosx, \
is_linux, clean_button_text
from openlp.core import common
from openlp.core.common import check_directory_exists, clean_button_text, de_hump, extension_loader, is_macosx, \
is_linux, is_win, path_to_module, trace_error_handler, translate
class TestCommonFunctions(TestCase):
@ -72,6 +74,72 @@ class TestCommonFunctions(TestCase):
mocked_exists.assert_called_with(directory_to_check)
self.assertRaises(ValueError, check_directory_exists, directory_to_check)
def test_extension_loader_no_files_found(self):
"""
Test the `extension_loader` function when no files are found
"""
# GIVEN: A mocked `Path.glob` method which does not match any files
with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
patch.object(common.Path, 'glob', return_value=[]), \
patch('openlp.core.common.importlib.import_module') as mocked_import_module:
# WHEN: Calling `extension_loader`
extension_loader('glob', ['file2.py', 'file3.py'])
# THEN: `extension_loader` should not try to import any files
self.assertFalse(mocked_import_module.called)
def test_extension_loader_files_found(self):
"""
Test the `extension_loader` function when it successfully finds and loads some files
"""
# GIVEN: A mocked `Path.glob` method which returns a list of files
with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
patch.object(common.Path, 'glob', return_value=[Path('/app/dir/openlp/import_dir/file1.py'),
Path('/app/dir/openlp/import_dir/file2.py'),
Path('/app/dir/openlp/import_dir/file3.py'),
Path('/app/dir/openlp/import_dir/file4.py')]), \
patch('openlp.core.common.importlib.import_module') as mocked_import_module:
# WHEN: Calling `extension_loader` with a list of files to exclude
extension_loader('glob', ['file2.py', 'file3.py'])
# THEN: `extension_loader` should only try to import the files that are matched by the blob, excluding the
# files listed in the `excluded_files` argument
mocked_import_module.assert_has_calls([call('openlp.import_dir.file1'), call('openlp.import_dir.file4')])
def test_extension_loader_import_error(self):
"""
Test the `extension_loader` function when `SourceFileLoader` raises a `ImportError`
"""
# GIVEN: A mocked `import_module` which raises an `ImportError`
with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
patch.object(common.Path, 'glob', return_value=[Path('/app/dir/openlp/import_dir/file1.py')]), \
patch('openlp.core.common.importlib.import_module', side_effect=ImportError()), \
patch('openlp.core.common.log') as mocked_logger:
# WHEN: Calling `extension_loader`
extension_loader('glob')
# THEN: The `ImportError` should be caught and logged
self.assertTrue(mocked_logger.warning.called)
def test_extension_loader_os_error(self):
"""
Test the `extension_loader` function when `import_module` raises a `ImportError`
"""
# GIVEN: A mocked `SourceFileLoader` which raises an `OSError`
with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
patch.object(common.Path, 'glob', return_value=[Path('/app/dir/openlp/import_dir/file1.py')]), \
patch('openlp.core.common.importlib.import_module', side_effect=OSError()), \
patch('openlp.core.common.log') as mocked_logger:
# WHEN: Calling `extension_loader`
extension_loader('glob')
# THEN: The `OSError` should be caught and logged
self.assertTrue(mocked_logger.warning.called)
def test_de_hump_conversion(self):
"""
Test the de_hump function with a class name
@ -83,7 +151,7 @@ class TestCommonFunctions(TestCase):
new_string = de_hump(string)
# THEN: the new string should be converted to python format
self.assertTrue(new_string == "my_class", 'The class name should have been converted')
self.assertEqual(new_string, "my_class", 'The class name should have been converted')
def test_de_hump_static(self):
"""
@ -96,7 +164,20 @@ class TestCommonFunctions(TestCase):
new_string = de_hump(string)
# THEN: the new string should be converted to python format
self.assertTrue(new_string == "my_class", 'The class name should have been preserved')
self.assertEqual(new_string, "my_class", 'The class name should have been preserved')
def test_path_to_module(self):
"""
Test `path_to_module` when supplied with a `Path` object
"""
# GIVEN: A `Path` object
path = Path('openlp/core/ui/media/webkitplayer.py')
# WHEN: Calling path_to_module with the `Path` object
result = path_to_module(path)
# THEN: path_to_module should return the module name
self.assertEqual(result, 'openlp.core.ui.media.webkitplayer')
def test_trace_error_handler(self):
"""

View File

@ -29,7 +29,6 @@ class TestProjectorConstants(TestCase):
"""
Test specific functions in the projector constants module.
"""
@skip('Waiting for merge of ~alisonken1/openlp/pjlink2-resource-data')
def build_pjlink_video_label_test(self):
"""
Test building PJLINK_DEFAULT_CODES dictionary

View File

@ -25,13 +25,13 @@ Package to test the openlp.core.lib.projector.pjlink1 package.
from unittest import TestCase
from unittest.mock import call, patch, MagicMock
from openlp.core.lib.projector.pjlink1 import PJLink1
from openlp.core.lib.projector.pjlink1 import PJLink
from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING, S_OFF, S_STANDBY, S_ON, \
PJLINK_POWR_STATUS, S_CONNECTED
from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE, TEST_HASH
pjlink_test = PJLink1(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True)
pjlink_test = PJLink(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True)
class TestPJLink(TestCase):
@ -164,23 +164,36 @@ class TestPJLink(TestCase):
'Lamp 3 hours should have been set to 33333')
@patch.object(pjlink_test, 'projectorReceivedData')
def test_projector_process_power_on(self, mock_projectorReceivedData):
@patch.object(pjlink_test, 'projectorUpdateIcons')
@patch.object(pjlink_test, 'send_command')
@patch.object(pjlink_test, 'change_status')
def test_projector_process_power_on(self, mock_change_status,
mock_send_command,
mock_UpdateIcons,
mock_ReceivedData):
"""
Test status power to ON
"""
# GIVEN: Test object and preset
pjlink = pjlink_test
pjlink.power = S_STANDBY
pjlink.socket_timer = MagicMock()
# WHEN: Call process_command with turn power on command
pjlink.process_command('POWR', PJLINK_POWR_STATUS[S_ON])
# THEN: Power should be set to ON
self.assertEquals(pjlink.power, S_ON, 'Power should have been set to ON')
mock_send_command.assert_called_once_with('INST')
self.assertEquals(mock_UpdateIcons.emit.called, True, 'projectorUpdateIcons should have been called')
@patch.object(pjlink_test, 'projectorReceivedData')
def test_projector_process_power_off(self, mock_projectorReceivedData):
@patch.object(pjlink_test, 'projectorUpdateIcons')
@patch.object(pjlink_test, 'send_command')
@patch.object(pjlink_test, 'change_status')
def test_projector_process_power_off(self, mock_change_status,
mock_send_command,
mock_UpdateIcons,
mock_ReceivedData):
"""
Test status power to STANDBY
"""
@ -193,6 +206,8 @@ class TestPJLink(TestCase):
# THEN: Power should be set to STANDBY
self.assertEquals(pjlink.power, S_STANDBY, 'Power should have been set to STANDBY')
self.assertEquals(mock_send_command.called, False, 'send_command should not have been called')
self.assertEquals(mock_UpdateIcons.emit.called, True, 'projectorUpdateIcons should have been called')
@patch.object(pjlink_test, 'projectorUpdateIcons')
def test_projector_process_avmt_closed_unmuted(self, mock_projectorReceivedData):
@ -372,7 +387,7 @@ class TestPJLink(TestCase):
@patch.object(pjlink_test, '_not_implemented')
def not_implemented_test(self, mock_not_implemented):
"""
Test pjlink1._not_implemented method being called
Test PJLink._not_implemented method being called
"""
# GIVEN: test object
pjlink = pjlink_test
@ -381,13 +396,13 @@ class TestPJLink(TestCase):
# WHEN: A future command is called that is not implemented yet
pjlink.process_command(test_cmd, "Garbage data for test only")
# THEN: pjlink1.__not_implemented should have been called with test_cmd
# THEN: PJLink.__not_implemented should have been called with test_cmd
mock_not_implemented.assert_called_with(test_cmd)
@patch.object(pjlink_test, 'disconnect_from_host')
def socket_abort_test(self, mock_disconnect):
"""
Test PJLink1.socket_abort calls disconnect_from_host
Test PJLink.socket_abort calls disconnect_from_host
"""
# GIVEN: Test object
pjlink = pjlink_test
@ -400,7 +415,7 @@ class TestPJLink(TestCase):
def poll_loop_not_connected_test(self):
"""
Test PJLink1.poll_loop not connected return
Test PJLink.poll_loop not connected return
"""
# GIVEN: Test object and mocks
pjlink = pjlink_test
@ -409,7 +424,7 @@ class TestPJLink(TestCase):
pjlink.state.return_value = False
pjlink.ConnectedState = True
# WHEN: PJLink1.poll_loop called
# WHEN: PJLink.poll_loop called
pjlink.poll_loop()
# THEN: poll_loop should exit without calling any other method
@ -418,7 +433,7 @@ class TestPJLink(TestCase):
@patch.object(pjlink_test, 'send_command')
def poll_loop_start_test(self, mock_send_command):
"""
Test PJLink1.poll_loop makes correct calls
Test PJLink.poll_loop makes correct calls
"""
# GIVEN: test object and test data
pjlink = pjlink_test
@ -450,7 +465,7 @@ class TestPJLink(TestCase):
call('NAME', queue=True),
]
# WHEN: PJLink1.poll_loop is called
# WHEN: PJLink.poll_loop is called
pjlink.poll_loop()
# THEN: proper calls were made to retrieve projector data

View File

@ -26,13 +26,15 @@ record functions.
PREREQUISITE: add_record() and get_all() functions validated.
"""
import os
import shutil
from unittest import TestCase
from unittest.mock import MagicMock, patch
from openlp.core.lib.projector.db import Manufacturer, Model, Projector, ProjectorDB, ProjectorSource, Source
from openlp.core.lib.projector.constants import PJLINK_PORT
from tests.resources.projector.data import TEST_DB, TEST1_DATA, TEST2_DATA, TEST3_DATA
from tests.resources.projector.data import TEST_DB_PJLINK1, TEST_DB, TEST1_DATA, TEST2_DATA, TEST3_DATA
from tests.utils.constants import TEST_RESOURCES_PATH
def compare_data(one, two):
@ -45,7 +47,11 @@ def compare_data(one, two):
one.port == two.port and \
one.name == two.name and \
one.location == two.location and \
one.notes == two.notes
one.notes == two.notes and \
one.sw_version == two.sw_version and \
one.serial_no == two.serial_no and \
one.model_filter == two.model_filter and \
one.model_lamp == two.model_lamp
def compare_source(one, two):
@ -168,6 +174,10 @@ class TestProjectorDB(TestCase):
record.name = TEST3_DATA['name']
record.location = TEST3_DATA['location']
record.notes = TEST3_DATA['notes']
record.sw_version = TEST3_DATA['sw_version']
record.serial_no = TEST3_DATA['serial_no']
record.model_filter = TEST3_DATA['model_filter']
record.model_lamp = TEST3_DATA['model_lamp']
updated = self.projector.update_projector(record)
self.assertTrue(updated, 'Save updated record should have returned True')
record = self.projector.get_projector_by_ip(TEST3_DATA['ip'])
@ -246,7 +256,8 @@ class TestProjectorDB(TestCase):
projector = Projector()
# WHEN: projector() is populated
# NOTE: projector.pin, projector.other, projector.sources should all return None
# NOTE: projector.[pin, other, sources, sw_version, serial_no, sw_version, model_lamp, model_filter]
# should all return None.
# projector.source_list should return an empty list
projector.id = 0
projector.ip = '127.0.0.1'
@ -262,8 +273,9 @@ class TestProjectorDB(TestCase):
self.assertEqual(str(projector),
'< Projector(id="0", ip="127.0.0.1", port="4352", pin="None", name="Test One", '
'location="Somewhere over the rainbow", notes="Not again", pjlink_name="TEST", '
'manufacturer="IN YOUR DREAMS", model="OpenLP", other="None", sources="None", '
'source_list="[]") >',
'manufacturer="IN YOUR DREAMS", model="OpenLP", serial_no="None", other="None", '
'sources="None", source_list="[]", model_filter="None", model_lamp="None", '
'sw_version="None") >',
'Projector.__repr__() should have returned a proper representation string')
def test_projectorsource_repr(self):

View File

@ -32,60 +32,21 @@ class TestThemeManager(TestCase):
"""
Test the functions in the ThemeManager Class
"""
def test_select_image_file_dialog_cancelled(self):
"""
Test the select image file dialog when the user presses cancel
"""
# GIVEN: An instance of Theme Form and mocked QFileDialog which returns an empty string (similating a user
# pressing cancel)
with patch('openlp.core.ui.ThemeForm._setup'),\
patch('openlp.core.ui.themeform.get_images_filter',
**{'return_value': 'Image Files (*.bmp; *.gif)(*.bmp *.gif)'}),\
patch('openlp.core.ui.themeform.QtWidgets.QFileDialog.getOpenFileName',
**{'return_value': ('', '')}) as mocked_get_open_file_name,\
patch('openlp.core.ui.themeform.translate', **{'return_value': 'Translated String'}),\
patch('openlp.core.ui.ThemeForm.set_background_page_values') as mocked_set_background_page_values:
instance = ThemeForm(None)
mocked_image_file_edit = MagicMock()
mocked_image_file_edit.text.return_value = '/original_path/file.ext'
instance.image_file_edit = mocked_image_file_edit
def setUp(self):
with patch('openlp.core.ui.ThemeForm._setup'):
self.instance = ThemeForm(None)
# WHEN: on_image_browse_button is clicked
instance.on_image_browse_button_clicked()
def test_on_image_path_edit_path_changed(self):
"""
Test the `image_path_edit.pathChanged` handler
"""
# GIVEN: An instance of Theme Form
with patch.object(self.instance, 'set_background_page_values') as mocked_set_background_page_values:
self.instance.theme = MagicMock()
# THEN: The QFileDialog getOpenFileName and set_background_page_values moethods should have been called
# with known arguments
mocked_get_open_file_name.assert_called_once_with(instance, 'Translated String', '/original_path/file.ext',
'Image Files (*.bmp; *.gif)(*.bmp *.gif);;'
'All Files (*.*)')
# WHEN: `on_image_path_edit_path_changed` is clicked
self.instance.on_image_path_edit_path_changed('/new/pat.h')
# THEN: The theme background file should be set and `set_background_page_values` should have been called
self.assertEqual(self.instance.theme.background_filename, '/new/pat.h')
mocked_set_background_page_values.assert_called_once_with()
def test_select_image_file_dialog_new_file(self):
"""
Test the select image file dialog when the user presses ok
"""
# GIVEN: An instance of Theme Form and mocked QFileDialog which returns a file path
with patch('openlp.core.ui.ThemeForm._setup'),\
patch('openlp.core.ui.themeform.get_images_filter',
**{'return_value': 'Image Files (*.bmp; *.gif)(*.bmp *.gif)'}),\
patch('openlp.core.ui.themeform.QtWidgets.QFileDialog.getOpenFileName',
**{'return_value': ('/new_path/file.ext', '')}) as mocked_get_open_file_name,\
patch('openlp.core.ui.themeform.translate', **{'return_value': 'Translated String'}),\
patch('openlp.core.ui.ThemeForm.set_background_page_values') as mocked_background_page_values:
instance = ThemeForm(None)
mocked_image_file_edit = MagicMock()
mocked_image_file_edit.text.return_value = '/original_path/file.ext'
instance.image_file_edit = mocked_image_file_edit
instance.theme = MagicMock()
# WHEN: on_image_browse_button is clicked
instance.on_image_browse_button_clicked()
# THEN: The QFileDialog getOpenFileName and set_background_page_values moethods should have been called
# with known arguments and theme.background_filename should be set
mocked_get_open_file_name.assert_called_once_with(instance, 'Translated String', '/original_path/file.ext',
'Image Files (*.bmp; *.gif)(*.bmp *.gif);;'
'All Files (*.*)')
self.assertEqual(instance.theme.background_filename, '/new_path/file.ext',
'theme.background_filename should be set to the path that the file dialog returns')
mocked_background_page_values.assert_called_once_with()

View File

@ -20,12 +20,12 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
This module contains tests for the openlp.core.lib.filedialog module
This module contains tests for the openlp.core.ui.lib.colorbutton module
"""
from unittest import TestCase
from unittest.mock import MagicMock, call, patch
from openlp.core.ui.lib.colorbutton import ColorButton
from openlp.core.ui.lib import ColorButton
class TestColorDialog(TestCase):
@ -148,11 +148,10 @@ class TestColorDialog(TestCase):
widget.on_clicked()
# THEN: change_color should not have been called and the colorChanged signal should not have been emitted
self.assertEqual(
self.mocked_change_color.call_count, 0, 'change_color should not have been called with an invalid color')
self.assertEqual(
self.mocked_color_changed.emit.call_count, 0,
'colorChange signal should not have been emitted with an invalid color')
self.assertFalse(self.mocked_change_color.called,
'change_color should not have been called with an invalid color')
self.assertFalse(self.mocked_color_changed.emit.called,
'colorChange signal should not have been emitted with an invalid color')
def test_on_clicked_same_color(self):
"""
@ -171,12 +170,10 @@ class TestColorDialog(TestCase):
widget.on_clicked()
# THEN: change_color should not have been called and the colorChanged signal should not have been emitted
self.assertEqual(
self.mocked_change_color.call_count, 0,
'change_color should not have been called when the color has not changed')
self.assertEqual(
self.mocked_color_changed.emit.call_count, 0,
'colorChange signal should not have been emitted when the color has not changed')
self.assertFalse(self.mocked_change_color.called,
'change_color should not have been called when the color has not changed')
self.assertFalse(self.mocked_color_changed.emit.called,
'colorChange signal should not have been emitted when the color has not changed')
def test_on_clicked_new_color(self):
"""

View File

@ -0,0 +1,311 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 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.core.ui.lib.pathedit module
"""
from unittest import TestCase
from PyQt5 import QtWidgets
from openlp.core.ui.lib import PathEdit, PathType
from unittest.mock import MagicMock, PropertyMock, patch
class TestPathEdit(TestCase):
"""
Test the :class:`~openlp.core.lib.pathedit.PathEdit` class
"""
def setUp(self):
with patch('openlp.core.ui.lib.pathedit.PathEdit._setup'):
self.widget = PathEdit()
def test_path_getter(self):
"""
Test the `path` property getter.
"""
# GIVEN: An instance of PathEdit with the `_path` instance variable set
self.widget._path = 'getter/test/pat.h'
# WHEN: Reading the `path` property
# THEN: The value that we set should be returned
self.assertEqual(self.widget.path, 'getter/test/pat.h')
def test_path_setter(self):
"""
Test the `path` property setter.
"""
# GIVEN: An instance of the PathEdit object and a mocked `line_edit`
self.widget.line_edit = MagicMock()
# WHEN: Writing to the `path` property
self.widget.path = 'setter/test/pat.h'
# THEN: The `_path` instance variable should be set with the test data. The `line_edit` text and tooltip
# should have also been set.
self.assertEqual(self.widget._path, 'setter/test/pat.h')
self.widget.line_edit.setToolTip.assert_called_once_with('setter/test/pat.h')
self.widget.line_edit.setText.assert_called_once_with('setter/test/pat.h')
def test_path_type_getter(self):
"""
Test the `path_type` property getter.
"""
# GIVEN: An instance of PathEdit
# WHEN: Reading the `path` property
# THEN: The default value should be returned
self.assertEqual(self.widget.path_type, PathType.Files)
def test_path_type_setter(self):
"""
Test the `path_type` property setter.
"""
# GIVEN: An instance of the PathEdit object and a mocked `update_button_tool_tips` method.
with patch.object(self.widget, 'update_button_tool_tips') as mocked_update_button_tool_tips:
# WHEN: Writing to a different value than default to the `path_type` property
self.widget.path_type = PathType.Directories
# THEN: The `_path_type` instance variable should be set with the test data and not the default. The
# update_button_tool_tips should have been called.
self.assertEqual(self.widget._path_type, PathType.Directories)
mocked_update_button_tool_tips.assert_called_once_with()
def test_update_button_tool_tips_directories(self):
"""
Test the `update_button_tool_tips` method.
"""
# GIVEN: An instance of PathEdit with the `path_type` set to `Directories`
self.widget.browse_button = MagicMock()
self.widget.revert_button = MagicMock()
self.widget._path_type = PathType.Directories
# WHEN: Calling update_button_tool_tips
self.widget.update_button_tool_tips()
self.widget.browse_button.setToolTip.assert_called_once_with('Browse for directory.')
self.widget.revert_button.setToolTip.assert_called_once_with('Revert to default directory.')
def test_update_button_tool_tips_files(self):
"""
Test the `update_button_tool_tips` method.
"""
# GIVEN: An instance of PathEdit with the `path_type` set to `Files`
self.widget.browse_button = MagicMock()
self.widget.revert_button = MagicMock()
self.widget._path_type = PathType.Files
# WHEN: Calling update_button_tool_tips
self.widget.update_button_tool_tips()
self.widget.browse_button.setToolTip.assert_called_once_with('Browse for file.')
self.widget.revert_button.setToolTip.assert_called_once_with('Revert to default file.')
def test_on_browse_button_clicked_directory(self):
"""
Test the `browse_button` `clicked` handler on_browse_button_clicked when the `path_type` is set to Directories.
"""
# GIVEN: An instance of PathEdit with the `path_type` set to `Directories` and a mocked
# QFileDialog.getExistingDirectory
with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getExistingDirectory', return_value='') as \
mocked_get_existing_directory, \
patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName') as \
mocked_get_open_file_name, \
patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath:
self.widget._path_type = PathType.Directories
self.widget._path = 'test/path/'
# WHEN: Calling on_browse_button_clicked
self.widget.on_browse_button_clicked()
# THEN: The FileDialog.getExistingDirectory should have been called with the default caption
mocked_get_existing_directory.assert_called_once_with(self.widget, 'Select Directory', 'test/path/',
QtWidgets.QFileDialog.ShowDirsOnly)
self.assertFalse(mocked_get_open_file_name.called)
self.assertFalse(mocked_normpath.called)
def test_on_browse_button_clicked_directory_custom_caption(self):
"""
Test the `browse_button` `clicked` handler on_browse_button_clicked when the `path_type` is set to Directories,
and `dialog_caption` is set.
"""
# GIVEN: An instance of PathEdit with the `path_type` set to `Directories` and a mocked
# QFileDialog.getExistingDirectory with `default_caption` set.
with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getExistingDirectory', return_value='') as \
mocked_get_existing_directory, \
patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName') as \
mocked_get_open_file_name, \
patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath:
self.widget._path_type = PathType.Directories
self.widget._path = 'test/path/'
self.widget.dialog_caption = 'Directory Caption'
# WHEN: Calling on_browse_button_clicked
self.widget.on_browse_button_clicked()
# THEN: The FileDialog.getExistingDirectory should have been called with the custom caption
mocked_get_existing_directory.assert_called_once_with(self.widget, 'Directory Caption', 'test/path/',
QtWidgets.QFileDialog.ShowDirsOnly)
self.assertFalse(mocked_get_open_file_name.called)
self.assertFalse(mocked_normpath.called)
def test_on_browse_button_clicked_file(self):
"""
Test the `browse_button` `clicked` handler on_browse_button_clicked when the `path_type` is set to Files.
"""
# GIVEN: An instance of PathEdit with the `path_type` set to `Files` and a mocked QFileDialog.getOpenFileName
with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getExistingDirectory') as \
mocked_get_existing_directory, \
patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName', return_value=('', '')) as \
mocked_get_open_file_name, \
patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath:
self.widget._path_type = PathType.Files
self.widget._path = 'test/pat.h'
# WHEN: Calling on_browse_button_clicked
self.widget.on_browse_button_clicked()
# THEN: The FileDialog.getOpenFileName should have been called with the default caption
mocked_get_open_file_name.assert_called_once_with(self.widget, 'Select File', 'test/pat.h',
self.widget.filters)
self.assertFalse(mocked_get_existing_directory.called)
self.assertFalse(mocked_normpath.called)
def test_on_browse_button_clicked_file_custom_caption(self):
"""
Test the `browse_button` `clicked` handler on_browse_button_clicked when the `path_type` is set to Files and
`dialog_caption` is set.
"""
# GIVEN: An instance of PathEdit with the `path_type` set to `Files` and a mocked QFileDialog.getOpenFileName
# with `default_caption` set.
with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getExistingDirectory') as \
mocked_get_existing_directory, \
patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName', return_value=('', '')) as \
mocked_get_open_file_name, \
patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath:
self.widget._path_type = PathType.Files
self.widget._path = 'test/pat.h'
self.widget.dialog_caption = 'File Caption'
# WHEN: Calling on_browse_button_clicked
self.widget.on_browse_button_clicked()
# THEN: The FileDialog.getOpenFileName should have been called with the custom caption
mocked_get_open_file_name.assert_called_once_with(self.widget, 'File Caption', 'test/pat.h',
self.widget.filters)
self.assertFalse(mocked_get_existing_directory.called)
self.assertFalse(mocked_normpath.called)
def test_on_browse_button_clicked_user_cancels(self):
"""
Test the `browse_button` `clicked` handler on_browse_button_clicked when the user cancels the FileDialog (an
empty str is returned)
"""
# GIVEN: An instance of PathEdit with a mocked QFileDialog.getOpenFileName which returns an empty str for the
# file path.
with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName', return_value=('', '')) as \
mocked_get_open_file_name, \
patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath:
# WHEN: Calling on_browse_button_clicked
self.widget.on_browse_button_clicked()
# THEN: normpath should not have been called
self.assertTrue(mocked_get_open_file_name.called)
self.assertFalse(mocked_normpath.called)
def test_on_browse_button_clicked_user_accepts(self):
"""
Test the `browse_button` `clicked` handler on_browse_button_clicked when the user accepts the FileDialog (a path
is returned)
"""
# GIVEN: An instance of PathEdit with a mocked QFileDialog.getOpenFileName which returns a str for the file
# path.
with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName',
return_value=('/test/pat.h', '')) as mocked_get_open_file_name, \
patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath, \
patch.object(self.widget, 'on_new_path'):
# WHEN: Calling on_browse_button_clicked
self.widget.on_browse_button_clicked()
# THEN: normpath and `on_new_path` should have been called
self.assertTrue(mocked_get_open_file_name.called)
mocked_normpath.assert_called_once_with('/test/pat.h')
self.assertTrue(self.widget.on_new_path.called)
def test_on_revert_button_clicked(self):
"""
Test that the default path is set as the path when the `revert_button.clicked` handler is called.
"""
# GIVEN: An instance of PathEdit with a mocked `on_new_path`, and the `default_path` set.
with patch.object(self.widget, 'on_new_path') as mocked_on_new_path:
self.widget.default_path = '/default/pat.h'
# WHEN: Calling `on_revert_button_clicked`
self.widget.on_revert_button_clicked()
# THEN: on_new_path should have been called with the default path
mocked_on_new_path.assert_called_once_with('/default/pat.h')
def test_on_line_edit_editing_finished(self):
"""
Test that the new path is set as the path when the `line_edit.editingFinished` handler is called.
"""
# GIVEN: An instance of PathEdit with a mocked `line_edit` and `on_new_path`.
with patch.object(self.widget, 'on_new_path') as mocked_on_new_path:
self.widget.line_edit = MagicMock(**{'text.return_value': '/test/pat.h'})
# WHEN: Calling `on_line_edit_editing_finished`
self.widget.on_line_edit_editing_finished()
# THEN: on_new_path should have been called with the path enetered in `line_edit`
mocked_on_new_path.assert_called_once_with('/test/pat.h')
def test_on_new_path_no_change(self):
"""
Test `on_new_path` when called with a path that is the same as the existing path.
"""
# GIVEN: An instance of PathEdit with a test path and mocked `pathChanged` signal
with patch('openlp.core.ui.lib.pathedit.PathEdit.path', new_callable=PropertyMock):
self.widget._path = '/old/test/pat.h'
self.widget.pathChanged = MagicMock()
# WHEN: Calling `on_new_path` with the same path as the existing path
self.widget.on_new_path('/old/test/pat.h')
# THEN: The `pathChanged` signal should not be emitted
self.assertFalse(self.widget.pathChanged.emit.called)
def test_on_new_path_change(self):
"""
Test `on_new_path` when called with a path that is the different to the existing path.
"""
# GIVEN: An instance of PathEdit with a test path and mocked `pathChanged` signal
with patch('openlp.core.ui.lib.pathedit.PathEdit.path', new_callable=PropertyMock):
self.widget._path = '/old/test/pat.h'
self.widget.pathChanged = MagicMock()
# WHEN: Calling `on_new_path` with the a new path
self.widget.on_new_path('/new/test/pat.h')
# THEN: The `pathChanged` signal should be emitted
self.widget.pathChanged.emit.assert_called_once_with('/new/test/pat.h')

View File

@ -187,7 +187,7 @@ class TestEasyWorshipSongImport(TestCase):
def test_find_field_exists(self):
"""
Test finding an existing field in a given list using the :mod:`find_field`
Test finding an existing field in a given list using the :mod:`db_find_field`
"""
# GIVEN: A mocked out SongImport class, a mocked out "manager" and a list of field descriptions.
with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'):
@ -201,11 +201,11 @@ class TestEasyWorshipSongImport(TestCase):
for field_name in existing_fields:
# THEN: The item corresponding the index returned should have the same name attribute
self.assertEqual(importer.field_descriptions[importer.find_field(field_name)].name, field_name)
self.assertEqual(importer.field_descriptions[importer.db_find_field(field_name)].name, field_name)
def test_find_non_existing_field(self):
"""
Test finding an non-existing field in a given list using the :mod:`find_field`
Test finding an non-existing field in a given list using the :mod:`db_find_field`
"""
# GIVEN: A mocked out SongImport class, a mocked out "manager" and a list of field descriptions
with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'):
@ -218,11 +218,11 @@ class TestEasyWorshipSongImport(TestCase):
for field_name in non_existing_fields:
# THEN: The importer object should not be None
self.assertRaises(IndexError, importer.find_field, field_name)
self.assertRaises(IndexError, importer.db_find_field, field_name)
def test_set_record_struct(self):
"""
Test the :mod:`set_record_struct` module
Test the :mod:`db_set_record_struct` module
"""
# GIVEN: A mocked out SongImport class, a mocked out struct class, and a mocked out "manager" and a list of
# field descriptions
@ -231,17 +231,17 @@ class TestEasyWorshipSongImport(TestCase):
mocked_manager = MagicMock()
importer = EasyWorshipSongImport(mocked_manager, filenames=[])
# WHEN: set_record_struct is called with a list of field descriptions
return_value = importer.set_record_struct(TEST_FIELD_DESCS)
# WHEN: db_set_record_struct is called with a list of field descriptions
return_value = importer.db_set_record_struct(TEST_FIELD_DESCS)
# THEN: set_record_struct should return None and Struct should be called with a value representing
# THEN: db_set_record_struct should return None and Struct should be called with a value representing
# the list of field descriptions
self.assertIsNone(return_value, 'set_record_struct should return None')
self.assertIsNone(return_value, 'db_set_record_struct should return None')
mocked_struct.Struct.assert_called_with('>50sHIB250s250s10sQ')
def test_get_field(self):
"""
Test the :mod:`get_field` module
Test the :mod:`db_get_field` module
"""
# GIVEN: A mocked out SongImport class, a mocked out "manager", an encoding and some test data and known results
with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'):
@ -254,16 +254,16 @@ class TestEasyWorshipSongImport(TestCase):
# WHEN: Called with test data
for field_index, result in field_results:
return_value = importer.get_field(field_index)
return_value = importer.db_get_field(field_index)
# THEN: get_field should return the known results
# THEN: db_get_field should return the known results
self.assertEqual(return_value, result,
'get_field should return "%s" when called with "%s"' %
'db_get_field should return "%s" when called with "%s"' %
(result, TEST_FIELDS[field_index]))
def test_get_memo_field(self):
"""
Test the :mod:`get_field` module
Test the :mod:`db_get_field` module
"""
for test_results in GET_MEMO_FIELD_TEST_RESULTS:
# GIVEN: A mocked out SongImport class, a mocked out "manager", a mocked out memo_file and an encoding
@ -283,8 +283,9 @@ class TestEasyWorshipSongImport(TestCase):
get_field_read_calls = test_results[2]['read']
get_field_seek_calls = test_results[2]['seek']
# THEN: get_field should return the appropriate value with the appropriate mocked objects being called
self.assertEqual(importer.get_field(field_index), get_field_result)
# THEN: db_get_field should return the appropriate value with the appropriate mocked objects being
# called
self.assertEqual(importer.db_get_field(field_index), get_field_result)
for call in get_field_read_calls:
mocked_memo_file.read.assert_any_call(call)
for call in get_field_seek_calls:
@ -405,6 +406,12 @@ class TestEasyWorshipSongImport(TestCase):
mocked_retrieve_windows_encoding.assert_any_call(encoding)
def test_db_file_import(self):
return self._test_db_file_import(os.path.join(TEST_PATH, 'Songs.DB'))
def test_sqlite_db_file_import(self):
return self._test_db_file_import(os.path.join(TEST_PATH, 'ew6'))
def _test_db_file_import(self, source_path):
"""
Test the actual import of real song database files and check that the imported data is correct.
"""
@ -432,7 +439,7 @@ class TestEasyWorshipSongImport(TestCase):
importer.topics = []
# WHEN: Importing each file
importer.import_source = os.path.join(TEST_PATH, 'Songs.DB')
importer.import_source = source_path
import_result = importer.do_import()
# THEN: do_import should return none, the song data should be as expected, and finish should have been

View File

@ -28,11 +28,12 @@ from unittest.mock import MagicMock, patch
from PyQt5 import QtWidgets
from openlp.core.common import Registry
from openlp.plugins.bibles.forms import bibleimportform
from openlp.plugins.bibles.forms.bibleimportform import BibleImportForm, PYSWORD_AVAILABLE
from tests.helpers.testmixin import TestMixin
@skip('One of the QFormLayouts in the BibleImportForm is causing a segfault')
class TestBibleImportForm(TestCase, TestMixin):
"""
Test the BibleImportForm class
@ -46,9 +47,9 @@ class TestBibleImportForm(TestCase, TestMixin):
self.setup_application()
self.main_window = QtWidgets.QMainWindow()
Registry().register('main_window', self.main_window)
bibleimportform.PYSWORD_AVAILABLE = False
PYSWORD_AVAILABLE = False
self.mocked_manager = MagicMock()
self.form = bibleimportform.BibleImportForm(self.main_window, self.mocked_manager, MagicMock())
self.form = BibleImportForm(self.main_window, self.mocked_manager, MagicMock())
def tearDown(self):
"""

View File

@ -27,6 +27,8 @@ import os
from tempfile import gettempdir
# Test data
TEST_DB_PJLINK1 = 'projector_pjlink1.sqlite'
TEST_DB = os.path.join(gettempdir(), 'openlp-test-projectordb.sql')
TEST_SALT = '498e4a67'
@ -44,21 +46,33 @@ TEST1_DATA = dict(ip='111.111.111.111',
pin='1111',
name='___TEST_ONE___',
location='location one',
notes='notes one')
notes='notes one',
serial_no='Serial Number 1',
sw_version='Version 1',
model_filter='Filter type 1',
model_lamp='Lamp type 1')
TEST2_DATA = dict(ip='222.222.222.222',
port='2222',
pin='2222',
name='___TEST_TWO___',
location='location two',
notes='notes two')
notes='notes one',
serial_no='Serial Number 2',
sw_version='Version 2',
model_filter='Filter type 2',
model_lamp='Lamp type 2')
TEST3_DATA = dict(ip='333.333.333.333',
port='3333',
pin='3333',
name='___TEST_THREE___',
location='location three',
notes='notes three')
notes='notes one',
serial_no='Serial Number 3',
sw_version='Version 3',
model_filter='Filter type 3',
model_lamp='Lamp type 3')
TEST_VIDEO_CODES = {
'11': 'RGB 1',

Binary file not shown.