merge trunk

This commit is contained in:
Tomas Groth 2017-12-21 22:26:39 +01:00
commit 219e13a4bc
242 changed files with 4966 additions and 4422 deletions

View File

@ -1,5 +1,5 @@
[unittest] [unittest]
verbose = true verbose = True
plugins = nose2.plugins.mp plugins = nose2.plugins.mp
[log-capture] [log-capture]
@ -9,14 +9,19 @@ filter = -nose
log-level = ERROR log-level = ERROR
[test-result] [test-result]
always-on = true always-on = True
descriptions = true descriptions = True
[coverage] [coverage]
always-on = true always-on = True
coverage = openlp coverage = openlp
coverage-report = html coverage-report = html
[multiprocess] [multiprocess]
always-on = false always-on = False
processes = 4 processes = 4
[output-buffer]
always-on = True
stderr = True
stdout = False

View File

@ -46,6 +46,7 @@ if __name__ == '__main__':
""" """
Instantiate and run the application. Instantiate and run the application.
""" """
faulthandler.enable()
set_up_fault_handling() set_up_fault_handling()
# Add support for using multiprocessing from frozen Windows executable (built using PyInstaller), # Add support for using multiprocessing from frozen Windows executable (built using PyInstaller),
# see https://docs.python.org/3/library/multiprocessing.html#multiprocessing.freeze_support # see https://docs.python.org/3/library/multiprocessing.html#multiprocessing.freeze_support

View File

@ -22,7 +22,6 @@
""" """
Download and "install" the remote web client Download and "install" the remote web client
""" """
import os
from zipfile import ZipFile from zipfile import ZipFile
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
@ -30,18 +29,18 @@ from openlp.core.common.registry import Registry
from openlp.core.common.httputils import url_get_file, get_web_page, get_url_file_size from openlp.core.common.httputils import url_get_file, get_web_page, get_url_file_size
def deploy_zipfile(app_root, zip_name): def deploy_zipfile(app_root_path, zip_name):
""" """
Process the downloaded zip file and add to the correct directory Process the downloaded zip file and add to the correct directory
:param zip_name: the zip file to be processed :param str zip_name: the zip file name to be processed
:param app_root: the directory where the zip get expanded to :param openlp.core.common.path.Path app_root_path: The directory to expand the zip to
:return: None :return: None
""" """
zip_file = os.path.join(app_root, zip_name) zip_path = app_root_path / zip_name
web_zip = ZipFile(zip_file) web_zip = ZipFile(str(zip_path))
web_zip.extractall(app_root) web_zip.extractall(str(app_root_path))
def download_sha256(): def download_sha256():
@ -53,6 +52,8 @@ def download_sha256():
web_config = get_web_page('https://get.openlp.org/webclient/download.cfg', headers={'User-Agent': user_agent}) web_config = get_web_page('https://get.openlp.org/webclient/download.cfg', headers={'User-Agent': user_agent})
except ConnectionError: except ConnectionError:
return False return False
if not web_config:
return None
file_bits = web_config.split() file_bits = web_config.split()
return file_bits[0], file_bits[2] return file_bits[0], file_bits[2]
@ -67,4 +68,4 @@ def download_and_check(callback=None):
if url_get_file(callback, 'https://get.openlp.org/webclient/site.zip', if url_get_file(callback, 'https://get.openlp.org/webclient/site.zip',
AppLocation.get_section_data_path('remotes') / 'site.zip', AppLocation.get_section_data_path('remotes') / 'site.zip',
sha256=sha256): sha256=sha256):
deploy_zipfile(str(AppLocation.get_section_data_path('remotes')), 'site.zip') deploy_zipfile(AppLocation.get_section_data_path('remotes'), 'site.zip')

View File

@ -28,6 +28,7 @@ import json
from openlp.core.api.http.endpoint import Endpoint from openlp.core.api.http.endpoint import Endpoint
from openlp.core.api.http import requires_auth from openlp.core.api.http import requires_auth
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.path import Path
from openlp.core.common.registry import Registry from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.lib import ItemCapabilities, create_thumb from openlp.core.lib import ItemCapabilities, create_thumb
@ -66,12 +67,12 @@ def controller_text(request):
elif current_item.is_image() and not frame.get('image', '') and Settings().value('api/thumbnails'): elif current_item.is_image() and not frame.get('image', '') and Settings().value('api/thumbnails'):
item['tag'] = str(index + 1) item['tag'] = str(index + 1)
thumbnail_path = os.path.join('images', 'thumbnails', frame['title']) thumbnail_path = os.path.join('images', 'thumbnails', frame['title'])
full_thumbnail_path = str(AppLocation.get_data_path() / thumbnail_path) full_thumbnail_path = AppLocation.get_data_path() / thumbnail_path
# Create thumbnail if it doesn't exists # Create thumbnail if it doesn't exists
if not os.path.exists(full_thumbnail_path): if not full_thumbnail_path.exists():
create_thumb(current_item.get_frame_path(index), full_thumbnail_path, False) create_thumb(Path(current_item.get_frame_path(index)), full_thumbnail_path, False)
Registry().get('image_manager').add_image(full_thumbnail_path, frame['title'], None, 88, 88) Registry().get('image_manager').add_image(str(full_thumbnail_path), frame['title'], None, 88, 88)
item['img'] = urllib.request.pathname2url(os.path.sep + thumbnail_path) item['img'] = urllib.request.pathname2url(os.path.sep + str(thumbnail_path))
item['text'] = str(frame['title']) item['text'] = str(frame['title'])
item['html'] = str(frame['title']) item['html'] = str(frame['title'])
else: else:

View File

@ -172,15 +172,3 @@ def main_image(request):
'slide_image': 'data:image/png;base64,' + str(image_to_byte(live_controller.slide_image)) 'slide_image': 'data:image/png;base64,' + str(image_to_byte(live_controller.slide_image))
} }
return {'results': result} return {'results': result}
def get_content_type(file_name):
"""
Examines the extension of the file and determines what the content_type should be, defaults to text/plain
Returns the extension and the content_type
:param file_name: name of file
"""
ext = os.path.splitext(file_name)[1]
content_type = FILE_TYPES.get(ext, 'text/plain')
return ext, content_type

View File

@ -19,7 +19,6 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
import os
import json import json
import re import re
import urllib import urllib
@ -103,7 +102,7 @@ def display_thumbnails(request, controller_name, log, dimensions, file_name, sli
:param controller_name: which controller is requesting the image :param controller_name: which controller is requesting the image
:param log: the logger object :param log: the logger object
:param dimensions: the image size eg 88x88 :param dimensions: the image size eg 88x88
:param file_name: the file name of the image :param str file_name: the file name of the image
:param slide: the individual image name :param slide: the individual image name
:return: :return:
""" """
@ -124,12 +123,10 @@ def display_thumbnails(request, controller_name, log, dimensions, file_name, sli
if controller_name and file_name: if controller_name and file_name:
file_name = urllib.parse.unquote(file_name) file_name = urllib.parse.unquote(file_name)
if '..' not in file_name: # no hacking please if '..' not in file_name: # no hacking please
full_path = AppLocation.get_section_data_path(controller_name) / 'thumbnails' / file_name
if slide: if slide:
full_path = str(AppLocation.get_section_data_path(controller_name) / 'thumbnails' / file_name / slide) full_path = full_path / slide
else: if full_path.exists():
full_path = str(AppLocation.get_section_data_path(controller_name) / 'thumbnails' / file_name) Registry().get('image_manager').add_image(full_path, full_path.name, None, width, height)
if os.path.exists(full_path): image = Registry().get('image_manager').get_image(full_path, full_path.name, width, height)
path, just_file_name = os.path.split(full_path)
Registry().get('image_manager').add_image(full_path, just_file_name, None, width, height)
image = Registry().get('image_manager').get_image(full_path, just_file_name, width, height)
return Response(body=image_to_byte(image, False), status=200, content_type='image/png', charset='utf8') return Response(body=image_to_byte(image, False), status=200, content_type='image/png', charset='utf8')

View File

@ -27,7 +27,7 @@ from openlp.core.api.endpoint.core import TRANSLATED_STRINGS
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
remote_endpoint = Endpoint('remote', template_dir='remotes', static_dir='remotes') remote_endpoint = Endpoint('remote', template_dir='remotes')
@remote_endpoint.route('{view}') @remote_endpoint.route('{view}')

View File

@ -22,8 +22,6 @@
""" """
The Endpoint class, which provides plugins with a way to serve their own portion of the API The Endpoint class, which provides plugins with a way to serve their own portion of the API
""" """
import os
from mako.template import Template from mako.template import Template
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
@ -67,13 +65,17 @@ class Endpoint(object):
def render_template(self, filename, **kwargs): def render_template(self, filename, **kwargs):
""" """
Render a mako template Render a mako template
:param str filename: The file name of the template to render.
:return: The rendered template object.
:rtype: mako.template.Template
""" """
root = str(AppLocation.get_section_data_path('remotes')) root_path = AppLocation.get_section_data_path('remotes')
if not self.template_dir: if not self.template_dir:
raise Exception('No template directory specified') raise Exception('No template directory specified')
path = os.path.join(root, self.template_dir, filename) path = root_path / self.template_dir / filename
if self.static_dir: if self.static_dir:
kwargs['static_url'] = '/{prefix}/static'.format(prefix=self.url_prefix) kwargs['static_url'] = '/{prefix}/static'.format(prefix=self.url_prefix)
kwargs['static_url'] = kwargs['static_url'].replace('//', '/') kwargs['static_url'] = kwargs['static_url'].replace('//', '/')
kwargs['assets_url'] = '/assets' kwargs['assets_url'] = '/assets'
return Template(filename=path, input_encoding='utf-8').render(**kwargs) return Template(filename=str(path), input_encoding='utf-8').render(**kwargs)

View File

@ -39,9 +39,9 @@ from openlp.core.api.http import application
from openlp.core.api.poll import Poller from openlp.core.api.poll import Poller
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import UiStrings from openlp.core.common.i18n import UiStrings
from openlp.core.common.mixins import OpenLPMixin, RegistryMixin from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import create_paths from openlp.core.common.path import create_paths
from openlp.core.common.registry import RegistryProperties, Registry from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
@ -67,13 +67,16 @@ class HttpWorker(QtCore.QObject):
address = Settings().value('api/ip address') address = Settings().value('api/ip address')
port = Settings().value('api/port') port = Settings().value('api/port')
Registry().execute('get_website_version') Registry().execute('get_website_version')
serve(application, host=address, port=port) try:
serve(application, host=address, port=port)
except OSError:
log.exception('An error occurred when serving the application.')
def stop(self): def stop(self):
pass pass
class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin): class HttpServer(RegistryBase, RegistryProperties, LogMixin):
""" """
Wrapper round a server instance Wrapper round a server instance
""" """
@ -82,13 +85,14 @@ class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin):
Initialise the http server, and start the http server Initialise the http server, and start the http server
""" """
super(HttpServer, self).__init__(parent) super(HttpServer, self).__init__(parent)
self.worker = HttpWorker() if Registry().get_flag('no_web_server'):
self.thread = QtCore.QThread() self.worker = HttpWorker()
self.worker.moveToThread(self.thread) self.thread = QtCore.QThread()
self.thread.started.connect(self.worker.run) self.worker.moveToThread(self.thread)
self.thread.start() self.thread.started.connect(self.worker.run)
Registry().register_function('download_website', self.first_time) self.thread.start()
Registry().register_function('get_website_version', self.website_version) Registry().register_function('download_website', self.first_time)
Registry().register_function('get_website_version', self.website_version)
Registry().set_flag('website_version', '0.0') Registry().set_flag('website_version', '0.0')
def bootstrap_post_set_up(self): def bootstrap_post_set_up(self):

View File

@ -25,7 +25,6 @@ App stuff
""" """
import json import json
import logging import logging
import os
import re import re
from webob import Request, Response from webob import Request, Response
@ -138,12 +137,11 @@ class WSGIApplication(object):
Add a static directory as a route Add a static directory as a route
""" """
if route not in self.static_routes: if route not in self.static_routes:
root = str(AppLocation.get_section_data_path('remotes')) static_path = AppLocation.get_section_data_path('remotes') / static_dir
static_path = os.path.abspath(os.path.join(root, static_dir)) if not static_path.exists():
if not os.path.exists(static_path):
log.error('Static path "%s" does not exist. Skipping creating static route/', static_path) log.error('Static path "%s" does not exist. Skipping creating static route/', static_path)
return return
self.static_routes[route] = DirectoryApp(static_path) self.static_routes[route] = DirectoryApp(str(static_path.resolve()))
def dispatch(self, request): def dispatch(self, request):
""" """

View File

@ -23,7 +23,7 @@
import json import json
from openlp.core.common.httputils import get_web_page from openlp.core.common.httputils import get_web_page
from openlp.core.common.registry import RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings

View File

@ -31,8 +31,8 @@ import time
from PyQt5 import QtCore from PyQt5 import QtCore
from openlp.core.common.mixins import OpenLPMixin from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -61,7 +61,7 @@ class WebSocketWorker(QtCore.QObject):
self.ws_server.stop = True self.ws_server.stop = True
class WebSocketServer(RegistryProperties, OpenLPMixin): class WebSocketServer(RegistryProperties, LogMixin):
""" """
Wrapper round a server instance Wrapper round a server instance
""" """
@ -70,12 +70,13 @@ class WebSocketServer(RegistryProperties, OpenLPMixin):
Initialise and start the WebSockets server Initialise and start the WebSockets server
""" """
super(WebSocketServer, self).__init__() super(WebSocketServer, self).__init__()
self.settings_section = 'api' if Registry().get_flag('no_web_server'):
self.worker = WebSocketWorker(self) self.settings_section = 'api'
self.thread = QtCore.QThread() self.worker = WebSocketWorker(self)
self.worker.moveToThread(self.thread) self.thread = QtCore.QThread()
self.thread.started.connect(self.worker.run) self.worker.moveToThread(self.thread)
self.thread.start() self.thread.started.connect(self.worker.run)
self.thread.start()
def start_server(self): def start_server(self):
""" """

View File

@ -38,7 +38,7 @@ from PyQt5 import QtCore, QtWidgets
from openlp.core.common import is_macosx, is_win from openlp.core.common import is_macosx, is_win
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import LanguageManager, UiStrings, translate from openlp.core.common.i18n import LanguageManager, UiStrings, translate
from openlp.core.common.mixins import OpenLPMixin from openlp.core.common.mixins import LogMixin
from openlp.core.common.path import create_paths, copytree from openlp.core.common.path import create_paths, copytree
from openlp.core.common.registry import Registry from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
@ -59,7 +59,7 @@ __all__ = ['OpenLP', 'main']
log = logging.getLogger() log = logging.getLogger()
class OpenLP(OpenLPMixin, QtWidgets.QApplication): class OpenLP(QtWidgets.QApplication, LogMixin):
""" """
The core application class. This class inherits from Qt's QApplication The core application class. This class inherits from Qt's QApplication
class in order to provide the core of the application. class in order to provide the core of the application.
@ -403,8 +403,8 @@ def main(args=None):
.format(back_up_path=back_up_path)) .format(back_up_path=back_up_path))
QtWidgets.QMessageBox.information( QtWidgets.QMessageBox.information(
None, translate('OpenLP', 'Settings Upgrade'), None, translate('OpenLP', 'Settings Upgrade'),
translate('OpenLP', 'Your settings are about to upgraded. A backup will be created at {back_up_path}') translate('OpenLP', 'Your settings are about to be upgraded. A backup will be created at '
.format(back_up_path=back_up_path)) '{back_up_path}').format(back_up_path=back_up_path))
settings.export(back_up_path) settings.export(back_up_path)
settings.upgrade_settings() settings.upgrade_settings()
# First time checks in settings # First time checks in settings

View File

@ -43,9 +43,13 @@ log = logging.getLogger(__name__ + '.__init__')
FIRST_CAMEL_REGEX = re.compile('(.)([A-Z][a-z]+)') FIRST_CAMEL_REGEX = re.compile('(.)([A-Z][a-z]+)')
SECOND_CAMEL_REGEX = re.compile('([a-z0-9])([A-Z])') SECOND_CAMEL_REGEX = re.compile('([a-z0-9])([A-Z])')
CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]', re.UNICODE) CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]')
INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]', re.UNICODE) INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]')
IMAGES_FILTER = None IMAGES_FILTER = None
REPLACMENT_CHARS_MAP = str.maketrans({'\u2018': '\'', '\u2019': '\'', '\u201c': '"', '\u201d': '"', '\u2026': '...',
'\u2013': '-', '\u2014': '-', '\v': '\n\n', '\f': '\n\n'})
NEW_LINE_REGEX = re.compile(r' ?(\r\n?|\n) ?')
WHITESPACE_REGEX = re.compile(r'[ \t]+')
def trace_error_handler(logger): def trace_error_handler(logger):
@ -314,17 +318,6 @@ def get_filesystem_encoding():
return encoding return encoding
def split_filename(path):
"""
Return a list of the parts in a given path.
"""
path = os.path.abspath(path)
if not os.path.isfile(path):
return path, ''
else:
return os.path.split(path)
def delete_file(file_path): def delete_file(file_path):
""" """
Deletes a file from the system. Deletes a file from the system.
@ -339,7 +332,7 @@ def delete_file(file_path):
if file_path.exists(): if file_path.exists():
file_path.unlink() file_path.unlink()
return True return True
except (IOError, OSError): except OSError:
log.exception('Unable to delete file {file_path}'.format(file_path=file_path)) log.exception('Unable to delete file {file_path}'.format(file_path=file_path))
return False return False
@ -436,3 +429,17 @@ def get_file_encoding(file_path):
return detector.result return detector.result
except OSError: except OSError:
log.exception('Error detecting file encoding') log.exception('Error detecting file encoding')
def normalize_str(irreg_str):
"""
Normalize the supplied string. Remove unicode control chars and tidy up white space.
:param str irreg_str: The string to normalize.
:return: The normalized string
:rtype: str
"""
irreg_str = irreg_str.translate(REPLACMENT_CHARS_MAP)
irreg_str = CONTROL_CHARS.sub('', irreg_str)
irreg_str = NEW_LINE_REGEX.sub('\n', irreg_str)
return WHITESPACE_REGEX.sub(' ', irreg_str)

View File

@ -83,7 +83,7 @@ class AppLocation(object):
""" """
# Check if we have a different data location. # Check if we have a different data location.
if Settings().contains('advanced/data path'): if Settings().contains('advanced/data path'):
path = Settings().value('advanced/data path') path = Path(Settings().value('advanced/data path'))
else: else:
path = AppLocation.get_directory(AppLocation.DataDir) path = AppLocation.get_directory(AppLocation.DataDir)
create_paths(path) create_paths(path)

View File

@ -97,8 +97,8 @@ def get_web_page(url, headers=None, update_openlp=False, proxies=None):
response = requests.get(url, headers=headers, proxies=proxies, timeout=float(CONNECTION_TIMEOUT)) response = requests.get(url, headers=headers, proxies=proxies, timeout=float(CONNECTION_TIMEOUT))
log.debug('Downloaded page {url}'.format(url=response.url)) log.debug('Downloaded page {url}'.format(url=response.url))
break break
except IOError: except OSError:
# For now, catch IOError. All requests errors inherit from IOError # For now, catch OSError. All requests errors inherit from OSError
log.exception('Unable to connect to {url}'.format(url=url)) log.exception('Unable to connect to {url}'.format(url=url))
response = None response = None
if retries >= CONNECTION_RETRIES: if retries >= CONNECTION_RETRIES:
@ -127,7 +127,7 @@ def get_url_file_size(url):
try: try:
response = requests.head(url, timeout=float(CONNECTION_TIMEOUT), allow_redirects=True) response = requests.head(url, timeout=float(CONNECTION_TIMEOUT), allow_redirects=True)
return int(response.headers['Content-Length']) return int(response.headers['Content-Length'])
except IOError: except OSError:
if retries > CONNECTION_RETRIES: if retries > CONNECTION_RETRIES:
raise ConnectionError('Unable to download {url}'.format(url=url)) raise ConnectionError('Unable to download {url}'.format(url=url))
else: else:
@ -173,7 +173,7 @@ def url_get_file(callback, url, file_path, sha256=None):
file_path.unlink() file_path.unlink()
return False return False
break break
except IOError: except OSError:
trace_error_handler(log) trace_error_handler(log)
if retries > CONNECTION_RETRIES: if retries > CONNECTION_RETRIES:
if file_path.exists(): if file_path.exists():

View File

@ -53,7 +53,7 @@ def translate(context, text, comment=None, qt_translate=QtCore.QCoreApplication.
Language = namedtuple('Language', ['id', 'name', 'code']) Language = namedtuple('Language', ['id', 'name', 'code'])
ICU_COLLATOR = None ICU_COLLATOR = None
DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE) DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+')
LANGUAGES = sorted([ LANGUAGES = sorted([
Language(1, translate('common.languages', '(Afan) Oromo', 'Language code: om'), 'om'), Language(1, translate('common.languages', '(Afan) Oromo', 'Language code: om'), 'om'),
Language(2, translate('common.languages', 'Abkhazian', 'Language code: ab'), 'ab'), Language(2, translate('common.languages', 'Abkhazian', 'Language code: ab'), 'ab'),

View File

@ -25,25 +25,29 @@ Provide Error Handling and login Services
import logging import logging
import inspect import inspect
from openlp.core.common import trace_error_handler, de_hump from openlp.core.common import is_win, trace_error_handler
from openlp.core.common.registry import Registry from openlp.core.common.registry import Registry
DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed', DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed',
'preview_size_changed', 'resizeEvent'] 'preview_size_changed', 'resizeEvent']
class OpenLPMixin(object): class LogMixin(object):
""" """
Base Calling object for OpenLP classes. Base Calling object for OpenLP classes.
""" """
def __init__(self, *args, **kwargs): @property
super(OpenLPMixin, self).__init__(*args, **kwargs) def logger(self):
self.logger = logging.getLogger("%s.%s" % (self.__module__, self.__class__.__name__)) if hasattr(self, '_logger') and self._logger:
if self.logger.getEffectiveLevel() == logging.DEBUG: return self._logger
for name, m in inspect.getmembers(self, inspect.ismethod): else:
if name not in DO_NOT_TRACE_EVENTS: self._logger = logging.getLogger("%s.%s" % (self.__module__, self.__class__.__name__))
if not name.startswith("_") and not name.startswith("log"): if self._logger.getEffectiveLevel() == logging.DEBUG:
setattr(self, name, self.logging_wrapper(m, self)) for name, m in inspect.getmembers(self, inspect.ismethod):
if name not in DO_NOT_TRACE_EVENTS:
if not name.startswith("_") and not name.startswith("log"):
setattr(self, name, self.logging_wrapper(m, self))
return self._logger
def logging_wrapper(self, func, parent): def logging_wrapper(self, func, parent):
""" """
@ -93,30 +97,141 @@ class OpenLPMixin(object):
self.logger.exception(message) self.logger.exception(message)
class RegistryMixin(object): class RegistryProperties(object):
""" """
This adds registry components to classes to use at run time. This adds registry components to classes to use at run time.
""" """
def __init__(self, parent): _application = None
""" _plugin_manager = None
Register the class and bootstrap hooks. _image_manager = None
""" _media_controller = None
try: _service_manager = None
super(RegistryMixin, self).__init__(parent) _preview_controller = None
except TypeError: _live_controller = None
super(RegistryMixin, self).__init__() _main_window = None
Registry().register(de_hump(self.__class__.__name__), self) _renderer = None
Registry().register_function('bootstrap_initialise', self.bootstrap_initialise) _theme_manager = None
Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up) _settings_form = None
_alerts_manager = None
_projector_manager = None
def bootstrap_initialise(self): @property
def application(self):
""" """
Dummy method to be overridden Adds the openlp to the class dynamically.
Windows needs to access the application in a dynamic manner.
""" """
pass if is_win():
return Registry().get('application')
else:
if not hasattr(self, '_application') or not self._application:
self._application = Registry().get('application')
return self._application
def bootstrap_post_set_up(self): @property
def plugin_manager(self):
""" """
Dummy method to be overridden Adds the plugin manager to the class dynamically
""" """
pass if not hasattr(self, '_plugin_manager') or not self._plugin_manager:
self._plugin_manager = Registry().get('plugin_manager')
return self._plugin_manager
@property
def image_manager(self):
"""
Adds the image manager to the class dynamically
"""
if not hasattr(self, '_image_manager') or not self._image_manager:
self._image_manager = Registry().get('image_manager')
return self._image_manager
@property
def media_controller(self):
"""
Adds the media controller to the class dynamically
"""
if not hasattr(self, '_media_controller') or not self._media_controller:
self._media_controller = Registry().get('media_controller')
return self._media_controller
@property
def service_manager(self):
"""
Adds the service manager to the class dynamically
"""
if not hasattr(self, '_service_manager') or not self._service_manager:
self._service_manager = Registry().get('service_manager')
return self._service_manager
@property
def preview_controller(self):
"""
Adds the preview controller to the class dynamically
"""
if not hasattr(self, '_preview_controller') or not self._preview_controller:
self._preview_controller = Registry().get('preview_controller')
return self._preview_controller
@property
def live_controller(self):
"""
Adds the live controller to the class dynamically
"""
if not hasattr(self, '_live_controller') or not self._live_controller:
self._live_controller = Registry().get('live_controller')
return self._live_controller
@property
def main_window(self):
"""
Adds the main window to the class dynamically
"""
if not hasattr(self, '_main_window') or not self._main_window:
self._main_window = Registry().get('main_window')
return self._main_window
@property
def renderer(self):
"""
Adds the Renderer to the class dynamically
"""
if not hasattr(self, '_renderer') or not self._renderer:
self._renderer = Registry().get('renderer')
return self._renderer
@property
def theme_manager(self):
"""
Adds the theme manager to the class dynamically
"""
if not hasattr(self, '_theme_manager') or not self._theme_manager:
self._theme_manager = Registry().get('theme_manager')
return self._theme_manager
@property
def settings_form(self):
"""
Adds the settings form to the class dynamically
"""
if not hasattr(self, '_settings_form') or not self._settings_form:
self._settings_form = Registry().get('settings_form')
return self._settings_form
@property
def alerts_manager(self):
"""
Adds the alerts manager to the class dynamically
"""
if not hasattr(self, '_alerts_manager') or not self._alerts_manager:
self._alerts_manager = Registry().get('alerts_manager')
return self._alerts_manager
@property
def projector_manager(self):
"""
Adds the projector manager to the class dynamically
"""
if not hasattr(self, '_projector_manager') or not self._projector_manager:
self._projector_manager = Registry().get('projector_manager')
return self._projector_manager

View File

@ -69,6 +69,16 @@ class Path(PathVariant):
path = path.relative_to(base_path) path = path.relative_to(base_path)
return {'__Path__': path.parts} return {'__Path__': path.parts}
def rmtree(self, ignore_errors=False, onerror=None):
"""
Provide an interface to :func:`shutil.rmtree`
:param bool ignore_errors: Ignore errors
:param onerror: Handler function to handle any errors
:rtype: None
"""
shutil.rmtree(str(self), ignore_errors, onerror)
def replace_params(args, kwargs, params): def replace_params(args, kwargs, params):
""" """
@ -153,23 +163,6 @@ def copytree(*args, **kwargs):
return str_to_path(shutil.copytree(*args, **kwargs)) return str_to_path(shutil.copytree(*args, **kwargs))
def rmtree(*args, **kwargs):
"""
Wraps :func:shutil.rmtree` so that we can accept Path objects.
:param openlp.core.common.path.Path path: Takes a Path object which is then converted to a str object
:return: Passes the return from :func:`shutil.rmtree` back
:rtype: None
See the following link for more information on the other parameters:
https://docs.python.org/3/library/shutil.html#shutil.rmtree
"""
args, kwargs = replace_params(args, kwargs, ((0, 'path', path_to_str),))
return shutil.rmtree(*args, **kwargs)
def which(*args, **kwargs): def which(*args, **kwargs):
""" """
Wraps :func:shutil.which` so that it return a Path objects. Wraps :func:shutil.which` so that it return a Path objects.
@ -233,7 +226,7 @@ def create_paths(*paths, **kwargs):
try: try:
if not path.exists(): if not path.exists():
path.mkdir(parents=True) path.mkdir(parents=True)
except IOError: except OSError:
if not kwargs.get('do_not_log', False): if not kwargs.get('do_not_log', False):
log.exception('failed to check if directory exists or create directory') log.exception('failed to check if directory exists or create directory')

View File

@ -25,7 +25,7 @@ Provide Registry Services
import logging import logging
import sys import sys
from openlp.core.common import is_win, trace_error_handler from openlp.core.common import de_hump, trace_error_handler
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -61,6 +61,15 @@ class Registry(object):
registry.initialising = True registry.initialising = True
return registry return registry
@classmethod
def destroy(cls):
"""
Destroy the Registry.
"""
if cls.__instance__.running_under_test:
del cls.__instance__
cls.__instance__ = None
def get(self, key): def get(self, key):
""" """
Extracts the registry value from the list based on the key passed in Extracts the registry value from the list based on the key passed in
@ -119,7 +128,7 @@ class Registry(object):
:param event: The function description.. :param event: The function description..
:param function: The function to be called when the event happens. :param function: The function to be called when the event happens.
""" """
if event in self.functions_list: if event in self.functions_list and function in self.functions_list[event]:
self.functions_list[event].remove(function) self.functions_list[event].remove(function)
def execute(self, event, *args, **kwargs): def execute(self, event, *args, **kwargs):
@ -142,8 +151,9 @@ class Registry(object):
trace_error_handler(log) trace_error_handler(log)
log.exception('Exception for function {function}'.format(function=function)) log.exception('Exception for function {function}'.format(function=function))
else: else:
trace_error_handler(log) if log.getEffectiveLevel() == logging.DEBUG:
log.exception('Event {event} called but not registered'.format(event=event)) trace_error_handler(log)
log.exception('Event {event} called but not registered'.format(event=event))
return results return results
def get_flag(self, key): def get_flag(self, key):
@ -178,128 +188,30 @@ class Registry(object):
del self.working_flags[key] del self.working_flags[key]
class RegistryProperties(object): class RegistryBase(object):
""" """
This adds registry components to classes to use at run time. This adds registry components to classes to use at run time.
""" """
def __init__(self, *args, **kwargs):
"""
Register the class and bootstrap hooks.
"""
try:
super().__init__(*args, **kwargs)
except TypeError:
super().__init__()
Registry().register(de_hump(self.__class__.__name__), self)
Registry().register_function('bootstrap_initialise', self.bootstrap_initialise)
Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up)
@property def bootstrap_initialise(self):
def application(self):
""" """
Adds the openlp to the class dynamically. Dummy method to be overridden
Windows needs to access the application in a dynamic manner.
""" """
if is_win(): pass
return Registry().get('application')
else:
if not hasattr(self, '_application') or not self._application:
self._application = Registry().get('application')
return self._application
@property def bootstrap_post_set_up(self):
def plugin_manager(self):
""" """
Adds the plugin manager to the class dynamically Dummy method to be overridden
""" """
if not hasattr(self, '_plugin_manager') or not self._plugin_manager: pass
self._plugin_manager = Registry().get('plugin_manager')
return self._plugin_manager
@property
def image_manager(self):
"""
Adds the image manager to the class dynamically
"""
if not hasattr(self, '_image_manager') or not self._image_manager:
self._image_manager = Registry().get('image_manager')
return self._image_manager
@property
def media_controller(self):
"""
Adds the media controller to the class dynamically
"""
if not hasattr(self, '_media_controller') or not self._media_controller:
self._media_controller = Registry().get('media_controller')
return self._media_controller
@property
def service_manager(self):
"""
Adds the service manager to the class dynamically
"""
if not hasattr(self, '_service_manager') or not self._service_manager:
self._service_manager = Registry().get('service_manager')
return self._service_manager
@property
def preview_controller(self):
"""
Adds the preview controller to the class dynamically
"""
if not hasattr(self, '_preview_controller') or not self._preview_controller:
self._preview_controller = Registry().get('preview_controller')
return self._preview_controller
@property
def live_controller(self):
"""
Adds the live controller to the class dynamically
"""
if not hasattr(self, '_live_controller') or not self._live_controller:
self._live_controller = Registry().get('live_controller')
return self._live_controller
@property
def main_window(self):
"""
Adds the main window to the class dynamically
"""
if not hasattr(self, '_main_window') or not self._main_window:
self._main_window = Registry().get('main_window')
return self._main_window
@property
def renderer(self):
"""
Adds the Renderer to the class dynamically
"""
if not hasattr(self, '_renderer') or not self._renderer:
self._renderer = Registry().get('renderer')
return self._renderer
@property
def theme_manager(self):
"""
Adds the theme manager to the class dynamically
"""
if not hasattr(self, '_theme_manager') or not self._theme_manager:
self._theme_manager = Registry().get('theme_manager')
return self._theme_manager
@property
def settings_form(self):
"""
Adds the settings form to the class dynamically
"""
if not hasattr(self, '_settings_form') or not self._settings_form:
self._settings_form = Registry().get('settings_form')
return self._settings_form
@property
def alerts_manager(self):
"""
Adds the alerts manager to the class dynamically
"""
if not hasattr(self, '_alerts_manager') or not self._alerts_manager:
self._alerts_manager = Registry().get('alerts_manager')
return self._alerts_manager
@property
def projector_manager(self):
"""
Adds the projector manager to the class dynamically
"""
if not hasattr(self, '_projector_manager') or not self._projector_manager:
self._projector_manager = Registry().get('projector_manager')
return self._projector_manager

View File

