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]
verbose = true
verbose = True
plugins = nose2.plugins.mp
[log-capture]
@ -9,14 +9,19 @@ filter = -nose
log-level = ERROR
[test-result]
always-on = true
descriptions = true
always-on = True
descriptions = True
[coverage]
always-on = true
always-on = True
coverage = openlp
coverage-report = html
[multiprocess]
always-on = false
always-on = False
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.
"""
faulthandler.enable()
set_up_fault_handling()
# Add support for using multiprocessing from frozen Windows executable (built using PyInstaller),
# 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
"""
import os
from zipfile import ZipFile
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
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
:param zip_name: the zip file to be processed
:param app_root: the directory where the zip get expanded to
:param str zip_name: the zip file name to be processed
:param openlp.core.common.path.Path app_root_path: The directory to expand the zip to
:return: None
"""
zip_file = os.path.join(app_root, zip_name)
web_zip = ZipFile(zip_file)
web_zip.extractall(app_root)
zip_path = app_root_path / zip_name
web_zip = ZipFile(str(zip_path))
web_zip.extractall(str(app_root_path))
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})
except ConnectionError:
return False
if not web_config:
return None
file_bits = web_config.split()
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',
AppLocation.get_section_data_path('remotes') / 'site.zip',
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 import requires_auth
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.settings import Settings
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'):
item['tag'] = str(index + 1)
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
if not os.path.exists(full_thumbnail_path):
create_thumb(current_item.get_frame_path(index), full_thumbnail_path, False)
Registry().get('image_manager').add_image(full_thumbnail_path, frame['title'], None, 88, 88)
item['img'] = urllib.request.pathname2url(os.path.sep + thumbnail_path)
if not full_thumbnail_path.exists():
create_thumb(Path(current_item.get_frame_path(index)), full_thumbnail_path, False)
Registry().get('image_manager').add_image(str(full_thumbnail_path), frame['title'], None, 88, 88)
item['img'] = urllib.request.pathname2url(os.path.sep + str(thumbnail_path))
item['text'] = str(frame['title'])
item['html'] = str(frame['title'])
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))
}
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 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
import os
import json
import re
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 log: the logger object
: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
:return:
"""
@ -124,12 +123,10 @@ def display_thumbnails(request, controller_name, log, dimensions, file_name, sli
if controller_name and file_name:
file_name = urllib.parse.unquote(file_name)
if '..' not in file_name: # no hacking please
full_path = AppLocation.get_section_data_path(controller_name) / 'thumbnails' / file_name
if slide:
full_path = str(AppLocation.get_section_data_path(controller_name) / 'thumbnails' / file_name / slide)
else:
full_path = str(AppLocation.get_section_data_path(controller_name) / 'thumbnails' / file_name)
if os.path.exists(full_path):
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)
full_path = full_path / slide
if full_path.exists():
Registry().get('image_manager').add_image(full_path, full_path.name, None, width, height)
image = Registry().get('image_manager').get_image(full_path, full_path.name, width, height)
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__)
remote_endpoint = Endpoint('remote', template_dir='remotes', static_dir='remotes')
remote_endpoint = Endpoint('remote', template_dir='remotes')
@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
"""
import os
from mako.template import Template
from openlp.core.common.applocation import AppLocation
@ -67,13 +65,17 @@ class Endpoint(object):
def render_template(self, filename, **kwargs):
"""
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:
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:
kwargs['static_url'] = '/{prefix}/static'.format(prefix=self.url_prefix)
kwargs['static_url'] = kwargs['static_url'].replace('//', '/')
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.common.applocation import AppLocation
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.registry import RegistryProperties, Registry
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings
from openlp.core.common.i18n import translate
@ -67,13 +67,16 @@ class HttpWorker(QtCore.QObject):
address = Settings().value('api/ip address')
port = Settings().value('api/port')
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):
pass
class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin):
class HttpServer(RegistryBase, RegistryProperties, LogMixin):
"""
Wrapper round a server instance
"""
@ -82,13 +85,14 @@ class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin):
Initialise the http server, and start the http server
"""
super(HttpServer, self).__init__(parent)
self.worker = HttpWorker()
self.thread = QtCore.QThread()
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run)
self.thread.start()
Registry().register_function('download_website', self.first_time)
Registry().register_function('get_website_version', self.website_version)
if Registry().get_flag('no_web_server'):
self.worker = HttpWorker()
self.thread = QtCore.QThread()
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run)
self.thread.start()
Registry().register_function('download_website', self.first_time)
Registry().register_function('get_website_version', self.website_version)
Registry().set_flag('website_version', '0.0')
def bootstrap_post_set_up(self):

View File

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

View File

@ -23,7 +23,7 @@
import json
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

View File

@ -31,8 +31,8 @@ import time
from PyQt5 import QtCore
from openlp.core.common.mixins import OpenLPMixin
from openlp.core.common.registry import Registry, RegistryProperties
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
log = logging.getLogger(__name__)
@ -61,7 +61,7 @@ class WebSocketWorker(QtCore.QObject):
self.ws_server.stop = True
class WebSocketServer(RegistryProperties, OpenLPMixin):
class WebSocketServer(RegistryProperties, LogMixin):
"""
Wrapper round a server instance
"""
@ -70,12 +70,13 @@ class WebSocketServer(RegistryProperties, OpenLPMixin):
Initialise and start the WebSockets server
"""
super(WebSocketServer, self).__init__()
self.settings_section = 'api'
self.worker = WebSocketWorker(self)
self.thread = QtCore.QThread()
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run)
self.thread.start()
if Registry().get_flag('no_web_server'):
self.settings_section = 'api'
self.worker = WebSocketWorker(self)
self.thread = QtCore.QThread()
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run)
self.thread.start()
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.applocation import AppLocation
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.registry import Registry
from openlp.core.common.settings import Settings
@ -59,7 +59,7 @@ __all__ = ['OpenLP', 'main']
log = logging.getLogger()
class OpenLP(OpenLPMixin, QtWidgets.QApplication):
class OpenLP(QtWidgets.QApplication, LogMixin):
"""
The core application class. This class inherits from Qt's QApplication
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))
QtWidgets.QMessageBox.information(
None, translate('OpenLP', 'Settings Upgrade'),
translate('OpenLP', 'Your settings are about to upgraded. A backup will be created at {back_up_path}')
.format(back_up_path=back_up_path))
translate('OpenLP', 'Your settings are about to be upgraded. A backup will be created at '
'{back_up_path}').format(back_up_path=back_up_path))
settings.export(back_up_path)
settings.upgrade_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]+)')
SECOND_CAMEL_REGEX = re.compile('([a-z0-9])([A-Z])')
CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]', re.UNICODE)
INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]', re.UNICODE)
CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]')
INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]')
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):
@ -314,17 +318,6 @@ def get_filesystem_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):
"""
Deletes a file from the system.
@ -339,7 +332,7 @@ def delete_file(file_path):
if file_path.exists():
file_path.unlink()
return True
except (IOError, OSError):
except OSError:
log.exception('Unable to delete file {file_path}'.format(file_path=file_path))
return False
@ -436,3 +429,17 @@ def get_file_encoding(file_path):
return detector.result
except OSError:
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.
if Settings().contains('advanced/data path'):
path = Settings().value('advanced/data path')
path = Path(Settings().value('advanced/data path'))
else:
path = AppLocation.get_directory(AppLocation.DataDir)
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))
log.debug('Downloaded page {url}'.format(url=response.url))
break
except IOError:
# For now, catch IOError. All requests errors inherit from IOError
except OSError:
# For now, catch OSError. All requests errors inherit from OSError
log.exception('Unable to connect to {url}'.format(url=url))
response = None
if retries >= CONNECTION_RETRIES:
@ -127,7 +127,7 @@ def get_url_file_size(url):
try:
response = requests.head(url, timeout=float(CONNECTION_TIMEOUT), allow_redirects=True)
return int(response.headers['Content-Length'])
except IOError:
except OSError:
if retries > CONNECTION_RETRIES:
raise ConnectionError('Unable to download {url}'.format(url=url))
else:
@ -173,7 +173,7 @@ def url_get_file(callback, url, file_path, sha256=None):
file_path.unlink()
return False
break
except IOError:
except OSError:
trace_error_handler(log)
if retries > CONNECTION_RETRIES:
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'])
ICU_COLLATOR = None
DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE)
DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+')
LANGUAGES = sorted([
Language(1, translate('common.languages', '(Afan) Oromo', 'Language code: om'), 'om'),
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 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
DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed',
'preview_size_changed', 'resizeEvent']
class OpenLPMixin(object):
class LogMixin(object):
"""
Base Calling object for OpenLP classes.
"""
def __init__(self, *args, **kwargs):
super(OpenLPMixin, self).__init__(*args, **kwargs)
self.logger = logging.getLogger("%s.%s" % (self.__module__, self.__class__.__name__))
if self.logger.getEffectiveLevel() == logging.DEBUG:
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))
@property
def logger(self):
if hasattr(self, '_logger') and self._logger:
return self._logger
else:
self._logger = logging.getLogger("%s.%s" % (self.__module__, self.__class__.__name__))
if self._logger.getEffectiveLevel() == logging.DEBUG:
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):
"""
@ -93,30 +97,141 @@ class OpenLPMixin(object):
self.logger.exception(message)
class RegistryMixin(object):
class RegistryProperties(object):
"""
This adds registry components to classes to use at run time.
"""
def __init__(self, parent):
"""
Register the class and bootstrap hooks.
"""
try:
super(RegistryMixin, self).__init__(parent)
except TypeError:
super(RegistryMixin, self).__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)
_application = None
_plugin_manager = None
_image_manager = None
_media_controller = None
_service_manager = None
_preview_controller = None
_live_controller = None
_main_window = None
_renderer = None
_theme_manager = None
_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)
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):
"""
@ -153,23 +163,6 @@ def 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):
"""
Wraps :func:shutil.which` so that it return a Path objects.
@ -233,7 +226,7 @@ def create_paths(*paths, **kwargs):
try:
if not path.exists():
path.mkdir(parents=True)
except IOError:
except OSError:
if not kwargs.get('do_not_log', False):
log.exception('failed to check if directory exists or create directory')

View File

@ -25,7 +25,7 @@ Provide Registry Services
import logging
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__)
@ -61,6 +61,15 @@ class Registry(object):
registry.initialising = True
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):
"""
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 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)
def execute(self, event, *args, **kwargs):
@ -142,8 +151,9 @@ class Registry(object):
trace_error_handler(log)
log.exception('Exception for function {function}'.format(function=function))
else:
trace_error_handler(log)
log.exception('Event {event} called but not registered'.format(event=event))
if log.getEffectiveLevel() == logging.DEBUG:
trace_error_handler(log)
log.exception('Event {event} called but not registered'.format(event=event))
return results
def get_flag(self, key):
@ -178,128 +188,30 @@ class Registry(object):
del self.working_flags[key]
class RegistryProperties(object):
class RegistryBase(object):
"""
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 application(self):
def bootstrap_initialise(self):
"""
Adds the openlp to the class dynamically.
Windows needs to access the application in a dynamic manner.
Dummy method to be overridden
"""
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
pass
@property
def plugin_manager(self):
def bootstrap_post_set_up(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:
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
pass

View File

@ -40,7 +40,7 @@ __version__ = 2
# Fix for bug #1014422.
X11_BYPASS_DEFAULT = True
if is_linux():
if is_linux(): # pragma: no cover
# Default to False on Gnome.
X11_BYPASS_DEFAULT = bool(not os.environ.get('GNOME_DESKTOP_SESSION_ID'))
# Default to False on Xfce.
@ -206,11 +206,14 @@ class Settings(QtCore.QSettings):
'projector/source dialog type': 0 # Source select dialog box type
}
__file_path__ = ''
# Settings upgrades prior to 3.0
__setting_upgrade_1__ = [
# Changed during 2.2.x development.
('songs/search as type', 'advanced/search as type', []),
('media/players', 'media/players_temp', [(media_players_conv, None)]), # Convert phonon to system
('media/players_temp', 'media/players', []), # Move temp setting from above to correct setting
]
# 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 image', 'core/logo file', []), # Default image renamed + moved to general after 2.4.
('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.
('songs/last search type', 'songs/last used search type', []),
('bibles/last search type', '', []),
('custom/last search type', 'custom/last used search type', [])]
__setting_upgrade_2__ = [
('custom/last search type', 'custom/last used search type', []),
# 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)]),
('servicemanager/last directory', 'servicemanager/last directory', [(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)]),
('presentations/last directory', 'presentations/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
@ -464,32 +468,38 @@ class Settings(QtCore.QSettings):
for version in range(current_version, __version__):
version += 1
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
# 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
if new_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
# the default value from the central settings dict.
if rules:
default_value = rules[0][1]
old_value = self._convert_value(old_value, default_value)
default_values = rules[0][1]
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.
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
# convert values. E. g. an old value 1 results in True, and 0 in False.
if callable(new):
old_value = new(old_value)
elif old == old_value:
old_value = new
if callable(new_rule):
new_value = new_rule(*old_values)
elif old_rule in old_values:
new_value = new_rule
break
self.setValue(new_key, old_value)
if new_key != old_key:
self.remove(old_key)
self.setValue('settings/version', version)
self.setValue(new_key, new_value)
[self.remove(old_key) for old_key in old_keys if old_key != new_key]
self.setValue('settings/version', version)
def value(self, key):
"""

