forked from openlp/openlp
HEAD
This commit is contained in:
commit
187714cd87
15
nose2.cfg
15
nose2.cfg
@ -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
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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}')
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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():
|
||||
|
@ -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'),
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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())
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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.
|
||||
"""
|
||||
|
||||
|
@ -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.
|
@ -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:
|
@ -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')
|
@ -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)
|
@ -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):
|
||||
"""
|
@ -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__)
|
||||
|
@ -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')
|
@ -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',
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.'))
|
||||
|
@ -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):
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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())]
|
@ -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)
|
@ -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)
|
@ -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()))
|
@ -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)
|
@ -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.
|
||||
"""
|
||||
|
@ -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.'))
|
||||
@ -504,9 +503,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)
|
||||
@ -649,8 +647,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):
|
||||
"""
|
||||
@ -759,7 +757,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/')
|
||||
@ -1225,7 +1223,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'))
|
||||
@ -1249,7 +1247,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())
|
||||
@ -1265,26 +1263,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):
|
||||
"""
|
||||
@ -1296,20 +1292,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):
|
||||
"""
|
||||
@ -1372,7 +1361,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',
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
24
openlp/core/ui/media/vendor/mediainfoWrapper.py
vendored
24
openlp/core/ui/media/vendor/mediainfoWrapper.py
vendored
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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__)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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'),
|
||||
@ -1421,6 +1423,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
|
||||
@ -1505,7 +1516,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.
|
||||
"""
|
||||
@ -1513,11 +1524,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'
|
||||
@ -1529,7 +1541,7 @@ class PreviewController(RegistryMixin, OpenLPMixin, SlideController):
|
||||
self.post_set_up()
|
||||
|
||||
|
||||
class LiveController(RegistryMixin, OpenLPMixin, SlideController):
|
||||
class LiveController(RegistryBase, SlideController):
|
||||
"""
|
||||
Set up the Live Controller.
|
||||
"""
|
||||
@ -1541,11 +1553,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'
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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
|
583
openlp/core/widgets/edits.py
Normal file
583
openlp/core/widgets/edits.py
Normal 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())]
|
@ -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
|
@ -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 = {}
|
@ -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)
|
@ -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__)
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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']
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
||||
|
24
openlp/plugins/bibles/lib/importers/__init__.py
Normal file
24
openlp/plugins/bibles/lib/importers/__init__.py
Normal 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.
|
||||
"""
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user