@ -40,7 +40,7 @@ __version__ = 2
# Fix for bug #1014422. # Fix for bug #1014422.
X11_BYPASS_DEFAULT = True X11_BYPASS_DEFAULT = True
if is_linux(): if is_linux(): # pragma: no cover
# Default to False on Gnome. # Default to False on Gnome.
X11_BYPASS_DEFAULT = bool(not os.environ.get('GNOME_DESKTOP_SESSION_ID')) X11_BYPASS_DEFAULT = bool(not os.environ.get('GNOME_DESKTOP_SESSION_ID'))
# Default to False on Xfce. # Default to False on Xfce.
@ -206,11 +206,14 @@ class Settings(QtCore.QSettings):
'projector/source dialog type': 0 # Source select dialog box type 'projector/source dialog type': 0 # Source select dialog box type
} }
__file_path__ = '' __file_path__ = ''
# Settings upgrades prior to 3.0
__setting_upgrade_1__ = [ __setting_upgrade_1__ = [
# Changed during 2.2.x development.
('songs/search as type', 'advanced/search as type', []), ('songs/search as type', 'advanced/search as type', []),
('media/players', 'media/players_temp', [(media_players_conv, None)]), # Convert phonon to system ('media/players', 'media/players_temp', [(media_players_conv, None)]), # Convert phonon to system
('media/players_temp', 'media/players', []), # Move temp setting from above to correct setting ('media/players_temp', 'media/players', []), # Move temp setting from above to correct setting
]
# Settings upgrades for 3.0 (aka 2.6)
__setting_upgrade_2__ = [
('advanced/default color', 'core/logo background color', []), # Default image renamed + moved to general > 2.4. ('advanced/default color', 'core/logo background color', []), # Default image renamed + moved to general > 2.4.
('advanced/default image', 'core/logo file', []), # Default image renamed + moved to general after 2.4. ('advanced/default image', 'core/logo file', []), # Default image renamed + moved to general after 2.4.
('remotes/https enabled', '', []), ('remotes/https enabled', '', []),
@ -231,11 +234,9 @@ class Settings(QtCore.QSettings):
# Last search type was renamed to last used search type in 2.6 since Bible search value type changed in 2.6. # Last search type was renamed to last used search type in 2.6 since Bible search value type changed in 2.6.
('songs/last search type', 'songs/last used search type', []), ('songs/last search type', 'songs/last used search type', []),
('bibles/last search type', '', []), ('bibles/last search type', '', []),
('custom/last search type', 'custom/last used search type', [])] ('custom/last search type', 'custom/last used search type', []),
__setting_upgrade_2__ = [
# The following changes are being made for the conversion to using Path objects made in 2.6 development # The following changes are being made for the conversion to using Path objects made in 2.6 development
('advanced/data path', 'advanced/data path', [(str_to_path, None)]), ('advanced/data path', 'advanced/data path', [(lambda p: Path(p) if p is not None else None, None)]),
('crashreport/last directory', 'crashreport/last directory', [(str_to_path, None)]), ('crashreport/last directory', 'crashreport/last directory', [(str_to_path, None)]),
('servicemanager/last directory', 'servicemanager/last directory', [(str_to_path, None)]), ('servicemanager/last directory', 'servicemanager/last directory', [(str_to_path, None)]),
('servicemanager/last file', 'servicemanager/last file', [(str_to_path, None)]), ('servicemanager/last file', 'servicemanager/last file', [(str_to_path, None)]),
@ -255,7 +256,10 @@ class Settings(QtCore.QSettings):
('core/logo file', 'core/logo file', [(str_to_path, None)]), ('core/logo file', 'core/logo file', [(str_to_path, None)]),
('presentations/last directory', 'presentations/last directory', [(str_to_path, None)]), ('presentations/last directory', 'presentations/last directory', [(str_to_path, None)]),
('images/last directory', 'images/last directory', [(str_to_path, None)]), ('images/last directory', 'images/last directory', [(str_to_path, None)]),
('media/last directory', 'media/last directory', [(str_to_path, None)]) ('media/last directory', 'media/last directory', [(str_to_path, None)]),
('songuasge/db password', 'songusage/db password', []),
('songuasge/db hostname', 'songusage/db hostname', []),
('songuasge/db database', 'songusage/db database', [])
] ]
@staticmethod @staticmethod
@ -464,32 +468,38 @@ class Settings(QtCore.QSettings):
for version in range(current_version, __version__): for version in range(current_version, __version__):
version += 1 version += 1
upgrade_list = getattr(self, '__setting_upgrade_{version}__'.format(version=version)) upgrade_list = getattr(self, '__setting_upgrade_{version}__'.format(version=version))
for old_key, new_key, rules in upgrade_list: for old_keys, new_key, rules in upgrade_list:
# Once removed we don't have to do this again. - Can be removed once fully switched to the versioning # Once removed we don't have to do this again. - Can be removed once fully switched to the versioning
# system. # system.
if not self.contains(old_key): if not isinstance(old_keys, (tuple, list)):
old_keys = [old_keys]
if any([not self.contains(old_key) for old_key in old_keys]):
log.warning('One of {} does not exist, skipping upgrade'.format(old_keys))
continue continue
if new_key: if new_key:
# Get the value of the old_key. # Get the value of the old_key.
old_value = super(Settings, self).value(old_key) old_values = [super(Settings, self).value(old_key) for old_key in old_keys]
# When we want to convert the value, we have to figure out the default value (because we cannot get # When we want to convert the value, we have to figure out the default value (because we cannot get
# the default value from the central settings dict. # the default value from the central settings dict.
if rules: if rules:
default_value = rules[0][1] default_values = rules[0][1]
old_value = self._convert_value(old_value, default_value) if not isinstance(default_values, (list, tuple)):
default_values = [default_values]
old_values = [self._convert_value(old_value, default_value)
for old_value, default_value in zip(old_values, default_values)]
# Iterate over our rules and check what the old_value should be "converted" to. # Iterate over our rules and check what the old_value should be "converted" to.
for new, old in rules: new_value = None
for new_rule, old_rule in rules:
# If the value matches with the condition (rule), then use the provided value. This is used to # If the value matches with the condition (rule), then use the provided value. This is used to
# convert values. E. g. an old value 1 results in True, and 0 in False. # convert values. E. g. an old value 1 results in True, and 0 in False.
if callable(new): if callable(new_rule):
old_value = new(old_value) new_value = new_rule(*old_values)
elif old == old_value: elif old_rule in old_values:
old_value = new new_value = new_rule
break break
self.setValue(new_key, old_value) self.setValue(new_key, new_value)
if new_key != old_key: [self.remove(old_key) for old_key in old_keys if old_key != new_key]
self.remove(old_key) self.setValue('settings/version', version)
self.setValue('settings/version', version)
def value(self, key): def value(self, key):
""" """

View File

@ -25,9 +25,9 @@ import re
from string import Template from string import Template
from PyQt5 import QtGui, QtCore, QtWebKitWidgets from PyQt5 import QtGui, QtCore, QtWebKitWidgets
from openlp.core.common.mixins import OpenLPMixin, RegistryMixin from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import path_to_str from openlp.core.common.path import path_to_str
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList from openlp.core.display.screens import ScreenList
from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ServiceItem, expand_tags, build_chords_css, \ from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ServiceItem, expand_tags, build_chords_css, \
@ -46,7 +46,7 @@ VERSE_FOR_LINE_COUNT = '\n'.join(map(str, range(100)))
FOOTER = ['Arky Arky (Unknown)', 'Public Domain', 'CCLI 123456'] FOOTER = ['Arky Arky (Unknown)', 'Public Domain', 'CCLI 123456']
class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties): class Renderer(RegistryBase, LogMixin, RegistryProperties):
""" """
Class to pull all Renderer interactions into one place. The plugins will call helper methods to do the rendering but Class to pull all Renderer interactions into one place. The plugins will call helper methods to do the rendering but
this class will provide display defense code. this class will provide display defense code.
@ -198,6 +198,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
:param theme_data: The theme to generated a preview for. :param theme_data: The theme to generated a preview for.
:param force_page: Flag to tell message lines per page need to be generated. :param force_page: Flag to tell message lines per page need to be generated.
:rtype: QtGui.QPixmap
""" """
# save value for use in format_slide # save value for use in format_slide
self.force_page = force_page self.force_page = force_page
@ -222,8 +223,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
self.display.build_html(service_item) self.display.build_html(service_item)
raw_html = service_item.get_rendered_frame(0) raw_html = service_item.get_rendered_frame(0)
self.display.text(raw_html, False) self.display.text(raw_html, False)
preview = self.display.preview() return self.display.preview()
return preview
self.force_page = False self.force_page = False
def format_slide(self, text, item): def format_slide(self, text, item):

View File

@ -104,7 +104,7 @@ def get_text_file_string(text_file_path):
# no BOM was found # no BOM was found
file_handle.seek(0) file_handle.seek(0)
content = file_handle.read() content = file_handle.read()
except (IOError, UnicodeError): except (OSError, UnicodeError):
log.exception('Failed to open text file {text}'.format(text=text_file_path)) log.exception('Failed to open text file {text}'.format(text=text_file_path))
return content return content
@ -179,8 +179,9 @@ def create_thumb(image_path, thumb_path, return_icon=True, size=None):
height of 88 is used. height of 88 is used.
:return: The final icon. :return: The final icon.
""" """
ext = os.path.splitext(thumb_path)[1].lower() # TODO: To path object
reader = QtGui.QImageReader(image_path) thumb_path = Path(thumb_path)
reader = QtGui.QImageReader(str(image_path))
if size is None: if size is None:
# No size given; use default height of 88 # No size given; use default height of 88
if reader.size().isEmpty(): if reader.size().isEmpty():
@ -207,10 +208,10 @@ def create_thumb(image_path, thumb_path, return_icon=True, size=None):
# Invalid; use default height of 88 # Invalid; use default height of 88
reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88)) reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
thumb = reader.read() thumb = reader.read()
thumb.save(thumb_path, ext[1:]) thumb.save(str(thumb_path), thumb_path.suffix[1:].lower())
if not return_icon: if not return_icon:
return return
if os.path.exists(thumb_path): if thumb_path.exists():
return build_icon(thumb_path) return build_icon(thumb_path)
# Fallback for files with animation support. # Fallback for files with animation support.
return build_icon(image_path) return build_icon(image_path)
@ -620,6 +621,3 @@ from .serviceitem import ServiceItem, ServiceItemType, ItemCapabilities
from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css, build_chords_css from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css, build_chords_css
from .imagemanager import ImageManager from .imagemanager import ImageManager
from .mediamanageritem import MediaManagerItem from .mediamanageritem import MediaManagerItem
from .projector.db import ProjectorDB, Projector
from .projector.pjlink import PJLink
from .projector.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING

View File

@ -350,6 +350,7 @@ class Manager(object):
resulting in the plugin_name being used. resulting in the plugin_name being used.
:param upgrade_mod: The upgrade_schema function for this database :param upgrade_mod: The upgrade_schema function for this database
""" """
super().__init__()
self.is_dirty = False self.is_dirty = False
self.session = None self.session = None
self.db_url = None self.db_url = None

View File

@ -30,14 +30,15 @@ from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.common.path import Path, path_to_str, str_to_path
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.lib import ServiceItem, StringContent, ServiceItemContext from openlp.core.lib import ServiceItem, StringContent, ServiceItemContext
from openlp.core.lib.searchedit import SearchEdit
from openlp.core.lib.ui import create_widget_action, critical_error_message_box from openlp.core.lib.ui import create_widget_action, critical_error_message_box
from openlp.core.ui.lib.filedialog import FileDialog from openlp.core.widgets.dialogs import FileDialog
from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD from openlp.core.widgets.edits import SearchEdit
from openlp.core.ui.lib.toolbar import OpenLPToolbar from openlp.core.widgets.toolbar import OpenLPToolbar
from openlp.core.widgets.views import ListWidgetWithDnD
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -91,7 +92,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
Run some initial setup. This method is separate from __init__ in order to mock it out in tests. Run some initial setup. This method is separate from __init__ in order to mock it out in tests.
""" """
self.hide() self.hide()
self.whitespace = re.compile(r'[\W_]+', re.UNICODE) self.whitespace = re.compile(r'[\W_]+')
visible_title = self.plugin.get_string(StringContent.VisibleName) visible_title = self.plugin.get_string(StringContent.VisibleName)
self.title = str(visible_title['title']) self.title = str(visible_title['title'])
Registry().register(self.plugin.name, self) Registry().register(self.plugin.name, self)
@ -317,10 +318,10 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
self, self.on_new_prompt, self, self.on_new_prompt,
Settings().value(self.settings_section + '/last directory'), Settings().value(self.settings_section + '/last directory'),
self.on_new_file_masks) self.on_new_file_masks)
log.info('New files(s) {file_paths}'.format(file_paths=file_paths)) log.info('New file(s) {file_paths}'.format(file_paths=file_paths))
if file_paths: if file_paths:
self.application.set_busy_cursor() self.application.set_busy_cursor()
self.validate_and_load([path_to_str(path) for path in file_paths]) self.validate_and_load(file_paths)
self.application.set_normal_cursor() self.application.set_normal_cursor()
def load_file(self, data): def load_file(self, data):
@ -329,21 +330,24 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
:param data: A dictionary containing the list of files to be loaded and the target :param data: A dictionary containing the list of files to be loaded and the target
""" """
new_files = [] new_file_paths = []
error_shown = False error_shown = False
for file_name in data['files']: for file_name in data['files']:
file_type = file_name.split('.')[-1] file_path = str_to_path(file_name)
if file_type.lower() not in self.on_new_file_masks: if file_path.suffix[1:].lower() not in self.on_new_file_masks:
if not error_shown: if not error_shown:
critical_error_message_box(translate('OpenLP.MediaManagerItem', 'Invalid File Type'), critical_error_message_box(
translate('OpenLP.MediaManagerItem', translate('OpenLP.MediaManagerItem', 'Invalid File Type'),
'Invalid File {name}.\n' translate('OpenLP.MediaManagerItem',
'Suffix not supported').format(name=file_name)) 'Invalid File {file_path}.\nFile extension not supported').format(
file_path=file_path))
error_shown = True error_shown = True
else: else:
new_files.append(file_name) new_file_paths.append(file_path)
if new_files: if new_file_paths:
self.validate_and_load(new_files, data['target']) if 'target' in data:
self.validate_and_load(new_file_paths, data['target'])
self.validate_and_load(new_file_paths)
def dnd_move_internal(self, target): def dnd_move_internal(self, target):
""" """
@ -353,12 +357,12 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
""" """
pass pass
def validate_and_load(self, files, target_group=None): def validate_and_load(self, file_paths, target_group=None):
""" """
Process a list for files either from the File Dialog or from Drag and Process a list for files either from the File Dialog or from Drag and
Drop Drop
:param files: The files to be loaded. :param list[openlp.core.common.path.Path] file_paths: The files to be loaded.
:param target_group: The QTreeWidgetItem of the group that will be the parent of the added files :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
""" """
full_list = [] full_list = []
@ -366,18 +370,17 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
full_list.append(self.list_view.item(count).data(QtCore.Qt.UserRole)) full_list.append(self.list_view.item(count).data(QtCore.Qt.UserRole))
duplicates_found = False duplicates_found = False
files_added = False files_added = False
for file_path in files: for file_path in file_paths:
if file_path in full_list: if path_to_str(file_path) in full_list:
duplicates_found = True duplicates_found = True
else: else:
files_added = True files_added = True
full_list.append(file_path) full_list.append(path_to_str(file_path))
if full_list and files_added: if full_list and files_added:
if target_group is None: if target_group is None:
self.list_view.clear() self.list_view.clear()
self.load_list(full_list, target_group) self.load_list(full_list, target_group)
last_dir = os.path.split(files[0])[0] Settings().setValue(self.settings_section + '/last directory', file_paths[0].parent)
Settings().setValue(self.settings_section + '/last directory', Path(last_dir))
Settings().setValue('{section}/{section} files'.format(section=self.settings_section), self.get_file_list()) Settings().setValue('{section}/{section} files'.format(section=self.settings_section), self.get_file_list())
if duplicates_found: if duplicates_found:
critical_error_message_box(UiStrings().Duplicate, critical_error_message_box(UiStrings().Duplicate,

View File

@ -26,9 +26,10 @@ import logging
from PyQt5 import QtCore from PyQt5 import QtCore
from openlp.core.common.registry import Registry, RegistryProperties
from openlp.core.common.settings import Settings
from openlp.core.common.i18n import UiStrings from openlp.core.common.i18n import UiStrings
from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.version import get_version from openlp.core.version import get_version
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -138,10 +139,6 @@ class Plugin(QtCore.QObject, RegistryProperties):
self.text_strings = {} self.text_strings = {}
self.set_plugin_text_strings() self.set_plugin_text_strings()
self.name_strings = self.text_strings[StringContent.Name] self.name_strings = self.text_strings[StringContent.Name]
if version:
self.version = version
else:
self.version = get_version()['version']
self.settings_section = self.name self.settings_section = self.name
self.icon = None self.icon = None
self.media_item_class = media_item_class self.media_item_class = media_item_class
@ -161,6 +158,19 @@ class Plugin(QtCore.QObject, RegistryProperties):
Settings.extend_default_settings(default_settings) Settings.extend_default_settings(default_settings)
Registry().register_function('{name}_add_service_item'.format(name=self.name), self.process_add_service_event) Registry().register_function('{name}_add_service_item'.format(name=self.name), self.process_add_service_event)
Registry().register_function('{name}_config_updated'.format(name=self.name), self.config_update) Registry().register_function('{name}_config_updated'.format(name=self.name), self.config_update)
self._setup(version)
def _setup(self, version):
"""
Run some initial setup. This method is separate from __init__ in order to mock it out in tests.
:param version: Defaults to *None*, which means that the same version number is used as OpenLP's version number.
:rtype: None
"""
if version:
self.version = version
else:
self.version = get_version()['version']
def check_pre_conditions(self): def check_pre_conditions(self):
""" """

View File

@ -26,12 +26,12 @@ import os
from openlp.core.common import extension_loader from openlp.core.common import extension_loader
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.registry import RegistryProperties from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.mixins import OpenLPMixin, RegistryMixin from openlp.core.common.registry import RegistryBase
from openlp.core.lib import Plugin, PluginStatus from openlp.core.lib import Plugin, PluginStatus
class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties): class PluginManager(RegistryBase, LogMixin, RegistryProperties):
""" """
This is the Plugin manager, which loads all the plugins, This is the Plugin manager, which loads all the plugins,
and executes all the hooks, as and when necessary. and executes all the hooks, as and when necessary.
@ -43,8 +43,7 @@ class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
""" """
super(PluginManager, self).__init__(parent) super(PluginManager, self).__init__(parent)
self.log_info('Plugin manager Initialising') self.log_info('Plugin manager Initialising')
self.base_path = os.path.abspath(str(AppLocation.get_directory(AppLocation.PluginsDir))) self.log_debug('Base path {path}'.format(path=AppLocation.get_directory(AppLocation.PluginsDir)))
self.log_debug('Base path {path}'.format(path=self.base_path))
self.plugins = [] self.plugins = []
self.log_info('Plugin manager Initialised') self.log_info('Plugin manager Initialised')

View File

@ -1,175 +0,0 @@
# -*- 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 #
###############################################################################
import logging
from PyQt5 import QtCore, QtWidgets
from openlp.core.common.settings import Settings
from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_widget_action
log = logging.getLogger(__name__)
class SearchEdit(QtWidgets.QLineEdit):
"""
This is a specialised QLineEdit with a "clear" button inside for searches.
"""
searchTypeChanged = QtCore.pyqtSignal(QtCore.QVariant)
cleared = QtCore.pyqtSignal()
def __init__(self, parent, settings_section):
"""
Constructor.
"""
super().__init__(parent)
self.settings_section = settings_section
self._current_search_type = -1
self.clear_button = QtWidgets.QToolButton(self)
self.clear_button.setIcon(build_icon(':/system/clear_shortcut.png'))
self.clear_button.setCursor(QtCore.Qt.ArrowCursor)
self.clear_button.setStyleSheet('QToolButton { border: none; padding: 0px; }')
self.clear_button.resize(18, 18)
self.clear_button.hide()
self.clear_button.clicked.connect(self._on_clear_button_clicked)
self.textChanged.connect(self._on_search_edit_text_changed)
self._update_style_sheet()
self.setAcceptDrops(False)
def _update_style_sheet(self):
"""
Internal method to update the stylesheet depending on which widgets are available and visible.
"""
frame_width = self.style().pixelMetric(QtWidgets.QStyle.PM_DefaultFrameWidth)
right_padding = self.clear_button.width() + frame_width
if hasattr(self, 'menu_button'):
left_padding = self.menu_button.width()
stylesheet = 'QLineEdit {{ padding-left:{left}px; padding-right: {right}px; }} '.format(left=left_padding,
right=right_padding)
else:
stylesheet = 'QLineEdit {{ padding-right: {right}px; }} '.format(right=right_padding)
self.setStyleSheet(stylesheet)
msz = self.minimumSizeHint()
self.setMinimumSize(max(msz.width(), self.clear_button.width() + (frame_width * 2) + 2),
max(msz.height(), self.clear_button.height() + (frame_width * 2) + 2))
def resizeEvent(self, event):
"""
Reimplemented method to react to resizing of the widget.
:param event: The event that happened.
"""
size = self.clear_button.size()
frame_width = self.style().pixelMetric(QtWidgets.QStyle.PM_DefaultFrameWidth)
self.clear_button.move(self.rect().right() - frame_width - size.width(),
(self.rect().bottom() + 1 - size.height()) // 2)
if hasattr(self, 'menu_button'):
size = self.menu_button.size()
self.menu_button.move(self.rect().left() + frame_width + 2, (self.rect().bottom() + 1 - size.height()) // 2)
def current_search_type(self):
"""
Readonly property to return the current search type.
"""
return self._current_search_type
def set_current_search_type(self, identifier):
"""
Set a new current search type.
:param identifier: The search type identifier (int).
"""
menu = self.menu_button.menu()
for action in menu.actions():
if identifier == action.data():
self.setPlaceholderText(action.placeholder_text)
self.menu_button.setDefaultAction(action)
self._current_search_type = identifier
Settings().setValue('{section}/last used search type'.format(section=self.settings_section), identifier)
self.searchTypeChanged.emit(identifier)
return True
def set_search_types(self, items):
"""
A list of tuples to be used in the search type menu. The first item in the list will be preselected as the
default.
:param items: The list of tuples to use. The tuples should contain an integer identifier, an icon (QIcon
instance or string) and a title for the item in the menu. In short, they should look like this::
(<identifier>, <icon>, <title>, <place holder text>)
For instance::
(1, <QIcon instance>, "Titles", "Search Song Titles...")
Or::
(2, ":/songs/authors.png", "Authors", "Search Authors...")
"""
menu = QtWidgets.QMenu(self)
for identifier, icon, title, placeholder in items:
action = create_widget_action(
menu, text=title, icon=icon, data=identifier, triggers=self._on_menu_action_triggered)
action.placeholder_text = placeholder
if not hasattr(self, 'menu_button'):
self.menu_button = QtWidgets.QToolButton(self)
self.menu_button.setIcon(build_icon(':/system/clear_shortcut.png'))
self.menu_button.setCursor(QtCore.Qt.ArrowCursor)
self.menu_button.setPopupMode(QtWidgets.QToolButton.InstantPopup)
self.menu_button.setStyleSheet('QToolButton { border: none; padding: 0px 10px 0px 0px; }')
self.menu_button.resize(QtCore.QSize(28, 18))
self.menu_button.setMenu(menu)
self.set_current_search_type(
Settings().value('{section}/last used search type'.format(section=self.settings_section)))
self.menu_button.show()
self._update_style_sheet()
def _on_search_edit_text_changed(self, text):
"""
Internally implemented slot to react to when the text in the line edit has changed so that we can show or hide
the clear button.
:param text: A :class:`~PyQt5.QtCore.QString` instance which represents the text in the line edit.
"""
self.clear_button.setVisible(bool(text))
def _on_clear_button_clicked(self):
"""
Internally implemented slot to react to the clear button being clicked to clear the line edit. Once it has
cleared the line edit, it emits the ``cleared()`` signal so that an application can react to the clearing of the
line edit.
"""
self.clear()
self.cleared.emit()
def _on_menu_action_triggered(self):
"""
Internally implemented slot to react to the select of one of the search types in the menu. Once it has set the
correct action on the button, and set the current search type (using the list of identifiers provided by the
developer), the ``searchTypeChanged(int)`` signal is emitted with the identifier.
"""
for action in self.menu_button.menu().actions():
# Why is this needed?
action.setChecked(False)
self.set_current_search_type(self.sender().data())

View File

@ -35,7 +35,7 @@ from PyQt5 import QtGui
from openlp.core.common import md5_hash from openlp.core.common import md5_hash
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.common.registry import RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, expand_chords from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, expand_chords

View File

@ -25,7 +25,7 @@ own tab to the settings dialog.
""" """
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from openlp.core.common.registry import RegistryProperties from openlp.core.common.mixins import RegistryProperties
class SettingsTab(QtWidgets.QWidget, RegistryProperties): class SettingsTab(QtWidgets.QWidget, RegistryProperties):

View File

@ -20,9 +20,9 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
""" """
:mod:`openlp.core.ui.projector` :mod:`openlp.core.projectors`
Initialization for the openlp.core.ui.projector modules. Initialization for the openlp.core.projectors modules.
""" """

View File

@ -144,6 +144,24 @@ PJLINK_VALID_CMD = {
} }
} }
# QAbstractSocketState enums converted to string
S_QSOCKET_STATE = {
0: 'QSocketState - UnconnectedState',
1: 'QSocketState - HostLookupState',
2: 'QSocketState - ConnectingState',
3: 'QSocketState - ConnectedState',
4: 'QSocketState - BoundState',
5: 'QSocketState - ListeningState (internal use only)',
6: 'QSocketState - ClosingState',
'UnconnectedState': 0,
'HostLookupState': 1,
'ConnectingState': 2,
'ConnectedState': 3,
'BoundState': 4,
'ListeningState': 5,
'ClosingState': 6
}
# Error and status codes # Error and status codes
S_OK = E_OK = 0 # E_OK included since I sometimes forget S_OK = E_OK = 0 # E_OK included since I sometimes forget
# Error codes. Start at 200 so we don't duplicate system error codes. # Error codes. Start at 200 so we don't duplicate system error codes.

View File

@ -43,13 +43,13 @@ from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from openlp.core.lib.db import Manager, init_db, init_url from openlp.core.lib.db import Manager, init_db, init_url
from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES from openlp.core.projectors.constants import PJLINK_DEFAULT_CODES
from openlp.core.lib.projector import upgrade from openlp.core.projectors import upgrade
Base = declarative_base(MetaData()) Base = declarative_base(MetaData())
class CommonBase(object): class CommonMixin(object):
""" """
Base class to automate table name and ID column. Base class to automate table name and ID column.
""" """
@ -60,7 +60,7 @@ class CommonBase(object):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
class Manufacturer(CommonBase, Base): class Manufacturer(Base, CommonMixin):
""" """
Projector manufacturer table. Projector manufacturer table.
@ -85,7 +85,7 @@ class Manufacturer(CommonBase, Base):
lazy='joined') lazy='joined')
class Model(CommonBase, Base): class Model(Base, CommonMixin):
""" """
Projector model table. Projector model table.
@ -113,7 +113,7 @@ class Model(CommonBase, Base):
lazy='joined') lazy='joined')
class Source(CommonBase, Base): class Source(Base, CommonMixin):
""" """
Projector video source table. Projector video source table.
@ -140,7 +140,7 @@ class Source(CommonBase, Base):
text = Column(String(30)) text = Column(String(30))
class Projector(CommonBase, Base): class Projector(Base, CommonMixin):
""" """
Projector table. Projector table.
@ -213,7 +213,7 @@ class Projector(CommonBase, Base):
lazy='joined') lazy='joined')
class ProjectorSource(CommonBase, Base): class ProjectorSource(Base, CommonMixin):
""" """
Projector local source table Projector local source table
This table allows mapping specific projector source input to a local This table allows mapping specific projector source input to a local
@ -415,7 +415,7 @@ class ProjectorDB(Manager):
for key in projector.source_available: for key in projector.source_available:
item = self.get_object_filtered(ProjectorSource, item = self.get_object_filtered(ProjectorSource,
and_(ProjectorSource.code == key, and_(ProjectorSource.code == key,
ProjectorSource.projector_id == projector.dbid)) ProjectorSource.projector_id == projector.id))
if item is None: if item is None:
source_dict[key] = PJLINK_DEFAULT_CODES[key] source_dict[key] = PJLINK_DEFAULT_CODES[key]
else: else:

View File

@ -30,8 +30,8 @@ from PyQt5 import QtCore, QtWidgets
from openlp.core.common import verify_ip_address from openlp.core.common import verify_ip_address
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
from openlp.core.lib.projector.db import Projector from openlp.core.projectors.db import Projector
from openlp.core.lib.projector.constants import PJLINK_PORT from openlp.core.projectors.constants import PJLINK_PORT
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.debug('editform loaded') log.debug('editform loaded')

View File

@ -20,9 +20,9 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
""" """
:mod: openlp.core.ui.projector.manager` module :mod: openlp.core.ui.projector.manager` module
Provides the functions for the display/control of Projectors. Provides the functions for the display/control of Projectors.
""" """
import logging import logging
@ -30,19 +30,19 @@ import logging
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.common.mixins import OpenLPMixin, RegistryMixin from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import RegistryProperties from openlp.core.common.registry import RegistryBase
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.lib.ui import create_widget_action from openlp.core.lib.ui import create_widget_action
from openlp.core.lib.projector import DialogSourceStyle from openlp.core.projectors import DialogSourceStyle
from openlp.core.lib.projector.constants import ERROR_MSG, ERROR_STRING, E_AUTHENTICATION, E_ERROR, \ from openlp.core.projectors.constants import ERROR_MSG, ERROR_STRING, E_AUTHENTICATION, E_ERROR, \
E_NETWORK, E_NOT_CONNECTED, E_UNKNOWN_SOCKET_ERROR, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_COOLDOWN, \ 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 S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP
from openlp.core.lib.projector.db import ProjectorDB from openlp.core.projectors.db import ProjectorDB
from openlp.core.lib.projector.pjlink import PJLink, PJLinkUDP from openlp.core.projectors.pjlink import PJLink, PJLinkUDP
from openlp.core.ui.lib import OpenLPToolbar from openlp.core.projectors.editform import ProjectorEditForm
from openlp.core.ui.projector.editform import ProjectorEditForm from openlp.core.projectors.sourceselectform import SourceSelectTabs, SourceSelectSingle
from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle from openlp.core.widgets.toolbar import OpenLPToolbar
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.debug('projectormanager loaded') log.debug('projectormanager loaded')
@ -276,7 +276,7 @@ class UiProjectorManager(object):
self.update_icons() self.update_icons()
class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjectorManager, RegistryProperties): class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogMixin, RegistryProperties):
""" """
Manage the projectors. Manage the projectors.
""" """
@ -288,7 +288,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
:param projectordb: Database session inherited from superclass. :param projectordb: Database session inherited from superclass.
""" """
log.debug('__init__()') log.debug('__init__()')
super().__init__(parent) super(ProjectorManager, self).__init__(parent)
self.settings_section = 'projector' self.settings_section = 'projector'
self.projectordb = projectordb self.projectordb = projectordb
self.projector_list = [] self.projector_list = []
@ -518,7 +518,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
projector.thread.quit() projector.thread.quit()
new_list = [] new_list = []
for item in self.projector_list: for item in self.projector_list:
if item.link.dbid == projector.link.dbid: if item.link.db_item.id == projector.link.db_item.id:
continue continue
new_list.append(item) new_list.append(item)
self.projector_list = new_list self.projector_list = new_list
@ -672,14 +672,16 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
data=projector.model_filter) data=projector.model_filter)
count = 1 count = 1
for item in projector.link.lamp: for item in projector.link.lamp:
if item['On'] is None:
status = translate('OpenLP.ProjectorManager', 'Unavailable')
elif item['On']:
status = translate('OpenLP.ProjectorManager', 'ON')
else:
status = translate('OpenLP.ProjectorManager', 'OFF')
message += '<b>{title} {count}</b> {status} '.format(title=translate('OpenLP.ProjectorManager', message += '<b>{title} {count}</b> {status} '.format(title=translate('OpenLP.ProjectorManager',
'Lamp'), 'Lamp'),
count=count, count=count,
status=translate('OpenLP.ProjectorManager', status=status)
'ON')
if item['On']
else translate('OpenLP.ProjectorManager',
'OFF'))
message += '<b>{title}</b>: {hours}<br />'.format(title=translate('OpenLP.ProjectorManager', 'Hours'), message += '<b>{title}</b>: {hours}<br />'.format(title=translate('OpenLP.ProjectorManager', 'Hours'),
hours=item['Hours']) hours=item['Hours'])
@ -730,7 +732,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
thread.started.connect(item.link.thread_started) thread.started.connect(item.link.thread_started)
thread.finished.connect(item.link.thread_stopped) thread.finished.connect(item.link.thread_stopped)
thread.finished.connect(thread.deleteLater) thread.finished.connect(thread.deleteLater)
item.link.projectorNetwork.connect(self.update_status)
item.link.changeStatus.connect(self.update_status) item.link.changeStatus.connect(self.update_status)
item.link.projectorAuthentication.connect(self.authentication_error) item.link.projectorAuthentication.connect(self.authentication_error)
item.link.projectorNoAuthentication.connect(self.no_authentication_error) item.link.projectorNoAuthentication.connect(self.no_authentication_error)

View File