View File

@ -25,9 +25,9 @@ import re
from string import Template
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.registry import Registry, RegistryProperties
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList
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']
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
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 force_page: Flag to tell message lines per page need to be generated.
:rtype: QtGui.QPixmap
"""
# save value for use in format_slide
self.force_page = force_page
@ -222,8 +223,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
self.display.build_html(service_item)
raw_html = service_item.get_rendered_frame(0)
self.display.text(raw_html, False)
preview = self.display.preview()
return preview
return self.display.preview()
self.force_page = False
def format_slide(self, text, item):

View File

@ -104,7 +104,7 @@ def get_text_file_string(text_file_path):
# no BOM was found
file_handle.seek(0)
content = file_handle.read()
except (IOError, UnicodeError):
except (OSError, UnicodeError):
log.exception('Failed to open text file {text}'.format(text=text_file_path))
return content
@ -179,8 +179,9 @@ def create_thumb(image_path, thumb_path, return_icon=True, size=None):
height of 88 is used.
:return: The final icon.
"""
ext = os.path.splitext(thumb_path)[1].lower()
reader = QtGui.QImageReader(image_path)
# TODO: To path object
thumb_path = Path(thumb_path)
reader = QtGui.QImageReader(str(image_path))
if size is None:
# No size given; use default height of 88
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
reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
thumb = reader.read()
thumb.save(thumb_path, ext[1:])
thumb.save(str(thumb_path), thumb_path.suffix[1:].lower())
if not return_icon:
return
if os.path.exists(thumb_path):
if thumb_path.exists():
return build_icon(thumb_path)
# Fallback for files with animation support.
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 .imagemanager import ImageManager
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.
:param upgrade_mod: The upgrade_schema function for this database
"""
super().__init__()
self.is_dirty = False
self.session = 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.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.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.ui.lib.filedialog import FileDialog
from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD
from openlp.core.ui.lib.toolbar import OpenLPToolbar
from openlp.core.widgets.dialogs import FileDialog
from openlp.core.widgets.edits import SearchEdit
from openlp.core.widgets.toolbar import OpenLPToolbar
from openlp.core.widgets.views import ListWidgetWithDnD
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.
"""
self.hide()
self.whitespace = re.compile(r'[\W_]+', re.UNICODE)
self.whitespace = re.compile(r'[\W_]+')
visible_title = self.plugin.get_string(StringContent.VisibleName)
self.title = str(visible_title['title'])
Registry().register(self.plugin.name, self)
@ -317,10 +318,10 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
self, self.on_new_prompt,
Settings().value(self.settings_section + '/last directory'),
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:
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()
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
"""
new_files = []
new_file_paths = []
error_shown = False
for file_name in data['files']:
file_type = file_name.split('.')[-1]
if file_type.lower() not in self.on_new_file_masks:
file_path = str_to_path(file_name)
if file_path.suffix[1:].lower() not in self.on_new_file_masks:
if not error_shown:
critical_error_message_box(translate('OpenLP.MediaManagerItem', 'Invalid File Type'),
translate('OpenLP.MediaManagerItem',
'Invalid File {name}.\n'
'Suffix not supported').format(name=file_name))
critical_error_message_box(
translate('OpenLP.MediaManagerItem', 'Invalid File Type'),
translate('OpenLP.MediaManagerItem',
'Invalid File {file_path}.\nFile extension not supported').format(
file_path=file_path))
error_shown = True
else:
new_files.append(file_name)
if new_files:
self.validate_and_load(new_files, data['target'])
new_file_paths.append(file_path)
if new_file_paths:
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):
"""
@ -353,12 +357,12 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
"""
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
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
"""
full_list = []
@ -366,18 +370,17 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
full_list.append(self.list_view.item(count).data(QtCore.Qt.UserRole))
duplicates_found = False
files_added = False
for file_path in files:
if file_path in full_list:
for file_path in file_paths:
if path_to_str(file_path) in full_list:
duplicates_found = True
else:
files_added = True
full_list.append(file_path)
full_list.append(path_to_str(file_path))
if full_list and files_added:
if target_group is None:
self.list_view.clear()
self.load_list(full_list, target_group)
last_dir = os.path.split(files[0])[0]
Settings().setValue(self.settings_section + '/last directory', Path(last_dir))
Settings().setValue(self.settings_section + '/last directory', file_paths[0].parent)
Settings().setValue('{section}/{section} files'.format(section=self.settings_section), self.get_file_list())
if duplicates_found:
critical_error_message_box(UiStrings().Duplicate,

View File

@ -26,9 +26,10 @@ import logging
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.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.version import get_version
log = logging.getLogger(__name__)
@ -138,10 +139,6 @@ class Plugin(QtCore.QObject, RegistryProperties):
self.text_strings = {}
self.set_plugin_text_strings()
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.icon = None
self.media_item_class = media_item_class
@ -161,6 +158,19 @@ class Plugin(QtCore.QObject, RegistryProperties):
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}_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):
"""

View File

@ -26,12 +26,12 @@ import os
from openlp.core.common import extension_loader
from openlp.core.common.applocation import AppLocation
from openlp.core.common.registry import RegistryProperties
from openlp.core.common.mixins import OpenLPMixin, RegistryMixin
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import RegistryBase
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,
and executes all the hooks, as and when necessary.
@ -43,8 +43,7 @@ class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
"""
super(PluginManager, self).__init__(parent)
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=self.base_path))
self.log_debug('Base path {path}'.format(path=AppLocation.get_directory(AppLocation.PluginsDir)))
self.plugins = []
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.applocation import AppLocation
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.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 openlp.core.common.registry import RegistryProperties
from openlp.core.common.mixins import RegistryProperties
class SettingsTab(QtWidgets.QWidget, RegistryProperties):

View File

@ -20,9 +20,9 @@
# 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
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.

View File

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

View File

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

View File