@ -54,12 +54,11 @@ from PyQt5 import QtCore, QtNetwork
from openlp.core.common import qmd5_hash from openlp.core.common import qmd5_hash
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.lib.projector.constants import CONNECTION_ERRORS, CR, ERROR_MSG, ERROR_STRING, \ from openlp.core.projectors.constants import CONNECTION_ERRORS, CR, ERROR_MSG, ERROR_STRING, \
E_AUTHENTICATION, E_CONNECTION_REFUSED, E_GENERAL, E_INVALID_DATA, E_NETWORK, E_NOT_CONNECTED, E_OK, \ E_AUTHENTICATION, E_CONNECTION_REFUSED, E_GENERAL, E_INVALID_DATA, E_NETWORK, E_NOT_CONNECTED, E_OK, \
E_PARAMETER, E_PROJECTOR, E_SOCKET_TIMEOUT, E_UNAVAILABLE, E_UNDEFINED, PJLINK_ERRORS, PJLINK_ERST_DATA, \ E_PARAMETER, E_PROJECTOR, E_SOCKET_TIMEOUT, E_UNAVAILABLE, E_UNDEFINED, PJLINK_ERRORS, PJLINK_ERST_DATA, \
PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PORT, PJLINK_POWR_STATUS, PJLINK_VALID_CMD, \ PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PORT, PJLINK_POWR_STATUS, PJLINK_VALID_CMD, \
STATUS_STRING, S_CONNECTED, S_CONNECTING, S_INFO, S_NETWORK_RECEIVED, S_NETWORK_SENDING, \ STATUS_STRING, S_CONNECTED, S_CONNECTING, S_INFO, S_NOT_CONNECTED, S_OFF, S_OK, S_ON, S_QSOCKET_STATE, S_STATUS
S_NOT_CONNECTED, S_OFF, S_OK, S_ON, S_STATUS
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.debug('pjlink loaded') log.debug('pjlink loaded')
@ -111,7 +110,7 @@ class PJLinkCommands(object):
""" """
log.debug('PJlinkCommands(args={args} kwargs={kwargs})'.format(args=args, kwargs=kwargs)) log.debug('PJlinkCommands(args={args} kwargs={kwargs})'.format(args=args, kwargs=kwargs))
super().__init__() super().__init__()
# Map command to function # Map PJLink command to method
self.pjlink_functions = { self.pjlink_functions = {
'AVMT': self.process_avmt, 'AVMT': self.process_avmt,
'CLSS': self.process_clss, 'CLSS': self.process_clss,
@ -123,7 +122,9 @@ class PJLinkCommands(object):
'INST': self.process_inst, 'INST': self.process_inst,
'LAMP': self.process_lamp, 'LAMP': self.process_lamp,
'NAME': self.process_name, 'NAME': self.process_name,
'PJLINK': self.check_login, 'PJLINK': self.process_pjlink,
# TODO: Part of check_login refactor - remove when done
# 'PJLINK': self.check_login,
'POWR': self.process_powr, 'POWR': self.process_powr,
'SNUM': self.process_snum, 'SNUM': self.process_snum,
'SVER': self.process_sver, 'SVER': self.process_sver,
@ -135,7 +136,8 @@ class PJLinkCommands(object):
""" """
Initialize instance variables. Also used to reset projector-specific information to default. Initialize instance variables. Also used to reset projector-specific information to default.
""" """
log.debug('({ip}) reset_information() connect status is {state}'.format(ip=self.ip, state=self.state())) log.debug('({ip}) reset_information() connect status is {state}'.format(ip=self.ip,
state=S_QSOCKET_STATE[self.state()]))
self.fan = None # ERST self.fan = None # ERST
self.filter_time = None # FILT self.filter_time = None # FILT
self.lamp = None # LAMP self.lamp = None # LAMP
@ -165,6 +167,7 @@ class PJLinkCommands(object):
self.socket_timer.stop() self.socket_timer.stop()
self.send_busy = False self.send_busy = False
self.send_queue = [] self.send_queue = []
self.priority_queue = []
def process_command(self, cmd, data): def process_command(self, cmd, data):
""" """
@ -176,18 +179,19 @@ class PJLinkCommands(object):
log.debug('({ip}) Processing command "{cmd}" with data "{data}"'.format(ip=self.ip, log.debug('({ip}) Processing command "{cmd}" with data "{data}"'.format(ip=self.ip,
cmd=cmd, cmd=cmd,
data=data)) data=data))
# Check if we have a future command not available yet # cmd should already be in uppercase, but data may be in mixed-case.
_cmd = cmd.upper() # Due to some replies should stay as mixed-case, validate using separate uppercase check
_data = data.upper() _data = data.upper()
if _cmd not in PJLINK_VALID_CMD: # Check if we have a future command not available yet
log.error("({ip}) Ignoring command='{cmd}' (Invalid/Unknown)".format(ip=self.ip, cmd=cmd)) if cmd not in PJLINK_VALID_CMD:
log.error('({ip}) Ignoring command="{cmd}" (Invalid/Unknown)'.format(ip=self.ip, cmd=cmd))
return return
elif _data == 'OK': elif _data == 'OK':
log.debug('({ip}) Command "{cmd}" returned OK'.format(ip=self.ip, cmd=cmd)) log.debug('({ip}) Command "{cmd}" returned OK'.format(ip=self.ip, cmd=cmd))
# A command returned successfully, no further processing needed # A command returned successfully, so do a query on command to verify status
return return self.send_command(cmd=cmd)
elif _cmd not in self.pjlink_functions: elif cmd not in self.pjlink_functions:
log.warning("({ip}) Unable to process command='{cmd}' (Future option)".format(ip=self.ip, cmd=cmd)) log.warning('({ip}) Unable to process command="{cmd}" (Future option?)'.format(ip=self.ip, cmd=cmd))
return return
elif _data in PJLINK_ERRORS: elif _data in PJLINK_ERRORS:
# Oops - projector error # Oops - projector error
@ -211,12 +215,10 @@ class PJLinkCommands(object):
elif _data == PJLINK_ERRORS[E_PROJECTOR]: elif _data == PJLINK_ERRORS[E_PROJECTOR]:
# Projector/display error # Projector/display error
self.change_status(E_PROJECTOR) self.change_status(E_PROJECTOR)
self.receive_data_signal()
return return
# Command checks already passed # Command checks already passed
log.debug('({ip}) Calling function for {cmd}'.format(ip=self.ip, cmd=cmd)) log.debug('({ip}) Calling function for {cmd}'.format(ip=self.ip, cmd=cmd))
self.receive_data_signal() self.pjlink_functions[cmd](data)
self.pjlink_functions[_cmd](data)
def process_avmt(self, data): def process_avmt(self, data):
""" """
@ -259,19 +261,19 @@ class PJLinkCommands(object):
# : Received: '%1CLSS=Class 1' (Optoma) # : Received: '%1CLSS=Class 1' (Optoma)
# : Received: '%1CLSS=Version1' (BenQ) # : Received: '%1CLSS=Version1' (BenQ)
if len(data) > 1: if len(data) > 1:
log.warning("({ip}) Non-standard CLSS reply: '{data}'".format(ip=self.ip, data=data)) log.warning('({ip}) Non-standard CLSS reply: "{data}"'.format(ip=self.ip, data=data))
# Due to stupid projectors not following standards (Optoma, BenQ comes to mind), # Due to stupid projectors not following standards (Optoma, BenQ comes to mind),
# AND the different responses that can be received, the semi-permanent way to # AND the different responses that can be received, the semi-permanent way to
# fix the class reply is to just remove all non-digit characters. # fix the class reply is to just remove all non-digit characters.
try: try:
clss = re.findall('\d', data)[0] # Should only be the first match clss = re.findall('\d', data)[0] # Should only be the first match
except IndexError: except IndexError:
log.error("({ip}) No numbers found in class version reply '{data}' - " log.error('({ip}) No numbers found in class version reply "{data}" - '
"defaulting to class '1'".format(ip=self.ip, data=data)) 'defaulting to class "1"'.format(ip=self.ip, data=data))
clss = '1' clss = '1'
elif not data.isdigit(): elif not data.isdigit():
log.error("({ip}) NAN clss version reply '{data}' - " log.error('({ip}) NAN CLSS version reply "{data}" - '
"defaulting to class '1'".format(ip=self.ip, data=data)) 'defaulting to class "1"'.format(ip=self.ip, data=data))
clss = '1' clss = '1'
else: else:
clss = data clss = data
@ -289,7 +291,7 @@ class PJLinkCommands(object):
""" """
if len(data) != PJLINK_ERST_DATA['DATA_LENGTH']: if len(data) != PJLINK_ERST_DATA['DATA_LENGTH']:
count = PJLINK_ERST_DATA['DATA_LENGTH'] count = PJLINK_ERST_DATA['DATA_LENGTH']
log.warning("{ip}) Invalid error status response '{data}': length != {count}".format(ip=self.ip, log.warning('{ip}) Invalid error status response "{data}": length != {count}'.format(ip=self.ip,
data=data, data=data,
count=count)) count=count))
return return
@ -297,7 +299,7 @@ class PJLinkCommands(object):
datacheck = int(data) datacheck = int(data)
except ValueError: except ValueError:
# Bad data - ignore # Bad data - ignore
log.warning("({ip}) Invalid error status response '{data}'".format(ip=self.ip, data=data)) log.warning('({ip}) Invalid error status response "{data}"'.format(ip=self.ip, data=data))
return return
if datacheck == 0: if datacheck == 0:
self.projector_errors = None self.projector_errors = None
@ -402,17 +404,20 @@ class PJLinkCommands(object):
:param data: Lamp(s) status. :param data: Lamp(s) status.
""" """
lamps = [] lamps = []
data_dict = data.split() lamp_list = data.split()
while data_dict: if len(lamp_list) < 2:
try: lamps.append({'Hours': int(lamp_list[0]), 'On': None})
fill = {'Hours': int(data_dict[0]), 'On': False if data_dict[1] == '0' else True} else:
except ValueError: while lamp_list:
# In case of invalid entry try:
log.warning('({ip}) process_lamp(): Invalid data "{data}"'.format(ip=self.ip, data=data)) fill = {'Hours': int(lamp_list[0]), 'On': False if lamp_list[1] == '0' else True}
return except ValueError:
lamps.append(fill) # In case of invalid entry
data_dict.pop(0) # Remove lamp hours log.warning('({ip}) process_lamp(): Invalid data "{data}"'.format(ip=self.ip, data=data))
data_dict.pop(0) # Remove lamp on/off return
lamps.append(fill)
lamp_list.pop(0) # Remove lamp hours
lamp_list.pop(0) # Remove lamp on/off
self.lamp = lamps self.lamp = lamps
return return
@ -427,6 +432,51 @@ class PJLinkCommands(object):
log.debug('({ip}) Setting projector PJLink name to "{data}"'.format(ip=self.ip, data=self.pjlink_name)) log.debug('({ip}) Setting projector PJLink name to "{data}"'.format(ip=self.ip, data=self.pjlink_name))
return return
def process_pjlink(self, data):
"""
Process initial socket connection to terminal.
:param data: Initial packet with authentication scheme
"""
log.debug('({ip}) Processing PJLINK command'.format(ip=self.ip))
chk = data.split(' ')
if len(chk[0]) != 1:
# Invalid - after splitting, first field should be 1 character, either '0' or '1' only
log.error('({ip}) Invalid initial authentication scheme - aborting'.format(ip=self.ip))
return self.disconnect_from_host()
elif chk[0] == '0':
# Normal connection no authentication
if len(chk) > 1:
# Invalid data - there should be nothing after a normal authentication scheme
log.error('({ip}) Normal connection with extra information - aborting'.format(ip=self.ip))
return self.disconnect_from_host()
elif self.pin:
log.error('({ip}) Normal connection but PIN set - aborting'.format(ip=self.ip))
return self.disconnect_from_host()
else:
data_hash = None
elif chk[0] == '1':
if len(chk) < 2:
# Not enough information for authenticated connection
log.error('({ip}) Authenticated connection but not enough info - aborting'.format(ip=self.ip))
return self.disconnect_from_host()
elif not self.pin:
log.error('({ip}) Authenticate connection but no PIN - aborting'.format(ip=self.ip))
return self.disconnect_from_host()
else:
data_hash = str(qmd5_hash(salt=chk[1].encode('utf-8'), data=self.pin.encode('utf-8')),
encoding='ascii')
# Passed basic checks, so start connection
self.readyRead.connect(self.get_socket)
if not self.no_poll:
log.debug('({ip}) process_pjlink(): Starting timer'.format(ip=self.ip))
self.timer.setInterval(2000) # Set 2 seconds for initial information
self.timer.start()
self.change_status(S_CONNECTED)
log.debug('({ip}) process_pjlink(): Sending "CLSS" initial command'.format(ip=self.ip))
# Since this is an initial connection, make it a priority just in case
return self.send_command(cmd="CLSS", salt=data_hash, priority=True)
def process_powr(self, data): def process_powr(self, data):
""" """
Power status. See PJLink specification for format. Power status. See PJLink specification for format.
@ -447,7 +497,7 @@ class PJLinkCommands(object):
self.send_command('INST') self.send_command('INST')
else: else:
# Log unknown status response # Log unknown status response
log.warning('({ip}) Unknown power response: {data}'.format(ip=self.ip, data=data)) log.warning('({ip}) Unknown power response: "{data}"'.format(ip=self.ip, data=data))
return return
def process_rfil(self, data): def process_rfil(self, data):
@ -457,9 +507,9 @@ class PJLinkCommands(object):
if self.model_filter is None: if self.model_filter is None:
self.model_filter = data self.model_filter = data
else: else:
log.warning("({ip}) Filter model already set".format(ip=self.ip)) log.warning('({ip}) Filter model already set'.format(ip=self.ip))
log.warning("({ip}) Saved model: '{old}'".format(ip=self.ip, old=self.model_filter)) log.warning('({ip}) Saved model: "{old}"'.format(ip=self.ip, old=self.model_filter))
log.warning("({ip}) New model: '{new}'".format(ip=self.ip, new=data)) log.warning('({ip}) New model: "{new}"'.format(ip=self.ip, new=data))
def process_rlmp(self, data): def process_rlmp(self, data):
""" """
@ -468,9 +518,9 @@ class PJLinkCommands(object):
if self.model_lamp is None: if self.model_lamp is None:
self.model_lamp = data self.model_lamp = data
else: else:
log.warning("({ip}) Lamp model already set".format(ip=self.ip)) log.warning('({ip}) Lamp model already set'.format(ip=self.ip))
log.warning("({ip}) Saved lamp: '{old}'".format(ip=self.ip, old=self.model_lamp)) log.warning('({ip}) Saved lamp: "{old}"'.format(ip=self.ip, old=self.model_lamp))
log.warning("({ip}) New lamp: '{new}'".format(ip=self.ip, new=data)) log.warning('({ip}) New lamp: "{new}"'.format(ip=self.ip, new=data))
def process_snum(self, data): def process_snum(self, data):
""" """
@ -479,16 +529,16 @@ class PJLinkCommands(object):
:param data: Serial number from projector. :param data: Serial number from projector.
""" """
if self.serial_no is None: if self.serial_no is None:
log.debug("({ip}) Setting projector serial number to '{data}'".format(ip=self.ip, data=data)) log.debug('({ip}) Setting projector serial number to "{data}"'.format(ip=self.ip, data=data))
self.serial_no = data self.serial_no = data
self.db_update = False self.db_update = False
else: else:
# Compare serial numbers and see if we got the same projector # Compare serial numbers and see if we got the same projector
if self.serial_no != data: if self.serial_no != data:
log.warning("({ip}) Projector serial number does not match saved serial number".format(ip=self.ip)) log.warning('({ip}) Projector serial number does not match saved serial number'.format(ip=self.ip))
log.warning("({ip}) Saved: '{old}'".format(ip=self.ip, old=self.serial_no)) log.warning('({ip}) Saved: "{old}"'.format(ip=self.ip, old=self.serial_no))
log.warning("({ip}) Received: '{new}'".format(ip=self.ip, new=data)) log.warning('({ip}) Received: "{new}"'.format(ip=self.ip, new=data))
log.warning("({ip}) NOT saving serial number".format(ip=self.ip)) log.warning('({ip}) NOT saving serial number'.format(ip=self.ip))
self.serial_no_received = data self.serial_no_received = data
def process_sver(self, data): def process_sver(self, data):
@ -497,30 +547,29 @@ class PJLinkCommands(object):
""" """
if len(data) > 32: if len(data) > 32:
# Defined in specs max version is 32 characters # Defined in specs max version is 32 characters
log.warning("Invalid software version - too long") log.warning('Invalid software version - too long')
return return
elif self.sw_version is None: elif self.sw_version is None:
log.debug("({ip}) Setting projector software version to '{data}'".format(ip=self.ip, data=data)) log.debug('({ip}) Setting projector software version to "{data}"'.format(ip=self.ip, data=data))
self.sw_version = data self.sw_version = data
self.db_update = True self.db_update = True
else: else:
# Compare software version and see if we got the same projector # Compare software version and see if we got the same projector
if self.serial_no != data: if self.serial_no != data:
log.warning("({ip}) Projector software version does not match saved " log.warning('({ip}) Projector software version does not match saved '
"software version".format(ip=self.ip)) 'software version'.format(ip=self.ip))
log.warning("({ip}) Saved: '{old}'".format(ip=self.ip, old=self.sw_version)) log.warning('({ip}) Saved: "{old}"'.format(ip=self.ip, old=self.sw_version))
log.warning("({ip}) Received: '{new}'".format(ip=self.ip, new=data)) log.warning('({ip}) Received: "{new}"'.format(ip=self.ip, new=data))
log.warning("({ip}) Saving new serial number as sw_version_received".format(ip=self.ip)) log.warning('({ip}) Saving new serial number as sw_version_received'.format(ip=self.ip))
self.sw_version_received = data self.sw_version_received = data
class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): class PJLink(QtNetwork.QTcpSocket, PJLinkCommands):
""" """
Socket service for PJLink TCP socket. Socket service for PJLink TCP socket.
""" """
# Signals sent by this module # Signals sent by this module
changeStatus = QtCore.pyqtSignal(str, int, str) changeStatus = QtCore.pyqtSignal(str, int, str)
projectorNetwork = QtCore.pyqtSignal(int) # Projector network activity
projectorStatus = QtCore.pyqtSignal(int) # Status update projectorStatus = QtCore.pyqtSignal(int) # Status update
projectorAuthentication = QtCore.pyqtSignal(str) # Authentication error projectorAuthentication = QtCore.pyqtSignal(str) # Authentication error
projectorNoAuthentication = QtCore.pyqtSignal(str) # PIN set and no authentication needed projectorNoAuthentication = QtCore.pyqtSignal(str) # PIN set and no authentication needed
@ -538,9 +587,9 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
:param poll_time: Time (in seconds) to poll connected projector :param poll_time: Time (in seconds) to poll connected projector
:param socket_timeout: Time (in seconds) to abort the connection if no response :param socket_timeout: Time (in seconds) to abort the connection if no response
""" """
log.debug('PJlink(projector={projector}, args={args} kwargs={kwargs})'.format(projector=projector, log.debug('PJlink(projector="{projector}", args="{args}" kwargs="{kwargs}")'.format(projector=projector,
args=args, args=args,
kwargs=kwargs)) kwargs=kwargs))
super().__init__() super().__init__()
self.entry = projector self.entry = projector
self.ip = self.entry.ip self.ip = self.entry.ip
@ -571,6 +620,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
self.widget = None # QListBox entry self.widget = None # QListBox entry
self.timer = None # Timer that calls the poll_loop self.timer = None # Timer that calls the poll_loop
self.send_queue = [] self.send_queue = []
self.priority_queue = []
self.send_busy = False self.send_busy = False
# Socket timer for some possible brain-dead projectors or network cable pulled # Socket timer for some possible brain-dead projectors or network cable pulled
self.socket_timer = None self.socket_timer = None
@ -584,6 +634,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
self.connected.connect(self.check_login) self.connected.connect(self.check_login)
self.disconnected.connect(self.disconnect_from_host) self.disconnected.connect(self.disconnect_from_host)
self.error.connect(self.get_error) self.error.connect(self.get_error)
self.projectorReceivedData.connect(self._send_command)
def thread_stopped(self): def thread_stopped(self):
""" """
@ -606,6 +657,10 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
self.projectorReceivedData.disconnect(self._send_command) self.projectorReceivedData.disconnect(self._send_command)
except TypeError: except TypeError:
pass pass
try:
self.readyRead.disconnect(self.get_socket) # Set in process_pjlink
except TypeError:
pass
self.disconnect_from_host() self.disconnect_from_host()
self.deleteLater() self.deleteLater()
self.i_am_running = False self.i_am_running = False
@ -623,10 +678,10 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
Retrieve information from projector that changes. Retrieve information from projector that changes.
Normally called by timer(). Normally called by timer().
""" """
if self.state() != self.ConnectedState: if self.state() != S_QSOCKET_STATE['ConnectedState']:
log.warning("({ip}) poll_loop(): Not connected - returning".format(ip=self.ip)) log.warning('({ip}) poll_loop(): Not connected - returning'.format(ip=self.ip))
return return
log.debug('({ip}) Updating projector status'.format(ip=self.ip)) log.debug('({ip}) poll_loop(): Updating projector status'.format(ip=self.ip))
# Reset timer in case we were called from a set command # Reset timer in case we were called from a set command
if self.timer.interval() < self.poll_time: if self.timer.interval() < self.poll_time:
# Reset timer to 5 seconds # Reset timer to 5 seconds
@ -638,28 +693,28 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
if self.pjlink_class == '2': if self.pjlink_class == '2':
check_list.extend(['FILT', 'FREZ']) check_list.extend(['FILT', 'FREZ'])
for command in check_list: for command in check_list:
self.send_command(command, queue=True) self.send_command(command)
# The following commands do not change, so only check them once # The following commands do not change, so only check them once
if self.power == S_ON and self.source_available is None: if self.power == S_ON and self.source_available is None:
self.send_command('INST', queue=True) self.send_command('INST')
if self.other_info is None: if self.other_info is None:
self.send_command('INFO', queue=True) self.send_command('INFO')
if self.manufacturer is None: if self.manufacturer is None:
self.send_command('INF1', queue=True) self.send_command('INF1')
if self.model is None: if self.model is None:
self.send_command('INF2', queue=True) self.send_command('INF2')
if self.pjlink_name is None: if self.pjlink_name is None:
self.send_command('NAME', queue=True) self.send_command('NAME')
if self.pjlink_class == '2': if self.pjlink_class == '2':
# Class 2 specific checks # Class 2 specific checks
if self.serial_no is None: if self.serial_no is None:
self.send_command('SNUM', queue=True) self.send_command('SNUM')
if self.sw_version is None: if self.sw_version is None:
self.send_command('SVER', queue=True) self.send_command('SVER')
if self.model_filter is None: if self.model_filter is None:
self.send_command('RFIL', queue=True) self.send_command('RFIL')
if self.model_lamp is None: if self.model_lamp is None:
self.send_command('RLMP', queue=True) self.send_command('RLMP')
def _get_status(self, status): def _get_status(self, status):
""" """
@ -711,14 +766,12 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
code=status_code, code=status_code,
message=status_message if msg is None else msg)) message=status_message if msg is None else msg))
self.changeStatus.emit(self.ip, status, message) self.changeStatus.emit(self.ip, status, message)
self.projectorUpdateIcons.emit()
@QtCore.pyqtSlot() @QtCore.pyqtSlot()
def check_login(self, data=None): def check_login(self, data=None):
""" """
Processes the initial connection and authentication (if needed). Processes the initial connection and convert to a PJLink packet if valid initial connection
Starts poll timer if connection is established.
NOTE: Qt md5 hash function doesn't work with projector authentication. Use the python md5 hash function.
:param data: Optional data if called from another routine :param data: Optional data if called from another routine
""" """
@ -731,12 +784,12 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
self.change_status(E_SOCKET_TIMEOUT) self.change_status(E_SOCKET_TIMEOUT)
return return
read = self.readLine(self.max_size) read = self.readLine(self.max_size)
self.readLine(self.max_size) # Clean out the trailing \r\n self.readLine(self.max_size) # Clean out any trailing whitespace
if read is None: if read is None:
log.warning('({ip}) read is None - socket error?'.format(ip=self.ip)) log.warning('({ip}) read is None - socket error?'.format(ip=self.ip))
return return
elif len(read) < 8: elif len(read) < 8:
log.warning('({ip}) Not enough data read)'.format(ip=self.ip)) log.warning('({ip}) Not enough data read - skipping'.format(ip=self.ip))
return return
data = decode(read, 'utf-8') data = decode(read, 'utf-8')
# Possibility of extraneous data on input when reading. # Possibility of extraneous data on input when reading.
@ -748,9 +801,16 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
# PJLink initial login will be: # PJLink initial login will be:
# 'PJLink 0' - Unauthenticated login - no extra steps required. # 'PJLink 0' - Unauthenticated login - no extra steps required.
# 'PJLink 1 XXXXXX' Authenticated login - extra processing required. # 'PJLink 1 XXXXXX' Authenticated login - extra processing required.
if not data.upper().startswith('PJLINK'): if not data.startswith('PJLINK'):
# Invalid response # Invalid initial packet - close socket
log.error('({ip}) Invalid initial packet received - closing socket'.format(ip=self.ip))
return self.disconnect_from_host() return self.disconnect_from_host()
log.debug('({ip}) check_login(): Formatting initial connection prompt to PJLink packet'.format(ip=self.ip))
return self.get_data('{start}{clss}{data}'.format(start=PJLINK_PREFIX,
clss='1',
data=data.replace(' ', '=', 1)).encode('utf-8'))
# TODO: The below is replaced by process_pjlink() - remove when working properly
"""
if '=' in data: if '=' in data:
# Processing a login reply # Processing a login reply
data_check = data.strip().split('=') data_check = data.strip().split('=')
@ -799,18 +859,19 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
log.debug('({ip}) Starting timer'.format(ip=self.ip)) log.debug('({ip}) Starting timer'.format(ip=self.ip))
self.timer.setInterval(2000) # Set 2 seconds for initial information self.timer.setInterval(2000) # Set 2 seconds for initial information
self.timer.start() self.timer.start()
"""
def _trash_buffer(self, msg=None): def _trash_buffer(self, msg=None):
""" """
Clean out extraneous stuff in the buffer. Clean out extraneous stuff in the buffer.
""" """
log.warning("({ip}) {message}".format(ip=self.ip, message='Invalid packet' if msg is None else msg)) log.warning('({ip}) {message}'.format(ip=self.ip, message='Invalid packet' if msg is None else msg))
self.send_busy = False self.send_busy = False
trash_count = 0 trash_count = 0
while self.bytesAvailable() > 0: while self.bytesAvailable() > 0:
trash = self.read(self.max_size) trash = self.read(self.max_size)
trash_count += len(trash) trash_count += len(trash)
log.debug("({ip}) Finished cleaning buffer - {count} bytes dropped".format(ip=self.ip, log.debug('({ip}) Finished cleaning buffer - {count} bytes dropped'.format(ip=self.ip,
count=trash_count)) count=trash_count))
return return
@ -822,7 +883,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
:param data: Data to process. buffer must be formatted as a proper PJLink packet. :param data: Data to process. buffer must be formatted as a proper PJLink packet.
:param ip: Destination IP for buffer. :param ip: Destination IP for buffer.
""" """
log.debug("({ip}) get_buffer(data='{buff}' ip='{ip_in}'".format(ip=self.ip, buff=data, ip_in=ip)) log.debug('({ip}) get_buffer(data="{buff}" ip="{ip_in}"'.format(ip=self.ip, buff=data, ip_in=ip))
if ip is None: if ip is None:
log.debug("({ip}) get_buffer() Don't know who data is for - exiting".format(ip=self.ip)) log.debug("({ip}) get_buffer() Don't know who data is for - exiting".format(ip=self.ip))
return return
@ -840,39 +901,52 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
return return
# Although we have a packet length limit, go ahead and use a larger buffer # Although we have a packet length limit, go ahead and use a larger buffer
read = self.readLine(1024) read = self.readLine(1024)
log.debug("({ip}) get_socket(): '{buff}'".format(ip=self.ip, buff=read)) log.debug('({ip}) get_socket(): "{buff}"'.format(ip=self.ip, buff=read))
if read == -1: if read == -1:
# No data available # No data available
log.debug('({ip}) get_socket(): No data available (-1)'.format(ip=self.ip)) log.debug('({ip}) get_socket(): No data available (-1)'.format(ip=self.ip))
return self.receive_data_signal() return self.receive_data_signal()
self.socket_timer.stop() self.socket_timer.stop()
self.projectorNetwork.emit(S_NETWORK_RECEIVED) self.get_data(buff=read, ip=self.ip)
return self.get_data(buff=read, ip=self.ip) return self.receive_data_signal()
def get_data(self, buff, ip): def get_data(self, buff, ip=None):
""" """
Process received data Process received data
:param buff: Data to process. :param buff: Data to process.
:param ip: (optional) Destination IP. :param ip: (optional) Destination IP.
""" """
log.debug("({ip}) get_data(ip='{ip_in}' buffer='{buff}'".format(ip=self.ip, ip_in=ip, buff=buff)) # Since "self" is not available to options and the "ip" keyword is a "maybe I'll use in the future",
# set to default here
if ip is None:
ip = self.ip
log.debug('({ip}) get_data(ip="{ip_in}" buffer="{buff}"'.format(ip=self.ip, ip_in=ip, buff=buff))
# NOTE: Class2 has changed to some values being UTF-8 # NOTE: Class2 has changed to some values being UTF-8
data_in = decode(buff, 'utf-8') data_in = decode(buff, 'utf-8')
data = data_in.strip() data = data_in.strip()
if (len(data) < 7) or (not data.startswith(PJLINK_PREFIX)): # Initial packet checks
return self._trash_buffer(msg='get_data(): Invalid packet - length or prefix') if (len(data) < 7):
return self._trash_buffer(msg='get_data(): Invalid packet - length')
elif len(data) > self.max_size: elif len(data) > self.max_size:
return self._trash_buffer(msg='get_data(): Invalid packet - too long') return self._trash_buffer(msg='get_data(): Invalid packet - too long')
elif not data.startswith(PJLINK_PREFIX):
return self._trash_buffer(msg='get_data(): Invalid packet - PJLink prefix missing')
elif '=' not in data: elif '=' not in data:
return self._trash_buffer(msg='get_data(): Invalid packet does not have equal') return self._trash_buffer(msg='get_data(): Invalid reply - Does not have "="')
log.debug('({ip}) get_data(): Checking new data "{data}"'.format(ip=self.ip, data=data)) log.debug('({ip}) get_data(): Checking new data "{data}"'.format(ip=self.ip, data=data))
header, data = data.split('=') header, data = data.split('=')
# At this point, the header should contain:
# "PVCCCC"
# Where:
# P = PJLINK_PREFIX
# V = PJLink class or version
# C = PJLink command
try: try:
version, cmd = header[1], header[2:] version, cmd = header[1], header[2:].upper()
except ValueError as e: except ValueError as e:
self.change_status(E_INVALID_DATA) self.change_status(E_INVALID_DATA)
log.warning('({ip}) get_data(): Received data: "{data}"'.format(ip=self.ip, data=data_in.strip())) log.warning('({ip}) get_data(): Received data: "{data}"'.format(ip=self.ip, data=data_in))
return self._trash_buffer('get_data(): Expected header + command + data') return self._trash_buffer('get_data(): Expected header + command + data')
if cmd not in PJLINK_VALID_CMD: if cmd not in PJLINK_VALID_CMD:
log.warning('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd)) log.warning('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd))
@ -880,6 +954,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
if int(self.pjlink_class) < int(version): if int(self.pjlink_class) < int(version):
log.warning('({ip}) get_data(): Projector returned class reply higher ' log.warning('({ip}) get_data(): Projector returned class reply higher '
'than projector stated class'.format(ip=self.ip)) 'than projector stated class'.format(ip=self.ip))
self.send_busy = False
return self.process_command(cmd, data) return self.process_command(cmd, data)
@QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError) @QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError)
@ -909,23 +984,21 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
self.reset_information() self.reset_information()
return return
def send_command(self, cmd, opts='?', salt=None, queue=False): def send_command(self, cmd, opts='?', salt=None, priority=False):
""" """
Add command to output queue if not already in queue. Add command to output queue if not already in queue.
:param cmd: Command to send :param cmd: Command to send
:param opts: Command option (if any) - defaults to '?' (get information) :param opts: Command option (if any) - defaults to '?' (get information)
:param salt: Optional salt for md5 hash initial authentication :param salt: Optional salt for md5 hash initial authentication
:param queue: Option to force add to queue rather than sending directly :param priority: Option to send packet now rather than queue it up
""" """
if self.state() != self.ConnectedState: if self.state() != self.ConnectedState:
log.warning('({ip}) send_command(): Not connected - returning'.format(ip=self.ip)) log.warning('({ip}) send_command(): Not connected - returning'.format(ip=self.ip))
self.send_queue = [] return self.reset_information()
return
if cmd not in PJLINK_VALID_CMD: if cmd not in PJLINK_VALID_CMD:
log.error('({ip}) send_command(): Invalid command requested - ignoring.'.format(ip=self.ip)) log.error('({ip}) send_command(): Invalid command requested - ignoring.'.format(ip=self.ip))
return return
self.projectorNetwork.emit(S_NETWORK_SENDING)
log.debug('({ip}) send_command(): Building cmd="{command}" opts="{data}"{salt}'.format(ip=self.ip, log.debug('({ip}) send_command(): Building cmd="{command}" opts="{data}"{salt}'.format(ip=self.ip,
command=cmd, command=cmd,
data=opts, data=opts,
@ -939,28 +1012,26 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
header = PJLINK_HEADER.format(linkclass=cmd_ver[0]) header = PJLINK_HEADER.format(linkclass=cmd_ver[0])
else: else:
# NOTE: Once we get to version 3 then think about looping # NOTE: Once we get to version 3 then think about looping
log.error('({ip}): send_command(): PJLink class check issue? aborting'.format(ip=self.ip)) log.error('({ip}): send_command(): PJLink class check issue? Aborting'.format(ip=self.ip))
return return
out = '{salt}{header}{command} {options}{suffix}'.format(salt="" if salt is None else salt, out = '{salt}{header}{command} {options}{suffix}'.format(salt="" if salt is None else salt,
header=header, header=header,
command=cmd, command=cmd,
options=opts, options=opts,
suffix=CR) suffix=CR)
if out in self.send_queue: if out in self.priority_queue:
# Already there, so don't add log.debug('({ip}) send_command(): Already in priority queue - skipping'.format(ip=self.ip))
log.debug('({ip}) send_command(out="{data}") Already in queue - skipping'.format(ip=self.ip, elif out in self.send_queue:
data=out.strip())) log.debug('({ip}) send_command(): Already in normal queue - skipping'.format(ip=self.ip))
elif not queue and len(self.send_queue) == 0:
# Nothing waiting to send, so just send it
log.debug('({ip}) send_command(out="{data}") Sending data'.format(ip=self.ip, data=out.strip()))
return self._send_command(data=out)
else: else:
log.debug('({ip}) send_command(out="{data}") adding to queue'.format(ip=self.ip, data=out.strip())) if priority:
self.send_queue.append(out) log.debug('({ip}) send_command(): Adding to priority queue'.format(ip=self.ip))
self.projectorReceivedData.emit() self.priority_queue.append(out)
log.debug('({ip}) send_command(): send_busy is {data}'.format(ip=self.ip, data=self.send_busy)) else:
if not self.send_busy: log.debug('({ip}) send_command(): Adding to normal queue'.format(ip=self.ip))
log.debug('({ip}) send_command() calling _send_string()'.format(ip=self.ip)) self.send_queue.append(out)
if self.priority_queue or self.send_queue:
# May be some initial connection setup so make sure we send data
self._send_command() self._send_command()
@QtCore.pyqtSlot() @QtCore.pyqtSlot()
@ -971,44 +1042,53 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
:param data: Immediate data to send :param data: Immediate data to send
:param utf8: Send as UTF-8 string otherwise send as ASCII string :param utf8: Send as UTF-8 string otherwise send as ASCII string
""" """
log.debug('({ip}) _send_string()'.format(ip=self.ip)) # Funny looking data check, but it's a quick check for data=None
log.debug('({ip}) _send_string(): Connection status: {data}'.format(ip=self.ip, data=self.state())) log.debug('({ip}) _send_command(data="{data}")'.format(ip=self.ip, data=data.strip() if data else data))
log.debug('({ip}) _send_command(): Connection status: {data}'.format(ip=self.ip,
data=S_QSOCKET_STATE[self.state()]))
if self.state() != self.ConnectedState: if self.state() != self.ConnectedState:
log.debug('({ip}) _send_string() Not connected - abort'.format(ip=self.ip)) log.debug('({ip}) _send_command() Not connected - abort'.format(ip=self.ip))
self.send_queue = []
self.send_busy = False self.send_busy = False
return return self.disconnect_from_host()
if data and data not in self.priority_queue:
log.debug('({ip}) _send_command(): Priority packet - adding to priority queue'.format(ip=self.ip))
self.priority_queue.append(data)
if self.send_busy: if self.send_busy:
# Still waiting for response from last command sent # Still waiting for response from last command sent
log.debug('({ip}) _send_command(): Still busy, returning'.format(ip=self.ip))
log.debug('({ip}) _send_command(): Priority queue = {data}'.format(ip=self.ip, data=self.priority_queue))
log.debug('({ip}) _send_command(): Normal queue = {data}'.format(ip=self.ip, data=self.send_queue))
return return
if data is not None:
out = data if len(self.priority_queue) != 0:
log.debug('({ip}) _send_string(data="{data}")'.format(ip=self.ip, data=out.strip())) out = self.priority_queue.pop(0)
log.debug('({ip}) _send_command(): Getting priority queued packet'.format(ip=self.ip))
elif len(self.send_queue) != 0: elif len(self.send_queue) != 0:
out = self.send_queue.pop(0) out = self.send_queue.pop(0)
log.debug('({ip}) _send_string(queued data="{data}"%s)'.format(ip=self.ip, data=out.strip())) log.debug('({ip}) _send_command(): Getting normal queued packet'.format(ip=self.ip))
else: else:
# No data to send # No data to send
log.debug('({ip}) _send_string(): No data to send'.format(ip=self.ip)) log.debug('({ip}) _send_command(): No data to send'.format(ip=self.ip))
self.send_busy = False self.send_busy = False
return return
self.send_busy = True self.send_busy = True
log.debug('({ip}) _send_string(): Sending "{data}"'.format(ip=self.ip, data=out.strip())) log.debug('({ip}) _send_command(): Sending "{data}"'.format(ip=self.ip, data=out.strip()))
log.debug('({ip}) _send_string(): Queue = {data}'.format(ip=self.ip, data=self.send_queue))
self.socket_timer.start() self.socket_timer.start()
self.projectorNetwork.emit(S_NETWORK_SENDING)
sent = self.write(out.encode('{string_encoding}'.format(string_encoding='utf-8' if utf8 else 'ascii'))) sent = self.write(out.encode('{string_encoding}'.format(string_encoding='utf-8' if utf8 else 'ascii')))
self.waitForBytesWritten(2000) # 2 seconds should be enough self.waitForBytesWritten(2000) # 2 seconds should be enough
if sent == -1: if sent == -1:
# Network error? # Network error?
log.warning("({ip}) _send_command(): -1 received".format(ip=self.ip)) log.warning('({ip}) _send_command(): -1 received - disconnecting from host'.format(ip=self.ip))
self.change_status(E_NETWORK, self.change_status(E_NETWORK,
translate('OpenLP.PJLink', 'Error while sending data to projector')) translate('OpenLP.PJLink', 'Error while sending data to projector'))
self.disconnect_from_host()
def connect_to_host(self): def connect_to_host(self):
""" """
Initiate connection to projector. Initiate connection to projector.
""" """
log.debug('{ip}) connect_to_host(): Starting connection'.format(ip=self.ip))
if self.state() == self.ConnectedState: if self.state() == self.ConnectedState:
log.warning('({ip}) connect_to_host(): Already connected - returning'.format(ip=self.ip)) log.warning('({ip}) connect_to_host(): Already connected - returning'.format(ip=self.ip))
return return
@ -1024,22 +1104,19 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
if abort: if abort:
log.warning('({ip}) disconnect_from_host(): Aborting connection'.format(ip=self.ip)) log.warning('({ip}) disconnect_from_host(): Aborting connection'.format(ip=self.ip))
else: else:
log.warning('({ip}) disconnect_from_host(): Not connected - returning'.format(ip=self.ip)) log.warning('({ip}) disconnect_from_host(): Not connected'.format(ip=self.ip))
self.reset_information()
self.disconnectFromHost() self.disconnectFromHost()
try: try:
self.readyRead.disconnect(self.get_socket) self.readyRead.disconnect(self.get_socket)
except TypeError: except TypeError:
pass pass
log.debug('({ip}) disconnect_from_host() '
'Current status {data}'.format(ip=self.ip, data=self._get_status(self.status_connect)[0]))
if abort: if abort:
self.change_status(E_NOT_CONNECTED) self.change_status(E_NOT_CONNECTED)
else: else:
log.debug('({ip}) disconnect_from_host() ' self.change_status(S_NOT_CONNECTED)
'Current status {data}'.format(ip=self.ip, data=self._get_status(self.status_connect)[0]))
if self.status_connect != E_NOT_CONNECTED:
self.change_status(S_NOT_CONNECTED)
self.reset_information() self.reset_information()
self.projectorUpdateIcons.emit()
def get_av_mute_status(self): def get_av_mute_status(self):
""" """

View File

@ -31,8 +31,8 @@ from PyQt5 import QtCore, QtWidgets
from openlp.core.common import is_macosx from openlp.core.common import is_macosx
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
from openlp.core.lib.projector.db import ProjectorSource from openlp.core.projectors.db import ProjectorSource
from openlp.core.lib.projector.constants import PJLINK_DEFAULT_SOURCES, PJLINK_DEFAULT_CODES from openlp.core.projectors.constants import PJLINK_DEFAULT_SOURCES, PJLINK_DEFAULT_CODES
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@ -29,7 +29,7 @@ from PyQt5 import QtWidgets
from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.lib import SettingsTab from openlp.core.lib import SettingsTab
from openlp.core.lib.projector import DialogSourceStyle from openlp.core.projectors import DialogSourceStyle
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.debug('projectortab module loaded') log.debug('projectortab module loaded')

View File

@ -115,9 +115,10 @@ from .formattingtagcontroller import FormattingTagController
from .shortcutlistform import ShortcutListForm from .shortcutlistform import ShortcutListForm
from .servicemanager import ServiceManager from .servicemanager import ServiceManager
from .thememanager import ThemeManager from .thememanager import ThemeManager
from .projector.manager import ProjectorManager
from .projector.tab import ProjectorTab from openlp.core.projectors.editform import ProjectorEditForm
from .projector.editform import ProjectorEditForm from openlp.core.projectors.manager import ProjectorManager
from openlp.core.projectors.tab import ProjectorTab
__all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', 'ThemeForm', __all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', 'ThemeForm',
'ThemeManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', 'Display', 'AudioPlayer', 'ThemeManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', 'Display', 'AudioPlayer',

View File

@ -32,8 +32,9 @@ from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import UiStrings, format_time, translate from openlp.core.common.i18n import UiStrings, format_time, translate
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.lib import SettingsTab, build_icon from openlp.core.lib import SettingsTab, build_icon
from openlp.core.ui.lib import PathEdit, PathType
from openlp.core.ui.style import HAS_DARK_STYLE from openlp.core.ui.style import HAS_DARK_STYLE
from openlp.core.widgets.edits import PathEdit
from openlp.core.widgets.enums import PathEditType
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -122,7 +123,7 @@ class AdvancedTab(SettingsTab):
self.data_directory_layout.setObjectName('data_directory_layout') self.data_directory_layout.setObjectName('data_directory_layout')
self.data_directory_new_label = QtWidgets.QLabel(self.data_directory_group_box) self.data_directory_new_label = QtWidgets.QLabel(self.data_directory_group_box)
self.data_directory_new_label.setObjectName('data_directory_current_label') self.data_directory_new_label.setObjectName('data_directory_current_label')
self.data_directory_path_edit = PathEdit(self.data_directory_group_box, path_type=PathType.Directories, self.data_directory_path_edit = PathEdit(self.data_directory_group_box, path_type=PathEditType.Directories,
default_path=AppLocation.get_directory(AppLocation.DataDir)) default_path=AppLocation.get_directory(AppLocation.DataDir))
self.data_directory_layout.addRow(self.data_directory_new_label, self.data_directory_path_edit) 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 = QtWidgets.QLabel(self.data_directory_group_box)

View File

@ -72,10 +72,10 @@ except ImportError:
from openlp.core.common import is_linux from openlp.core.common import is_linux
from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.registry import RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.ui.exceptiondialog import Ui_ExceptionDialog from openlp.core.ui.exceptiondialog import Ui_ExceptionDialog
from openlp.core.ui.lib.filedialog import FileDialog from openlp.core.widgets.dialogs import FileDialog
from openlp.core.version import get_version from openlp.core.version import get_version
@ -155,7 +155,7 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
try: try:
with file_path.open('w') as report_file: with file_path.open('w') as report_file:
report_file.write(report_text) report_file.write(report_text)
except IOError: except OSError:
log.exception('Failed to write crash report') log.exception('Failed to write crash report')
def on_send_report_button_clicked(self): def on_send_report_button_clicked(self):

View File

@ -25,7 +25,8 @@ The file rename dialog.
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.ui.filerenamedialog import Ui_FileRenameDialog from openlp.core.ui.filerenamedialog import Ui_FileRenameDialog

View File

@ -38,7 +38,8 @@ from openlp.core.common import clean_button_text, trace_error_handler
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.common.path import Path, create_paths from openlp.core.common.path import Path, create_paths
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.lib import PluginStatus, build_icon from openlp.core.lib import PluginStatus, build_icon
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box

View File

@ -261,8 +261,8 @@ class UiFirstTimeWizard(object):
self.alert_check_box.setText(translate('OpenLP.FirstTimeWizard', self.alert_check_box.setText(translate('OpenLP.FirstTimeWizard',
'Alerts Display informative messages while showing other slides')) 'Alerts Display informative messages while showing other slides'))
self.projectors_check_box.setText(translate('OpenLP.FirstTimeWizard', self.projectors_check_box.setText(translate('OpenLP.FirstTimeWizard',
'Projectors Control PJLink compatible projects on your network' 'Projector Controller Control PJLink compatible projects on your'
' from OpenLP')) ' network from OpenLP'))
self.no_internet_page.setTitle(translate('OpenLP.FirstTimeWizard', 'No Internet Connection')) self.no_internet_page.setTitle(translate('OpenLP.FirstTimeWizard', 'No Internet Connection'))
self.no_internet_page.setSubTitle( self.no_internet_page.setSubTitle(
translate('OpenLP.FirstTimeWizard', 'Unable to detect an Internet connection.')) translate('OpenLP.FirstTimeWizard', 'Unable to detect an Internet connection.'))

View File

@ -43,7 +43,7 @@ class FormattingTagController(object):
r'(?P<tag>[^\s/!\?>]+)(?:\s+[^\s=]+="[^"]*")*\s*(?P<empty>/)?' r'(?P<tag>[^\s/!\?>]+)(?:\s+[^\s=]+="[^"]*")*\s*(?P<empty>/)?'
r'|(?P<cdata>!\[CDATA\[(?:(?!\]\]>).)*\]\])' r'|(?P<cdata>!\[CDATA\[(?:(?!\]\]>).)*\]\])'
r'|(?P<procinst>\?(?:(?!\?>).)*\?)' r'|(?P<procinst>\?(?:(?!\?>).)*\?)'
r'|(?P<comment>!--(?:(?!-->).)*--))>', re.UNICODE) r'|(?P<comment>!--(?:(?!-->).)*--))>')
self.html_regex = re.compile(r'^(?:[^<>]*%s)*[^<>]*$' % self.html_tag_regex.pattern) self.html_regex = re.compile(r'^(?:[^<>]*%s)*[^<>]*$' % self.html_tag_regex.pattern)
def pre_save(self): def pre_save(self):

View File

@ -33,7 +33,8 @@ from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList from openlp.core.display.screens import ScreenList
from openlp.core.lib import SettingsTab from openlp.core.lib import SettingsTab
from openlp.core.ui.lib import ColorButton, PathEdit from openlp.core.widgets.buttons import ColorButton
from openlp.core.widgets.edits import PathEdit
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@ -1,84 +0,0 @@
# -*- 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 #
###############################################################################
"""
The :mod:`~openlp.core.ui.lib.historycombobox` module contains the HistoryComboBox widget
"""
from PyQt5 import QtCore, QtWidgets
class HistoryComboBox(QtWidgets.QComboBox):
"""
The :class:`~openlp.core.common.historycombobox.HistoryComboBox` widget emulates the QLineEdit ``returnPressed``
signal for when the :kbd:`Enter` or :kbd:`Return` keys are pressed, and saves anything that is typed into the edit
box into its list.
"""
returnPressed = QtCore.pyqtSignal()
def __init__(self, parent=None):
"""
Initialise the combo box, setting duplicates to False and the insert policy to insert items at the top.
:param parent: The parent widget
"""
super().__init__(parent)
self.setDuplicatesEnabled(False)
self.setEditable(True)
self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
self.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
def keyPressEvent(self, event):
"""
Override the inherited keyPressEvent method to emit the ``returnPressed`` signal and to save the current text to
the dropdown list.
:param event: The keyboard event
"""
# Handle Enter and Return ourselves
if event.key() == QtCore.Qt.Key_Enter or event.key() == QtCore.Qt.Key_Return:
# Emit the returnPressed signal
self.returnPressed.emit()
# Save the current text to the dropdown list
if self.currentText() and self.findText(self.currentText()) == -1:
self.insertItem(0, self.currentText())
# Let the parent handle any keypress events
super().keyPressEvent(event)
def focusOutEvent(self, event):
"""
Override the inherited focusOutEvent to save the current text to the dropdown list.
:param event: The focus event
"""
# Save the current text to the dropdown list
if self.currentText() and self.findText(self.currentText()) == -1:
self.insertItem(0, self.currentText())
# Let the parent handle any keypress events
super().focusOutEvent(event)
def getItems(self):
"""
Get all the items from the history
:return: A list of strings
"""
return [self.itemText(i) for i in range(self.count())]

View File

@ -1,153 +0,0 @@
# -*- 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 #
###############################################################################
"""
Extend QListWidget to handle drag and drop functionality
"""
import os
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common.i18n import UiStrings
from openlp.core.common.registry import Registry
class ListWidgetWithDnD(QtWidgets.QListWidget):
"""
Provide a list widget to store objects and handle drag and drop events
"""
def __init__(self, parent=None, name=''):
"""
Initialise the list widget
"""
super().__init__(parent)
self.mime_data_text = name
self.no_results_text = UiStrings().NoResults
self.setSpacing(1)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.setAlternatingRowColors(True)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
def activateDnD(self):
"""
Activate DnD of widget
"""
self.setAcceptDrops(True)
self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file)
def clear(self, search_while_typing=False):
"""
Re-implement clear, so that we can customise feedback when using 'Search as you type'
:param search_while_typing: True if we want to display the customised message
:return: None
"""
if search_while_typing:
self.no_results_text = UiStrings().ShortResults
else:
self.no_results_text = UiStrings().NoResults
super().clear()
def mouseMoveEvent(self, event):
"""
Drag and drop event does not care what data is selected as the recipient will use events to request the data
move just tell it what plugin to call
"""
if event.buttons() != QtCore.Qt.LeftButton:
event.ignore()
return
if not self.selectedItems():
event.ignore()
return
drag = QtGui.QDrag(self)
mime_data = QtCore.QMimeData()
drag.setMimeData(mime_data)
mime_data.setText(self.mime_data_text)
drag.exec(QtCore.Qt.CopyAction)
def dragEnterEvent(self, event):
"""
When something is dragged into this object, check if you should be able to drop it in here.
"""
if event.mimeData().hasUrls():
event.accept()
else:
event.ignore()
def dragMoveEvent(self, event):
"""
Make an object droppable, and set it to copy the contents of the object, not move it.
"""
if event.mimeData().hasUrls():
event.setDropAction(QtCore.Qt.CopyAction)
event.accept()
else:
event.ignore()
def dropEvent(self, event):
"""
Receive drop event check if it is a file and process it if it is.
:param event: Handle of the event pint passed
"""
if event.mimeData().hasUrls():
event.setDropAction(QtCore.Qt.CopyAction)
event.accept()
files = []
for url in event.mimeData().urls():
local_file = os.path.normpath(url.toLocalFile())
if os.path.isfile(local_file):
files.append(local_file)
elif os.path.isdir(local_file):
listing = os.listdir(local_file)
for file in listing:
files.append(os.path.join(local_file, file))
Registry().execute('{mime_data}_dnd'.format(mime_data=self.mime_data_text),
{'files': files, 'target': self.itemAt(event.pos())})
else:
event.ignore()
def allItems(self):
"""
An generator to list all the items in the widget
:return: a generator
"""
for row in range(self.count()):
yield self.item(row)
def paintEvent(self, event):
"""
Re-implement paintEvent so that we can add 'No Results' text when the listWidget is empty.
:param event: A QPaintEvent
:return: None
"""
super().paintEvent(event)
if not self.count():
viewport = self.viewport()
painter = QtGui.QPainter(viewport)
font = QtGui.QFont()
font.setItalic(True)
painter.setFont(font)
painter.drawText(QtCore.QRect(0, 0, viewport.width(), viewport.height()),
(QtCore.Qt.AlignHCenter | QtCore.Qt.TextWordWrap), self.no_results_text)

View File

@ -1,196 +0,0 @@
# -*- 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
from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.path import Path, path_to_str, str_to_path
from openlp.core.lib import build_icon
from openlp.core.ui.lib.filedialog import FileDialog
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(Path)
def __init__(self, parent=None, path_type=PathType.Files, default_path=None, dialog_caption=None, show_revert=True):
"""
Initialise the PathEdit widget
:param QtWidget.QWidget | None: The parent of the widget. This is just passed to the super method.
:param str dialog_caption: Used to customise the caption in the QFileDialog.
:param openlp.core.common.path.Path default_path: The default path. This is set as the path when the revert
button is clicked
:param bool show_revert: Used to determine if the 'revert button' should be visible.
: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 bool show_revert: Show or hide the revert button
:rtype: None
"""
widget_layout = QtWidgets.QHBoxLayout()
widget_layout.setContentsMargins(0, 0, 0, 0)
self.line_edit = QtWidgets.QLineEdit(self)
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: openlp.core.common.path.Path
"""
return self._path
@path.setter
def path(self, path):
"""
A Property setter method to set the selected path
:param openlp.core.common.path.Path path: The path to set the widget to
:rtype: None
"""
self._path = path
text = path_to_str(path)
self.line_edit.setText(text)
self.line_edit.setToolTip(text)
@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: PathType
"""
return self._path_type
@path_type.setter
def path_type(self, path_type):
"""
A Property setter method to set the path type
:param PathType path_type: The type of path to select
:rtype: None
"""
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
:rtype: 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
:rtype: None
"""
caption = self.dialog_caption
path = None
if self._path_type == PathType.Directories:
if not caption:
caption = translate('OpenLP.PathEdit', 'Select Directory')
path = FileDialog.getExistingDirectory(self, caption, self._path, FileDialog.ShowDirsOnly)
elif self._path_type == PathType.Files:
if not caption:
caption = self.dialog_caption = translate('OpenLP.PathEdit', 'Select File')
path, filter_used = FileDialog.getOpenFileName(self, caption, self._path, self.filters)
if 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.
:rtype: 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.
:rtype: None
"""
path = str_to_path(self.line_edit.text())
self.on_new_path(path)
def on_new_path(self, path):
"""
A method called to validate and set a new path.
Emits the pathChanged Signal
:param openlp.core.common.path.Path path: The new path
:rtype: None
"""
if self._path != path:
self.path = path
self.pathChanged.emit(path)

View File

@ -1,205 +0,0 @@
# -*- 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 #
###############################################################################
"""
The :mod:`~openlp.core.lib.spelltextedit` module contains a classes to add spell checking to an edit widget.
"""
import logging
import re
try:
import enchant
from enchant import DictNotFoundError
from enchant.errors import Error
ENCHANT_AVAILABLE = True
except ImportError:
ENCHANT_AVAILABLE = False
# based on code from http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common.i18n import translate
from openlp.core.lib import FormattingTags
from openlp.core.lib.ui import create_action
log = logging.getLogger(__name__)
class SpellTextEdit(QtWidgets.QPlainTextEdit):
"""
Spell checking widget based on QPlanTextEdit.
"""
def __init__(self, parent=None, formatting_tags_allowed=True):
"""
Constructor.
"""
global ENCHANT_AVAILABLE
super(SpellTextEdit, self).__init__(parent)
self.formatting_tags_allowed = formatting_tags_allowed
# Default dictionary based on the current locale.
if ENCHANT_AVAILABLE:
try:
self.dictionary = enchant.Dict()
self.highlighter = Highlighter(self.document())
self.highlighter.spelling_dictionary = self.dictionary
except (Error, DictNotFoundError):
ENCHANT_AVAILABLE = False
log.debug('Could not load default dictionary')
def mousePressEvent(self, event):
"""
Handle mouse clicks within the text edit region.
"""
if event.button() == QtCore.Qt.RightButton:
# Rewrite the mouse event to a left button event so the cursor is moved to the location of the pointer.
event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress,
event.pos(), QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier)
QtWidgets.QPlainTextEdit.mousePressEvent(self, event)
def contextMenuEvent(self, event):
"""
Provide the context menu for the text edit region.
"""
popup_menu = self.createStandardContextMenu()
# Select the word under the cursor.
cursor = self.textCursor()
# only select text if not already selected
if not cursor.hasSelection():
cursor.select(QtGui.QTextCursor.WordUnderCursor)
self.setTextCursor(cursor)
# Add menu with available languages.
if ENCHANT_AVAILABLE:
lang_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Language:'))
for lang in enchant.list_languages():
action = create_action(lang_menu, lang, text=lang, checked=lang == self.dictionary.tag)
lang_menu.addAction(action)
popup_menu.insertSeparator(popup_menu.actions()[0])
popup_menu.insertMenu(popup_menu.actions()[0], lang_menu)
lang_menu.triggered.connect(self.set_language)
# Check if the selected word is misspelled and offer spelling suggestions if it is.
if ENCHANT_AVAILABLE and self.textCursor().hasSelection():
text = self.textCursor().selectedText()
if not self.dictionary.check(text):
spell_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Spelling Suggestions'))
for word in self.dictionary.suggest(text):
action = SpellAction(word, spell_menu)
action.correct.connect(self.correct_word)
spell_menu.addAction(action)
# Only add the spelling suggests to the menu if there are suggestions.
if spell_menu.actions():
popup_menu.insertMenu(popup_menu.actions()[0], spell_menu)
tag_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Formatting Tags'))
if self.formatting_tags_allowed:
for html in FormattingTags.get_html_tags():
action = SpellAction(html['desc'], tag_menu)
action.correct.connect(self.html_tag)
tag_menu.addAction(action)
popup_menu.insertSeparator(popup_menu.actions()[0])
popup_menu.insertMenu(popup_menu.actions()[0], tag_menu)
popup_menu.exec(event.globalPos())
def set_language(self, action):
"""
Changes the language for this spelltextedit.
:param action: The action.
"""
self.dictionary = enchant.Dict(action.text())
self.highlighter.spelling_dictionary = self.dictionary
self.highlighter.highlightBlock(self.toPlainText())
self.highlighter.rehighlight()
def correct_word(self, word):
"""
Replaces the selected text with word.
"""
cursor = self.textCursor()
cursor.beginEditBlock()
cursor.removeSelectedText()
cursor.insertText(word)
cursor.endEditBlock()
def html_tag(self, tag):
"""
Replaces the selected text with word.
"""
tag = tag.replace('&', '')
for html in FormattingTags.get_html_tags():
if tag == html['desc']:
cursor = self.textCursor()
if self.textCursor().hasSelection():
text = cursor.selectedText()
cursor.beginEditBlock()
cursor.removeSelectedText()
cursor.insertText(html['start tag'])
cursor.insertText(text)
cursor.insertText(html['end tag'])
cursor.endEditBlock()
else:
cursor = self.textCursor()
cursor.insertText(html['start tag'])
cursor.insertText(html['end tag'])
class Highlighter(QtGui.QSyntaxHighlighter):
"""
Provides a text highlighter for pointing out spelling errors in text.
"""
WORDS = r'(?iu)[\w\']+'
def __init__(self, *args):
"""
Constructor
"""
super(Highlighter, self).__init__(*args)
self.spelling_dictionary = None
def highlightBlock(self, text):
"""
Highlight mis spelt words in a block of text.
Note, this is a Qt hook.
"""
if not self.spelling_dictionary:
return
text = str(text)
char_format = QtGui.QTextCharFormat()
char_format.setUnderlineColor(QtCore.Qt.red)
char_format.setUnderlineStyle(QtGui.QTextCharFormat.SpellCheckUnderline)
for word_object in re.finditer(self.WORDS, text):
if not self.spelling_dictionary.check(word_object.group()):
self.setFormat(word_object.start(), word_object.end() - word_object.start(), char_format)
class SpellAction(QtWidgets.QAction):
"""
A special QAction that returns the text in a signal.
"""
correct = QtCore.pyqtSignal(str)
def __init__(self, *args):
"""
Constructor
"""
super(SpellAction, self).__init__(*args)
self.triggered.connect(lambda x: self.correct.emit(self.text()))