@ -20,9 +20,9 @@
# 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
@ -30,19 +30,19 @@ import logging
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common.i18n import translate
from openlp.core.common.mixins import OpenLPMixin, RegistryMixin
from openlp.core.common.registry import RegistryProperties
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import RegistryBase
from openlp.core.common.settings import Settings
from openlp.core.lib.ui import create_widget_action
from openlp.core.lib.projector import DialogSourceStyle
from openlp.core.lib.projector.constants import ERROR_MSG, ERROR_STRING, E_AUTHENTICATION, E_ERROR, \
from openlp.core.projectors import DialogSourceStyle
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, \
S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP
from openlp.core.lib.projector.db import ProjectorDB
from openlp.core.lib.projector.pjlink import PJLink, PJLinkUDP
from openlp.core.ui.lib import OpenLPToolbar
from openlp.core.ui.projector.editform import ProjectorEditForm
from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle
from openlp.core.projectors.db import ProjectorDB
from openlp.core.projectors.pjlink import PJLink, PJLinkUDP
from openlp.core.projectors.editform import ProjectorEditForm
from openlp.core.projectors.sourceselectform import SourceSelectTabs, SourceSelectSingle
from openlp.core.widgets.toolbar import OpenLPToolbar
log = logging.getLogger(__name__)
log.debug('projectormanager loaded')
@ -276,7 +276,7 @@ class UiProjectorManager(object):
self.update_icons()
class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjectorManager, RegistryProperties):
class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogMixin, RegistryProperties):
"""
Manage the projectors.
"""
@ -288,7 +288,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
:param projectordb: Database session inherited from superclass.
"""
log.debug('__init__()')
super().__init__(parent)
super(ProjectorManager, self).__init__(parent)
self.settings_section = 'projector'
self.projectordb = projectordb
self.projector_list = []
@ -518,7 +518,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
projector.thread.quit()
new_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
new_list.append(item)
self.projector_list = new_list
@ -672,14 +672,16 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
data=projector.model_filter)
count = 1
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',
'Lamp'),
count=count,
status=translate('OpenLP.ProjectorManager',
'ON')
if item['On']
else translate('OpenLP.ProjectorManager',
'OFF'))
status=status)
message += '<b>{title}</b>: {hours}<br />'.format(title=translate('OpenLP.ProjectorManager', 'Hours'),
hours=item['Hours'])
@ -730,7 +732,6 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
thread.started.connect(item.link.thread_started)
thread.finished.connect(item.link.thread_stopped)
thread.finished.connect(thread.deleteLater)
item.link.projectorNetwork.connect(self.update_status)
item.link.changeStatus.connect(self.update_status)
item.link.projectorAuthentication.connect(self.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.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_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, \
STATUS_STRING, S_CONNECTED, S_CONNECTING, S_INFO, S_NETWORK_RECEIVED, S_NETWORK_SENDING, \
S_NOT_CONNECTED, S_OFF, S_OK, S_ON, S_STATUS
STATUS_STRING, S_CONNECTED, S_CONNECTING, S_INFO, S_NOT_CONNECTED, S_OFF, S_OK, S_ON, S_QSOCKET_STATE, S_STATUS
log = logging.getLogger(__name__)
log.debug('pjlink loaded')
@ -111,7 +110,7 @@ class PJLinkCommands(object):
"""
log.debug('PJlinkCommands(args={args} kwargs={kwargs})'.format(args=args, kwargs=kwargs))
super().__init__()
# Map command to function
# Map PJLink command to method
self.pjlink_functions = {
'AVMT': self.process_avmt,
'CLSS': self.process_clss,
@ -123,7 +122,9 @@ class PJLinkCommands(object):
'INST': self.process_inst,
'LAMP': self.process_lamp,
'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,
'SNUM': self.process_snum,
'SVER': self.process_sver,
@ -135,7 +136,8 @@ class PJLinkCommands(object):
"""
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.filter_time = None # FILT
self.lamp = None # LAMP
@ -165,6 +167,7 @@ class PJLinkCommands(object):
self.socket_timer.stop()
self.send_busy = False
self.send_queue = []
self.priority_queue = []
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,
cmd=cmd,
data=data))
# Check if we have a future command not available yet
_cmd = cmd.upper()
# cmd should already be in uppercase, but data may be in mixed-case.
# Due to some replies should stay as mixed-case, validate using separate uppercase check
_data = data.upper()
if _cmd not in PJLINK_VALID_CMD:
log.error("({ip}) Ignoring command='{cmd}' (Invalid/Unknown)".format(ip=self.ip, cmd=cmd))
# Check if we have a future command not available yet
if cmd not in PJLINK_VALID_CMD:
log.error('({ip}) Ignoring command="{cmd}" (Invalid/Unknown)'.format(ip=self.ip, cmd=cmd))
return
elif _data == 'OK':
log.debug('({ip}) Command "{cmd}" returned OK'.format(ip=self.ip, cmd=cmd))
# A command returned successfully, no further processing needed
return
elif _cmd not in self.pjlink_functions:
log.warning("({ip}) Unable to process command='{cmd}' (Future option)".format(ip=self.ip, cmd=cmd))
# A command returned successfully, so do a query on command to verify status
return self.send_command(cmd=cmd)
elif cmd not in self.pjlink_functions:
log.warning('({ip}) Unable to process command="{cmd}" (Future option?)'.format(ip=self.ip, cmd=cmd))
return
elif _data in PJLINK_ERRORS:
# Oops - projector error
@ -211,12 +215,10 @@ class PJLinkCommands(object):
elif _data == PJLINK_ERRORS[E_PROJECTOR]:
# Projector/display error
self.change_status(E_PROJECTOR)
self.receive_data_signal()
return
# Command checks already passed
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):
"""
@ -259,19 +261,19 @@ class PJLinkCommands(object):
# : Received: '%1CLSS=Class 1' (Optoma)
# : Received: '%1CLSS=Version1' (BenQ)
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),
# 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.
try:
clss = re.findall('\d', data)[0] # Should only be the first match
except IndexError:
log.error("({ip}) No numbers found in class version reply '{data}' - "
"defaulting to class '1'".format(ip=self.ip, data=data))
log.error('({ip}) No numbers found in class version reply "{data}" - '
'defaulting to class "1"'.format(ip=self.ip, data=data))
clss = '1'
elif not data.isdigit():
log.error("({ip}) NAN clss version reply '{data}' - "
"defaulting to class '1'".format(ip=self.ip, data=data))
log.error('({ip}) NAN CLSS version reply "{data}" - '
'defaulting to class "1"'.format(ip=self.ip, data=data))
clss = '1'
else:
clss = data
@ -289,7 +291,7 @@ class PJLinkCommands(object):
"""
if len(data) != 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,
count=count))
return
@ -297,7 +299,7 @@ class PJLinkCommands(object):
datacheck = int(data)
except ValueError:
# 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
if datacheck == 0:
self.projector_errors = None
@ -402,17 +404,20 @@ class PJLinkCommands(object):
:param data: Lamp(s) status.
"""
lamps = []
data_dict = data.split()
while data_dict:
try:
fill = {'Hours': int(data_dict[0]), 'On': False if data_dict[1] == '0' else True}
except ValueError:
# In case of invalid entry
log.warning('({ip}) process_lamp(): Invalid data "{data}"'.format(ip=self.ip, data=data))
return
lamps.append(fill)
data_dict.pop(0) # Remove lamp hours
data_dict.pop(0) # Remove lamp on/off
lamp_list = data.split()
if len(lamp_list) < 2:
lamps.append({'Hours': int(lamp_list[0]), 'On': None})
else:
while lamp_list:
try:
fill = {'Hours': int(lamp_list[0]), 'On': False if lamp_list[1] == '0' else True}
except ValueError:
# In case of invalid entry
log.warning('({ip}) process_lamp(): Invalid data "{data}"'.format(ip=self.ip, data=data))
return
lamps.append(fill)
lamp_list.pop(0) # Remove lamp hours
lamp_list.pop(0) # Remove lamp on/off
self.lamp = lamps
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))
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):
"""
Power status. See PJLink specification for format.
@ -447,7 +497,7 @@ class PJLinkCommands(object):
self.send_command('INST')
else:
# 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
def process_rfil(self, data):
@ -457,9 +507,9 @@ class PJLinkCommands(object):
if self.model_filter is None:
self.model_filter = data
else:
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}) New model: '{new}'".format(ip=self.ip, new=data))
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}) New model: "{new}"'.format(ip=self.ip, new=data))
def process_rlmp(self, data):
"""
@ -468,9 +518,9 @@ class PJLinkCommands(object):
if self.model_lamp is None:
self.model_lamp = data
else:
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}) New lamp: '{new}'".format(ip=self.ip, new=data))
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}) New lamp: "{new}"'.format(ip=self.ip, new=data))
def process_snum(self, data):
"""
@ -479,16 +529,16 @@ class PJLinkCommands(object):
:param data: Serial number from projector.
"""
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.db_update = False
else:
# Compare serial numbers and see if we got the same projector
if self.serial_no != data:
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}) Received: '{new}'".format(ip=self.ip, new=data))
log.warning("({ip}) NOT saving 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}) Received: "{new}"'.format(ip=self.ip, new=data))
log.warning('({ip}) NOT saving serial number'.format(ip=self.ip))
self.serial_no_received = data
def process_sver(self, data):
@ -497,30 +547,29 @@ class PJLinkCommands(object):
"""
if len(data) > 32:
# Defined in specs max version is 32 characters
log.warning("Invalid software version - too long")
log.warning('Invalid software version - too long')
return
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.db_update = True
else:
# Compare software version and see if we got the same projector
if self.serial_no != data:
log.warning("({ip}) Projector software version does not match saved "
"software version".format(ip=self.ip))
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}) Saving new serial number as sw_version_received".format(ip=self.ip))
log.warning('({ip}) Projector software version does not match saved '
'software version'.format(ip=self.ip))
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}) Saving new serial number as sw_version_received'.format(ip=self.ip))
self.sw_version_received = data
class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
class PJLink(QtNetwork.QTcpSocket, PJLinkCommands):
"""
Socket service for PJLink TCP socket.
"""
# Signals sent by this module
changeStatus = QtCore.pyqtSignal(str, int, str)
projectorNetwork = QtCore.pyqtSignal(int) # Projector network activity
projectorStatus = QtCore.pyqtSignal(int) # Status update
projectorAuthentication = QtCore.pyqtSignal(str) # Authentication error
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 socket_timeout: Time (in seconds) to abort the connection if no response
"""
log.debug('PJlink(projector={projector}, args={args} kwargs={kwargs})'.format(projector=projector,
args=args,
kwargs=kwargs))
log.debug('PJlink(projector="{projector}", args="{args}" kwargs="{kwargs}")'.format(projector=projector,
args=args,
kwargs=kwargs))
super().__init__()
self.entry = projector
self.ip = self.entry.ip
@ -571,6 +620,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
self.widget = None # QListBox entry
self.timer = None # Timer that calls the poll_loop
self.send_queue = []
self.priority_queue = []
self.send_busy = False
# Socket timer for some possible brain-dead projectors or network cable pulled
self.socket_timer = None
@ -584,6 +634,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
self.connected.connect(self.check_login)
self.disconnected.connect(self.disconnect_from_host)
self.error.connect(self.get_error)
self.projectorReceivedData.connect(self._send_command)
def thread_stopped(self):
"""
@ -606,6 +657,10 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
self.projectorReceivedData.disconnect(self._send_command)
except TypeError:
pass
try:
self.readyRead.disconnect(self.get_socket) # Set in process_pjlink
except TypeError:
pass
self.disconnect_from_host()
self.deleteLater()
self.i_am_running = False
@ -623,10 +678,10 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
Retrieve information from projector that changes.
Normally called by timer().
"""
if self.state() != self.ConnectedState:
log.warning("({ip}) poll_loop(): Not connected - returning".format(ip=self.ip))
if self.state() != S_QSOCKET_STATE['ConnectedState']:
log.warning('({ip}) poll_loop(): Not connected - returning'.format(ip=self.ip))
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
if self.timer.interval() < self.poll_time:
# Reset timer to 5 seconds
@ -638,28 +693,28 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
if self.pjlink_class == '2':
check_list.extend(['FILT', 'FREZ'])
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
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:
self.send_command('INFO', queue=True)
self.send_command('INFO')
if self.manufacturer is None:
self.send_command('INF1', queue=True)
self.send_command('INF1')
if self.model is None:
self.send_command('INF2', queue=True)
self.send_command('INF2')
if self.pjlink_name is None:
self.send_command('NAME', queue=True)
self.send_command('NAME')
if self.pjlink_class == '2':
# Class 2 specific checks
if self.serial_no is None:
self.send_command('SNUM', queue=True)
self.send_command('SNUM')
if self.sw_version is None:
self.send_command('SVER', queue=True)
self.send_command('SVER')
if self.model_filter is None:
self.send_command('RFIL', queue=True)
self.send_command('RFIL')
if self.model_lamp is None:
self.send_command('RLMP', queue=True)
self.send_command('RLMP')
def _get_status(self, status):
"""
@ -711,14 +766,12 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
code=status_code,
message=status_message if msg is None else msg))
self.changeStatus.emit(self.ip, status, message)
self.projectorUpdateIcons.emit()
@QtCore.pyqtSlot()
def check_login(self, data=None):
"""
Processes the initial connection and authentication (if needed).
Starts poll timer if connection is established.
NOTE: Qt md5 hash function doesn't work with projector authentication. Use the python md5 hash function.
Processes the initial connection and convert to a PJLink packet if valid initial connection
:param data: Optional data if called from another routine
"""
@ -731,12 +784,12 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
self.change_status(E_SOCKET_TIMEOUT)
return
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:
log.warning('({ip}) read is None - socket error?'.format(ip=self.ip))
return
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
data = decode(read, 'utf-8')
# Possibility of extraneous data on input when reading.
@ -748,9 +801,16 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
# PJLink initial login will be:
# 'PJLink 0' - Unauthenticated login - no extra steps required.
# 'PJLink 1 XXXXXX' Authenticated login - extra processing required.
if not data.upper().startswith('PJLINK'):
# Invalid response
if not data.startswith('PJLINK'):
# Invalid initial packet - close socket
log.error('({ip}) Invalid initial packet received - closing socket'.format(ip=self.ip))
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:
# Processing a login reply
data_check = data.strip().split('=')
@ -799,18 +859,19 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
log.debug('({ip}) Starting timer'.format(ip=self.ip))
self.timer.setInterval(2000) # Set 2 seconds for initial information
self.timer.start()
"""
def _trash_buffer(self, msg=None):
"""
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
trash_count = 0
while self.bytesAvailable() > 0:
trash = self.read(self.max_size)
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))
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 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:
log.debug("({ip}) get_buffer() Don't know who data is for - exiting".format(ip=self.ip))
return
@ -840,39 +901,52 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
return
# Although we have a packet length limit, go ahead and use a larger buffer
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:
# No data available
log.debug('({ip}) get_socket(): No data available (-1)'.format(ip=self.ip))
return self.receive_data_signal()
self.socket_timer.stop()
self.projectorNetwork.emit(S_NETWORK_RECEIVED)
return self.get_data(buff=read, ip=self.ip)
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
:param buff: Data to process.
: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
data_in = decode(buff, 'utf-8')
data = data_in.strip()
if (len(data) < 7) or (not data.startswith(PJLINK_PREFIX)):
return self._trash_buffer(msg='get_data(): Invalid packet - length or prefix')
# Initial packet checks
if (len(data) < 7):
return self._trash_buffer(msg='get_data(): Invalid packet - length')
elif len(data) > self.max_size:
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:
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))
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:
version, cmd = header[1], header[2:]
version, cmd = header[1], header[2:].upper()
except ValueError as e:
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')
if cmd not in PJLINK_VALID_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):
log.warning('({ip}) get_data(): Projector returned class reply higher '
'than projector stated class'.format(ip=self.ip))
self.send_busy = False
return self.process_command(cmd, data)
@QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError)
@ -909,23 +984,21 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
self.reset_information()
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.
:param cmd: Command to send
:param opts: Command option (if any) - defaults to '?' (get information)
:param salt: Optional salt for md5 hash initial authentication
:param queue: Option to force add to queue rather than sending directly
:param priority: Option to send packet now rather than queue it up
"""
if self.state() != self.ConnectedState:
log.warning('({ip}) send_command(): Not connected - returning'.format(ip=self.ip))
self.send_queue = []
return
return self.reset_information()
if cmd not in PJLINK_VALID_CMD:
log.error('({ip}) send_command(): Invalid command requested - ignoring.'.format(ip=self.ip))
return
self.projectorNetwork.emit(S_NETWORK_SENDING)
log.debug('({ip}) send_command(): Building cmd="{command}" opts="{data}"{salt}'.format(ip=self.ip,
command=cmd,
data=opts,
@ -939,28 +1012,26 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
header = PJLINK_HEADER.format(linkclass=cmd_ver[0])
else:
# 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
out = '{salt}{header}{command} {options}{suffix}'.format(salt="" if salt is None else salt,
header=header,
command=cmd,
options=opts,
suffix=CR)
if out in self.send_queue:
# Already there, so don't add
log.debug('({ip}) send_command(out="{data}") Already in queue - skipping'.format(ip=self.ip,
data=out.strip()))
elif not queue and len(self.send_queue) == 0:
# Nothing waiting to send, so just send it
log.debug('({ip}) send_command(out="{data}") Sending data'.format(ip=self.ip, data=out.strip()))
return self._send_command(data=out)
if out in self.priority_queue:
log.debug('({ip}) send_command(): Already in priority queue - skipping'.format(ip=self.ip))
elif out in self.send_queue:
log.debug('({ip}) send_command(): Already in normal queue - skipping'.format(ip=self.ip))
else:
log.debug('({ip}) send_command(out="{data}") adding to queue'.format(ip=self.ip, data=out.strip()))
self.send_queue.append(out)
self.projectorReceivedData.emit()
log.debug('({ip}) send_command(): send_busy is {data}'.format(ip=self.ip, data=self.send_busy))
if not self.send_busy:
log.debug('({ip}) send_command() calling _send_string()'.format(ip=self.ip))
if priority:
log.debug('({ip}) send_command(): Adding to priority queue'.format(ip=self.ip))
self.priority_queue.append(out)
else:
log.debug('({ip}) send_command(): Adding to normal queue'.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()
@QtCore.pyqtSlot()
@ -971,44 +1042,53 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
:param data: Immediate data to send
:param utf8: Send as UTF-8 string otherwise send as ASCII string
"""
log.debug('({ip}) _send_string()'.format(ip=self.ip))
log.debug('({ip}) _send_string(): Connection status: {data}'.format(ip=self.ip, data=self.state()))
# Funny looking data check, but it's a quick check for data=None
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:
log.debug('({ip}) _send_string() Not connected - abort'.format(ip=self.ip))
self.send_queue = []
log.debug('({ip}) _send_command() Not connected - abort'.format(ip=self.ip))
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:
# 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
if data is not None:
out = data
log.debug('({ip}) _send_string(data="{data}")'.format(ip=self.ip, data=out.strip()))
if len(self.priority_queue) != 0:
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:
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:
# 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
return
self.send_busy = True
log.debug('({ip}) _send_string(): Sending "{data}"'.format(ip=self.ip, data=out.strip()))
log.debug('({ip}) _send_string(): Queue = {data}'.format(ip=self.ip, data=self.send_queue))
log.debug('({ip}) _send_command(): Sending "{data}"'.format(ip=self.ip, data=out.strip()))
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')))
self.waitForBytesWritten(2000) # 2 seconds should be enough
if sent == -1:
# 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,
translate('OpenLP.PJLink', 'Error while sending data to projector'))
self.disconnect_from_host()
def connect_to_host(self):
"""
Initiate connection to projector.
"""
log.debug('{ip}) connect_to_host(): Starting connection'.format(ip=self.ip))
if self.state() == self.ConnectedState:
log.warning('({ip}) connect_to_host(): Already connected - returning'.format(ip=self.ip))
return
@ -1024,22 +1104,19 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
if abort:
log.warning('({ip}) disconnect_from_host(): Aborting connection'.format(ip=self.ip))
else:
log.warning('({ip}) disconnect_from_host(): Not connected - returning'.format(ip=self.ip))
self.reset_information()
log.warning('({ip}) disconnect_from_host(): Not connected'.format(ip=self.ip))
self.disconnectFromHost()
try:
self.readyRead.disconnect(self.get_socket)
except TypeError:
pass
log.debug('({ip}) disconnect_from_host() '
'Current status {data}'.format(ip=self.ip, data=self._get_status(self.status_connect)[0]))
if abort:
self.change_status(E_NOT_CONNECTED)
else:
log.debug('({ip}) disconnect_from_host() '
'Current status {data}'.format(ip=self.ip, data=self._get_status(self.status_connect)[0]))
if self.status_connect != E_NOT_CONNECTED:
self.change_status(S_NOT_CONNECTED)
self.change_status(S_NOT_CONNECTED)
self.reset_information()
self.projectorUpdateIcons.emit()
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.i18n import translate
from openlp.core.lib import build_icon
from openlp.core.lib.projector.db import ProjectorSource
from openlp.core.lib.projector.constants import PJLINK_DEFAULT_SOURCES, PJLINK_DEFAULT_CODES
from openlp.core.projectors.db import ProjectorSource
from openlp.core.projectors.constants import PJLINK_DEFAULT_SOURCES, PJLINK_DEFAULT_CODES
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.settings import Settings
from openlp.core.lib import SettingsTab
from openlp.core.lib.projector import DialogSourceStyle
from openlp.core.projectors import DialogSourceStyle
log = logging.getLogger(__name__)
log.debug('projectortab module loaded')

View File

@ -115,9 +115,10 @@ from .formattingtagcontroller import FormattingTagController
from .shortcutlistform import ShortcutListForm
from .servicemanager import ServiceManager
from .thememanager import ThemeManager
from .projector.manager import ProjectorManager
from .projector.tab import ProjectorTab
from .projector.editform import ProjectorEditForm
from openlp.core.projectors.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',
'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.settings import Settings
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.widgets.edits import PathEdit
from openlp.core.widgets.enums import PathEditType
log = logging.getLogger(__name__)
@ -122,7 +123,7 @@ class AdvancedTab(SettingsTab):
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.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))
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)

View File

@ -72,10 +72,10 @@ except ImportError:
from openlp.core.common import is_linux
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.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
@ -155,7 +155,7 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
try:
with file_path.open('w') as report_file:
report_file.write(report_text)
except IOError:
except OSError:
log.exception('Failed to write crash report')
def on_send_report_button_clicked(self):

View File

@ -25,7 +25,8 @@ The file rename dialog.
from PyQt5 import QtCore, QtWidgets
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

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.i18n import translate
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.lib import PluginStatus, build_icon
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',
'Alerts Display informative messages while showing other slides'))
self.projectors_check_box.setText(translate('OpenLP.FirstTimeWizard',
'Projectors Control PJLink compatible projects on your network'
' from OpenLP'))
'Projector Controller Control PJLink compatible projects on your'
' network from OpenLP'))
self.no_internet_page.setTitle(translate('OpenLP.FirstTimeWizard', 'No Internet Connection'))
self.no_internet_page.setSubTitle(
translate('OpenLP.FirstTimeWizard', 'Unable to detect an Internet connection.'))

View File

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

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 logging
import os
from PyQt5 import QtCore, QtWidgets, QtWebKit, QtWebKitWidgets, QtGui, QtMultimedia
from openlp.core.common import is_macosx, is_win
from openlp.core.common.applocation import AppLocation
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.registry import Registry, RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList
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
class MainDisplay(OpenLPMixin, Display, RegistryProperties):
class MainDisplay(Display, LogMixin, RegistryProperties):
"""
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):
"""
Generates a preview of the image displayed.
:rtype: QtGui.QPixmap
"""
was_visible = self.isVisible()
self.application.process_events()
@ -488,8 +489,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
service_item = ServiceItem()
service_item.title = 'webkit'
service_item.processor = 'webkit'
path = os.path.join(str(AppLocation.get_section_data_path('themes')),
self.service_item.theme_data.theme_name)
path = str(AppLocation.get_section_data_path('themes') / self.service_item.theme_data.theme_name)
service_item.add_from_command(path,
path_to_str(self.service_item.theme_data.background_filename),
':/media/slidecontroller_multimedia.png')
@ -603,7 +603,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
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.
"""

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.applocation import AppLocation
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.registry import Registry, RegistryProperties
from openlp.core.common.path import Path, copyfile, create_paths
from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList
from openlp.core.display.renderer import Renderer
from openlp.core.lib import PluginManager, ImageManager, PluginStatus, build_icon
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, \
ShortcutListForm, FormattingTagForm, PreviewController
from openlp.core.ui.firsttimeform import FirstTimeForm
from openlp.core.ui.lib.dockwidget import OpenLPDockWidget
from openlp.core.ui.lib.filedialog import FileDialog
from openlp.core.ui.lib.mediadockmanager import MediaDockManager
from openlp.core.widgets.dialogs import FileDialog
from openlp.core.widgets.docks import OpenLPDockWidget, MediaDockManager
from openlp.core.ui.media import MediaController
from openlp.core.ui.printserviceform import PrintServiceForm
from openlp.core.ui.projector.manager import ProjectorManager
from openlp.core.ui.style import PROGRESSBAR_STYLE, get_library_stylesheet
from openlp.core.version import get_version
@ -180,7 +180,7 @@ class Ui_MainWindow(object):
triggers=self.service_manager_contents.on_load_service_clicked)
self.file_save_item = create_action(main_window, 'fileSaveItem', icon=':/general/general_save.png',
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,
category=UiStrings().File,
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
self.about_item.setMenuRole(QtWidgets.QAction.AboutRole)
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():
self.local_help_file = os.path.join(str(AppLocation.get_directory(AppLocation.AppDir)),
'..', 'Resources', 'OpenLP.help')
self.local_help_file = AppLocation.get_directory(AppLocation.AppDir) / '..' / 'Resources' / 'OpenLP.help'
self.user_manual_item = create_action(main_window, 'userManualItem', icon=':/system/system_help_contents.png',
can_shortcuts=True, category=UiStrings().Help,
triggers=self.on_help_clicked)
@ -375,7 +374,7 @@ class Ui_MainWindow(object):
self.media_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Library'))
self.service_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Service'))
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.setToolTip(UiStrings().NewService)
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 '
'this or another machine.'))
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.setStatusTip(translate('OpenLP.MainWindow',
'Toggle visibility of the Projectors.'))
@ -505,9 +504,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
Settings().set_up_default_values()
self.about_form = AboutForm(self)
MediaController()
if Registry().get_flag('no_web_server'):
websockets.WebSocketServer()
server.HttpServer()
websockets.WebSocketServer()
server.HttpServer()
SettingsForm(self)
self.formatting_tag_form = FormattingTagForm(self)
self.shortcut_form = ShortcutListForm(self)
@ -650,8 +648,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
self.application.process_events()
plugin.first_time()
self.application.process_events()
temp_dir = os.path.join(str(gettempdir()), 'openlp')
shutil.rmtree(temp_dir, True)
temp_path = Path(gettempdir(), 'openlp')
temp_path.rmtree(True)
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)
"""
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:
import webbrowser
webbrowser.open_new('http://manual.openlp.org/')
@ -1220,7 +1218,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
settings.remove('custom slide')
settings.remove('service')
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.beginGroup(self.ui_settings_section)
self.move(settings.value('main window position'))
@ -1244,7 +1242,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
log.debug('Saving QSettings')
settings = Settings()
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.beginGroup(self.ui_settings_section)
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.
"""
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()
for file_id, filename in enumerate(recent_files_to_display):
log.debug('Recent file name: {name}'.format(name=filename))
count = 0
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, '',
text='&{n} {name}'.format(n=file_id + 1,
name=os.path.splitext(os.path.basename(str(filename)))[0]),
data=filename,
triggers=self.service_manager_contents.on_recent_service_clicked)
text='&{n} {name}'.format(n=count, name=recent_path.name),
data=recent_path, triggers=self.service_manager_contents.on_recent_service_clicked)
self.recent_files_menu.addAction(action)
clear_recent_files_action = create_action(self, '',
text=translate('OpenLP.MainWindow', 'Clear List', 'Clear List of '
'recent files'),
statustip=translate('OpenLP.MainWindow', 'Clear the list of recent '
'files.'),
enabled=bool(self.recent_files),
triggers=self.clear_recent_file_menu)
if count == recent_file_count:
break
clear_recent_files_action = \
create_action(self, '', text=translate('OpenLP.MainWindow', 'Clear List', 'Clear List of recent files'),
statustip=translate('OpenLP.MainWindow', 'Clear the list of recent files.'),
enabled=bool(self.recent_files), triggers=self.clear_recent_file_menu)
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):
"""
@ -1291,20 +1287,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
# actually stored in the settings therefore the default value of 20 will
# always be used.
max_recent_files = Settings().value('advanced/max recent files')
if filename:
# Add some cleanup to reduce duplication in the recent file list
filename = os.path.abspath(filename)
# abspath() only capitalises the drive letter if it wasn't provided
# in the given filename which then causes duplication.
if filename[1:3] == ':\\':
filename = filename[0].upper() + filename[1:]
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()
file_path = Path(filename)
# Some cleanup to reduce duplication in the recent file list
file_path = file_path.resolve()
if file_path in self.recent_files:
self.recent_files.remove(file_path)
self.recent_files.insert(0, file_path)
self.recent_files = self.recent_files[:max_recent_files]
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))
dir_util.copy_tree(str(old_data_path), str(self.new_data_path))
log.info('Copy successful')
except (IOError, os.error, DistutilsFileError) as why:
except (OSError, DistutilsFileError) as why:
self.application.set_normal_cursor()
log.exception('Data copy failed {err}'.format(err=str(why)))
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.common import extension_loader
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.mixins import OpenLPMixin, RegistryMixin
from openlp.core.common.registry import Registry, RegistryProperties
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings
from openlp.core.lib import ItemCapabilities
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 import MediaState, MediaInfo, MediaType, get_media_players, set_media_players,\
parse_optical_path
from openlp.core.ui.lib.toolbar import OpenLPToolbar
from openlp.core.widgets.toolbar import OpenLPToolbar
log = logging.getLogger(__name__)
@ -91,7 +91,7 @@ class MediaSlider(QtWidgets.QSlider):
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.
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.
:return: True if setup succeeded else False.
"""
if controller is None:
controller = self.display_controllers[DisplayControllerType.Plugin]
# stop running videos
self.media_reset(controller)
# Setup media info
@ -509,9 +507,9 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
controller.media_info.media_type = MediaType.CD
else:
controller.media_info.media_type = MediaType.DVD
controller.media_info.start_time = start // 1000
controller.media_info.end_time = end // 1000
controller.media_info.length = (end - start) // 1000
controller.media_info.start_time = start
controller.media_info.end_time = end
controller.media_info.length = (end - start)
controller.media_info.title_track = title
controller.media_info.audio_track = audio_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.
"""
from openlp.core.common.registry import RegistryProperties
from openlp.core.common.mixins import RegistryProperties
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.
"""
import platform
from PyQt5 import QtCore, QtWidgets
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.ui import create_button
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):

View File

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

View File

@ -280,7 +280,8 @@ class VlcPlayer(MediaPlayer):
start_time = controller.media_info.start_time
log.debug('mediatype: ' + str(controller.media_info.media_type))
# Set tracks for the optical device
if controller.media_info.media_type == MediaType.DVD:
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')
if controller.media_info.title_track > 0:
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 \
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():
display.vlc_media_player.set_time(seek_value)
@ -386,15 +387,15 @@ class VlcPlayer(MediaPlayer):
self.stop(display)
controller = display.controller
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.set_visible(display, False)
if not controller.seek_slider.isSliderDown():
controller.seek_slider.blockSignals(True)
if display.controller.media_info.media_type == MediaType.CD \
or display.controller.media_info.media_type == MediaType.DVD:
controller.seek_slider.setSliderPosition(display.vlc_media_player.get_time() -
int(display.controller.media_info.start_time * 1000))
controller.seek_slider.setSliderPosition(
display.vlc_media_player.get_time() - int(display.controller.media_info.start_time))
else:
controller.seek_slider.setSliderPosition(display.vlc_media_player.get_time())
controller.seek_slider.blockSignals(False)

View File

@ -27,7 +27,7 @@ import logging
from PyQt5 import QtCore, QtWidgets
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.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.lib import build_icon
from openlp.core.ui.lib import SpellTextEdit
from openlp.core.widgets.edits import SpellTextEdit
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.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.lib import get_text_file_string
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 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

View File

@ -32,19 +32,19 @@ from tempfile import mkstemp
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.applocation import AppLocation
from openlp.core.common.i18n import UiStrings, format_time, translate
from openlp.core.common.mixins import OpenLPMixin, RegistryMixin
from openlp.core.common.path import Path, create_paths, path_to_str, str_to_path
from openlp.core.common.registry import Registry, RegistryProperties
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import Path, create_paths, str_to_path
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings
from openlp.core.lib import ServiceItem, ItemCapabilities, PluginStatus, build_icon
from openlp.core.lib.ui import critical_error_message_box, create_widget_action, find_and_set_in_combo_box
from openlp.core.ui import ServiceNoteForm, ServiceItemEditForm, StartTimeForm
from openlp.core.ui.lib import OpenLPToolbar
from openlp.core.ui.lib.filedialog import FileDialog
from openlp.core.widgets.dialogs import FileDialog
from openlp.core.widgets.toolbar import OpenLPToolbar
class ServiceManagerList(QtWidgets.QTreeWidget):
@ -56,8 +56,24 @@ class ServiceManagerList(QtWidgets.QTreeWidget):
Constructor
"""
super(ServiceManagerList, self).__init__(parent)
self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
self.setAlternatingRowColors(True)
self.setHeaderHidden(True)
self.setExpandsOnDoubleClick(False)
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):
"""
Capture Key press and respond accordingly.
@ -117,7 +133,7 @@ class Ui_ServiceManager(object):
self.layout.setSpacing(0)
self.layout.setContentsMargins(0, 0, 0, 0)
# 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',
tooltip=UiStrings().CreateService, triggers=self.on_new_service_clicked)
self.toolbar.add_toolbar_action('openService', text=UiStrings().OpenService,
@ -147,78 +163,60 @@ class Ui_ServiceManager(object):
QtWidgets.QAbstractItemView.CurrentChanged |
QtWidgets.QAbstractItemView.DoubleClicked |
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.customContextMenuRequested.connect(self.context_menu)
self.service_manager_list.setObjectName('service_manager_list')
# enable drop
self.service_manager_list.__class__.dragEnterEvent = lambda x, event: event.accept()
self.service_manager_list.__class__.dragMoveEvent = lambda x, event: event.accept()
self.service_manager_list.__class__.dropEvent = self.drop_event
self.service_manager_list.dropEvent = self.drop_event
self.layout.addWidget(self.service_manager_list)
# Add the bottom toolbar
self.order_toolbar = OpenLPToolbar(widget)
action_list = ActionList.get_instance()
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',
text=translate('OpenLP.ServiceManager', 'Move to &top'), icon=':/services/service_top.png',
tooltip=translate('OpenLP.ServiceManager', 'Move item to the top of the service.'),
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',
text=translate('OpenLP.ServiceManager', 'Move &up'), icon=':/services/service_up.png',
tooltip=translate('OpenLP.ServiceManager', 'Move item up one position in the service.'),
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',
text=translate('OpenLP.ServiceManager', 'Move &down'), icon=':/services/service_down.png',
tooltip=translate('OpenLP.ServiceManager', 'Move item down one position in the service.'),
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',
text=translate('OpenLP.ServiceManager', 'Move to &bottom'), icon=':/services/service_bottom.png',
tooltip=translate('OpenLP.ServiceManager', 'Move item to the end of the service.'),
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.service_manager_list.delete = self.order_toolbar.add_toolbar_action(
self.delete_action = self.order_toolbar.add_toolbar_action(
'delete', can_shortcuts=True,
text=translate('OpenLP.ServiceManager', '&Delete From Service'), icon=':/general/general_delete.png',
tooltip=translate('OpenLP.ServiceManager', 'Delete the selected item from the service.'),
triggers=self.on_delete_from_service)
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,
text=translate('OpenLP.ServiceManager', '&Expand all'), icon=':/services/service_expand_all.png',
tooltip=translate('OpenLP.ServiceManager', 'Expand all the service items.'),
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,
text=translate('OpenLP.ServiceManager', '&Collapse all'), icon=':/services/service_collapse_all.png',
tooltip=translate('OpenLP.ServiceManager', 'Collapse all the service items.'),
category=UiStrings().Service, triggers=self.on_collapse_all)
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,
text=translate('OpenLP.ServiceManager', 'Go Live'), icon=':/general/general_live.png',
tooltip=translate('OpenLP.ServiceManager', 'Send the selected item to Live.'),
category=UiStrings().Service,
triggers=self.make_live)
triggers=self.on_make_live_action_triggered)
self.layout.addWidget(self.order_toolbar)
# Connect up our signals and slots
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',
triggers=self.on_auto_start)
# 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,
text=translate('OpenLP.ServiceManager', 'Create New &Custom '
'Slide'),
@ -285,28 +283,20 @@ class Ui_ServiceManager(object):
self.preview_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', 'Show &Preview'),
icon=':/general/general_preview.png', triggers=self.make_preview)
# 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.theme_menu = QtWidgets.QMenu(translate('OpenLP.ServiceManager', '&Change Item Theme'))
self.menu.addMenu(self.theme_menu)
self.service_manager_list.addActions(
[self.service_manager_list.move_down,
self.service_manager_list.move_up,
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
])
self.service_manager_list.addActions([self.move_down_action, self.move_up_action, self.make_live_action,
self.move_top_action, self.move_bottom_action, self.expand_action,
self.collapse_action])
Registry().register_function('theme_update_list', self.update_theme_list)
Registry().register_function('config_screen_changed', self.regenerate_service_items)
Registry().register_function('theme_update_global', self.theme_change)
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
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.
"""
super(ServiceManager, self).__init__(parent)
super().__init__(parent)
self.active = build_icon(':/media/auto-start_active.png')
self.inactive = build_icon(':/media/auto-start_inactive.png')
self.service_items = []
@ -329,7 +319,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
self.service_id = 0
# is a new service and has not been saved
self._modified = False
self._file_name = ''
self._service_path = None
self.service_has_all_original_files = True
self.list_double_clicked = False
@ -360,7 +350,10 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
if modified:
self.service_id += 1
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)
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
:rtype: None
"""
self._file_name = path_to_str(file_path)
self.main_window.set_service_modified(self.is_modified(), self.short_file_name())
self._service_path = file_path
self.main_window.set_service_modified(self.is_modified(), file_path.name)
Settings().setValue('servicemanager/last file', file_path)
if file_path and file_path.suffix == '.oszl':
self._save_lite = True
@ -387,14 +380,17 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
def file_name(self):
"""
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):
"""
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):
"""
@ -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.
: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()
self.load_file(sender.data())
@ -601,7 +603,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
if not os.path.exists(save_file):
shutil.copy(audio_from, save_file)
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.main_window.error_message(translate('OpenLP.ServiceManager', 'Error Saving 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)
# First we add service contents.
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.main_window.error_message(translate('OpenLP.ServiceManager', 'Error Saving 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')
if directory_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
# 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(
self.main_window, UiStrings().SaveService, default_file_path,
translate('OpenLP.ServiceManager',
'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)'))
'{packaged};; {lite}'.format(packaged=packaged_filter, lite=lite_filter),
default_filter)
else:
file_path, filter_used = FileDialog.getSaveFileName(
self.main_window, UiStrings().SaveService, file_path,
translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;'))
self.main_window, UiStrings().SaveService, default_file_path,
'{packaged};;'.format(packaged=packaged_filter))
if not file_path:
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.decide_save_method()
@ -789,11 +800,11 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
else:
critical_error_message_box(message=translate('OpenLP.ServiceManager', 'File is not a valid service.'))
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))
critical_error_message_box(message=translate('OpenLP.ServiceManager',
'File could not be opened because it is corrupt.'))
except zipfile.BadZipfile:
except zipfile.BadZipFile:
if os.path.getsize(file_name) == 0:
self.log_exception('Service file is zero sized: {name}'.format(name=file_name))
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:
return
if item is None:
end_pos = len(self.service_items)
end_pos = len(self.service_items) - 1
else:
end_pos = get_parent_item_data(item) - 1
service_item = self.service_items[start_pos]
self.service_items.remove(service_item)
self.service_items.insert(end_pos, service_item)
self.repaint_service_list(end_pos, child)
self.set_modified()
if start_pos != end_pos:
self.service_items.remove(service_item)
self.service_items.insert(end_pos, service_item)
self.repaint_service_list(end_pos, child)
self.set_modified()
else:
# we are not over anything so drop
replace = False
@ -1728,6 +1740,15 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
self.service_items[item]['service_item'].update_theme(theme)
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):
"""
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 openlp.core.common.i18n import translate
from openlp.core.common.registry import Registry, RegistryProperties
from openlp.core.ui.lib import SpellTextEdit
from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.lib.ui import create_button_box
from openlp.core.widgets.edits import SpellTextEdit
class ServiceNoteForm(QtWidgets.QDialog, RegistryProperties):

View File

@ -27,11 +27,12 @@ import logging
from PyQt5 import QtCore, QtWidgets
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.projectors.tab import ProjectorTab
from openlp.core.ui import AdvancedTab, GeneralTab, ThemesTab
from openlp.core.ui.media import PlayerTab
from openlp.core.ui.projector.tab import ProjectorTab
from openlp.core.ui.settingsdialog import Ui_SettingsDialog
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.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.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
"""
import copy
import os
from collections import deque
@ -33,15 +32,15 @@ from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import SlideLimits
from openlp.core.common.actions import ActionList, CategoryOrder
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.mixins import OpenLPMixin, RegistryMixin
from openlp.core.common.registry import Registry, RegistryProperties
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList
from openlp.core.lib import ItemCapabilities, ServiceItem, ImageSource, ServiceItemAction, build_icon, build_html
from openlp.core.lib.ui import create_action
from openlp.core.ui.lib.toolbar import OpenLPToolbar
from openlp.core.ui.lib.listpreviewwidget import ListPreviewWidget
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.
@ -82,11 +81,11 @@ class DisplayController(QtWidgets.QWidget):
"""
Controller is a general display controller widget.
"""
def __init__(self, parent):
def __init__(self, *args, **kwargs):
"""
Set up the general Controller.
"""
super(DisplayController, self).__init__(parent)
super().__init__(*args, **kwargs)
self.is_live = False
self.display = None
self.controller_type = None
@ -133,16 +132,16 @@ class InfoLabel(QtWidgets.QLabel):
super().setText(text)
class SlideController(DisplayController, RegistryProperties):
class SlideController(DisplayController, LogMixin, RegistryProperties):
"""
SlideController is the slide controller widget. This widget is what the
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.
"""
super(SlideController, self).__init__(parent)
super().__init__(*args, **kwargs)
def post_set_up(self):
"""
@ -237,6 +236,9 @@ class SlideController(DisplayController, RegistryProperties):
self.hide_menu.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
self.hide_menu.setMenu(QtWidgets.QMenu(translate('OpenLP.SlideController', 'Hide'), self.toolbar))
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.
self.desktop_screen_enable = create_action(self, 'desktopScreenEnable',
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.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):
"""
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())
class PreviewController(RegistryMixin, OpenLPMixin, SlideController):
class PreviewController(RegistryBase, SlideController):
"""
Set up the Preview Controller.
"""
@ -1521,11 +1532,12 @@ class PreviewController(RegistryMixin, OpenLPMixin, SlideController):
slidecontroller_preview_next = QtCore.pyqtSignal()
slidecontroller_preview_previous = QtCore.pyqtSignal()
def __init__(self, parent):
def __init__(self, *args, **kwargs):
"""
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.type_prefix = 'preview'
self.category = 'Preview Toolbar'
@ -1537,7 +1549,7 @@ class PreviewController(RegistryMixin, OpenLPMixin, SlideController):
self.post_set_up()
class LiveController(RegistryMixin, OpenLPMixin, SlideController):
class LiveController(RegistryBase, SlideController):
"""
Set up the Live Controller.
"""
@ -1549,11 +1561,11 @@ class LiveController(RegistryMixin, OpenLPMixin, SlideController):
mediacontroller_live_pause = QtCore.pyqtSignal()
mediacontroller_live_stop = QtCore.pyqtSignal()
def __init__(self, parent):
def __init__(self, *args, **kwargs):
"""
Set up the base Controller as a live.
"""
super(LiveController, self).__init__(parent)
super().__init__(*args, **kwargs)
self.is_live = True
self.split = 1
self.type_prefix = 'live'

View File

@ -25,7 +25,8 @@ The actual start time form.
from PyQt5 import QtCore, QtWidgets
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.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.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.ui import critical_error_message_box
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.applocation import AppLocation
from openlp.core.common.i18n import UiStrings, translate, get_locale_key
from openlp.core.common.mixins import OpenLPMixin, RegistryMixin
from openlp.core.common.path import Path, copyfile, create_paths, path_to_str, rmtree
from openlp.core.common.registry import Registry, RegistryProperties
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import Path, copyfile, create_paths, path_to_str
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings
from openlp.core.lib import ImageSource, ValidationError, get_text_file_string, build_icon, \
check_item_selected, create_thumb, validate_thumb
from openlp.core.lib.theme import Theme, BackgroundType
from openlp.core.lib.ui import critical_error_message_box, create_widget_action
from openlp.core.ui import FileRenameForm, ThemeForm
from openlp.core.ui.lib import OpenLPToolbar
from openlp.core.ui.lib.filedialog import FileDialog
from openlp.core.widgets.dialogs import FileDialog
from openlp.core.widgets.toolbar import OpenLPToolbar
class Ui_ThemeManager(object):
@ -125,7 +125,7 @@ class Ui_ThemeManager(object):
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.
"""
@ -376,7 +376,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
delete_file(self.theme_path / thumb)
delete_file(self.thumb_path / thumb)
try:
rmtree(self.theme_path / theme)
(self.theme_path / theme).rmtree()
except OSError:
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}')
.format(err=ose.strerror))
if theme_path.exists():
rmtree(theme_path, True)
theme_path.rmtree(ignore_errors=True)
return False
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)
else:
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)
if validate_thumb(theme_path, thumb):
icon = build_icon(thumb)
if validate_thumb(theme_path, thumb_path):
icon = build_icon(thumb_path)
else:
icon = create_thumb(str(theme_path), str(thumb))
icon = create_thumb(theme_path, thumb_path)
item_name.setIcon(icon)
item_name.setData(QtCore.Qt.UserRole, text_name)
self.theme_list_widget.addItem(item_name)
@ -604,7 +604,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
else:
with full_name.open('wb') as out_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))
raise 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)
try:
theme_path.write_text(theme_pretty)
except IOError:
except OSError:
self.log_exception('Saving theme to file failed')
if image_source_path and image_destination_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:
try:
copyfile(image_source_path, image_destination_path)
except IOError:
except OSError:
self.log_exception('Failed to save theme image')
self.generate_and_save_image(name, theme)
@ -692,7 +692,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
sample_path_name.unlink()
frame.save(str(sample_path_name), 'png')
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):
"""
@ -711,6 +711,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
:param theme_data: The theme to generated a preview for.
: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)

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.theme import HorizontalType, BackgroundType, BackgroundGradientType
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):

View File

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

View File

@ -20,15 +20,41 @@
# 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
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__)
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):
"""
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 #
# 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',
'OpenLPToolbar', 'OpenLPWizard', 'PathEdit', 'PathType', 'SpellTextEdit', 'TreeWidgetWithDnD',
'WizardStrings']
class PathEditType(Enum):
Files = 1
Directories = 2