View File

@ -1,145 +0,0 @@
# -*- 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 #
###############################################################################
"""
Extend QTreeWidget to handle drag and drop functionality
"""
import os
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import is_win
from openlp.core.common.registry import Registry
class TreeWidgetWithDnD(QtWidgets.QTreeWidget):
"""
Provide a tree widget to store objects and handle drag and drop events
"""
def __init__(self, parent=None, name=''):
"""
Initialise the tree widget
"""
super(TreeWidgetWithDnD, self).__init__(parent)
self.mime_data_text = name
self.allow_internal_dnd = False
self.header().close()
self.default_indentation = self.indentation()
self.setIndentation(0)
self.setAnimated(True)
def activateDnD(self):
"""
Activate DnD of widget
"""
self.setAcceptDrops(True)
self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file)
Registry().register_function(('%s_dnd_internal' % self.mime_data_text), self.parent().dnd_move_internal)
def mouseMoveEvent(self, event):
"""
Drag and drop event does not care what data is selected as the recipient will use events to request the data
move just tell it what plugin to call
:param event: The event that occurred
"""
if event.buttons() != QtCore.Qt.LeftButton:
event.ignore()
return
if not self.selectedItems():
event.ignore()
return
drag = QtGui.QDrag(self)
mime_data = QtCore.QMimeData()
drag.setMimeData(mime_data)
mime_data.setText(self.mime_data_text)
drag.exec(QtCore.Qt.CopyAction)
def dragEnterEvent(self, event):
"""
Receive drag enter event, check if it is a file or internal object and allow it if it is.
:param event: The event that occurred
"""
if event.mimeData().hasUrls():
event.accept()
elif self.allow_internal_dnd:
event.accept()
else:
event.ignore()
def dragMoveEvent(self, event):
"""
Receive drag move event, check if it is a file or internal object and allow it if it is.
:param event: The event that occurred
"""
QtWidgets.QTreeWidget.dragMoveEvent(self, event)
if event.mimeData().hasUrls():
event.setDropAction(QtCore.Qt.CopyAction)
event.accept()
elif self.allow_internal_dnd:
event.setDropAction(QtCore.Qt.CopyAction)
event.accept()
else:
event.ignore()
def dropEvent(self, event):
"""
Receive drop event, check if it is a file or internal object and process it if it is.
:param event: Handle of the event pint passed
"""
# If we are on Windows, OpenLP window will not be set on top. For example, user can drag images to Library and
# the folder stays on top of the group creation box. This piece of code fixes this issue.
if is_win():
self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
self.setWindowState(QtCore.Qt.WindowNoState)
if event.mimeData().hasUrls():
event.setDropAction(QtCore.Qt.CopyAction)
event.accept()
files = []
for url in event.mimeData().urls():
local_file = url.toLocalFile()
if os.path.isfile(local_file):
files.append(local_file)
elif os.path.isdir(local_file):
listing = os.listdir(local_file)
for file_name in listing:
files.append(os.path.join(local_file, file_name))
Registry().execute('%s_dnd' % self.mime_data_text, {'files': files, 'target': self.itemAt(event.pos())})
elif self.allow_internal_dnd:
event.setDropAction(QtCore.Qt.CopyAction)
event.accept()
Registry().execute('%s_dnd_internal' % self.mime_data_text, self.itemAt(event.pos()))
else:
event.ignore()
# Convenience methods for emulating a QListWidget. This helps keeping MediaManagerItem simple.
def addItem(self, item):
self.addTopLevelItem(item)
def count(self):
return self.topLevelItemCount()
def item(self, index):
return self.topLevelItem(index)

View File

@ -29,16 +29,15 @@ Some of the code for this form is based on the examples at:
""" """
import html import html
import logging import logging
import os
from PyQt5 import QtCore, QtWidgets, QtWebKit, QtWebKitWidgets, QtGui, QtMultimedia from PyQt5 import QtCore, QtWidgets, QtWebKit, QtWebKitWidgets, QtGui, QtMultimedia
from openlp.core.common import is_macosx, is_win from openlp.core.common import is_macosx, is_win
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.common.mixins import OpenLPMixin from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import path_to_str from openlp.core.common.path import path_to_str
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList from openlp.core.display.screens import ScreenList
from openlp.core.lib import ServiceItem, ImageSource, build_html, expand_tags, image_to_byte from openlp.core.lib import ServiceItem, ImageSource, build_html, expand_tags, image_to_byte
@ -131,7 +130,7 @@ class Display(QtWidgets.QGraphicsView):
self.web_loaded = True self.web_loaded = True
class MainDisplay(OpenLPMixin, Display, RegistryProperties): class MainDisplay(Display, LogMixin, RegistryProperties):
""" """
This is the display screen as a specialized class from the Display class This is the display screen as a specialized class from the Display class
""" """
@ -398,6 +397,8 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
def preview(self): def preview(self):
""" """
Generates a preview of the image displayed. Generates a preview of the image displayed.
:rtype: QtGui.QPixmap
""" """
was_visible = self.isVisible() was_visible = self.isVisible()
self.application.process_events() self.application.process_events()
@ -488,8 +489,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
service_item = ServiceItem() service_item = ServiceItem()
service_item.title = 'webkit' service_item.title = 'webkit'
service_item.processor = 'webkit' service_item.processor = 'webkit'
path = os.path.join(str(AppLocation.get_section_data_path('themes')), path = str(AppLocation.get_section_data_path('themes') / self.service_item.theme_data.theme_name)
self.service_item.theme_data.theme_name)
service_item.add_from_command(path, service_item.add_from_command(path,
path_to_str(self.service_item.theme_data.background_filename), path_to_str(self.service_item.theme_data.background_filename),
':/media/slidecontroller_multimedia.png') ':/media/slidecontroller_multimedia.png')
@ -603,7 +603,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
self.web_view.setGeometry(0, 0, self.width(), self.height()) self.web_view.setGeometry(0, 0, self.width(), self.height())
class AudioPlayer(OpenLPMixin, QtCore.QObject): class AudioPlayer(LogMixin, QtCore.QObject):
""" """
This Class will play audio only allowing components to work with a soundtrack independent of the user interface. This Class will play audio only allowing components to work with a soundtrack independent of the user interface.
""" """

View File

@ -40,22 +40,22 @@ from openlp.core.common import is_win, is_macosx, add_actions
from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.actions import ActionList, CategoryOrder
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import LanguageManager, UiStrings, translate from openlp.core.common.i18n import LanguageManager, UiStrings, translate
from openlp.core.common.path import Path, copyfile, create_paths, path_to_str, str_to_path from openlp.core.common.path import Path, copyfile, create_paths
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList from openlp.core.display.screens import ScreenList
from openlp.core.display.renderer import Renderer from openlp.core.display.renderer import Renderer
from openlp.core.lib import PluginManager, ImageManager, PluginStatus, build_icon from openlp.core.lib import PluginManager, ImageManager, PluginStatus, build_icon
from openlp.core.lib.ui import create_action from openlp.core.lib.ui import create_action
from openlp.core.projectors.manager import ProjectorManager
from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \ from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \
ShortcutListForm, FormattingTagForm, PreviewController ShortcutListForm, FormattingTagForm, PreviewController
from openlp.core.ui.firsttimeform import FirstTimeForm from openlp.core.ui.firsttimeform import FirstTimeForm
from openlp.core.ui.lib.dockwidget import OpenLPDockWidget from openlp.core.widgets.dialogs import FileDialog
from openlp.core.ui.lib.filedialog import FileDialog from openlp.core.widgets.docks import OpenLPDockWidget, MediaDockManager
from openlp.core.ui.lib.mediadockmanager import MediaDockManager
from openlp.core.ui.media import MediaController from openlp.core.ui.media import MediaController
from openlp.core.ui.printserviceform import PrintServiceForm from openlp.core.ui.printserviceform import PrintServiceForm
from openlp.core.ui.projector.manager import ProjectorManager
from openlp.core.ui.style import PROGRESSBAR_STYLE, get_library_stylesheet from openlp.core.ui.style import PROGRESSBAR_STYLE, get_library_stylesheet
from openlp.core.version import get_version from openlp.core.version import get_version
@ -180,7 +180,7 @@ class Ui_MainWindow(object):
triggers=self.service_manager_contents.on_load_service_clicked) triggers=self.service_manager_contents.on_load_service_clicked)
self.file_save_item = create_action(main_window, 'fileSaveItem', icon=':/general/general_save.png', self.file_save_item = create_action(main_window, 'fileSaveItem', icon=':/general/general_save.png',
can_shortcuts=True, category=UiStrings().File, can_shortcuts=True, category=UiStrings().File,
triggers=self.service_manager_contents.save_file) triggers=self.service_manager_contents.decide_save_method)
self.file_save_as_item = create_action(main_window, 'fileSaveAsItem', can_shortcuts=True, self.file_save_as_item = create_action(main_window, 'fileSaveAsItem', can_shortcuts=True,
category=UiStrings().File, category=UiStrings().File,
triggers=self.service_manager_contents.save_file_as) triggers=self.service_manager_contents.save_file_as)
@ -296,10 +296,9 @@ class Ui_MainWindow(object):
# Give QT Extra Hint that this is an About Menu Item # Give QT Extra Hint that this is an About Menu Item
self.about_item.setMenuRole(QtWidgets.QAction.AboutRole) self.about_item.setMenuRole(QtWidgets.QAction.AboutRole)
if is_win(): if is_win():
self.local_help_file = os.path.join(str(AppLocation.get_directory(AppLocation.AppDir)), 'OpenLP.chm') self.local_help_file = AppLocation.get_directory(AppLocation.AppDir) / 'OpenLP.chm'
elif is_macosx(): elif is_macosx():
self.local_help_file = os.path.join(str(AppLocation.get_directory(AppLocation.AppDir)), self.local_help_file = AppLocation.get_directory(AppLocation.AppDir) / '..' / 'Resources' / 'OpenLP.help'
'..', 'Resources', 'OpenLP.help')
self.user_manual_item = create_action(main_window, 'userManualItem', icon=':/system/system_help_contents.png', self.user_manual_item = create_action(main_window, 'userManualItem', icon=':/system/system_help_contents.png',
can_shortcuts=True, category=UiStrings().Help, can_shortcuts=True, category=UiStrings().Help,
triggers=self.on_help_clicked) triggers=self.on_help_clicked)
@ -375,7 +374,7 @@ class Ui_MainWindow(object):
self.media_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Library')) self.media_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Library'))
self.service_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Service')) self.service_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Service'))
self.theme_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Themes')) self.theme_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Themes'))
self.projector_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Projectors')) self.projector_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Projector Controller'))
self.file_new_item.setText(translate('OpenLP.MainWindow', '&New Service')) self.file_new_item.setText(translate('OpenLP.MainWindow', '&New Service'))
self.file_new_item.setToolTip(UiStrings().NewService) self.file_new_item.setToolTip(UiStrings().NewService)
self.file_new_item.setStatusTip(UiStrings().CreateService) self.file_new_item.setStatusTip(UiStrings().CreateService)
@ -407,7 +406,7 @@ class Ui_MainWindow(object):
translate('OpenLP.MainWindow', 'Import settings from a *.config file previously exported from ' translate('OpenLP.MainWindow', 'Import settings from a *.config file previously exported from '
'this or another machine.')) 'this or another machine.'))
self.settings_import_item.setText(translate('OpenLP.MainWindow', 'Settings')) self.settings_import_item.setText(translate('OpenLP.MainWindow', 'Settings'))
self.view_projector_manager_item.setText(translate('OpenLP.MainWindow', '&Projectors')) self.view_projector_manager_item.setText(translate('OpenLP.MainWindow', '&Projector Controller'))
self.view_projector_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Hide or show Projectors.')) self.view_projector_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Hide or show Projectors.'))
self.view_projector_manager_item.setStatusTip(translate('OpenLP.MainWindow', self.view_projector_manager_item.setStatusTip(translate('OpenLP.MainWindow',
'Toggle visibility of the Projectors.')) 'Toggle visibility of the Projectors.'))
@ -505,9 +504,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
Settings().set_up_default_values() Settings().set_up_default_values()
self.about_form = AboutForm(self) self.about_form = AboutForm(self)
MediaController() MediaController()
if Registry().get_flag('no_web_server'): websockets.WebSocketServer()
websockets.WebSocketServer() server.HttpServer()
server.HttpServer()
SettingsForm(self) SettingsForm(self)
self.formatting_tag_form = FormattingTagForm(self) self.formatting_tag_form = FormattingTagForm(self)
self.shortcut_form = ShortcutListForm(self) self.shortcut_form = ShortcutListForm(self)
@ -650,8 +648,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
self.application.process_events() self.application.process_events()
plugin.first_time() plugin.first_time()
self.application.process_events() self.application.process_events()
temp_dir = os.path.join(str(gettempdir()), 'openlp') temp_path = Path(gettempdir(), 'openlp')
shutil.rmtree(temp_dir, True) temp_path.rmtree(True)
def on_first_time_wizard_clicked(self): def on_first_time_wizard_clicked(self):
""" """
@ -760,7 +758,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
Use the Online manual in other cases. (Linux) Use the Online manual in other cases. (Linux)
""" """
if is_macosx() or is_win(): if is_macosx() or is_win():
QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(self.local_help_file)) QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(str(self.local_help_file)))
else: else:
import webbrowser import webbrowser
webbrowser.open_new('http://manual.openlp.org/') webbrowser.open_new('http://manual.openlp.org/')
@ -1220,7 +1218,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
settings.remove('custom slide') settings.remove('custom slide')
settings.remove('service') settings.remove('service')
settings.beginGroup(self.general_settings_section) settings.beginGroup(self.general_settings_section)
self.recent_files = [path_to_str(file_path) for file_path in settings.value('recent files')] self.recent_files = settings.value('recent files')
settings.endGroup() settings.endGroup()
settings.beginGroup(self.ui_settings_section) settings.beginGroup(self.ui_settings_section)
self.move(settings.value('main window position')) self.move(settings.value('main window position'))
@ -1244,7 +1242,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
log.debug('Saving QSettings') log.debug('Saving QSettings')
settings = Settings() settings = Settings()
settings.beginGroup(self.general_settings_section) settings.beginGroup(self.general_settings_section)
settings.setValue('recent files', [str_to_path(file) for file in self.recent_files]) settings.setValue('recent files', self.recent_files)
settings.endGroup() settings.endGroup()
settings.beginGroup(self.ui_settings_section) settings.beginGroup(self.ui_settings_section)
settings.setValue('main window position', self.pos()) settings.setValue('main window position', self.pos())
@ -1260,26 +1258,24 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
Updates the recent file menu with the latest list of service files accessed. Updates the recent file menu with the latest list of service files accessed.
""" """
recent_file_count = Settings().value('advanced/recent file count') recent_file_count = Settings().value('advanced/recent file count')
existing_recent_files = [recentFile for recentFile in self.recent_files if os.path.isfile(str(recentFile))]
recent_files_to_display = existing_recent_files[0:recent_file_count]
self.recent_files_menu.clear() self.recent_files_menu.clear()
for file_id, filename in enumerate(recent_files_to_display): count = 0
log.debug('Recent file name: {name}'.format(name=filename)) for recent_path in self.recent_files:
if not recent_path.is_file():
continue
count += 1
log.debug('Recent file name: {name}'.format(name=recent_path))
action = create_action(self, '', action = create_action(self, '',
text='&{n} {name}'.format(n=file_id + 1, text='&{n} {name}'.format(n=count, name=recent_path.name),
name=os.path.splitext(os.path.basename(str(filename)))[0]), data=recent_path, triggers=self.service_manager_contents.on_recent_service_clicked)
data=filename,
triggers=self.service_manager_contents.on_recent_service_clicked)
self.recent_files_menu.addAction(action) self.recent_files_menu.addAction(action)
clear_recent_files_action = create_action(self, '', if count == recent_file_count:
text=translate('OpenLP.MainWindow', 'Clear List', 'Clear List of ' break
'recent files'), clear_recent_files_action = \
statustip=translate('OpenLP.MainWindow', 'Clear the list of recent ' create_action(self, '', text=translate('OpenLP.MainWindow', 'Clear List', 'Clear List of recent files'),
'files.'), statustip=translate('OpenLP.MainWindow', 'Clear the list of recent files.'),
enabled=bool(self.recent_files), enabled=bool(self.recent_files), triggers=self.clear_recent_file_menu)
triggers=self.clear_recent_file_menu)
add_actions(self.recent_files_menu, (None, clear_recent_files_action)) add_actions(self.recent_files_menu, (None, clear_recent_files_action))
clear_recent_files_action.setEnabled(bool(self.recent_files))
def add_recent_file(self, filename): def add_recent_file(self, filename):
""" """
@ -1291,20 +1287,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
# actually stored in the settings therefore the default value of 20 will # actually stored in the settings therefore the default value of 20 will
# always be used. # always be used.
max_recent_files = Settings().value('advanced/max recent files') max_recent_files = Settings().value('advanced/max recent files')
if filename: file_path = Path(filename)
# Add some cleanup to reduce duplication in the recent file list # Some cleanup to reduce duplication in the recent file list
filename = os.path.abspath(filename) file_path = file_path.resolve()
# abspath() only capitalises the drive letter if it wasn't provided if file_path in self.recent_files:
# in the given filename which then causes duplication. self.recent_files.remove(file_path)
if filename[1:3] == ':\\': self.recent_files.insert(0, file_path)
filename = filename[0].upper() + filename[1:] self.recent_files = self.recent_files[:max_recent_files]
if filename in self.recent_files:
self.recent_files.remove(filename)
if not isinstance(self.recent_files, list):
self.recent_files = [self.recent_files]
self.recent_files.insert(0, filename)
while len(self.recent_files) > max_recent_files:
self.recent_files.pop()
def clear_recent_file_menu(self): def clear_recent_file_menu(self):
""" """
@ -1367,7 +1356,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
'- Please wait for copy to finish').format(path=self.new_data_path)) '- Please wait for copy to finish').format(path=self.new_data_path))
dir_util.copy_tree(str(old_data_path), str(self.new_data_path)) dir_util.copy_tree(str(old_data_path), str(self.new_data_path))
log.info('Copy successful') log.info('Copy successful')
except (IOError, os.error, DistutilsFileError) as why: except (OSError, DistutilsFileError) as why:
self.application.set_normal_cursor() self.application.set_normal_cursor()
log.exception('Data copy failed {err}'.format(err=str(why))) log.exception('Data copy failed {err}'.format(err=str(why)))
err_text = translate('OpenLP.MainWindow', err_text = translate('OpenLP.MainWindow',

View File

@ -31,8 +31,8 @@ from PyQt5 import QtCore, QtWidgets
from openlp.core.api.http import register_endpoint from openlp.core.api.http import register_endpoint
from openlp.core.common import extension_loader from openlp.core.common import extension_loader
from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.mixins import OpenLPMixin, RegistryMixin from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.lib import ItemCapabilities from openlp.core.lib import ItemCapabilities
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box
@ -42,7 +42,7 @@ from openlp.core.ui.media.vendor.mediainfoWrapper import MediaInfoWrapper
from openlp.core.ui.media.mediaplayer import MediaPlayer from openlp.core.ui.media.mediaplayer import MediaPlayer
from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players,\ from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players,\
parse_optical_path parse_optical_path
from openlp.core.ui.lib.toolbar import OpenLPToolbar from openlp.core.widgets.toolbar import OpenLPToolbar
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -91,7 +91,7 @@ class MediaSlider(QtWidgets.QSlider):
QtWidgets.QSlider.mouseReleaseEvent(self, event) QtWidgets.QSlider.mouseReleaseEvent(self, event)
class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): class MediaController(RegistryBase, LogMixin, RegistryProperties):
""" """
The implementation of the Media Controller. The Media Controller adds an own class for every Player. The implementation of the Media Controller. The Media Controller adds an own class for every Player.
Currently these are QtWebkit, Phonon and Vlc. display_controllers are an array of controllers keyed on the Currently these are QtWebkit, Phonon and Vlc. display_controllers are an array of controllers keyed on the
@ -498,8 +498,6 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
:param controller: The media controller. :param controller: The media controller.
:return: True if setup succeeded else False. :return: True if setup succeeded else False.
""" """
if controller is None:
controller = self.display_controllers[DisplayControllerType.Plugin]
# stop running videos # stop running videos
self.media_reset(controller) self.media_reset(controller)
# Setup media info # Setup media info
@ -509,9 +507,9 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
controller.media_info.media_type = MediaType.CD controller.media_info.media_type = MediaType.CD
else: else:
controller.media_info.media_type = MediaType.DVD controller.media_info.media_type = MediaType.DVD
controller.media_info.start_time = start // 1000 controller.media_info.start_time = start
controller.media_info.end_time = end // 1000 controller.media_info.end_time = end
controller.media_info.length = (end - start) // 1000 controller.media_info.length = (end - start)
controller.media_info.title_track = title controller.media_info.title_track = title
controller.media_info.audio_track = audio_track controller.media_info.audio_track = audio_track
controller.media_info.subtitle_track = subtitle_track controller.media_info.subtitle_track = subtitle_track

View File

@ -22,7 +22,7 @@
""" """
The :mod:`~openlp.core.ui.media.mediaplayer` module contains the MediaPlayer class. The :mod:`~openlp.core.ui.media.mediaplayer` module contains the MediaPlayer class.
""" """
from openlp.core.common.registry import RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.ui.media import MediaState from openlp.core.ui.media import MediaState

View File

@ -23,6 +23,7 @@
The :mod:`~openlp.core.ui.media.playertab` module holds the configuration tab for the media stuff. The :mod:`~openlp.core.ui.media.playertab` module holds the configuration tab for the media stuff.
""" """
import platform import platform
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.i18n import UiStrings, translate
@ -31,7 +32,7 @@ from openlp.core.common.settings import Settings
from openlp.core.lib import SettingsTab from openlp.core.lib import SettingsTab
from openlp.core.lib.ui import create_button from openlp.core.lib.ui import create_button
from openlp.core.ui.media import get_media_players, set_media_players from openlp.core.ui.media import get_media_players, set_media_players
from openlp.core.ui.lib.colorbutton import ColorButton from openlp.core.widgets.buttons import ColorButton
class MediaQCheckBox(QtWidgets.QCheckBox): class MediaQCheckBox(QtWidgets.QCheckBox):

View File

@ -25,10 +25,8 @@ information related to the rwquested media.
""" """
import json import json
import os import os
from subprocess import Popen from subprocess import check_output
from tempfile import mkstemp
import six
from bs4 import BeautifulSoup, NavigableString from bs4 import BeautifulSoup, NavigableString
ENV_DICT = os.environ ENV_DICT = os.environ
@ -80,7 +78,7 @@ class Track(object):
def to_data(self): def to_data(self):
data = {} data = {}
for k, v in six.iteritems(self.__dict__): for k, v in self.__dict__.items():
if k != 'xml_dom_fragment': if k != 'xml_dom_fragment':
data[k] = v data[k] = v
return data return data
@ -100,20 +98,10 @@ class MediaInfoWrapper(object):
@staticmethod @staticmethod
def parse(filename, environment=ENV_DICT): def parse(filename, environment=ENV_DICT):
command = ["mediainfo", "-f", "--Output=XML", filename] xml = check_output(['mediainfo', '-f', '--Output=XML', '--Inform=OLDXML', filename])
fileno_out, fname_out = mkstemp(suffix=".xml", prefix="media-") if not xml.startswith(b'<?xml'):
fileno_err, fname_err = mkstemp(suffix=".err", prefix="media-") xml = check_output(['mediainfo', '-f', '--Output=XML', filename])
fp_out = os.fdopen(fileno_out, 'r+b') xml_dom = MediaInfoWrapper.parse_xml_data_into_dom(xml)
fp_err = os.fdopen(fileno_err, 'r+b')
p = Popen(command, stdout=fp_out, stderr=fp_err, env=environment)
p.wait()
fp_out.seek(0)
xml_dom = MediaInfoWrapper.parse_xml_data_into_dom(fp_out.read())
fp_out.close()
fp_err.close()
os.unlink(fname_out)
os.unlink(fname_err)
return MediaInfoWrapper(xml_dom) return MediaInfoWrapper(xml_dom)
def _populate_tracks(self): def _populate_tracks(self):

View File

@ -280,7 +280,8 @@ class VlcPlayer(MediaPlayer):
start_time = controller.media_info.start_time start_time = controller.media_info.start_time
log.debug('mediatype: ' + str(controller.media_info.media_type)) log.debug('mediatype: ' + str(controller.media_info.media_type))
# Set tracks for the optical device # Set tracks for the optical device
if controller.media_info.media_type == MediaType.DVD: if controller.media_info.media_type == MediaType.DVD and \
self.get_live_state() != MediaState.Paused and self.get_preview_state() != MediaState.Paused:
log.debug('vlc play, playing started') log.debug('vlc play, playing started')
if controller.media_info.title_track > 0: if controller.media_info.title_track > 0:
log.debug('vlc play, title_track set: ' + str(controller.media_info.title_track)) log.debug('vlc play, title_track set: ' + str(controller.media_info.title_track))
@ -350,7 +351,7 @@ class VlcPlayer(MediaPlayer):
""" """
if display.controller.media_info.media_type == MediaType.CD \ if display.controller.media_info.media_type == MediaType.CD \
or display.controller.media_info.media_type == MediaType.DVD: or display.controller.media_info.media_type == MediaType.DVD:
seek_value += int(display.controller.media_info.start_time * 1000) seek_value += int(display.controller.media_info.start_time)
if display.vlc_media_player.is_seekable(): if display.vlc_media_player.is_seekable():
display.vlc_media_player.set_time(seek_value) display.vlc_media_player.set_time(seek_value)
@ -386,15 +387,15 @@ class VlcPlayer(MediaPlayer):
self.stop(display) self.stop(display)
controller = display.controller controller = display.controller
if controller.media_info.end_time > 0: if controller.media_info.end_time > 0:
if display.vlc_media_player.get_time() > controller.media_info.end_time * 1000: if display.vlc_media_player.get_time() > controller.media_info.end_time:
self.stop(display) self.stop(display)
self.set_visible(display, False) self.set_visible(display, False)
if not controller.seek_slider.isSliderDown(): if not controller.seek_slider.isSliderDown():
controller.seek_slider.blockSignals(True) controller.seek_slider.blockSignals(True)
if display.controller.media_info.media_type == MediaType.CD \ if display.controller.media_info.media_type == MediaType.CD \
or display.controller.media_info.media_type == MediaType.DVD: or display.controller.media_info.media_type == MediaType.DVD:
controller.seek_slider.setSliderPosition(display.vlc_media_player.get_time() - controller.seek_slider.setSliderPosition(
int(display.controller.media_info.start_time * 1000)) display.vlc_media_player.get_time() - int(display.controller.media_info.start_time))
else: else:
controller.seek_slider.setSliderPosition(display.vlc_media_player.get_time()) controller.seek_slider.setSliderPosition(display.vlc_media_player.get_time())
controller.seek_slider.blockSignals(False) controller.seek_slider.blockSignals(False)

View File

@ -27,7 +27,7 @@ import logging
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.common.registry import RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.lib import PluginStatus from openlp.core.lib import PluginStatus
from openlp.core.ui.plugindialog import Ui_PluginViewDialog from openlp.core.ui.plugindialog import Ui_PluginViewDialog

View File

@ -26,7 +26,7 @@ from PyQt5 import QtCore, QtWidgets, QtPrintSupport
from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.i18n import UiStrings, translate
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
from openlp.core.ui.lib import SpellTextEdit from openlp.core.widgets.edits import SpellTextEdit
class ZoomSize(object): class ZoomSize(object):

View File

@ -30,7 +30,8 @@ from PyQt5 import QtCore, QtGui, QtWidgets, QtPrintSupport
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.lib import get_text_file_string from openlp.core.lib import get_text_file_string
from openlp.core.ui.printservicedialog import Ui_PrintServiceDialog, ZoomSize from openlp.core.ui.printservicedialog import Ui_PrintServiceDialog, ZoomSize

View File

@ -24,7 +24,8 @@ The service item edit dialog
""" """
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.ui.serviceitemeditdialog import Ui_ServiceItemEditDialog from openlp.core.ui.serviceitemeditdialog import Ui_ServiceItemEditDialog

View File

@ -32,19 +32,19 @@ from tempfile import mkstemp
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import ThemeLevel, split_filename, delete_file from openlp.core.common import ThemeLevel, delete_file
from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.actions import ActionList, CategoryOrder
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import UiStrings, format_time, translate from openlp.core.common.i18n import UiStrings, format_time, translate
from openlp.core.common.mixins import OpenLPMixin, RegistryMixin from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import Path, create_paths, path_to_str, str_to_path from openlp.core.common.path import Path, create_paths, str_to_path
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.lib import ServiceItem, ItemCapabilities, PluginStatus, build_icon from openlp.core.lib import ServiceItem, ItemCapabilities, PluginStatus, build_icon
from openlp.core.lib.ui import critical_error_message_box, create_widget_action, find_and_set_in_combo_box from openlp.core.lib.ui import critical_error_message_box, create_widget_action, find_and_set_in_combo_box
from openlp.core.ui import ServiceNoteForm, ServiceItemEditForm, StartTimeForm from openlp.core.ui import ServiceNoteForm, ServiceItemEditForm, StartTimeForm
from openlp.core.ui.lib import OpenLPToolbar from openlp.core.widgets.dialogs import FileDialog
from openlp.core.ui.lib.filedialog import FileDialog from openlp.core.widgets.toolbar import OpenLPToolbar
class ServiceManagerList(QtWidgets.QTreeWidget): class ServiceManagerList(QtWidgets.QTreeWidget):
@ -56,8 +56,24 @@ class ServiceManagerList(QtWidgets.QTreeWidget):
Constructor Constructor
""" """
super(ServiceManagerList, self).__init__(parent) super(ServiceManagerList, self).__init__(parent)
self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
self.setAlternatingRowColors(True)
self.setHeaderHidden(True)
self.setExpandsOnDoubleClick(False)
self.service_manager = service_manager self.service_manager = service_manager
def dragEnterEvent(self, event):
"""
React to a drag enter event
"""
event.accept()
def dragMoveEvent(self, event):
"""
React to a drage move event
"""
event.accept()
def keyPressEvent(self, event): def keyPressEvent(self, event):
""" """
Capture Key press and respond accordingly. Capture Key press and respond accordingly.
@ -117,7 +133,7 @@ class Ui_ServiceManager(object):
self.layout.setSpacing(0) self.layout.setSpacing(0)
self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setContentsMargins(0, 0, 0, 0)
# Create the top toolbar # Create the top toolbar
self.toolbar = OpenLPToolbar(widget) self.toolbar = OpenLPToolbar(self)
self.toolbar.add_toolbar_action('newService', text=UiStrings().NewService, icon=':/general/general_new.png', self.toolbar.add_toolbar_action('newService', text=UiStrings().NewService, icon=':/general/general_new.png',
tooltip=UiStrings().CreateService, triggers=self.on_new_service_clicked) tooltip=UiStrings().CreateService, triggers=self.on_new_service_clicked)
self.toolbar.add_toolbar_action('openService', text=UiStrings().OpenService, self.toolbar.add_toolbar_action('openService', text=UiStrings().OpenService,
@ -147,78 +163,60 @@ class Ui_ServiceManager(object):
QtWidgets.QAbstractItemView.CurrentChanged | QtWidgets.QAbstractItemView.CurrentChanged |
QtWidgets.QAbstractItemView.DoubleClicked | QtWidgets.QAbstractItemView.DoubleClicked |
QtWidgets.QAbstractItemView.EditKeyPressed) QtWidgets.QAbstractItemView.EditKeyPressed)
self.service_manager_list.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
self.service_manager_list.setAlternatingRowColors(True)
self.service_manager_list.setHeaderHidden(True)
self.service_manager_list.setExpandsOnDoubleClick(False)
self.service_manager_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.service_manager_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.service_manager_list.customContextMenuRequested.connect(self.context_menu) self.service_manager_list.customContextMenuRequested.connect(self.context_menu)
self.service_manager_list.setObjectName('service_manager_list') self.service_manager_list.setObjectName('service_manager_list')
# enable drop # enable drop
self.service_manager_list.__class__.dragEnterEvent = lambda x, event: event.accept() self.service_manager_list.dropEvent = self.drop_event
self.service_manager_list.__class__.dragMoveEvent = lambda x, event: event.accept()
self.service_manager_list.__class__.dropEvent = self.drop_event
self.layout.addWidget(self.service_manager_list) self.layout.addWidget(self.service_manager_list)
# Add the bottom toolbar # Add the bottom toolbar
self.order_toolbar = OpenLPToolbar(widget) self.order_toolbar = OpenLPToolbar(widget)
action_list = ActionList.get_instance() action_list = ActionList.get_instance()
action_list.add_category(UiStrings().Service, CategoryOrder.standard_toolbar) action_list.add_category(UiStrings().Service, CategoryOrder.standard_toolbar)
self.service_manager_list.move_top = self.order_toolbar.add_toolbar_action( self.move_top_action = self.order_toolbar.add_toolbar_action(
'moveTop', 'moveTop',
text=translate('OpenLP.ServiceManager', 'Move to &top'), icon=':/services/service_top.png', text=translate('OpenLP.ServiceManager', 'Move to &top'), icon=':/services/service_top.png',
tooltip=translate('OpenLP.ServiceManager', 'Move item to the top of the service.'), tooltip=translate('OpenLP.ServiceManager', 'Move item to the top of the service.'),
can_shortcuts=True, category=UiStrings().Service, triggers=self.on_service_top) can_shortcuts=True, category=UiStrings().Service, triggers=self.on_service_top)
self.service_manager_list.move_up = self.order_toolbar.add_toolbar_action( self.move_up_action = self.order_toolbar.add_toolbar_action(
'moveUp', 'moveUp',
text=translate('OpenLP.ServiceManager', 'Move &up'), icon=':/services/service_up.png', text=translate('OpenLP.ServiceManager', 'Move &up'), icon=':/services/service_up.png',
tooltip=translate('OpenLP.ServiceManager', 'Move item up one position in the service.'), tooltip=translate('OpenLP.ServiceManager', 'Move item up one position in the service.'),
can_shortcuts=True, category=UiStrings().Service, triggers=self.on_service_up) can_shortcuts=True, category=UiStrings().Service, triggers=self.on_service_up)
self.service_manager_list.move_down = self.order_toolbar.add_toolbar_action( self.move_down_action = self.order_toolbar.add_toolbar_action(
'moveDown', 'moveDown',
text=translate('OpenLP.ServiceManager', 'Move &down'), icon=':/services/service_down.png', text=translate('OpenLP.ServiceManager', 'Move &down'), icon=':/services/service_down.png',
tooltip=translate('OpenLP.ServiceManager', 'Move item down one position in the service.'), tooltip=translate('OpenLP.ServiceManager', 'Move item down one position in the service.'),
can_shortcuts=True, category=UiStrings().Service, triggers=self.on_service_down) can_shortcuts=True, category=UiStrings().Service, triggers=self.on_service_down)
self.service_manager_list.move_bottom = self.order_toolbar.add_toolbar_action( self.move_bottom_action = self.order_toolbar.add_toolbar_action(
'moveBottom', 'moveBottom',
text=translate('OpenLP.ServiceManager', 'Move to &bottom'), icon=':/services/service_bottom.png', text=translate('OpenLP.ServiceManager', 'Move to &bottom'), icon=':/services/service_bottom.png',
tooltip=translate('OpenLP.ServiceManager', 'Move item to the end of the service.'), tooltip=translate('OpenLP.ServiceManager', 'Move item to the end of the service.'),
can_shortcuts=True, category=UiStrings().Service, triggers=self.on_service_end) can_shortcuts=True, category=UiStrings().Service, triggers=self.on_service_end)
self.service_manager_list.down = self.order_toolbar.add_toolbar_action(
'down',
text=translate('OpenLP.ServiceManager', 'Move &down'), can_shortcuts=True,
tooltip=translate('OpenLP.ServiceManager', 'Moves the selection down the window.'), visible=False,
triggers=self.on_move_selection_down)
action_list.add_action(self.service_manager_list.down)
self.service_manager_list.up = self.order_toolbar.add_toolbar_action(
'up',
text=translate('OpenLP.ServiceManager', 'Move up'), can_shortcuts=True,
tooltip=translate('OpenLP.ServiceManager', 'Moves the selection up the window.'), visible=False,
triggers=self.on_move_selection_up)
action_list.add_action(self.service_manager_list.up)
self.order_toolbar.addSeparator() self.order_toolbar.addSeparator()
self.service_manager_list.delete = self.order_toolbar.add_toolbar_action( self.delete_action = self.order_toolbar.add_toolbar_action(
'delete', can_shortcuts=True, 'delete', can_shortcuts=True,
text=translate('OpenLP.ServiceManager', '&Delete From Service'), icon=':/general/general_delete.png', text=translate('OpenLP.ServiceManager', '&Delete From Service'), icon=':/general/general_delete.png',
tooltip=translate('OpenLP.ServiceManager', 'Delete the selected item from the service.'), tooltip=translate('OpenLP.ServiceManager', 'Delete the selected item from the service.'),
triggers=self.on_delete_from_service) triggers=self.on_delete_from_service)
self.order_toolbar.addSeparator() self.order_toolbar.addSeparator()
self.service_manager_list.expand = self.order_toolbar.add_toolbar_action( self.expand_action = self.order_toolbar.add_toolbar_action(
'expand', can_shortcuts=True, 'expand', can_shortcuts=True,
text=translate('OpenLP.ServiceManager', '&Expand all'), icon=':/services/service_expand_all.png', text=translate('OpenLP.ServiceManager', '&Expand all'), icon=':/services/service_expand_all.png',
tooltip=translate('OpenLP.ServiceManager', 'Expand all the service items.'), tooltip=translate('OpenLP.ServiceManager', 'Expand all the service items.'),
category=UiStrings().Service, triggers=self.on_expand_all) category=UiStrings().Service, triggers=self.on_expand_all)
self.service_manager_list.collapse = self.order_toolbar.add_toolbar_action( self.collapse_action = self.order_toolbar.add_toolbar_action(
'collapse', can_shortcuts=True, 'collapse', can_shortcuts=True,
text=translate('OpenLP.ServiceManager', '&Collapse all'), icon=':/services/service_collapse_all.png', text=translate('OpenLP.ServiceManager', '&Collapse all'), icon=':/services/service_collapse_all.png',
tooltip=translate('OpenLP.ServiceManager', 'Collapse all the service items.'), tooltip=translate('OpenLP.ServiceManager', 'Collapse all the service items.'),
category=UiStrings().Service, triggers=self.on_collapse_all) category=UiStrings().Service, triggers=self.on_collapse_all)
self.order_toolbar.addSeparator() self.order_toolbar.addSeparator()
self.service_manager_list.make_live = self.order_toolbar.add_toolbar_action( self.make_live_action = self.order_toolbar.add_toolbar_action(
'make_live', can_shortcuts=True, 'make_live', can_shortcuts=True,
text=translate('OpenLP.ServiceManager', 'Go Live'), icon=':/general/general_live.png', text=translate('OpenLP.ServiceManager', 'Go Live'), icon=':/general/general_live.png',
tooltip=translate('OpenLP.ServiceManager', 'Send the selected item to Live.'), tooltip=translate('OpenLP.ServiceManager', 'Send the selected item to Live.'),
category=UiStrings().Service, category=UiStrings().Service,
triggers=self.make_live) triggers=self.on_make_live_action_triggered)
self.layout.addWidget(self.order_toolbar) self.layout.addWidget(self.order_toolbar)
# Connect up our signals and slots # Connect up our signals and slots
self.theme_combo_box.activated.connect(self.on_theme_combo_box_selected) self.theme_combo_box.activated.connect(self.on_theme_combo_box_selected)
@ -254,7 +252,7 @@ class Ui_ServiceManager(object):
icon=':/media/auto-start_active.png', icon=':/media/auto-start_active.png',
triggers=self.on_auto_start) triggers=self.on_auto_start)
# Add already existing delete action to the menu. # Add already existing delete action to the menu.
self.menu.addAction(self.service_manager_list.delete) self.menu.addAction(self.delete_action)
self.create_custom_action = create_widget_action(self.menu, self.create_custom_action = create_widget_action(self.menu,
text=translate('OpenLP.ServiceManager', 'Create New &Custom ' text=translate('OpenLP.ServiceManager', 'Create New &Custom '
'Slide'), 'Slide'),
@ -285,28 +283,20 @@ class Ui_ServiceManager(object):
self.preview_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', 'Show &Preview'), self.preview_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', 'Show &Preview'),
icon=':/general/general_preview.png', triggers=self.make_preview) icon=':/general/general_preview.png', triggers=self.make_preview)
# Add already existing make live action to the menu. # Add already existing make live action to the menu.
self.menu.addAction(self.service_manager_list.make_live) self.menu.addAction(self.make_live_action)
self.menu.addSeparator() self.menu.addSeparator()
self.theme_menu = QtWidgets.QMenu(translate('OpenLP.ServiceManager', '&Change Item Theme')) self.theme_menu = QtWidgets.QMenu(translate('OpenLP.ServiceManager', '&Change Item Theme'))
self.menu.addMenu(self.theme_menu) self.menu.addMenu(self.theme_menu)
self.service_manager_list.addActions( self.service_manager_list.addActions([self.move_down_action, self.move_up_action, self.make_live_action,
[self.service_manager_list.move_down, self.move_top_action, self.move_bottom_action, self.expand_action,
self.service_manager_list.move_up, self.collapse_action])
self.service_manager_list.make_live,
self.service_manager_list.move_top,
self.service_manager_list.move_bottom,
self.service_manager_list.up,
self.service_manager_list.down,
self.service_manager_list.expand,
self.service_manager_list.collapse
])
Registry().register_function('theme_update_list', self.update_theme_list) Registry().register_function('theme_update_list', self.update_theme_list)
Registry().register_function('config_screen_changed', self.regenerate_service_items) Registry().register_function('config_screen_changed', self.regenerate_service_items)
Registry().register_function('theme_update_global', self.theme_change) Registry().register_function('theme_update_global', self.theme_change)
Registry().register_function('mediaitem_suffix_reset', self.reset_supported_suffixes) Registry().register_function('mediaitem_suffix_reset', self.reset_supported_suffixes)
class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceManager, RegistryProperties): class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixin, RegistryProperties):
""" """
Manages the services. This involves taking text strings from plugins and adding them to the service. This service Manages the services. This involves taking text strings from plugins and adding them to the service. This service
can then be zipped up with all the resources used into one OSZ or oszl file for use on any OpenLP v2 installation. can then be zipped up with all the resources used into one OSZ or oszl file for use on any OpenLP v2 installation.
@ -320,7 +310,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
""" """
Sets up the service manager, toolbars, list view, et al. Sets up the service manager, toolbars, list view, et al.
""" """
super(ServiceManager, self).__init__(parent) super().__init__(parent)
self.active = build_icon(':/media/auto-start_active.png') self.active = build_icon(':/media/auto-start_active.png')
self.inactive = build_icon(':/media/auto-start_inactive.png') self.inactive = build_icon(':/media/auto-start_inactive.png')
self.service_items = [] self.service_items = []
@ -329,7 +319,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
self.service_id = 0 self.service_id = 0
# is a new service and has not been saved # is a new service and has not been saved
self._modified = False self._modified = False
self._file_name = '' self._service_path = None
self.service_has_all_original_files = True self.service_has_all_original_files = True
self.list_double_clicked = False self.list_double_clicked = False
@ -360,7 +350,10 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
if modified: if modified:
self.service_id += 1 self.service_id += 1
self._modified = modified self._modified = modified
service_file = self.short_file_name() or translate('OpenLP.ServiceManager', 'Untitled Service') if self._service_path:
service_file = self._service_path.name
else:
service_file = translate('OpenLP.ServiceManager', 'Untitled Service')
self.main_window.set_service_modified(modified, service_file) self.main_window.set_service_modified(modified, service_file)
def is_modified(self): def is_modified(self):
@ -376,8 +369,8 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
:param openlp.core.common.path.Path file_path: The service file name :param openlp.core.common.path.Path file_path: The service file name
:rtype: None :rtype: None
""" """
self._file_name = path_to_str(file_path) self._service_path = file_path
self.main_window.set_service_modified(self.is_modified(), self.short_file_name()) self.main_window.set_service_modified(self.is_modified(), file_path.name)
Settings().setValue('servicemanager/last file', file_path) Settings().setValue('servicemanager/last file', file_path)
if file_path and file_path.suffix == '.oszl': if file_path and file_path.suffix == '.oszl':
self._save_lite = True self._save_lite = True
@ -387,14 +380,17 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
def file_name(self): def file_name(self):
""" """
Return the current file name including path. Return the current file name including path.
:rtype: openlp.core.common.path.Path
""" """
return self._file_name return self._service_path
def short_file_name(self): def short_file_name(self):
""" """
Return the current file name, excluding the path. Return the current file name, excluding the path.
""" """
return split_filename(self._file_name)[1] if self._service_path:
return self._service_path.name
def reset_supported_suffixes(self): def reset_supported_suffixes(self):
""" """
@ -472,6 +468,12 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
Load a recent file as the service triggered by mainwindow recent service list. Load a recent file as the service triggered by mainwindow recent service list.
:param field: :param field:
""" """
if self.is_modified():
result = self.save_modified_service()
if result == QtWidgets.QMessageBox.Cancel:
return False
elif result == QtWidgets.QMessageBox.Save:
self.decide_save_method()
sender = self.sender() sender = self.sender()
self.load_file(sender.data()) self.load_file(sender.data())
@ -601,7 +603,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
if not os.path.exists(save_file): if not os.path.exists(save_file):
shutil.copy(audio_from, save_file) shutil.copy(audio_from, save_file)
zip_file.write(audio_from, audio_to) zip_file.write(audio_from, audio_to)
except IOError: except OSError:
self.log_exception('Failed to save service to disk: {name}'.format(name=temp_file_name)) self.log_exception('Failed to save service to disk: {name}'.format(name=temp_file_name))
self.main_window.error_message(translate('OpenLP.ServiceManager', 'Error Saving File'), self.main_window.error_message(translate('OpenLP.ServiceManager', 'Error Saving File'),
translate('OpenLP.ServiceManager', 'There was an error saving your file.')) translate('OpenLP.ServiceManager', 'There was an error saving your file.'))
@ -662,7 +664,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
zip_file = zipfile.ZipFile(temp_file_name, 'w', zipfile.ZIP_STORED, True) zip_file = zipfile.ZipFile(temp_file_name, 'w', zipfile.ZIP_STORED, True)
# First we add service contents. # First we add service contents.
zip_file.writestr(service_file_name, service_content) zip_file.writestr(service_file_name, service_content)
except IOError: except OSError:
self.log_exception('Failed to save service to disk: {name}'.format(name=temp_file_name)) self.log_exception('Failed to save service to disk: {name}'.format(name=temp_file_name))
self.main_window.error_message(translate('OpenLP.ServiceManager', 'Error Saving File'), self.main_window.error_message(translate('OpenLP.ServiceManager', 'Error Saving File'),
translate('OpenLP.ServiceManager', 'There was an error saving your file.')) translate('OpenLP.ServiceManager', 'There was an error saving your file.'))
@ -708,20 +710,29 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
directory_path = Settings().value(self.main_window.service_manager_settings_section + '/last directory') directory_path = Settings().value(self.main_window.service_manager_settings_section + '/last directory')
if directory_path: if directory_path:
default_file_path = directory_path / default_file_path default_file_path = directory_path / default_file_path
lite_filter = translate('OpenLP.ServiceManager', 'OpenLP Service Files - lite (*.oszl)')
packaged_filter = translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz)')
if self._service_path and self._service_path.suffix == '.oszl':
default_filter = lite_filter
else:
default_filter = packaged_filter
# SaveAs from osz to oszl is not valid as the files will be deleted on exit which is not sensible or usable in # SaveAs from osz to oszl is not valid as the files will be deleted on exit which is not sensible or usable in
# the long term. # the long term.
if self._file_name.endswith('oszl') or self.service_has_all_original_files: if self._service_path and self._service_path.suffix == '.oszl' or self.service_has_all_original_files:
file_path, filter_used = FileDialog.getSaveFileName( file_path, filter_used = FileDialog.getSaveFileName(
self.main_window, UiStrings().SaveService, default_file_path, self.main_window, UiStrings().SaveService, default_file_path,
translate('OpenLP.ServiceManager', '{packaged};; {lite}'.format(packaged=packaged_filter, lite=lite_filter),
'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)')) default_filter)
else: else:
file_path, filter_used = FileDialog.getSaveFileName( file_path, filter_used = FileDialog.getSaveFileName(
self.main_window, UiStrings().SaveService, file_path, self.main_window, UiStrings().SaveService, default_file_path,
translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;')) '{packaged};;'.format(packaged=packaged_filter))
if not file_path: if not file_path:
return False return False
file_path.with_suffix('.osz') if filter_used == lite_filter:
file_path = file_path.with_suffix('.oszl')
else:
file_path = file_path.with_suffix('.osz')
self.set_file_name(file_path) self.set_file_name(file_path)
self.decide_save_method() self.decide_save_method()
@ -789,11 +800,11 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
else: else:
critical_error_message_box(message=translate('OpenLP.ServiceManager', 'File is not a valid service.')) critical_error_message_box(message=translate('OpenLP.ServiceManager', 'File is not a valid service.'))
self.log_error('File contains no service data') self.log_error('File contains no service data')
except (IOError, NameError): except (OSError, NameError):
self.log_exception('Problem loading service file {name}'.format(name=file_name)) self.log_exception('Problem loading service file {name}'.format(name=file_name))
critical_error_message_box(message=translate('OpenLP.ServiceManager', critical_error_message_box(message=translate('OpenLP.ServiceManager',
'File could not be opened because it is corrupt.')) 'File could not be opened because it is corrupt.'))
except zipfile.BadZipfile: except zipfile.BadZipFile:
if os.path.getsize(file_name) == 0: if os.path.getsize(file_name) == 0:
self.log_exception('Service file is zero sized: {name}'.format(name=file_name)) self.log_exception('Service file is zero sized: {name}'.format(name=file_name))
QtWidgets.QMessageBox.information(self, translate('OpenLP.ServiceManager', 'Empty File'), QtWidgets.QMessageBox.information(self, translate('OpenLP.ServiceManager', 'Empty File'),
@ -1655,14 +1666,15 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
if start_pos == -1: if start_pos == -1:
return return
if item is None: if item is None:
end_pos = len(self.service_items) end_pos = len(self.service_items) - 1
else: else:
end_pos = get_parent_item_data(item) - 1 end_pos = get_parent_item_data(item) - 1
service_item = self.service_items[start_pos] service_item = self.service_items[start_pos]
self.service_items.remove(service_item) if start_pos != end_pos:
self.service_items.insert(end_pos, service_item) self.service_items.remove(service_item)
self.repaint_service_list(end_pos, child) self.service_items.insert(end_pos, service_item)
self.set_modified() self.repaint_service_list(end_pos, child)
self.set_modified()
else: else:
# we are not over anything so drop # we are not over anything so drop
replace = False replace = False
@ -1728,6 +1740,15 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
self.service_items[item]['service_item'].update_theme(theme) self.service_items[item]['service_item'].update_theme(theme)
self.regenerate_service_items(True) self.regenerate_service_items(True)
def on_make_live_action_triggered(self, checked):
"""
Handle `make_live_action` when the action is triggered.
:param bool checked: Not Used.
:rtype: None
"""
self.make_live()
def get_drop_position(self): def get_drop_position(self):
""" """
Getter for drop_position. Used in: MediaManagerItem Getter for drop_position. Used in: MediaManagerItem

View File

@ -25,9 +25,10 @@ The :mod:`~openlp.core.ui.servicenoteform` module contains the `ServiceNoteForm`
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.ui.lib import SpellTextEdit from openlp.core.common.registry import Registry
from openlp.core.lib.ui import create_button_box from openlp.core.lib.ui import create_button_box
from openlp.core.widgets.edits import SpellTextEdit
class ServiceNoteForm(QtWidgets.QDialog, RegistryProperties): class ServiceNoteForm(QtWidgets.QDialog, RegistryProperties):

View File

@ -27,11 +27,12 @@ import logging
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.api import ApiTab from openlp.core.api import ApiTab
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
from openlp.core.projectors.tab import ProjectorTab
from openlp.core.ui import AdvancedTab, GeneralTab, ThemesTab from openlp.core.ui import AdvancedTab, GeneralTab, ThemesTab
from openlp.core.ui.media import PlayerTab from openlp.core.ui.media import PlayerTab
from openlp.core.ui.projector.tab import ProjectorTab
from openlp.core.ui.settingsdialog import Ui_SettingsDialog from openlp.core.ui.settingsdialog import Ui_SettingsDialog
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@ -29,7 +29,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common.actions import ActionList from openlp.core.common.actions import ActionList
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.common.registry import RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.ui.shortcutlistdialog import Ui_ShortcutListDialog from openlp.core.ui.shortcutlistdialog import Ui_ShortcutListDialog

View File

@ -22,7 +22,6 @@
""" """
The :mod:`slidecontroller` module contains the most important part of OpenLP - the slide controller The :mod:`slidecontroller` module contains the most important part of OpenLP - the slide controller
""" """
import copy import copy
import os import os
from collections import deque from collections import deque
@ -33,15 +32,15 @@ from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import SlideLimits from openlp.core.common import SlideLimits
from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.actions import ActionList, CategoryOrder
from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.mixins import OpenLPMixin, RegistryMixin from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList from openlp.core.display.screens import ScreenList
from openlp.core.lib import ItemCapabilities, ServiceItem, ImageSource, ServiceItemAction, build_icon, build_html from openlp.core.lib import ItemCapabilities, ServiceItem, ImageSource, ServiceItemAction, build_icon, build_html
from openlp.core.lib.ui import create_action from openlp.core.lib.ui import create_action
from openlp.core.ui.lib.toolbar import OpenLPToolbar
from openlp.core.ui.lib.listpreviewwidget import ListPreviewWidget
from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType
from openlp.core.widgets.toolbar import OpenLPToolbar
from openlp.core.widgets.views import ListPreviewWidget
# Threshold which has to be trespassed to toggle. # Threshold which has to be trespassed to toggle.
@ -82,11 +81,11 @@ class DisplayController(QtWidgets.QWidget):
""" """
Controller is a general display controller widget. Controller is a general display controller widget.
""" """
def __init__(self, parent): def __init__(self, *args, **kwargs):
""" """
Set up the general Controller. Set up the general Controller.
""" """
super(DisplayController, self).__init__(parent) super().__init__(*args, **kwargs)
self.is_live = False self.is_live = False
self.display = None self.display = None
self.controller_type = None self.controller_type = None
@ -133,16 +132,16 @@ class InfoLabel(QtWidgets.QLabel):
super().setText(text) super().setText(text)
class SlideController(DisplayController, RegistryProperties): class SlideController(DisplayController, LogMixin, RegistryProperties):
""" """
SlideController is the slide controller widget. This widget is what the SlideController is the slide controller widget. This widget is what the
user uses to control the displaying of verses/slides/etc on the screen. user uses to control the displaying of verses/slides/etc on the screen.
""" """
def __init__(self, parent): def __init__(self, *args, **kwargs):
""" """
Set up the Slide Controller. Set up the Slide Controller.
""" """
super(SlideController, self).__init__(parent) super().__init__(*args, **kwargs)
def post_set_up(self): def post_set_up(self):
""" """
@ -237,6 +236,9 @@ class SlideController(DisplayController, RegistryProperties):
self.hide_menu.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) self.hide_menu.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
self.hide_menu.setMenu(QtWidgets.QMenu(translate('OpenLP.SlideController', 'Hide'), self.toolbar)) self.hide_menu.setMenu(QtWidgets.QMenu(translate('OpenLP.SlideController', 'Hide'), self.toolbar))
self.toolbar.add_toolbar_widget(self.hide_menu) self.toolbar.add_toolbar_widget(self.hide_menu)
self.toolbar.add_toolbar_action('goPreview', icon=':/general/general_live.png',
tooltip=translate('OpenLP.SlideController', 'Move to preview.'),
triggers=self.on_go_preview)
# The order of the blank to modes in Shortcuts list comes from here. # The order of the blank to modes in Shortcuts list comes from here.
self.desktop_screen_enable = create_action(self, 'desktopScreenEnable', self.desktop_screen_enable = create_action(self, 'desktopScreenEnable',
text=translate('OpenLP.SlideController', 'Show Desktop'), text=translate('OpenLP.SlideController', 'Show Desktop'),
@ -1429,6 +1431,15 @@ class SlideController(DisplayController, RegistryProperties):
self.live_controller.add_service_manager_item(self.service_item, row) self.live_controller.add_service_manager_item(self.service_item, row)
self.live_controller.preview_widget.setFocus() self.live_controller.preview_widget.setFocus()
def on_go_preview(self, field=None):
"""
If live copy slide item to preview controller from live Controller
"""
row = self.preview_widget.current_slide_number()
if -1 < row < self.preview_widget.slide_count():
self.preview_controller.add_service_manager_item(self.service_item, row)
self.preview_controller.preview_widget.setFocus()
def on_media_start(self, item): def on_media_start(self, item):
""" """
Respond to the arrival of a media service item Respond to the arrival of a media service item
@ -1513,7 +1524,7 @@ class SlideController(DisplayController, RegistryProperties):
self.display.audio_player.go_to(action.data()) self.display.audio_player.go_to(action.data())
class PreviewController(RegistryMixin, OpenLPMixin, SlideController): class PreviewController(RegistryBase, SlideController):
""" """
Set up the Preview Controller. Set up the Preview Controller.
""" """
@ -1521,11 +1532,12 @@ class PreviewController(RegistryMixin, OpenLPMixin, SlideController):
slidecontroller_preview_next = QtCore.pyqtSignal() slidecontroller_preview_next = QtCore.pyqtSignal()
slidecontroller_preview_previous = QtCore.pyqtSignal() slidecontroller_preview_previous = QtCore.pyqtSignal()
def __init__(self, parent): def __init__(self, *args, **kwargs):
""" """
Set up the base Controller as a preview. Set up the base Controller as a preview.
""" """
super(PreviewController, self).__init__(parent) self.__registry_name = 'preview_slidecontroller'
super().__init__(*args, **kwargs)
self.split = 0 self.split = 0
self.type_prefix = 'preview' self.type_prefix = 'preview'
self.category = 'Preview Toolbar' self.category = 'Preview Toolbar'
@ -1537,7 +1549,7 @@ class PreviewController(RegistryMixin, OpenLPMixin, SlideController):
self.post_set_up() self.post_set_up()
class LiveController(RegistryMixin, OpenLPMixin, SlideController): class LiveController(RegistryBase, SlideController):
""" """
Set up the Live Controller. Set up the Live Controller.
""" """
@ -1549,11 +1561,11 @@ class LiveController(RegistryMixin, OpenLPMixin, SlideController):
mediacontroller_live_pause = QtCore.pyqtSignal() mediacontroller_live_pause = QtCore.pyqtSignal()
mediacontroller_live_stop = QtCore.pyqtSignal() mediacontroller_live_stop = QtCore.pyqtSignal()
def __init__(self, parent): def __init__(self, *args, **kwargs):
""" """
Set up the base Controller as a live. Set up the base Controller as a live.
""" """
super(LiveController, self).__init__(parent) super().__init__(*args, **kwargs)
self.is_live = True self.is_live = True
self.split = 1 self.split = 1
self.type_prefix = 'live' self.type_prefix = 'live'

View File

@ -25,7 +25,8 @@ The actual start time form.
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui.starttimedialog import Ui_StartTimeDialog from openlp.core.ui.starttimedialog import Ui_StartTimeDialog

View File

@ -28,7 +28,8 @@ from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import get_images_filter, is_not_image_file from openlp.core.common import get_images_filter, is_not_image_file
from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.lib.theme import BackgroundType, BackgroundGradientType from openlp.core.lib.theme import BackgroundType, BackgroundGradientType
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui import ThemeLayoutForm from openlp.core.ui import ThemeLayoutForm

View File

@ -31,17 +31,17 @@ from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import delete_file from openlp.core.common import delete_file
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import UiStrings, translate, get_locale_key from openlp.core.common.i18n import UiStrings, translate, get_locale_key
from openlp.core.common.mixins import OpenLPMixin, RegistryMixin from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import Path, copyfile, create_paths, path_to_str, rmtree from openlp.core.common.path import Path, copyfile, create_paths, path_to_str
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.lib import ImageSource, ValidationError, get_text_file_string, build_icon, \ from openlp.core.lib import ImageSource, ValidationError, get_text_file_string, build_icon, \
check_item_selected, create_thumb, validate_thumb check_item_selected, create_thumb, validate_thumb
from openlp.core.lib.theme import Theme, BackgroundType from openlp.core.lib.theme import Theme, BackgroundType
from openlp.core.lib.ui import critical_error_message_box, create_widget_action from openlp.core.lib.ui import critical_error_message_box, create_widget_action
from openlp.core.ui import FileRenameForm, ThemeForm from openlp.core.ui import FileRenameForm, ThemeForm
from openlp.core.ui.lib import OpenLPToolbar from openlp.core.widgets.dialogs import FileDialog
from openlp.core.ui.lib.filedialog import FileDialog from openlp.core.widgets.toolbar import OpenLPToolbar
class Ui_ThemeManager(object): class Ui_ThemeManager(object):
@ -125,7 +125,7 @@ class Ui_ThemeManager(object):
self.theme_list_widget.currentItemChanged.connect(self.check_list_state) self.theme_list_widget.currentItemChanged.connect(self.check_list_state)
class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManager, RegistryProperties): class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, RegistryProperties):
""" """
Manages the orders of Theme. Manages the orders of Theme.
""" """
@ -376,7 +376,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
delete_file(self.theme_path / thumb) delete_file(self.theme_path / thumb)
delete_file(self.thumb_path / thumb) delete_file(self.thumb_path / thumb)
try: try:
rmtree(self.theme_path / theme) (self.theme_path / theme).rmtree()
except OSError: except OSError:
self.log_exception('Error deleting theme {name}'.format(name=theme)) self.log_exception('Error deleting theme {name}'.format(name=theme))
@ -431,7 +431,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
'The theme_name export failed because this error occurred: {err}') 'The theme_name export failed because this error occurred: {err}')
.format(err=ose.strerror)) .format(err=ose.strerror))
if theme_path.exists(): if theme_path.exists():
rmtree(theme_path, True) theme_path.rmtree(ignore_errors=True)
return False return False
def on_import_theme(self, checked=None): def on_import_theme(self, checked=None):
@ -497,12 +497,12 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
name = translate('OpenLP.ThemeManager', '{name} (default)').format(name=text_name) name = translate('OpenLP.ThemeManager', '{name} (default)').format(name=text_name)
else: else:
name = text_name name = text_name
thumb = self.thumb_path / '{name}.png'.format(name=text_name) thumb_path = self.thumb_path / '{name}.png'.format(name=text_name)
item_name = QtWidgets.QListWidgetItem(name) item_name = QtWidgets.QListWidgetItem(name)
if validate_thumb(theme_path, thumb): if validate_thumb(theme_path, thumb_path):
icon = build_icon(thumb) icon = build_icon(thumb_path)
else: else:
icon = create_thumb(str(theme_path), str(thumb)) icon = create_thumb(theme_path, thumb_path)
item_name.setIcon(icon) item_name.setIcon(icon)
item_name.setData(QtCore.Qt.UserRole, text_name) item_name.setData(QtCore.Qt.UserRole, text_name)
self.theme_list_widget.addItem(item_name) self.theme_list_widget.addItem(item_name)
@ -604,7 +604,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
else: else:
with full_name.open('wb') as out_file: with full_name.open('wb') as out_file:
out_file.write(theme_zip.read(zipped_file)) out_file.write(theme_zip.read(zipped_file))
except (IOError, zipfile.BadZipfile): except (OSError, zipfile.BadZipFile):
self.log_exception('Importing theme from zip failed {name}'.format(name=file_path)) self.log_exception('Importing theme from zip failed {name}'.format(name=file_path))
raise ValidationError raise ValidationError
except ValidationError: except ValidationError:
@ -667,7 +667,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
theme_path = theme_dir / '{file_name}.json'.format(file_name=name) theme_path = theme_dir / '{file_name}.json'.format(file_name=name)
try: try:
theme_path.write_text(theme_pretty) theme_path.write_text(theme_pretty)
except IOError: except OSError:
self.log_exception('Saving theme to file failed') self.log_exception('Saving theme to file failed')
if image_source_path and image_destination_path: if image_source_path and image_destination_path:
if self.old_background_image_path and image_destination_path != self.old_background_image_path: if self.old_background_image_path and image_destination_path != self.old_background_image_path:
@ -675,7 +675,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
if image_source_path != image_destination_path: if image_source_path != image_destination_path:
try: try:
copyfile(image_source_path, image_destination_path) copyfile(image_source_path, image_destination_path)
except IOError: except OSError:
self.log_exception('Failed to save theme image') self.log_exception('Failed to save theme image')
self.generate_and_save_image(name, theme) self.generate_and_save_image(name, theme)
@ -692,7 +692,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
sample_path_name.unlink() sample_path_name.unlink()
frame.save(str(sample_path_name), 'png') frame.save(str(sample_path_name), 'png')
thumb_path = self.thumb_path / '{name}.png'.format(name=theme_name) thumb_path = self.thumb_path / '{name}.png'.format(name=theme_name)
create_thumb(str(sample_path_name), str(thumb_path), False) create_thumb(sample_path_name, thumb_path, False)
def update_preview_images(self): def update_preview_images(self):
""" """
@ -711,6 +711,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
:param theme_data: The theme to generated a preview for. :param theme_data: The theme to generated a preview for.
:param force_page: Flag to tell message lines per page need to be generated. :param force_page: Flag to tell message lines per page need to be generated.
:rtype: QtGui.QPixmap
""" """
return self.renderer.generate_preview(theme_data, force_page) return self.renderer.generate_preview(theme_data, force_page)