View File

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

View File

@ -23,10 +23,14 @@
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.
"""
import os
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.lib import ImageSource, ItemCapabilities, ServiceItem
@ -238,3 +242,241 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties):
Returns the number of slides this widget holds.
"""
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.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.lib import build_icon
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__)

View File

@ -26,12 +26,12 @@ displaying of alerts.
from PyQt5 import QtCore
from openlp.core.common.i18n import translate
from openlp.core.common.mixins import OpenLPMixin, RegistryMixin
from openlp.core.common.registry import Registry, RegistryProperties
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry, RegistryBase
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.
"""

View File

@ -26,7 +26,7 @@ from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.settings import Settings
from openlp.core.lib import SettingsTab
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):

View File

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

View File

@ -26,7 +26,7 @@ import re
from PyQt5 import QtCore, QtWidgets
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 .editbibledialog import Ui_EditBibleDialog
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})?' \
'(?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)
REFERENCE_MATCHES['range'] = re.compile(r'^\s*{range}\s*$'.format(range=range_regex), re.UNICODE)
REFERENCE_MATCHES['range_separator'] = re.compile(REFERENCE_SEPARATORS['sep_l'], 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'])
# full reference match: <book>(<range>(,(?!$)|(?=$)))+
REFERENCE_MATCHES['full'] = \
re.compile(r'^\s*(?!\s)(?P<book>[\d]*[.]?[^\d\.]+)\.*(?<!\s)\s*'
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):

View File

@ -23,37 +23,37 @@
from lxml import etree, objectify
from zipfile import is_zipfile
from openlp.core.common.mixins import OpenLPMixin
from openlp.core.common.registry import Registry, RegistryProperties
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.common.i18n import get_language, translate
from openlp.core.lib import ValidationError
from openlp.core.lib.ui import critical_error_message_box
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
"""
def __init__(self, *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.stop_import_flag = False
Registry().register_function('openlp_stop_wizard', self.stop_import)
@staticmethod
def is_compressed(file):
def is_compressed(file_path):
"""
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(
message=translate('BiblesPlugin.BibleImport',
'The file "{file}" you supplied is compressed. You must decompress it before import.'
).format(file=file))
).format(file=file_path))
return True
return False
@ -96,6 +96,8 @@ class BibleImport(OpenLPMixin, RegistryProperties, BibleDB):
if language_form.exec(bible_name):
combo_box = language_form.language_combo_box
language_id = combo_box.itemData(combo_box.currentIndex())
else:
return False
if not language_id:
return None
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')
book_ref_id = guess_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)
if book_details is None:
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'])
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.
: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 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.
:return: The root element of the xml document
"""
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
# encoding detection, and the two mechanisms together interfere with each other.
if not use_objectify:
@ -205,17 +207,17 @@ class BibleImport(OpenLPMixin, RegistryProperties, BibleDB):
self.log_debug('Stopping import')
self.stop_import_flag = True
def validate_xml_file(self, filename, tag):
def validate_xml_file(self, file_path, tag):
"""
Validate the supplied file
:param filename: The supplied file
:param file_path: The supplied file
:param tag: The expected root tag type
:return: True if valid. ValidationError is raised otherwise.
"""
if BibleImport.is_compressed(filename):
if BibleImport.is_compressed(file_path):
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:
raise ValidationError(msg='Error when opening file')
root_tag = bible.tag.lower()

View File

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

View File

@ -19,7 +19,6 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
import chardet
import logging
import os
@ -36,6 +35,7 @@ from sqlalchemy.orm.exc import UnmappedClassError
from openlp.core.common import clean_filename
from openlp.core.common.applocation import AppLocation
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.ui import critical_error_message_box
from openlp.plugins.bibles.lib import BibleStrings, LanguageSelection, upgrade
@ -130,10 +130,15 @@ class BibleDB(Manager):
:param parent:
:param kwargs:
``path``
The path to the bible database file.
The path to the bible database file. Type: openlp.core.common.path.Path
``name``
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')
self._setup(parent, **kwargs)
@ -146,20 +151,20 @@ class BibleDB(Manager):
self.session = None
if 'path' not in kwargs:
raise KeyError('Missing keyword argument "path".')
self.path = kwargs['path']
if 'name' not in kwargs and 'file' not in kwargs:
raise KeyError('Missing keyword argument "name" or "file".')
if 'name' in kwargs:
self.name = kwargs['name']
if not isinstance(self.name, str):
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:
self.file = kwargs['file']
Manager.__init__(self, 'bibles', init_schema, self.file, upgrade)
file_path = kwargs['file']
Manager.__init__(self, 'bibles', init_schema, file_path, upgrade)
if self.session and 'file' in kwargs:
self.get_name()
if 'path' in kwargs:
self.path = kwargs['path']
self._is_web_bible = None
def get_name(self):
@ -308,8 +313,7 @@ class BibleDB(Manager):
book_escaped = book
for character in RESERVED_CHARACTERS:
book_escaped = book_escaped.replace(character, '\\' + character)
regex_book = re.compile('\\s*{book}\\s*'.format(book='\\s*'.join(book_escaped.split())),
re.UNICODE | re.IGNORECASE)
regex_book = re.compile('\\s*{book}\\s*'.format(book='\\s*'.join(book_escaped.split())), re.IGNORECASE)
if language_selection == LanguageSelection.Bible:
db_book = self.get_book(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.
"""
if BiblesResourcesDB.cursor is None:
file_path = os.path.join(str(AppLocation.get_directory(AppLocation.PluginsDir)),
'bibles', 'resources', 'bibles_resources.sqlite')
conn = sqlite3.connect(file_path)
file_path = \
AppLocation.get_directory(AppLocation.PluginsDir) / 'bibles' / 'resources' / 'bibles_resources.sqlite'
conn = sqlite3.connect(str(file_path))
BiblesResourcesDB.cursor = conn.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 AlternativeBookNamesDB.cursor is None:
file_path = os.path.join(
str(AppLocation.get_directory(AppLocation.DataDir)), 'bibles', 'alternative_book_names.sqlite')
if not os.path.exists(file_path):
file_path = AppLocation.get_directory(AppLocation.DataDir) / 'bibles' / 'alternative_book_names.sqlite'
AlternativeBookNamesDB.conn = sqlite3.connect(str(file_path))
if not file_path.exists():
# create new DB, create table alternative_book_names
AlternativeBookNamesDB.conn = sqlite3.connect(file_path)
AlternativeBookNamesDB.conn.execute(
'CREATE TABLE alternative_book_names(id INTEGER NOT NULL, '
'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()
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)
self.log_info(self.__class__.__name__)
self.books_file = kwargs['booksfile']
self.verses_file = kwargs['versefile']
self.books_path = kwargs['books_path']
self.verses_path = kwargs['verse_path']
@staticmethod
def get_book_name(name, books):
@ -92,21 +92,22 @@ class CSVBible(BibleImport):
return book_name
@staticmethod
def parse_csv_file(filename, results_tuple):
def parse_csv_file(file_path, results_tuple):
"""
Parse the supplied CSV file.
:param filename: The name of the file to parse. Str
:param results_tuple: The namedtuple to use to store the results. namedtuple
:return: An iterable yielding namedtuples of type results_tuple
:param openlp.core.common.path.Path file_path: The name of the file to parse.
:param namedtuple results_tuple: The namedtuple to use to store the results.
:return: An list of namedtuples of type results_tuple
:rtype: list[namedtuple]
"""
try:
encoding = get_file_encoding(Path(filename))['encoding']
with open(filename, 'r', encoding=encoding, newline='') as csv_file:
encoding = get_file_encoding(file_path)['encoding']
with file_path.open('r', encoding=encoding, newline='') as csv_file:
csv_reader = csv.reader(csv_file, delimiter=',', quotechar='"')
return [results_tuple(*line) for line in csv_reader]
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):
"""
@ -159,12 +160,12 @@ class CSVBible(BibleImport):
self.language_id = self.get_language(bible_name)
if not self.language_id:
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.setMinimum(0)
self.wizard.progress_bar.setMaximum(len(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.setMaximum(len(books) + 1)
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.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.plugins.bibles.lib import SearchResults
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 previous_number: The previous chapter number
:return: Number of current chapter. (Int)
:return: Number of current chapter.
:rtype: int
"""
if number:
return int(number.split()[-1])
@ -132,13 +133,13 @@ class OpenSongBible(BibleImport):
:param bible_name: The name of the bible being imported
:return: True if import completed, False if import was unsuccessful
"""
self.log_debug('Starting OpenSong import from "{name}"'.format(name=self.filename))
self.validate_xml_file(self.filename, 'bible')
bible = self.parse_xml(self.filename, use_objectify=True)
self.log_debug('Starting OpenSong import from "{name}"'.format(name=self.file_path))
self.validate_xml_file(self.file_path, 'bible')
bible = self.parse_xml(self.file_path, use_objectify=True)
if bible is None:
return False
# 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:
return False
self.process_books(bible.b)

View File

@ -159,14 +159,14 @@ class OSISBible(BibleImport):
"""
Loads a Bible from file.
"""
self.log_debug('Starting OSIS import from "{name}"'.format(name=self.filename))
self.validate_xml_file(self.filename, '{http://www.bibletechnologies.net/2003/osis/namespace}osis')
bible = self.parse_xml(self.filename, elements=REMOVABLE_ELEMENTS, tags=REMOVABLE_TAGS)
self.log_debug('Starting OSIS import from "{name}"'.format(name=self.file_path))
self.validate_xml_file(self.file_path, '{http://www.bibletechnologies.net/2003/osis/namespace}osis')
bible = self.parse_xml(self.file_path, elements=REMOVABLE_ELEMENTS, tags=REMOVABLE_TAGS)
if bible is None:
return False
# Find bible language
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:
return False
self.process_books(bible)

View File

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

View File

@ -45,13 +45,13 @@ class ZefaniaBible(BibleImport):
"""
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
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
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:
return False
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.')
book_ref_id = int(bnumber)
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
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'])

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