View File

@ -29,7 +29,8 @@ from openlp.core.common.i18n import UiStrings, translate
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType 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.lib.ui import add_welcome_page, create_valign_selection_widgets
from openlp.core.ui.lib import ColorButton, PathEdit from openlp.core.widgets.buttons import ColorButton
from openlp.core.widgets.edits import PathEdit
class Ui_ThemeWizard(object): class Ui_ThemeWizard(object):

View File

@ -23,7 +23,6 @@
The :mod:`openlp.core.version` module downloads the version details for OpenLP. The :mod:`openlp.core.version` module downloads the version details for OpenLP.
""" """
import logging import logging
import os
import platform import platform
import sys import sys
import time import time
@ -96,7 +95,7 @@ class VersionWorker(QtCore.QObject):
remote_version = response.text remote_version = response.text
log.debug('New version found: %s', remote_version) log.debug('New version found: %s', remote_version)
break break
except IOError: except OSError:
log.exception('Unable to connect to OpenLP server to download version file') log.exception('Unable to connect to OpenLP server to download version file')
retries += 1 retries += 1
else: else:
@ -176,18 +175,12 @@ def get_version():
full_version = '{tag}-bzr{tree}'.format(tag=tag_version.strip(), tree=tree_revision.strip()) full_version = '{tag}-bzr{tree}'.format(tag=tag_version.strip(), tree=tree_revision.strip())
else: else:
# We're not running the development version, let's use the file. # We're not running the development version, let's use the file.
file_path = str(AppLocation.get_directory(AppLocation.VersionDir)) file_path = AppLocation.get_directory(AppLocation.VersionDir) / '.version'
file_path = os.path.join(file_path, '.version')
version_file = None
try: try:
version_file = open(file_path, 'r') full_version = file_path.read_text().rstrip()
full_version = str(version_file.read()).rstrip() except OSError:
except IOError:
log.exception('Error in version file.') log.exception('Error in version file.')
full_version = '0.0.0-bzr000' full_version = '0.0.0-bzr000'
finally:
if version_file:
version_file.close()
bits = full_version.split('-') bits = full_version.split('-')
APPLICATION_VERSION = { APPLICATION_VERSION = {
'full': full_version, 'full': full_version,

View File

@ -20,15 +20,41 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
""" """
The media manager dock. The :mod:`~openlp.core.widgets.docks` module contains a customised base dock widget and dock widgets
""" """
import logging import logging
from openlp.core.lib import StringContent from PyQt5 import QtWidgets
from openlp.core.display.screens import ScreenList
from openlp.core.lib import StringContent, build_icon
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class OpenLPDockWidget(QtWidgets.QDockWidget):
"""
Custom DockWidget class to handle events
"""
def __init__(self, parent=None, name=None, icon=None):
"""
Initialise the DockWidget
"""
log.debug('Initialise the %s widget' % name)
super(OpenLPDockWidget, self).__init__(parent)
if name:
self.setObjectName(name)
if icon:
self.setWindowIcon(build_icon(icon))
# Sort out the minimum width.
screens = ScreenList()
main_window_docbars = screens.current['size'].width() // 5
if main_window_docbars > 300:
self.setMinimumWidth(300)
else:
self.setMinimumWidth(main_window_docbars)
class MediaDockManager(object): class MediaDockManager(object):
""" """
Provide a repository for MediaManagerItems Provide a repository for MediaManagerItems

View File

@ -0,0 +1,583 @@
# -*- 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 #
###############################################################################
"""
The :mod:`~openlp.core.widgets.edits` module contains all the customised edit widgets used in OpenLP
"""
import logging
import re
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import CONTROL_CHARS
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.path import Path, path_to_str, str_to_path
from openlp.core.common.settings import Settings
from openlp.core.lib import FormattingTags, build_icon
from openlp.core.lib.ui import create_widget_action, create_action
from openlp.core.widgets.dialogs import FileDialog
from openlp.core.widgets.enums import PathEditType
try:
import enchant
from enchant import DictNotFoundError
from enchant.errors import Error
ENCHANT_AVAILABLE = True
except ImportError:
ENCHANT_AVAILABLE = False
log = logging.getLogger(__name__)
class SearchEdit(QtWidgets.QLineEdit):
"""
This is a specialised QLineEdit with a "clear" button inside for searches.
"""
searchTypeChanged = QtCore.pyqtSignal(QtCore.QVariant)
cleared = QtCore.pyqtSignal()
def __init__(self, parent, settings_section):
"""
Constructor.
"""
super().__init__(parent)
self.settings_section = settings_section
self._current_search_type = -1
self.clear_button = QtWidgets.QToolButton(self)
self.clear_button.setIcon(build_icon(':/system/clear_shortcut.png'))
self.clear_button.setCursor(QtCore.Qt.ArrowCursor)
self.clear_button.setStyleSheet('QToolButton { border: none; padding: 0px; }')
self.clear_button.resize(18, 18)
self.clear_button.hide()
self.clear_button.clicked.connect(self._on_clear_button_clicked)
self.textChanged.connect(self._on_search_edit_text_changed)
self._update_style_sheet()
self.setAcceptDrops(False)
def _update_style_sheet(self):
"""
Internal method to update the stylesheet depending on which widgets are available and visible.
"""
frame_width = self.style().pixelMetric(QtWidgets.QStyle.PM_DefaultFrameWidth)
right_padding = self.clear_button.width() + frame_width
if hasattr(self, 'menu_button'):
left_padding = self.menu_button.width()
stylesheet = 'QLineEdit {{ padding-left:{left}px; padding-right: {right}px; }} '.format(left=left_padding,
right=right_padding)
else:
stylesheet = 'QLineEdit {{ padding-right: {right}px; }} '.format(right=right_padding)
self.setStyleSheet(stylesheet)
msz = self.minimumSizeHint()
self.setMinimumSize(max(msz.width(), self.clear_button.width() + (frame_width * 2) + 2),
max(msz.height(), self.clear_button.height() + (frame_width * 2) + 2))
def resizeEvent(self, event):
"""
Reimplemented method to react to resizing of the widget.
:param event: The event that happened.
"""
size = self.clear_button.size()
frame_width = self.style().pixelMetric(QtWidgets.QStyle.PM_DefaultFrameWidth)
self.clear_button.move(self.rect().right() - frame_width - size.width(),
(self.rect().bottom() + 1 - size.height()) // 2)
if hasattr(self, 'menu_button'):
size = self.menu_button.size()
self.menu_button.move(self.rect().left() + frame_width + 2, (self.rect().bottom() + 1 - size.height()) // 2)
def current_search_type(self):
"""
Readonly property to return the current search type.
"""
return self._current_search_type
def set_current_search_type(self, identifier):
"""
Set a new current search type.
:param identifier: The search type identifier (int).
"""
menu = self.menu_button.menu()
for action in menu.actions():
if identifier == action.data():
self.setPlaceholderText(action.placeholder_text)
self.menu_button.setDefaultAction(action)
self._current_search_type = identifier
Settings().setValue('{section}/last used search type'.format(section=self.settings_section), identifier)
self.searchTypeChanged.emit(identifier)
return True
def set_search_types(self, items):
"""
A list of tuples to be used in the search type menu. The first item in the list will be preselected as the
default.
:param items: The list of tuples to use. The tuples should contain an integer identifier, an icon (QIcon
instance or string) and a title for the item in the menu. In short, they should look like this::
(<identifier>, <icon>, <title>, <place holder text>)
For instance::
(1, <QIcon instance>, "Titles", "Search Song Titles...")
Or::
(2, ":/songs/authors.png", "Authors", "Search Authors...")
"""
menu = QtWidgets.QMenu(self)
for identifier, icon, title, placeholder in items:
action = create_widget_action(
menu, text=title, icon=icon, data=identifier, triggers=self._on_menu_action_triggered)
action.placeholder_text = placeholder
if not hasattr(self, 'menu_button'):
self.menu_button = QtWidgets.QToolButton(self)
self.menu_button.setIcon(build_icon(':/system/clear_shortcut.png'))
self.menu_button.setCursor(QtCore.Qt.ArrowCursor)
self.menu_button.setPopupMode(QtWidgets.QToolButton.InstantPopup)
self.menu_button.setStyleSheet('QToolButton { border: none; padding: 0px 10px 0px 0px; }')
self.menu_button.resize(QtCore.QSize(28, 18))
self.menu_button.setMenu(menu)
self.set_current_search_type(
Settings().value('{section}/last used search type'.format(section=self.settings_section)))
self.menu_button.show()
self._update_style_sheet()
def _on_search_edit_text_changed(self, text):
"""
Internally implemented slot to react to when the text in the line edit has changed so that we can show or hide
the clear button.
:param text: A :class:`~PyQt5.QtCore.QString` instance which represents the text in the line edit.
"""
self.clear_button.setVisible(bool(text))
def _on_clear_button_clicked(self):
"""
Internally implemented slot to react to the clear button being clicked to clear the line edit. Once it has
cleared the line edit, it emits the ``cleared()`` signal so that an application can react to the clearing of the
line edit.
"""
self.clear()
self.cleared.emit()
def _on_menu_action_triggered(self):
"""
Internally implemented slot to react to the select of one of the search types in the menu. Once it has set the
correct action on the button, and set the current search type (using the list of identifiers provided by the
developer), the ``searchTypeChanged(int)`` signal is emitted with the identifier.
"""
for action in self.menu_button.menu().actions():
# Why is this needed?
action.setChecked(False)
self.set_current_search_type(self.sender().data())
class PathEdit(QtWidgets.QWidget):
"""
The :class:`~openlp.core.widgets.edits.PathEdit` class subclasses QWidget to create a custom widget for use when
a file or directory needs to be selected.
"""
pathChanged = QtCore.pyqtSignal(Path)
def __init__(self, parent=None, path_type=PathEditType.Files, default_path=None, dialog_caption=None,
show_revert=True):
"""
Initialise the PathEdit widget
:param QtWidget.QWidget | None: The parent of the widget. This is just passed to the super method.
:param str dialog_caption: Used to customise the caption in the QFileDialog.
:param openlp.core.common.path.Path default_path: The default path. This is set as the path when the revert
button is clicked
:param bool show_revert: Used to determine if the 'revert button' should be visible.
: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 bool show_revert: Show or hide the revert button
:rtype: None
"""
widget_layout = QtWidgets.QHBoxLayout()
widget_layout.setContentsMargins(0, 0, 0, 0)
self.line_edit = QtWidgets.QLineEdit(self)
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()
@QtCore.pyqtProperty('QVariant')
def path(self):
"""
A property getter method to return the selected path.
:return: The selected path
:rtype: openlp.core.common.path.Path
"""
return self._path
@path.setter
def path(self, path):
"""
A Property setter method to set the selected path
:param openlp.core.common.path.Path path: The path to set the widget to
:rtype: None
"""
self._path = path
text = path_to_str(path)
self.line_edit.setText(text)
self.line_edit.setToolTip(text)
@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: PathType
"""
return self._path_type
@path_type.setter
def path_type(self, path_type):
"""
A Property setter method to set the path type
:param PathType path_type: The type of path to select
:rtype: None
"""
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
:rtype: None
"""
if self._path_type == PathEditType.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
:rtype: None
"""
caption = self.dialog_caption
path = None
if self._path_type == PathEditType.Directories:
if not caption:
caption = translate('OpenLP.PathEdit', 'Select Directory')
path = FileDialog.getExistingDirectory(self, caption, self._path, FileDialog.ShowDirsOnly)
elif self._path_type == PathEditType.Files:
if not caption:
caption = self.dialog_caption = translate('OpenLP.PathEdit', 'Select File')
path, filter_used = FileDialog.getOpenFileName(self, caption, self._path, self.filters)
if 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.
:rtype: 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.
:rtype: None
"""
path = str_to_path(self.line_edit.text())
self.on_new_path(path)
def on_new_path(self, path):
"""
A method called to validate and set a new path.
Emits the pathChanged Signal
:param openlp.core.common.path.Path path: The new path
:rtype: None
"""
if self._path != path:
self._path = path
self.pathChanged.emit(path)
class SpellTextEdit(QtWidgets.QPlainTextEdit):
"""
Spell checking widget based on QPlanTextEdit.
Based on code from http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check
"""
def __init__(self, parent=None, formatting_tags_allowed=True):
"""
Constructor.
"""
global ENCHANT_AVAILABLE
super(SpellTextEdit, self).__init__(parent)
self.formatting_tags_allowed = formatting_tags_allowed
# Default dictionary based on the current locale.
if ENCHANT_AVAILABLE:
try:
self.dictionary = enchant.Dict()
self.highlighter = Highlighter(self.document())
self.highlighter.spelling_dictionary = self.dictionary
except (Error, DictNotFoundError):
ENCHANT_AVAILABLE = False
log.debug('Could not load default dictionary')
def mousePressEvent(self, event):
"""
Handle mouse clicks within the text edit region.
"""
if event.button() == QtCore.Qt.RightButton:
# Rewrite the mouse event to a left button event so the cursor is moved to the location of the pointer.
event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress,
event.pos(), QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier)
QtWidgets.QPlainTextEdit.mousePressEvent(self, event)
def contextMenuEvent(self, event):
"""
Provide the context menu for the text edit region.
"""
popup_menu = self.createStandardContextMenu()
# Select the word under the cursor.
cursor = self.textCursor()
# only select text if not already selected
if not cursor.hasSelection():
cursor.select(QtGui.QTextCursor.WordUnderCursor)
self.setTextCursor(cursor)
# Add menu with available languages.
if ENCHANT_AVAILABLE:
lang_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Language:'))
for lang in enchant.list_languages():
action = create_action(lang_menu, lang, text=lang, checked=lang == self.dictionary.tag)
lang_menu.addAction(action)
popup_menu.insertSeparator(popup_menu.actions()[0])
popup_menu.insertMenu(popup_menu.actions()[0], lang_menu)
lang_menu.triggered.connect(self.set_language)
# Check if the selected word is misspelled and offer spelling suggestions if it is.
if ENCHANT_AVAILABLE and self.textCursor().hasSelection():
text = self.textCursor().selectedText()
if not self.dictionary.check(text):
spell_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Spelling Suggestions'))
for word in self.dictionary.suggest(text):
action = SpellAction(word, spell_menu)
action.correct.connect(self.correct_word)
spell_menu.addAction(action)
# Only add the spelling suggests to the menu if there are suggestions.
if spell_menu.actions():
popup_menu.insertMenu(popup_menu.actions()[0], spell_menu)
tag_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Formatting Tags'))
if self.formatting_tags_allowed:
for html in FormattingTags.get_html_tags():
action = SpellAction(html['desc'], tag_menu)
action.correct.connect(self.html_tag)
tag_menu.addAction(action)
popup_menu.insertSeparator(popup_menu.actions()[0])
popup_menu.insertMenu(popup_menu.actions()[0], tag_menu)
popup_menu.exec(event.globalPos())
def set_language(self, action):
"""
Changes the language for this spelltextedit.
:param action: The action.
"""
self.dictionary = enchant.Dict(action.text())
self.highlighter.spelling_dictionary = self.dictionary
self.highlighter.highlightBlock(self.toPlainText())
self.highlighter.rehighlight()
def correct_word(self, word):
"""
Replaces the selected text with word.
"""
cursor = self.textCursor()
cursor.beginEditBlock()
cursor.removeSelectedText()
cursor.insertText(word)
cursor.endEditBlock()
def html_tag(self, tag):
"""
Replaces the selected text with word.
"""
tag = tag.replace('&', '')
for html in FormattingTags.get_html_tags():
if tag == html['desc']:
cursor = self.textCursor()
if self.textCursor().hasSelection():
text = cursor.selectedText()
cursor.beginEditBlock()
cursor.removeSelectedText()
cursor.insertText(html['start tag'])
cursor.insertText(text)
cursor.insertText(html['end tag'])
cursor.endEditBlock()
else:
cursor = self.textCursor()
cursor.insertText(html['start tag'])
cursor.insertText(html['end tag'])
def insertFromMimeData(self, source):
"""
Reimplement `insertFromMimeData` so that we can remove any control characters
:param QtCore.QMimeData source: The mime data to insert
:rtype: None
"""
self.insertPlainText(CONTROL_CHARS.sub('', source.text()))
class Highlighter(QtGui.QSyntaxHighlighter):
"""
Provides a text highlighter for pointing out spelling errors in text.
"""
WORDS = r'(?i)[\w\']+'
def __init__(self, *args):
"""
Constructor
"""
super(Highlighter, self).__init__(*args)
self.spelling_dictionary = None
def highlightBlock(self, text):
"""
Highlight mis spelt words in a block of text.
Note, this is a Qt hook.
"""
if not self.spelling_dictionary:
return
text = str(text)
char_format = QtGui.QTextCharFormat()
char_format.setUnderlineColor(QtCore.Qt.red)
char_format.setUnderlineStyle(QtGui.QTextCharFormat.SpellCheckUnderline)
for word_object in re.finditer(self.WORDS, text):
if not self.spelling_dictionary.check(word_object.group()):
self.setFormat(word_object.start(), word_object.end() - word_object.start(), char_format)
class SpellAction(QtWidgets.QAction):
"""
A special QAction that returns the text in a signal.
"""
correct = QtCore.pyqtSignal(str)
def __init__(self, *args):
"""
Constructor
"""
super(SpellAction, self).__init__(*args)
self.triggered.connect(lambda x: self.correct.emit(self.text()))
class HistoryComboBox(QtWidgets.QComboBox):
"""
The :class:`~openlp.core.common.historycombobox.HistoryComboBox` widget emulates the QLineEdit ``returnPressed``
signal for when the :kbd:`Enter` or :kbd:`Return` keys are pressed, and saves anything that is typed into the edit
box into its list.
"""
returnPressed = QtCore.pyqtSignal()
def __init__(self, parent=None):
"""
Initialise the combo box, setting duplicates to False and the insert policy to insert items at the top.
:param parent: The parent widget
"""
super().__init__(parent)
self.setDuplicatesEnabled(False)
self.setEditable(True)
self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
self.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
def keyPressEvent(self, event):
"""
Override the inherited keyPressEvent method to emit the ``returnPressed`` signal and to save the current text to
the dropdown list.
:param event: The keyboard event
"""
# Handle Enter and Return ourselves
if event.key() == QtCore.Qt.Key_Enter or event.key() == QtCore.Qt.Key_Return:
# Emit the returnPressed signal
self.returnPressed.emit()
# Save the current text to the dropdown list
if self.currentText() and self.findText(self.currentText()) == -1:
self.insertItem(0, self.currentText())
# Let the parent handle any keypress events
super().keyPressEvent(event)
def focusOutEvent(self, event):
"""
Override the inherited focusOutEvent to save the current text to the dropdown list.
:param event: The focus event
"""
# Save the current text to the dropdown list
if self.currentText() and self.findText(self.currentText()) == -1:
self.insertItem(0, self.currentText())
# Let the parent handle any keypress events
super().focusOutEvent(event)
def getItems(self):
"""
Get all the items from the history
:return: A list of strings
"""
return [self.itemText(i) for i in range(self.count())]

View File

@ -19,18 +19,12 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
"""
The :mod:`~openlp.core.widgets.enums` module contains enumerations used by the widgets
"""
from enum import Enum
from .colorbutton import ColorButton
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', 'MediaDockManager', 'OpenLPDockWidget', class PathEditType(Enum):
'OpenLPToolbar', 'OpenLPWizard', 'PathEdit', 'PathType', 'SpellTextEdit', 'TreeWidgetWithDnD', Files = 1
'WizardStrings'] Directories = 2

View File

@ -40,7 +40,7 @@ class OpenLPToolbar(QtWidgets.QToolBar):
""" """
Initialise the toolbar. Initialise the toolbar.
""" """
super(OpenLPToolbar, self).__init__(parent) super().__init__(parent)
# useful to be able to reuse button icons... # useful to be able to reuse button icons...
self.setIconSize(QtCore.QSize(20, 20)) self.setIconSize(QtCore.QSize(20, 20))
self.actions = {} self.actions = {}

View File

@ -23,10 +23,14 @@
The :mod:`listpreviewwidget` is a widget that lists the slides in the slide controller. The :mod:`listpreviewwidget` is a widget that lists the slides in the slide controller.
It is based on a QTableWidget but represents its contents in list form. It is based on a QTableWidget but represents its contents in list form.
""" """
import os
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common.registry import RegistryProperties from openlp.core.common import is_win
from openlp.core.common.i18n import UiStrings
from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.lib import ImageSource, ItemCapabilities, ServiceItem from openlp.core.lib import ImageSource, ItemCapabilities, ServiceItem
@ -238,3 +242,241 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties):
Returns the number of slides this widget holds. Returns the number of slides this widget holds.
""" """
return super(ListPreviewWidget, self).rowCount() return super(ListPreviewWidget, self).rowCount()
class ListWidgetWithDnD(QtWidgets.QListWidget):
"""
Provide a list widget to store objects and handle drag and drop events
"""
def __init__(self, parent=None, name=''):
"""
Initialise the list widget
"""
super().__init__(parent)
self.mime_data_text = name
self.no_results_text = UiStrings().NoResults
self.setSpacing(1)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.setAlternatingRowColors(True)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
def activateDnD(self):
"""
Activate DnD of widget
"""
self.setAcceptDrops(True)
self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file)
def clear(self, search_while_typing=False):
"""
Re-implement clear, so that we can customise feedback when using 'Search as you type'
:param search_while_typing: True if we want to display the customised message
:return: None
"""
if search_while_typing:
self.no_results_text = UiStrings().ShortResults
else:
self.no_results_text = UiStrings().NoResults
super().clear()
def mouseMoveEvent(self, event):
"""
Drag and drop event does not care what data is selected as the recipient will use events to request the data
move just tell it what plugin to call
"""
if event.buttons() != QtCore.Qt.LeftButton:
event.ignore()
return
if not self.selectedItems():
event.ignore()
return
drag = QtGui.QDrag(self)
mime_data = QtCore.QMimeData()
drag.setMimeData(mime_data)
mime_data.setText(self.mime_data_text)
drag.exec(QtCore.Qt.CopyAction)
def dragEnterEvent(self, event):
"""
When something is dragged into this object, check if you should be able to drop it in here.
"""
if event.mimeData().hasUrls():
event.accept()
else:
event.ignore()
def dragMoveEvent(self, event):
"""
Make an object droppable, and set it to copy the contents of the object, not move it.
"""
if event.mimeData().hasUrls():
event.setDropAction(QtCore.Qt.CopyAction)
event.accept()
else:
event.ignore()
def dropEvent(self, event):
"""
Receive drop event check if it is a file and process it if it is.
:param event: Handle of the event pint passed
"""
if event.mimeData().hasUrls():
event.setDropAction(QtCore.Qt.CopyAction)
event.accept()
files = []
for url in event.mimeData().urls():
local_file = os.path.normpath(url.toLocalFile())
if os.path.isfile(local_file):
files.append(local_file)
elif os.path.isdir(local_file):
listing = os.listdir(local_file)
for file in listing:
files.append(os.path.join(local_file, file))
Registry().execute('{mime_data}_dnd'.format(mime_data=self.mime_data_text),
{'files': files})
else:
event.ignore()
def allItems(self):
"""
An generator to list all the items in the widget
:return: a generator
"""
for row in range(self.count()):
yield self.item(row)
def paintEvent(self, event):
"""
Re-implement paintEvent so that we can add 'No Results' text when the listWidget is empty.
:param event: A QPaintEvent
:return: None
"""
super().paintEvent(event)
if not self.count():
viewport = self.viewport()
painter = QtGui.QPainter(viewport)
font = QtGui.QFont()
font.setItalic(True)
painter.setFont(font)
painter.drawText(QtCore.QRect(0, 0, viewport.width(), viewport.height()),
(QtCore.Qt.AlignHCenter | QtCore.Qt.TextWordWrap), self.no_results_text)
class TreeWidgetWithDnD(QtWidgets.QTreeWidget):
"""
Provide a tree widget to store objects and handle drag and drop events
"""
def __init__(self, parent=None, name=''):
"""
Initialise the tree widget
"""
super(TreeWidgetWithDnD, self).__init__(parent)
self.mime_data_text = name
self.allow_internal_dnd = False
self.header().close()
self.default_indentation = self.indentation()
self.setIndentation(0)
self.setAnimated(True)
def activateDnD(self):
"""
Activate DnD of widget
"""
self.setAcceptDrops(True)
self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file)
Registry().register_function(('%s_dnd_internal' % self.mime_data_text), self.parent().dnd_move_internal)
def mouseMoveEvent(self, event):
"""
Drag and drop event does not care what data is selected as the recipient will use events to request the data
move just tell it what plugin to call
:param event: The event that occurred
"""
if event.buttons() != QtCore.Qt.LeftButton:
event.ignore()
return
if not self.selectedItems():
event.ignore()
return
drag = QtGui.QDrag(self)
mime_data = QtCore.QMimeData()
drag.setMimeData(mime_data)
mime_data.setText(self.mime_data_text)
drag.exec(QtCore.Qt.CopyAction)
def dragEnterEvent(self, event):
"""
Receive drag enter event, check if it is a file or internal object and allow it if it is.
:param event: The event that occurred
"""
if event.mimeData().hasUrls():
event.accept()
elif self.allow_internal_dnd:
event.accept()
else:
event.ignore()
def dragMoveEvent(self, event):
"""
Receive drag move event, check if it is a file or internal object and allow it if it is.
:param event: The event that occurred
"""
QtWidgets.QTreeWidget.dragMoveEvent(self, event)
if event.mimeData().hasUrls():
event.setDropAction(QtCore.Qt.CopyAction)
event.accept()
elif self.allow_internal_dnd:
event.setDropAction(QtCore.Qt.CopyAction)
event.accept()
else:
event.ignore()
def dropEvent(self, event):
"""
Receive drop event, check if it is a file or internal object and process it if it is.
:param event: Handle of the event pint passed
"""
# If we are on Windows, OpenLP window will not be set on top. For example, user can drag images to Library and
# the folder stays on top of the group creation box. This piece of code fixes this issue.
if is_win():
self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
self.setWindowState(QtCore.Qt.WindowNoState)
if event.mimeData().hasUrls():
event.setDropAction(QtCore.Qt.CopyAction)
event.accept()
files = []
for url in event.mimeData().urls():
local_file = url.toLocalFile()
if os.path.isfile(local_file):
files.append(local_file)
elif os.path.isdir(local_file):
listing = os.listdir(local_file)
for file_name in listing:
files.append(os.path.join(local_file, file_name))
Registry().execute('%s_dnd' % self.mime_data_text, {'files': files, 'target': self.itemAt(event.pos())})
elif self.allow_internal_dnd:
event.setDropAction(QtCore.Qt.CopyAction)
event.accept()
Registry().execute('%s_dnd_internal' % self.mime_data_text, self.itemAt(event.pos()))
else:
event.ignore()
# Convenience methods for emulating a QListWidget. This helps keeping MediaManagerItem simple.
def addItem(self, item):
self.addTopLevelItem(item)
def count(self):
return self.topLevelItemCount()
def item(self, index):
return self.topLevelItem(index)

View File

@ -28,11 +28,12 @@ from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import is_macosx from openlp.core.common import is_macosx
from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
from openlp.core.lib.ui import add_welcome_page from openlp.core.lib.ui import add_welcome_page
from openlp.core.ui.lib.filedialog import FileDialog from openlp.core.widgets.dialogs import FileDialog
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@ -26,12 +26,12 @@ displaying of alerts.
from PyQt5 import QtCore from PyQt5 import QtCore
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.common.mixins import OpenLPMixin, RegistryMixin from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
class AlertsManager(OpenLPMixin, RegistryMixin, QtCore.QObject, RegistryProperties): class AlertsManager(QtCore.QObject, RegistryBase, LogMixin, RegistryProperties):
""" """
AlertsManager manages the settings of Alerts. AlertsManager manages the settings of Alerts.
""" """

View File

@ -26,7 +26,7 @@ from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.lib import SettingsTab from openlp.core.lib import SettingsTab
from openlp.core.lib.ui import create_valign_selection_widgets from openlp.core.lib.ui import create_valign_selection_widgets
from openlp.core.ui.lib.colorbutton import ColorButton from openlp.core.widgets.buttons import ColorButton
class AlertsTab(SettingsTab): class AlertsTab(SettingsTab):

View File

@ -23,7 +23,6 @@
The bible import functions for OpenLP The bible import functions for OpenLP
""" """
import logging import logging
import os
import urllib.error import urllib.error
from lxml import etree from lxml import etree
@ -41,7 +40,8 @@ from openlp.core.common.settings import Settings
from openlp.core.lib.db import delete_database from openlp.core.lib.db import delete_database
from openlp.core.lib.exceptions import ValidationError from openlp.core.lib.exceptions import ValidationError
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings from openlp.core.widgets.edits import PathEdit
from openlp.core.widgets.wizard import OpenLPWizard, WizardStrings
from openlp.plugins.bibles.lib.db import clean_filename from openlp.plugins.bibles.lib.db import clean_filename
from openlp.plugins.bibles.lib.importers.http import CWExtract, BGExtract, BSExtract from openlp.plugins.bibles.lib.importers.http import CWExtract, BGExtract, BSExtract
from openlp.plugins.bibles.lib.manager import BibleFormat from openlp.plugins.bibles.lib.manager import BibleFormat
@ -122,15 +122,9 @@ class BibleImportForm(OpenLPWizard):
Set up the signals used in the bible importer. Set up the signals used in the bible importer.
""" """
self.web_source_combo_box.currentIndexChanged.connect(self.on_web_source_combo_box_index_changed) self.web_source_combo_box.currentIndexChanged.connect(self.on_web_source_combo_box_index_changed)
self.osis_browse_button.clicked.connect(self.on_osis_browse_button_clicked)
self.csv_books_button.clicked.connect(self.on_csv_books_browse_button_clicked)
self.csv_verses_button.clicked.connect(self.on_csv_verses_browse_button_clicked)
self.open_song_browse_button.clicked.connect(self.on_open_song_browse_button_clicked)
self.zefania_browse_button.clicked.connect(self.on_zefania_browse_button_clicked)
self.wordproject_browse_button.clicked.connect(self.on_wordproject_browse_button_clicked)
self.web_update_button.clicked.connect(self.on_web_update_button_clicked) self.web_update_button.clicked.connect(self.on_web_update_button_clicked)
self.sword_browse_button.clicked.connect(self.on_sword_browse_button_clicked) self.sword_folder_path_edit.pathChanged.connect(self.on_sword_folder_path_edit_path_changed)
self.sword_zipbrowse_button.clicked.connect(self.on_sword_zipbrowse_button_clicked) self.sword_zipfile_path_edit.pathChanged.connect(self.on_sword_zipfile_path_edit_path_changed)
def add_custom_pages(self): def add_custom_pages(self):
""" """
@ -161,17 +155,12 @@ class BibleImportForm(OpenLPWizard):
self.osis_layout.setObjectName('OsisLayout') self.osis_layout.setObjectName('OsisLayout')
self.osis_file_label = QtWidgets.QLabel(self.osis_widget) self.osis_file_label = QtWidgets.QLabel(self.osis_widget)
self.osis_file_label.setObjectName('OsisFileLabel') self.osis_file_label.setObjectName('OsisFileLabel')
self.osis_file_layout = QtWidgets.QHBoxLayout() self.osis_path_edit = PathEdit(
self.osis_file_layout.setObjectName('OsisFileLayout') self.osis_widget,
self.osis_file_edit = QtWidgets.QLineEdit(self.osis_widget) default_path=Settings().value('bibles/last directory import'),
self.osis_file_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.OSIS),
self.osis_file_edit.setObjectName('OsisFileEdit') show_revert=False)
self.osis_file_layout.addWidget(self.osis_file_edit) self.osis_layout.addRow(self.osis_file_label, self.osis_path_edit)
self.osis_browse_button = QtWidgets.QToolButton(self.osis_widget)
self.osis_browse_button.setIcon(self.open_icon)
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.osis_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacer) self.osis_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacer)
self.select_stack.addWidget(self.osis_widget) self.select_stack.addWidget(self.osis_widget)
self.csv_widget = QtWidgets.QWidget(self.select_page) self.csv_widget = QtWidgets.QWidget(self.select_page)
@ -181,30 +170,27 @@ class BibleImportForm(OpenLPWizard):
self.csv_layout.setObjectName('CsvLayout') self.csv_layout.setObjectName('CsvLayout')
self.csv_books_label = QtWidgets.QLabel(self.csv_widget) self.csv_books_label = QtWidgets.QLabel(self.csv_widget)
self.csv_books_label.setObjectName('CsvBooksLabel') self.csv_books_label.setObjectName('CsvBooksLabel')
self.csv_books_layout = QtWidgets.QHBoxLayout() self.csv_books_path_edit = PathEdit(
self.csv_books_layout.setObjectName('CsvBooksLayout') self.csv_widget,
self.csv_books_edit = QtWidgets.QLineEdit(self.csv_widget) default_path=Settings().value('bibles/last directory import'),
self.csv_books_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.CSV),
self.csv_books_edit.setObjectName('CsvBooksEdit') show_revert=False,
self.csv_books_layout.addWidget(self.csv_books_edit) )
self.csv_books_button = QtWidgets.QToolButton(self.csv_widget) self.csv_books_path_edit.filters = \
self.csv_books_button.setIcon(self.open_icon) '{name} (*.csv)'.format(name=translate('BiblesPlugin.ImportWizardForm', 'CSV File'))
self.csv_books_button.setObjectName('CsvBooksButton') self.csv_layout.addRow(self.csv_books_label, self.csv_books_path_edit)
self.csv_books_layout.addWidget(self.csv_books_button)
self.csv_layout.addRow(self.csv_books_label, self.csv_books_layout)
self.csv_verses_label = QtWidgets.QLabel(self.csv_widget) self.csv_verses_label = QtWidgets.QLabel(self.csv_widget)
self.csv_verses_label.setObjectName('CsvVersesLabel') self.csv_verses_label.setObjectName('CsvVersesLabel')
self.csv_verses_layout = QtWidgets.QHBoxLayout() self.csv_verses_path_edit = PathEdit(
self.csv_verses_layout.setObjectName('CsvVersesLayout') self.csv_widget,
self.csv_verses_edit = QtWidgets.QLineEdit(self.csv_widget) default_path=Settings().value('bibles/last directory import'),
self.csv_verses_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.CSV),
self.csv_verses_edit.setObjectName('CsvVersesEdit') show_revert=False,
self.csv_verses_layout.addWidget(self.csv_verses_edit) )
self.csv_verses_button = QtWidgets.QToolButton(self.csv_widget) self.csv_verses_path_edit.filters = \
self.csv_verses_button.setIcon(self.open_icon) '{name} (*.csv)'.format(name=translate('BiblesPlugin.ImportWizardForm', 'CSV File'))
self.csv_verses_button.setObjectName('CsvVersesButton') self.csv_layout.addRow(self.csv_books_label, self.csv_books_path_edit)
self.csv_verses_layout.addWidget(self.csv_verses_button) self.csv_layout.addRow(self.csv_verses_label, self.csv_verses_path_edit)
self.csv_layout.addRow(self.csv_verses_label, self.csv_verses_layout)
self.csv_layout.setItem(3, QtWidgets.QFormLayout.LabelRole, self.spacer) self.csv_layout.setItem(3, QtWidgets.QFormLayout.LabelRole, self.spacer)
self.select_stack.addWidget(self.csv_widget) self.select_stack.addWidget(self.csv_widget)
self.open_song_widget = QtWidgets.QWidget(self.select_page) self.open_song_widget = QtWidgets.QWidget(self.select_page)
@ -214,17 +200,13 @@ class BibleImportForm(OpenLPWizard):
self.open_song_layout.setObjectName('OpenSongLayout') self.open_song_layout.setObjectName('OpenSongLayout')
self.open_song_file_label = QtWidgets.QLabel(self.open_song_widget) self.open_song_file_label = QtWidgets.QLabel(self.open_song_widget)
self.open_song_file_label.setObjectName('OpenSongFileLabel') self.open_song_file_label.setObjectName('OpenSongFileLabel')
self.open_song_file_layout = QtWidgets.QHBoxLayout() self.open_song_path_edit = PathEdit(
self.open_song_file_layout.setObjectName('OpenSongFileLayout') self.open_song_widget,
self.open_song_file_edit = QtWidgets.QLineEdit(self.open_song_widget) default_path=Settings().value('bibles/last directory import'),
self.open_song_file_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.OS),
self.open_song_file_edit.setObjectName('OpenSongFileEdit') show_revert=False,
self.open_song_file_layout.addWidget(self.open_song_file_edit) )
self.open_song_browse_button = QtWidgets.QToolButton(self.open_song_widget) self.open_song_layout.addRow(self.open_song_file_label, self.open_song_path_edit)
self.open_song_browse_button.setIcon(self.open_icon)
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.open_song_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacer) self.open_song_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacer)
self.select_stack.addWidget(self.open_song_widget) self.select_stack.addWidget(self.open_song_widget)
self.web_tab_widget = QtWidgets.QTabWidget(self.select_page) self.web_tab_widget = QtWidgets.QTabWidget(self.select_page)
@ -292,17 +274,14 @@ class BibleImportForm(OpenLPWizard):
self.zefania_layout.setObjectName('ZefaniaLayout') self.zefania_layout.setObjectName('ZefaniaLayout')
self.zefania_file_label = QtWidgets.QLabel(self.zefania_widget) self.zefania_file_label = QtWidgets.QLabel(self.zefania_widget)
self.zefania_file_label.setObjectName('ZefaniaFileLabel') self.zefania_file_label.setObjectName('ZefaniaFileLabel')
self.zefania_file_layout = QtWidgets.QHBoxLayout() self.zefania_path_edit = PathEdit(
self.zefania_file_layout.setObjectName('ZefaniaFileLayout') self.zefania_widget,
self.zefania_file_edit = QtWidgets.QLineEdit(self.zefania_widget) default_path=Settings().value('bibles/last directory import'),
self.zefania_file_edit.setObjectName('ZefaniaFileEdit') dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.ZEF),
self.zefania_file_layout.addWidget(self.zefania_file_edit) show_revert=False,
self.zefania_browse_button = QtWidgets.QToolButton(self.zefania_widget) )
self.zefania_browse_button.setIcon(self.open_icon) self.zefania_layout.addRow(self.zefania_file_label, self.zefania_path_edit)
self.zefania_browse_button.setObjectName('ZefaniaBrowseButton') self.zefania_layout.setItem(2, QtWidgets.QFormLayout.LabelRole, self.spacer)
self.zefania_file_layout.addWidget(self.zefania_browse_button)
self.zefania_layout.addRow(self.zefania_file_label, self.zefania_file_layout)
self.zefania_layout.setItem(5, QtWidgets.QFormLayout.LabelRole, self.spacer)
self.select_stack.addWidget(self.zefania_widget) self.select_stack.addWidget(self.zefania_widget)
self.sword_widget = QtWidgets.QWidget(self.select_page) self.sword_widget = QtWidgets.QWidget(self.select_page)
self.sword_widget.setObjectName('SwordWidget') self.sword_widget.setObjectName('SwordWidget')
@ -318,16 +297,13 @@ class BibleImportForm(OpenLPWizard):
self.sword_folder_label = QtWidgets.QLabel(self.sword_folder_tab) self.sword_folder_label = QtWidgets.QLabel(self.sword_folder_tab)
self.sword_folder_label.setObjectName('SwordSourceLabel') self.sword_folder_label.setObjectName('SwordSourceLabel')
self.sword_folder_label.setObjectName('SwordFolderLabel') self.sword_folder_label.setObjectName('SwordFolderLabel')
self.sword_folder_edit = QtWidgets.QLineEdit(self.sword_folder_tab) self.sword_folder_path_edit = PathEdit(
self.sword_folder_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) self.sword_folder_tab,
self.sword_folder_edit.setObjectName('SwordFolderEdit') default_path=Settings().value('bibles/last directory import'),
self.sword_browse_button = QtWidgets.QToolButton(self.sword_folder_tab) dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.SWORD),
self.sword_browse_button.setIcon(self.open_icon) show_revert=False,
self.sword_browse_button.setObjectName('SwordBrowseButton') )
self.sword_folder_layout = QtWidgets.QHBoxLayout() self.sword_folder_tab_layout.addRow(self.sword_folder_label, self.sword_folder_path_edit)
self.sword_folder_layout.addWidget(self.sword_folder_edit)
self.sword_folder_layout.addWidget(self.sword_browse_button)
self.sword_folder_tab_layout.addRow(self.sword_folder_label, self.sword_folder_layout)
self.sword_bible_label = QtWidgets.QLabel(self.sword_folder_tab) self.sword_bible_label = QtWidgets.QLabel(self.sword_folder_tab)
self.sword_bible_label.setObjectName('SwordBibleLabel') self.sword_bible_label.setObjectName('SwordBibleLabel')
self.sword_bible_combo_box = QtWidgets.QComboBox(self.sword_folder_tab) self.sword_bible_combo_box = QtWidgets.QComboBox(self.sword_folder_tab)
@ -342,16 +318,13 @@ class BibleImportForm(OpenLPWizard):
self.sword_zip_layout.setObjectName('SwordZipLayout') self.sword_zip_layout.setObjectName('SwordZipLayout')
self.sword_zipfile_label = QtWidgets.QLabel(self.sword_zip_tab) self.sword_zipfile_label = QtWidgets.QLabel(self.sword_zip_tab)
self.sword_zipfile_label.setObjectName('SwordZipFileLabel') self.sword_zipfile_label.setObjectName('SwordZipFileLabel')
self.sword_zipfile_edit = QtWidgets.QLineEdit(self.sword_zip_tab) self.sword_zipfile_path_edit = PathEdit(
self.sword_zipfile_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) self.sword_zip_tab,
self.sword_zipfile_edit.setObjectName('SwordZipFileEdit') default_path=Settings().value('bibles/last directory import'),
self.sword_zipbrowse_button = QtWidgets.QToolButton(self.sword_zip_tab) dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.SWORD),
self.sword_zipbrowse_button.setIcon(self.open_icon) show_revert=False,
self.sword_zipbrowse_button.setObjectName('SwordZipBrowseButton') )
self.sword_zipfile_layout = QtWidgets.QHBoxLayout() self.sword_zip_layout.addRow(self.sword_zipfile_label, self.sword_zipfile_path_edit)
self.sword_zipfile_layout.addWidget(self.sword_zipfile_edit)
self.sword_zipfile_layout.addWidget(self.sword_zipbrowse_button)
self.sword_zip_layout.addRow(self.sword_zipfile_label, self.sword_zipfile_layout)
self.sword_zipbible_label = QtWidgets.QLabel(self.sword_folder_tab) self.sword_zipbible_label = QtWidgets.QLabel(self.sword_folder_tab)
self.sword_zipbible_label.setObjectName('SwordZipBibleLabel') self.sword_zipbible_label.setObjectName('SwordZipBibleLabel')
self.sword_zipbible_combo_box = QtWidgets.QComboBox(self.sword_zip_tab) self.sword_zipbible_combo_box = QtWidgets.QComboBox(self.sword_zip_tab)
@ -372,18 +345,13 @@ class BibleImportForm(OpenLPWizard):
self.wordproject_layout.setObjectName('WordProjectLayout') self.wordproject_layout.setObjectName('WordProjectLayout')
self.wordproject_file_label = QtWidgets.QLabel(self.wordproject_widget) self.wordproject_file_label = QtWidgets.QLabel(self.wordproject_widget)
self.wordproject_file_label.setObjectName('WordProjectFileLabel') self.wordproject_file_label.setObjectName('WordProjectFileLabel')
self.wordproject_file_layout = QtWidgets.QHBoxLayout() self.wordproject_path_edit = PathEdit(
self.wordproject_file_layout.setObjectName('WordProjectFileLayout') self.wordproject_widget,
self.wordproject_file_edit = QtWidgets.QLineEdit(self.wordproject_widget) default_path=Settings().value('bibles/last directory import'),
self.wordproject_file_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.WordProject),
self.wordproject_file_edit.setObjectName('WordProjectFileEdit') show_revert=False)
self.wordproject_file_layout.addWidget(self.wordproject_file_edit) self.wordproject_layout.addRow(self.wordproject_file_label, self.wordproject_path_edit)
self.wordproject_browse_button = QtWidgets.QToolButton(self.wordproject_widget) self.wordproject_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacer)
self.wordproject_browse_button.setIcon(self.open_icon)
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.wordproject_layout.setItem(5, QtWidgets.QFormLayout.LabelRole, self.spacer)
self.select_stack.addWidget(self.wordproject_widget) self.select_stack.addWidget(self.wordproject_widget)
self.select_page_layout.addLayout(self.select_stack) self.select_page_layout.addLayout(self.select_stack)
self.addPage(self.select_page) self.addPage(self.select_page)
@ -468,8 +436,6 @@ class BibleImportForm(OpenLPWizard):
self.sword_bible_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bibles:')) self.sword_bible_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bibles:'))
self.sword_folder_label.setText(translate('BiblesPlugin.ImportWizardForm', 'SWORD data folder:')) self.sword_folder_label.setText(translate('BiblesPlugin.ImportWizardForm', 'SWORD data folder:'))
self.sword_zipfile_label.setText(translate('BiblesPlugin.ImportWizardForm', 'SWORD zip-file:')) self.sword_zipfile_label.setText(translate('BiblesPlugin.ImportWizardForm', 'SWORD zip-file:'))
self.sword_folder_edit.setPlaceholderText(translate('BiblesPlugin.ImportWizardForm',
'Defaults to the standard SWORD data folder'))
self.sword_zipbible_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bibles:')) self.sword_zipbible_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bibles:'))
self.sword_tab_widget.setTabText(self.sword_tab_widget.indexOf(self.sword_folder_tab), self.sword_tab_widget.setTabText(self.sword_tab_widget.indexOf(self.sword_folder_tab),
translate('BiblesPlugin.ImportWizardForm', 'Import from folder')) translate('BiblesPlugin.ImportWizardForm', 'Import from folder'))
@ -518,7 +484,7 @@ class BibleImportForm(OpenLPWizard):
if self.field('source_format') == BibleFormat.OSIS: if self.field('source_format') == BibleFormat.OSIS:
if not self.field('osis_location'): if not self.field('osis_location'):
critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.OSIS) critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.OSIS)
self.osis_file_edit.setFocus() self.osis_path_edit.setFocus()
return False return False
elif self.field('source_format') == BibleFormat.CSV: elif self.field('source_format') == BibleFormat.CSV:
if not self.field('csv_booksfile'): if not self.field('csv_booksfile'):
@ -538,18 +504,18 @@ class BibleImportForm(OpenLPWizard):
elif self.field('source_format') == BibleFormat.OpenSong: elif self.field('source_format') == BibleFormat.OpenSong:
if not self.field('opensong_file'): if not self.field('opensong_file'):
critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.OS) critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.OS)
self.open_song_file_edit.setFocus() self.open_song_path_edit.setFocus()
return False return False
elif self.field('source_format') == BibleFormat.Zefania: elif self.field('source_format') == BibleFormat.Zefania:
if not self.field('zefania_file'): if not self.field('zefania_file'):
critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.ZEF) critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.ZEF)
self.zefania_file_edit.setFocus() self.zefania_path_edit.setFocus()
return False return False
elif self.field('source_format') == BibleFormat.WordProject: elif self.field('source_format') == BibleFormat.WordProject:
if not self.field('wordproject_file'): if not self.field('wordproject_file'):
critical_error_message_box(UiStrings().NFSs, critical_error_message_box(UiStrings().NFSs,
WizardStrings.YouSpecifyFile % WizardStrings.WordProject) WizardStrings.YouSpecifyFile % WizardStrings.WordProject)
self.wordproject_file_edit.setFocus() self.wordproject_path_edit.setFocus()
return False return False
elif self.field('source_format') == BibleFormat.WebDownload: elif self.field('source_format') == BibleFormat.WebDownload:
# If count is 0 the bible list has not yet been downloaded # If count is 0 the bible list has not yet been downloaded
@ -563,7 +529,7 @@ class BibleImportForm(OpenLPWizard):
if not self.field('sword_folder_path') and self.sword_bible_combo_box.count() == 0: if not self.field('sword_folder_path') and self.sword_bible_combo_box.count() == 0:
critical_error_message_box(UiStrings().NFSs, critical_error_message_box(UiStrings().NFSs,
WizardStrings.YouSpecifyFolder % WizardStrings.SWORD) WizardStrings.YouSpecifyFolder % WizardStrings.SWORD)
self.sword_folder_edit.setFocus() self.sword_folder_path_edit.setFocus()
return False return False
key = self.sword_bible_combo_box.itemData(self.sword_bible_combo_box.currentIndex()) key = self.sword_bible_combo_box.itemData(self.sword_bible_combo_box.currentIndex())
if 'description' in self.pysword_folder_modules_json[key]: if 'description' in self.pysword_folder_modules_json[key]:
@ -575,7 +541,7 @@ class BibleImportForm(OpenLPWizard):
elif self.sword_tab_widget.currentIndex() == self.sword_tab_widget.indexOf(self.sword_zip_tab): elif self.sword_tab_widget.currentIndex() == self.sword_tab_widget.indexOf(self.sword_zip_tab):
if not self.field('sword_zip_path'): if not self.field('sword_zip_path'):
critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.SWORD) critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.SWORD)
self.sword_zipfile_edit.setFocus() self.sword_zipfile_path_edit.setFocus()
return False return False
key = self.sword_zipbible_combo_box.itemData(self.sword_zipbible_combo_box.currentIndex()) key = self.sword_zipbible_combo_box.itemData(self.sword_zipbible_combo_box.currentIndex())
if 'description' in self.pysword_zip_modules_json[key]: if 'description' in self.pysword_zip_modules_json[key]:
@ -586,7 +552,6 @@ class BibleImportForm(OpenLPWizard):
elif self.currentPage() == self.license_details_page: elif self.currentPage() == self.license_details_page:
license_version = self.field('license_version') license_version = self.field('license_version')
license_copyright = self.field('license_copyright') license_copyright = self.field('license_copyright')
path = str(AppLocation.get_section_data_path('bibles'))
if not license_version: if not license_version:
critical_error_message_box( critical_error_message_box(
UiStrings().EmptyField, UiStrings().EmptyField,
@ -608,7 +573,7 @@ class BibleImportForm(OpenLPWizard):
'existing one.')) 'existing one.'))
self.version_name_edit.setFocus() self.version_name_edit.setFocus()
return False return False
elif os.path.exists(os.path.join(path, clean_filename(license_version))): elif (AppLocation.get_section_data_path('bibles') / clean_filename(license_version)).exists():
critical_error_message_box( critical_error_message_box(
translate('BiblesPlugin.ImportWizardForm', 'Bible Exists'), translate('BiblesPlugin.ImportWizardForm', 'Bible Exists'),
translate('BiblesPlugin.ImportWizardForm', 'This Bible already exists. Please import ' translate('BiblesPlugin.ImportWizardForm', 'This Bible already exists. Please import '
@ -631,57 +596,6 @@ class BibleImportForm(OpenLPWizard):
bibles.sort(key=get_locale_key) bibles.sort(key=get_locale_key)
self.web_translation_combo_box.addItems(bibles) self.web_translation_combo_box.addItems(bibles)
def on_osis_browse_button_clicked(self):
"""
Show the file open dialog for the OSIS file.
"""
self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.OSIS, self.osis_file_edit,
'last directory import')
def on_csv_books_browse_button_clicked(self):
"""
Show the file open dialog for the books CSV file.
"""
# TODO: Verify format() with varible template
self.get_file_name(
WizardStrings.OpenTypeFile % WizardStrings.CSV,
self.csv_books_edit,
'last directory import',
'{name} (*.csv)'.format(name=translate('BiblesPlugin.ImportWizardForm', 'CSV File')))
def on_csv_verses_browse_button_clicked(self):
"""
Show the file open dialog for the verses CSV file.
"""
# TODO: Verify format() with variable template
self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.CSV, self.csv_verses_edit,
'last directory import',
'{name} (*.csv)'.format(name=translate('BiblesPlugin.ImportWizardForm', 'CSV File')))
def on_open_song_browse_button_clicked(self):
"""
Show the file open dialog for the OpenSong file.
"""
# TODO: Verify format() with variable template
self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.OS, self.open_song_file_edit,
'last directory import')
def on_zefania_browse_button_clicked(self):
"""
Show the file open dialog for the Zefania file.
"""
# TODO: Verify format() with variable template
self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.ZEF, self.zefania_file_edit,
'last directory import')
def on_wordproject_browse_button_clicked(self):
"""
Show the file open dialog for the WordProject file.
"""
# TODO: Verify format() with variable template
self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.WordProject, self.wordproject_file_edit,
'last directory import')
def on_web_update_button_clicked(self): def on_web_update_button_clicked(self):
""" """
Download list of bibles from Crosswalk, BibleServer and BibleGateway. Download list of bibles from Crosswalk, BibleServer and BibleGateway.
@ -718,15 +632,13 @@ class BibleImportForm(OpenLPWizard):
self.web_update_button.setEnabled(True) self.web_update_button.setEnabled(True)
self.web_progress_bar.setVisible(False) self.web_progress_bar.setVisible(False)
def on_sword_browse_button_clicked(self): def on_sword_folder_path_edit_path_changed(self, new_path):
""" """
Show the file open dialog for the SWORD folder. Show the file open dialog for the SWORD folder.
""" """
self.get_folder(WizardStrings.OpenTypeFolder % WizardStrings.SWORD, self.sword_folder_edit, if new_path:
'last directory import')
if self.sword_folder_edit.text():
try: try:
self.pysword_folder_modules = modules.SwordModules(self.sword_folder_edit.text()) self.pysword_folder_modules = modules.SwordModules(str(new_path))
self.pysword_folder_modules_json = self.pysword_folder_modules.parse_modules() self.pysword_folder_modules_json = self.pysword_folder_modules.parse_modules()
bible_keys = self.pysword_folder_modules_json.keys() bible_keys = self.pysword_folder_modules_json.keys()
self.sword_bible_combo_box.clear() self.sword_bible_combo_box.clear()
@ -735,15 +647,13 @@ class BibleImportForm(OpenLPWizard):
except: except:
self.sword_bible_combo_box.clear() self.sword_bible_combo_box.clear()
def on_sword_zipbrowse_button_clicked(self): def on_sword_zipfile_path_edit_path_changed(self, new_path):
""" """
Show the file open dialog for a SWORD zip-file. Show the file open dialog for a SWORD zip-file.
""" """
self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.SWORD, self.sword_zipfile_edit, if new_path:
'last directory import')
if self.sword_zipfile_edit.text():
try: try:
self.pysword_zip_modules = modules.SwordModules(self.sword_zipfile_edit.text()) self.pysword_zip_modules = modules.SwordModules(str(new_path))
self.pysword_zip_modules_json = self.pysword_zip_modules.parse_modules() self.pysword_zip_modules_json = self.pysword_zip_modules.parse_modules()
bible_keys = self.pysword_zip_modules_json.keys() bible_keys = self.pysword_zip_modules_json.keys()
self.sword_zipbible_combo_box.clear() self.sword_zipbible_combo_box.clear()
@ -757,16 +667,16 @@ class BibleImportForm(OpenLPWizard):
Register the bible import wizard fields. Register the bible import wizard fields.
""" """
self.select_page.registerField('source_format', self.format_combo_box) self.select_page.registerField('source_format', self.format_combo_box)
self.select_page.registerField('osis_location', self.osis_file_edit) self.select_page.registerField('osis_location', self.osis_path_edit, 'path', PathEdit.pathChanged)
self.select_page.registerField('csv_booksfile', self.csv_books_edit) self.select_page.registerField('csv_booksfile', self.csv_books_path_edit, 'path', PathEdit.pathChanged)
self.select_page.registerField('csv_versefile', self.csv_verses_edit) self.select_page.registerField('csv_versefile', self.csv_verses_path_edit, 'path', PathEdit.pathChanged)
self.select_page.registerField('opensong_file', self.open_song_file_edit) self.select_page.registerField('opensong_file', self.open_song_path_edit, 'path', PathEdit.pathChanged)
self.select_page.registerField('zefania_file', self.zefania_file_edit) self.select_page.registerField('zefania_file', self.zefania_path_edit, 'path', PathEdit.pathChanged)
self.select_page.registerField('wordproject_file', self.wordproject_file_edit) self.select_page.registerField('wordproject_file', self.wordproject_path_edit, 'path', PathEdit.pathChanged)
self.select_page.registerField('web_location', self.web_source_combo_box) self.select_page.registerField('web_location', self.web_source_combo_box)
self.select_page.registerField('web_biblename', self.web_translation_combo_box) self.select_page.registerField('web_biblename', self.web_translation_combo_box)
self.select_page.registerField('sword_folder_path', self.sword_folder_edit) self.select_page.registerField('sword_folder_path', self.sword_folder_path_edit, 'path', PathEdit.pathChanged)
self.select_page.registerField('sword_zip_path', self.sword_zipfile_edit) self.select_page.registerField('sword_zip_path', self.sword_zipfile_path_edit, 'path', PathEdit.pathChanged)
self.select_page.registerField('proxy_server', self.web_server_edit) self.select_page.registerField('proxy_server', self.web_server_edit)
self.select_page.registerField('proxy_username', self.web_user_edit) self.select_page.registerField('proxy_username', self.web_user_edit)
self.select_page.registerField('proxy_password', self.web_password_edit) self.select_page.registerField('proxy_password', self.web_password_edit)
@ -785,13 +695,13 @@ class BibleImportForm(OpenLPWizard):
self.finish_button.setVisible(False) self.finish_button.setVisible(False)
self.cancel_button.setVisible(True) self.cancel_button.setVisible(True)
self.setField('source_format', 0) self.setField('source_format', 0)
self.setField('osis_location', '') self.setField('osis_location', None)
self.setField('csv_booksfile', '') self.setField('csv_booksfile', None)
self.setField('csv_versefile', '') self.setField('csv_versefile', None)
self.setField('opensong_file', '') self.setField('opensong_file', None)
self.setField('zefania_file', '') self.setField('zefania_file', None)
self.setField('sword_folder_path', '') self.setField('sword_folder_path', None)
self.setField('sword_zip_path', '') self.setField('sword_zip_path', None)
self.setField('web_location', WebDownload.Crosswalk) self.setField('web_location', WebDownload.Crosswalk)
self.setField('web_biblename', self.web_translation_combo_box.currentIndex()) self.setField('web_biblename', self.web_translation_combo_box.currentIndex())
self.setField('proxy_server', settings.value('proxy address')) self.setField('proxy_server', settings.value('proxy address'))
@ -833,16 +743,16 @@ class BibleImportForm(OpenLPWizard):
if bible_type == BibleFormat.OSIS: if bible_type == BibleFormat.OSIS:
# Import an OSIS bible. # Import an OSIS bible.
importer = self.manager.import_bible(BibleFormat.OSIS, name=license_version, importer = self.manager.import_bible(BibleFormat.OSIS, name=license_version,
filename=self.field('osis_location')) file_path=self.field('osis_location'))
elif bible_type == BibleFormat.CSV: elif bible_type == BibleFormat.CSV:
# Import a CSV bible. # Import a CSV bible.
importer = self.manager.import_bible(BibleFormat.CSV, name=license_version, importer = self.manager.import_bible(BibleFormat.CSV, name=license_version,
booksfile=self.field('csv_booksfile'), books_path=self.field('csv_booksfile'),
versefile=self.field('csv_versefile')) verse_path=self.field('csv_versefile'))
elif bible_type == BibleFormat.OpenSong: elif bible_type == BibleFormat.OpenSong:
# Import an OpenSong bible. # Import an OpenSong bible.
importer = self.manager.import_bible(BibleFormat.OpenSong, name=license_version, importer = self.manager.import_bible(BibleFormat.OpenSong, name=license_version,
filename=self.field('opensong_file')) file_path=self.field('opensong_file'))
elif bible_type == BibleFormat.WebDownload: elif bible_type == BibleFormat.WebDownload:
# Import a bible from the web. # Import a bible from the web.
self.progress_bar.setMaximum(1) self.progress_bar.setMaximum(1)
@ -861,11 +771,11 @@ class BibleImportForm(OpenLPWizard):
elif bible_type == BibleFormat.Zefania: elif bible_type == BibleFormat.Zefania:
# Import a Zefania bible. # Import a Zefania bible.
importer = self.manager.import_bible(BibleFormat.Zefania, name=license_version, importer = self.manager.import_bible(BibleFormat.Zefania, name=license_version,
filename=self.field('zefania_file')) file_path=self.field('zefania_file'))
elif bible_type == BibleFormat.WordProject: elif bible_type == BibleFormat.WordProject:
# Import a WordProject bible. # Import a WordProject bible.
importer = self.manager.import_bible(BibleFormat.WordProject, name=license_version, importer = self.manager.import_bible(BibleFormat.WordProject, name=license_version,
filename=self.field('wordproject_file')) file_path=self.field('wordproject_file'))
elif bible_type == BibleFormat.SWORD: elif bible_type == BibleFormat.SWORD:
# Import a SWORD bible. # Import a SWORD bible.
if self.sword_tab_widget.currentIndex() == self.sword_tab_widget.indexOf(self.sword_folder_tab): if self.sword_tab_widget.currentIndex() == self.sword_tab_widget.indexOf(self.sword_folder_tab):

View File

@ -113,8 +113,7 @@ class BookNameForm(QDialog, Ui_BookNameDialog):
cor_book = self.corresponding_combo_box.currentText() cor_book = self.corresponding_combo_box.currentText()
for character in '\\.^$*+?{}[]()': for character in '\\.^$*+?{}[]()':
cor_book = cor_book.replace(character, '\\' + character) cor_book = cor_book.replace(character, '\\' + character)
books = [key for key in list(self.book_names.keys()) if re.match(cor_book, str(self.book_names[key]), books = [key for key in list(self.book_names.keys()) if re.match(cor_book, str(self.book_names[key]))]
re.UNICODE)]
books = [_f for _f in map(BiblesResourcesDB.get_book, books) if _f] books = [_f for _f in map(BiblesResourcesDB.get_book, books) if _f]
if books: if books:
self.book_id = books[0]['id'] self.book_id = books[0]['id']

View File

@ -26,7 +26,7 @@ import re
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.registry import RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box
from .editbibledialog import Ui_EditBibleDialog from .editbibledialog import Ui_EditBibleDialog
from openlp.plugins.bibles.lib import BibleStrings from openlp.plugins.bibles.lib import BibleStrings

View File

@ -224,13 +224,13 @@ def update_reference_separators():
range_regex = '(?:(?P<from_chapter>[0-9]+){sep_v})?' \ range_regex = '(?:(?P<from_chapter>[0-9]+){sep_v})?' \
'(?P<from_verse>[0-9]+)(?P<range_to>{sep_r}(?:(?:(?P<to_chapter>' \ '(?P<from_verse>[0-9]+)(?P<range_to>{sep_r}(?:(?:(?P<to_chapter>' \
'[0-9]+){sep_v})?(?P<to_verse>[0-9]+)|{sep_e})?)?'.format_map(REFERENCE_SEPARATORS) '[0-9]+){sep_v})?(?P<to_verse>[0-9]+)|{sep_e})?)?'.format_map(REFERENCE_SEPARATORS)
REFERENCE_MATCHES['range'] = re.compile(r'^\s*{range}\s*$'.format(range=range_regex), re.UNICODE) REFERENCE_MATCHES['range'] = re.compile(r'^\s*{range}\s*$'.format(range=range_regex))
REFERENCE_MATCHES['range_separator'] = re.compile(REFERENCE_SEPARATORS['sep_l'], re.UNICODE) REFERENCE_MATCHES['range_separator'] = re.compile(REFERENCE_SEPARATORS['sep_l'])
# full reference match: <book>(<range>(,(?!$)|(?=$)))+ # full reference match: <book>(<range>(,(?!$)|(?=$)))+
REFERENCE_MATCHES['full'] = \ REFERENCE_MATCHES['full'] = \
re.compile(r'^\s*(?!\s)(?P<book>[\d]*[.]?[^\d\.]+)\.*(?<!\s)\s*' re.compile(r'^\s*(?!\s)(?P<book>[\d]*[.]?[^\d\.]+)\.*(?<!\s)\s*'
r'(?P<ranges>(?:{range_regex}(?:{sep_l}(?!\s*$)|(?=\s*$)))+)\s*$'.format( r'(?P<ranges>(?:{range_regex}(?:{sep_l}(?!\s*$)|(?=\s*$)))+)\s*$'.format(
range_regex=range_regex, sep_l=REFERENCE_SEPARATORS['sep_l']), re.UNICODE) range_regex=range_regex, sep_l=REFERENCE_SEPARATORS['sep_l']))
def get_reference_separator(separator_type): def get_reference_separator(separator_type):

View File

@ -23,37 +23,37 @@
from lxml import etree, objectify from lxml import etree, objectify
from zipfile import is_zipfile from zipfile import is_zipfile
from openlp.core.common.mixins import OpenLPMixin from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.registry import Registry
from openlp.core.common.i18n import get_language, translate from openlp.core.common.i18n import get_language, translate
from openlp.core.lib import ValidationError from openlp.core.lib import ValidationError
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box
from openlp.plugins.bibles.lib.db import AlternativeBookNamesDB, BibleDB, BiblesResourcesDB from openlp.plugins.bibles.lib.db import AlternativeBookNamesDB, BibleDB, BiblesResourcesDB
class BibleImport(OpenLPMixin, RegistryProperties, BibleDB): class BibleImport(BibleDB, LogMixin, RegistryProperties):
""" """
Helper class to import bibles from a third party source into OpenLP Helper class to import bibles from a third party source into OpenLP
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.filename = kwargs['filename'] if 'filename' in kwargs else None self.file_path = kwargs.get('file_path')
self.wizard = None self.wizard = None
self.stop_import_flag = False self.stop_import_flag = False
Registry().register_function('openlp_stop_wizard', self.stop_import) Registry().register_function('openlp_stop_wizard', self.stop_import)
@staticmethod @staticmethod
def is_compressed(file): def is_compressed(file_path):
""" """
Check if the supplied file is compressed Check if the supplied file is compressed
:param file: A path to the file to check :param file_path: A path to the file to check
""" """
if is_zipfile(file): if is_zipfile(str(file_path)):
critical_error_message_box( critical_error_message_box(
message=translate('BiblesPlugin.BibleImport', message=translate('BiblesPlugin.BibleImport',
'The file "{file}" you supplied is compressed. You must decompress it before import.' 'The file "{file}" you supplied is compressed. You must decompress it before import.'
).format(file=file)) ).format(file=file_path))
return True return True
return False return False
@ -96,6 +96,8 @@ class BibleImport(OpenLPMixin, RegistryProperties, BibleDB):
if language_form.exec(bible_name): if language_form.exec(bible_name):
combo_box = language_form.language_combo_box combo_box = language_form.language_combo_box
language_id = combo_box.itemData(combo_box.currentIndex()) language_id = combo_box.itemData(combo_box.currentIndex())
else:
return False
if not language_id: if not language_id:
return None return None
self.save_meta('language_id', language_id) self.save_meta('language_id', language_id)
@ -141,24 +143,24 @@ class BibleImport(OpenLPMixin, RegistryProperties, BibleDB):
self.log_debug('No book name supplied. Falling back to guess_id') self.log_debug('No book name supplied. Falling back to guess_id')
book_ref_id = guess_id book_ref_id = guess_id
if not book_ref_id: if not book_ref_id:
raise ValidationError(msg='Could not resolve book_ref_id in "{}"'.format(self.filename)) raise ValidationError(msg='Could not resolve book_ref_id in "{}"'.format(self.file_path))
book_details = BiblesResourcesDB.get_book_by_id(book_ref_id) book_details = BiblesResourcesDB.get_book_by_id(book_ref_id)
if book_details is None: if book_details is None:
raise ValidationError(msg='book_ref_id: {book_ref} Could not be found in the BibleResourcesDB while ' raise ValidationError(msg='book_ref_id: {book_ref} Could not be found in the BibleResourcesDB while '
'importing {file}'.format(book_ref=book_ref_id, file=self.filename)) 'importing {file}'.format(book_ref=book_ref_id, file=self.file_path))
return self.create_book(name, book_ref_id, book_details['testament_id']) return self.create_book(name, book_ref_id, book_details['testament_id'])
def parse_xml(self, filename, use_objectify=False, elements=None, tags=None): def parse_xml(self, file_path, use_objectify=False, elements=None, tags=None):
""" """
Parse and clean the supplied file by removing any elements or tags we don't use. Parse and clean the supplied file by removing any elements or tags we don't use.
:param filename: The filename of the xml file to parse. Str :param file_path: The filename of the xml file to parse. Str
:param use_objectify: Use the objectify parser rather than the etree parser. (Bool) :param use_objectify: Use the objectify parser rather than the etree parser. (Bool)
:param elements: A tuple of element names (Str) to remove along with their content. :param elements: A tuple of element names (Str) to remove along with their content.
:param tags: A tuple of element names (Str) to remove, preserving their content. :param tags: A tuple of element names (Str) to remove, preserving their content.
:return: The root element of the xml document :return: The root element of the xml document
""" """
try: try:
with open(filename, 'rb') as import_file: with file_path.open('rb') as import_file:
# NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own # NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own
# encoding detection, and the two mechanisms together interfere with each other. # encoding detection, and the two mechanisms together interfere with each other.
if not use_objectify: if not use_objectify:
@ -205,17 +207,17 @@ class BibleImport(OpenLPMixin, RegistryProperties, BibleDB):
self.log_debug('Stopping import') self.log_debug('Stopping import')
self.stop_import_flag = True self.stop_import_flag = True
def validate_xml_file(self, filename, tag): def validate_xml_file(self, file_path, tag):
""" """
Validate the supplied file Validate the supplied file
:param filename: The supplied file :param file_path: The supplied file
:param tag: The expected root tag type :param tag: The expected root tag type
:return: True if valid. ValidationError is raised otherwise. :return: True if valid. ValidationError is raised otherwise.
""" """
if BibleImport.is_compressed(filename): if BibleImport.is_compressed(file_path):
raise ValidationError(msg='Compressed file') raise ValidationError(msg='Compressed file')
bible = self.parse_xml(filename, use_objectify=True) bible = self.parse_xml(file_path, use_objectify=True)
if bible is None: if bible is None:
raise ValidationError(msg='Error when opening file') raise ValidationError(msg='Error when opening file')
root_tag = bible.tag.lower() root_tag = bible.tag.lower()

View File

@ -41,11 +41,11 @@ class BiblesTab(SettingsTab):
""" """
log.info('Bible Tab loaded') log.info('Bible Tab loaded')
def _init_(self, parent, title, visible_title, icon_path): def _init_(self, *args, **kwargs):
self.paragraph_style = True self.paragraph_style = True
self.show_new_chapters = False self.show_new_chapters = False
self.display_style = 0 self.display_style = 0
super(BiblesTab, self).__init__(parent, title, visible_title, icon_path) super().__init__(*args, **kwargs)
def setupUi(self): def setupUi(self):
self.setObjectName('BiblesTab') self.setObjectName('BiblesTab')

View File

@ -19,7 +19,6 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
import chardet import chardet
import logging import logging
import os import os
@ -36,6 +35,7 @@ from sqlalchemy.orm.exc import UnmappedClassError
from openlp.core.common import clean_filename from openlp.core.common import clean_filename
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.common.path import Path
from openlp.core.lib.db import BaseModel, init_db, Manager from openlp.core.lib.db import BaseModel, init_db, Manager
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box
from openlp.plugins.bibles.lib import BibleStrings, LanguageSelection, upgrade from openlp.plugins.bibles.lib import BibleStrings, LanguageSelection, upgrade
@ -130,10 +130,15 @@ class BibleDB(Manager):
:param parent: :param parent:
:param kwargs: :param kwargs:
``path`` ``path``
The path to the bible database file. The path to the bible database file. Type: openlp.core.common.path.Path
``name`` ``name``
The name of the database. This is also used as the file name for SQLite databases. The name of the database. This is also used as the file name for SQLite databases.
``file``
Type: openlp.core.common.path.Path
:rtype: None
""" """
log.info('BibleDB loaded') log.info('BibleDB loaded')
self._setup(parent, **kwargs) self._setup(parent, **kwargs)
@ -146,20 +151,20 @@ class BibleDB(Manager):
self.session = None self.session = None
if 'path' not in kwargs: if 'path' not in kwargs:
raise KeyError('Missing keyword argument "path".') raise KeyError('Missing keyword argument "path".')
self.path = kwargs['path']
if 'name' not in kwargs and 'file' not in kwargs: if 'name' not in kwargs and 'file' not in kwargs:
raise KeyError('Missing keyword argument "name" or "file".') raise KeyError('Missing keyword argument "name" or "file".')
if 'name' in kwargs: if 'name' in kwargs:
self.name = kwargs['name'] self.name = kwargs['name']
if not isinstance(self.name, str): if not isinstance(self.name, str):
self.name = str(self.name, 'utf-8') self.name = str(self.name, 'utf-8')
self.file = clean_filename(self.name) + '.sqlite' # TODO: To path object
file_path = Path(clean_filename(self.name) + '.sqlite')
if 'file' in kwargs: if 'file' in kwargs:
self.file = kwargs['file'] file_path = kwargs['file']
Manager.__init__(self, 'bibles', init_schema, self.file, upgrade) Manager.__init__(self, 'bibles', init_schema, file_path, upgrade)
if self.session and 'file' in kwargs: if self.session and 'file' in kwargs:
self.get_name() self.get_name()
if 'path' in kwargs:
self.path = kwargs['path']
self._is_web_bible = None self._is_web_bible = None
def get_name(self): def get_name(self):
@ -308,8 +313,7 @@ class BibleDB(Manager):
book_escaped = book book_escaped = book
for character in RESERVED_CHARACTERS: for character in RESERVED_CHARACTERS:
book_escaped = book_escaped.replace(character, '\\' + character) book_escaped = book_escaped.replace(character, '\\' + character)
regex_book = re.compile('\\s*{book}\\s*'.format(book='\\s*'.join(book_escaped.split())), regex_book = re.compile('\\s*{book}\\s*'.format(book='\\s*'.join(book_escaped.split())), re.IGNORECASE)
re.UNICODE | re.IGNORECASE)
if language_selection == LanguageSelection.Bible: if language_selection == LanguageSelection.Bible:
db_book = self.get_book(book) db_book = self.get_book(book)
if db_book: if db_book:
@ -472,9 +476,9 @@ class BiblesResourcesDB(QtCore.QObject, Manager):
Return the cursor object. Instantiate one if it doesn't exist yet. Return the cursor object. Instantiate one if it doesn't exist yet.
""" """
if BiblesResourcesDB.cursor is None: if BiblesResourcesDB.cursor is None:
file_path = os.path.join(str(AppLocation.get_directory(AppLocation.PluginsDir)), file_path = \
'bibles', 'resources', 'bibles_resources.sqlite') AppLocation.get_directory(AppLocation.PluginsDir) / 'bibles' / 'resources' / 'bibles_resources.sqlite'
conn = sqlite3.connect(file_path) conn = sqlite3.connect(str(file_path))
BiblesResourcesDB.cursor = conn.cursor() BiblesResourcesDB.cursor = conn.cursor()
return BiblesResourcesDB.cursor return BiblesResourcesDB.cursor
@ -760,17 +764,13 @@ class AlternativeBookNamesDB(QtCore.QObject, Manager):
If necessary loads up the database and creates the tables if the database doesn't exist. If necessary loads up the database and creates the tables if the database doesn't exist.
""" """
if AlternativeBookNamesDB.cursor is None: if AlternativeBookNamesDB.cursor is None:
file_path = os.path.join( file_path = AppLocation.get_directory(AppLocation.DataDir) / 'bibles' / 'alternative_book_names.sqlite'
str(AppLocation.get_directory(AppLocation.DataDir)), 'bibles', 'alternative_book_names.sqlite') AlternativeBookNamesDB.conn = sqlite3.connect(str(file_path))
if not os.path.exists(file_path): if not file_path.exists():
# create new DB, create table alternative_book_names # create new DB, create table alternative_book_names
AlternativeBookNamesDB.conn = sqlite3.connect(file_path)
AlternativeBookNamesDB.conn.execute( AlternativeBookNamesDB.conn.execute(
'CREATE TABLE alternative_book_names(id INTEGER NOT NULL, ' 'CREATE TABLE alternative_book_names(id INTEGER NOT NULL, '
'book_reference_id INTEGER, language_id INTEGER, name VARCHAR(50), PRIMARY KEY (id))') 'book_reference_id INTEGER, language_id INTEGER, name VARCHAR(50), PRIMARY KEY (id))')
else:
# use existing DB
AlternativeBookNamesDB.conn = sqlite3.connect(file_path)
AlternativeBookNamesDB.cursor = AlternativeBookNamesDB.conn.cursor() AlternativeBookNamesDB.cursor = AlternativeBookNamesDB.conn.cursor()
return AlternativeBookNamesDB.cursor return AlternativeBookNamesDB.cursor

View File

@ -0,0 +1,24 @@
# -*- 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 #
###############################################################################
"""
The :mod:`~openlp.plugins.bibles.lib.importers` module contains importers for the Bibles plugin.
"""

View File

@ -73,8 +73,8 @@ class CSVBible(BibleImport):
""" """
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.log_info(self.__class__.__name__) self.log_info(self.__class__.__name__)
self.books_file = kwargs['booksfile'] self.books_path = kwargs['books_path']
self.verses_file = kwargs['versefile'] self.verses_path = kwargs['verse_path']
@staticmethod @staticmethod
def get_book_name(name, books): def get_book_name(name, books):
@ -92,21 +92,22 @@ class CSVBible(BibleImport):
return book_name return book_name
@staticmethod @staticmethod
def parse_csv_file(filename, results_tuple): def parse_csv_file(file_path, results_tuple):
""" """
Parse the supplied CSV file. Parse the supplied CSV file.
:param filename: The name of the file to parse. Str :param openlp.core.common.path.Path file_path: The name of the file to parse.
:param results_tuple: The namedtuple to use to store the results. namedtuple :param namedtuple results_tuple: The namedtuple to use to store the results.
:return: An iterable yielding namedtuples of type results_tuple :return: An list of namedtuples of type results_tuple
:rtype: list[namedtuple]
""" """
try: try:
encoding = get_file_encoding(Path(filename))['encoding'] encoding = get_file_encoding(file_path)['encoding']
with open(filename, 'r', encoding=encoding, newline='') as csv_file: with file_path.open('r', encoding=encoding, newline='') as csv_file:
csv_reader = csv.reader(csv_file, delimiter=',', quotechar='"') csv_reader = csv.reader(csv_file, delimiter=',', quotechar='"')
return [results_tuple(*line) for line in csv_reader] return [results_tuple(*line) for line in csv_reader]
except (OSError, csv.Error): except (OSError, csv.Error):
raise ValidationError(msg='Parsing "{file}" failed'.format(file=filename)) raise ValidationError(msg='Parsing "{file}" failed'.format(file=file_path))
def process_books(self, books): def process_books(self, books):
""" """
@ -159,12 +160,12 @@ class CSVBible(BibleImport):
self.language_id = self.get_language(bible_name) self.language_id = self.get_language(bible_name)
if not self.language_id: if not self.language_id:
return False return False
books = self.parse_csv_file(self.books_file, Book) books = self.parse_csv_file(self.books_path, Book)
self.wizard.progress_bar.setValue(0) self.wizard.progress_bar.setValue(0)
self.wizard.progress_bar.setMinimum(0) self.wizard.progress_bar.setMinimum(0)
self.wizard.progress_bar.setMaximum(len(books)) self.wizard.progress_bar.setMaximum(len(books))
book_list = self.process_books(books) book_list = self.process_books(books)
verses = self.parse_csv_file(self.verses_file, Verse) verses = self.parse_csv_file(self.verses_path, Verse)
self.wizard.progress_bar.setValue(0) self.wizard.progress_bar.setValue(0)
self.wizard.progress_bar.setMaximum(len(books) + 1) self.wizard.progress_bar.setMaximum(len(books) + 1)
self.process_verses(verses, book_list) self.process_verses(verses, book_list)

View File

@ -32,7 +32,8 @@ from bs4 import BeautifulSoup, NavigableString, Tag
from openlp.core.common.httputils import get_web_page from openlp.core.common.httputils import get_web_page
from openlp.core.common.i18n import translate from openlp.core.common.i18n import translate
from openlp.core.common.registry import Registry, RegistryProperties from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box
from openlp.plugins.bibles.lib import SearchResults from openlp.plugins.bibles.lib import SearchResults
from openlp.plugins.bibles.lib.bibleimport import BibleImport from openlp.plugins.bibles.lib.bibleimport import BibleImport

View File

@ -46,7 +46,8 @@ def parse_chapter_number(number, previous_number):
:param number: The raw data from the xml :param number: The raw data from the xml
:param previous_number: The previous chapter number :param previous_number: The previous chapter number
:return: Number of current chapter. (Int) :return: Number of current chapter.
:rtype: int
""" """
if number: if number:
return int(number.split()[-1]) return int(number.split()[-1])
@ -132,13 +133,13 @@ class OpenSongBible(BibleImport):
:param bible_name: The name of the bible being imported :param bible_name: The name of the bible being imported
:return: True if import completed, False if import was unsuccessful :return: True if import completed, False if import was unsuccessful
""" """
self.log_debug('Starting OpenSong import from "{name}"'.format(name=self.filename)) self.log_debug('Starting OpenSong import from "{name}"'.format(name=self.file_path))
self.validate_xml_file(self.filename, 'bible') self.validate_xml_file(self.file_path, 'bible')
bible = self.parse_xml(self.filename, use_objectify=True) bible = self.parse_xml(self.file_path, use_objectify=True)
if bible is None: if bible is None:
return False return False
# No language info in the opensong format, so ask the user # No language info in the opensong format, so ask the user
self.language_id = self.get_language_id(bible_name=self.filename) self.language_id = self.get_language_id(bible_name=str(self.file_path))
if not self.language_id: if not self.language_id:
return False return False
self.process_books(bible.b) self.process_books(bible.b)

View File

@ -159,14 +159,14 @@ class OSISBible(BibleImport):
""" """
Loads a Bible from file. Loads a Bible from file.
""" """
self.log_debug('Starting OSIS import from "{name}"'.format(name=self.filename)) self.log_debug('Starting OSIS import from "{name}"'.format(name=self.file_path))
self.validate_xml_file(self.filename, '{http://www.bibletechnologies.net/2003/osis/namespace}osis') self.validate_xml_file(self.file_path, '{http://www.bibletechnologies.net/2003/osis/namespace}osis')
bible = self.parse_xml(self.filename, elements=REMOVABLE_ELEMENTS, tags=REMOVABLE_TAGS) bible = self.parse_xml(self.file_path, elements=REMOVABLE_ELEMENTS, tags=REMOVABLE_TAGS)
if bible is None: if bible is None:
return False return False
# Find bible language # Find bible language
language = bible.xpath("//ns:osisText/@xml:lang", namespaces=NS) language = bible.xpath("//ns:osisText/@xml:lang", namespaces=NS)
self.language_id = self.get_language_id(language[0] if language else None, bible_name=self.filename) self.language_id = self.get_language_id(language[0] if language else None, bible_name=str(self.file_path))
if not self.language_id: if not self.language_id:
return False return False
self.process_books(bible) self.process_books(bible)

View File

@ -60,7 +60,7 @@ class SwordBible(BibleImport):
bible = pysword_modules.get_bible_from_module(self.sword_key) bible = pysword_modules.get_bible_from_module(self.sword_key)
language = pysword_module_json['lang'] language = pysword_module_json['lang']
language = language[language.find('.') + 1:] language = language[language.find('.') + 1:]
language_id = self.get_language_id(language, bible_name=self.filename) language_id = self.get_language_id(language, bible_name=str(self.file_path))
if not language_id: if not language_id:
return False return False
books = bible.get_structure().get_books() books = bible.get_structure().get_books()

View File

@ -19,15 +19,14 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
import os
import re
import logging import logging
from codecs import open as copen import re
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from zipfile import ZipFile from zipfile import ZipFile
from bs4 import BeautifulSoup, Tag, NavigableString from bs4 import BeautifulSoup, Tag, NavigableString
from openlp.core.common.path import Path
from openlp.plugins.bibles.lib.bibleimport import BibleImport from openlp.plugins.bibles.lib.bibleimport import BibleImport
BOOK_NUMBER_PATTERN = re.compile(r'\[(\d+)\]') BOOK_NUMBER_PATTERN = re.compile(r'\[(\d+)\]')
@ -51,9 +50,9 @@ class WordProjectBible(BibleImport):
Unzip the file to a temporary directory Unzip the file to a temporary directory
""" """
self.tmp = TemporaryDirectory() self.tmp = TemporaryDirectory()
zip_file = ZipFile(os.path.abspath(self.filename)) with ZipFile(str(self.file_path)) as zip_file:
zip_file.extractall(self.tmp.name) zip_file.extractall(self.tmp.name)
self.base_dir = os.path.join(self.tmp.name, os.path.splitext(os.path.basename(self.filename))[0]) self.base_path = Path(self.tmp.name, self.file_path.stem)
def process_books(self): def process_books(self):
""" """
@ -62,8 +61,7 @@ class WordProjectBible(BibleImport):
:param bible_data: parsed xml :param bible_data: parsed xml
:return: None :return: None
""" """
with copen(os.path.join(self.base_dir, 'index.htm'), encoding='utf-8', errors='ignore') as index_file: page = (self.base_path / 'index.htm').read_text(encoding='utf-8', errors='ignore')
page = index_file.read()
soup = BeautifulSoup(page, 'lxml') soup = BeautifulSoup(page, 'lxml')
bible_books = soup.find('div', 'textOptions').find_all('li') bible_books = soup.find('div', 'textOptions').find_all('li')
book_count = len(bible_books) book_count = len(bible_books)
@ -93,9 +91,7 @@ class WordProjectBible(BibleImport):
:return: None :return: None
""" """
log.debug(book_link) log.debug(book_link)
book_file = os.path.join(self.base_dir, os.path.normpath(book_link)) page = (self.base_path / book_link).read_text(encoding='utf-8', errors='ignore')
with copen(book_file, encoding='utf-8', errors='ignore') as f:
page = f.read()
soup = BeautifulSoup(page, 'lxml') soup = BeautifulSoup(page, 'lxml')
header_div = soup.find('div', 'textHeader') header_div = soup.find('div', 'textHeader')
chapters_p = header_div.find('p') chapters_p = header_div.find('p')
@ -114,9 +110,8 @@ class WordProjectBible(BibleImport):
""" """
Get the verses for a particular book Get the verses for a particular book
""" """
chapter_file_name = os.path.join(self.base_dir, '{:02d}'.format(book_number), '{}.htm'.format(chapter_number)) chapter_file_path = self.base_path / '{:02d}'.format(book_number) / '{}.htm'.format(chapter_number)
with copen(chapter_file_name, encoding='utf-8', errors='ignore') as chapter_file: page = chapter_file_path.read_text(encoding='utf-8', errors='ignore')
page = chapter_file.read()
soup = BeautifulSoup(page, 'lxml') soup = BeautifulSoup(page, 'lxml')
text_body = soup.find('div', 'textBody') text_body = soup.find('div', 'textBody')
if text_body: if text_body:
@ -158,9 +153,9 @@ class WordProjectBible(BibleImport):
""" """
Loads a Bible from file. Loads a Bible from file.
""" """
self.log_debug('Starting WordProject import from "{name}"'.format(name=self.filename)) self.log_debug('Starting WordProject import from "{name}"'.format(name=self.file_path))
self._unzip_file() self._unzip_file()
self.language_id = self.get_language_id(None, bible_name=self.filename) self.language_id = self.get_language_id(None, bible_name=str(self.file_path))
result = False result = False
if self.language_id: if self.language_id:
self.process_books() self.process_books()

View File

@ -45,13 +45,13 @@ class ZefaniaBible(BibleImport):
""" """
Loads a Bible from file. Loads a Bible from file.
""" """
log.debug('Starting Zefania import from "{name}"'.format(name=self.filename)) log.debug('Starting Zefania import from "{name}"'.format(name=self.file_path))
success = True success = True
try: try:
xmlbible = self.parse_xml(self.filename, elements=REMOVABLE_ELEMENTS, tags=REMOVABLE_TAGS) xmlbible = self.parse_xml(self.file_path, elements=REMOVABLE_ELEMENTS, tags=REMOVABLE_TAGS)
# Find bible language # Find bible language
language = xmlbible.xpath("/XMLBIBLE/INFORMATION/language/text()") language = xmlbible.xpath("/XMLBIBLE/INFORMATION/language/text()")
language_id = self.get_language_id(language[0] if language else None, bible_name=self.filename) language_id = self.get_language_id(language[0] if language else None, bible_name=str(self.file_path))
if not language_id: if not language_id:
return False return False
no_of_books = int(xmlbible.xpath('count(//BIBLEBOOK)')) no_of_books = int(xmlbible.xpath('count(//BIBLEBOOK)'))
@ -69,7 +69,7 @@ class ZefaniaBible(BibleImport):
log.debug('Could not find a name, will use number, basically a guess.') log.debug('Could not find a name, will use number, basically a guess.')
book_ref_id = int(bnumber) book_ref_id = int(bnumber)
if not book_ref_id: if not book_ref_id:
log.error('Importing books from "{name}" failed'.format(name=self.filename)) log.error('Importing books from "{name}" failed'.format(name=self.file_path))
return False return False
book_details = BiblesResourcesDB.get_book_by_id(book_ref_id) book_details = BiblesResourcesDB.get_book_by_id(book_ref_id)
db_book = self.create_book(book_details['name'], book_ref_id, book_details['testament_id']) db_book = self.create_book(book_details['name'], book_ref_id, book_details['testament_id'])

Some files were not shown because too many files have changed in this diff Show More