This commit is contained in:
Raoul Snyman 2017-09-06 21:56:14 -07:00
commit bf3e891567
252 changed files with 8293 additions and 24213 deletions

View File

@ -33,12 +33,14 @@ import os
import shutil import shutil
import sys import sys
import time import time
from datetime import datetime
from traceback import format_exception from traceback import format_exception
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManager, Settings, UiStrings, \ from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManager, Settings, UiStrings, \
check_directory_exists, is_macosx, is_win, translate check_directory_exists, is_macosx, is_win, translate
from openlp.core.common.path import Path
from openlp.core.common.versionchecker import VersionThread, get_application_version from openlp.core.common.versionchecker import VersionThread, get_application_version
from openlp.core.lib import ScreenList from openlp.core.lib import ScreenList
from openlp.core.resources import qInitResources from openlp.core.resources import qInitResources
@ -153,10 +155,8 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
self.processEvents() self.processEvents()
if not has_run_wizard: if not has_run_wizard:
self.main_window.first_time() self.main_window.first_time()
# update_check = Settings().value('core/update check') version = VersionThread(self.main_window)
# if update_check: version.start()
# version = VersionThread(self.main_window)
# version.start()
self.main_window.is_display_blank() self.main_window.is_display_blank()
self.main_window.app_startup() self.main_window.app_startup()
return self.exec() return self.exec()
@ -181,7 +181,7 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
""" """
Check if the data folder path exists. Check if the data folder path exists.
""" """
data_folder_path = AppLocation.get_data_path() data_folder_path = str(AppLocation.get_data_path())
if not os.path.exists(data_folder_path): if not os.path.exists(data_folder_path):
log.critical('Database was not found in: ' + data_folder_path) log.critical('Database was not found in: ' + data_folder_path)
status = QtWidgets.QMessageBox.critical(None, translate('OpenLP', 'Data Directory Error'), status = QtWidgets.QMessageBox.critical(None, translate('OpenLP', 'Data Directory Error'),
@ -251,10 +251,9 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
if QtWidgets.QMessageBox.question(None, translate('OpenLP', 'Backup'), if QtWidgets.QMessageBox.question(None, translate('OpenLP', 'Backup'),
translate('OpenLP', 'OpenLP has been upgraded, do you want to create\n' translate('OpenLP', 'OpenLP has been upgraded, do you want to create\n'
'a backup of the old data folder?'), 'a backup of the old data folder?'),
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, defaultButton=QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes:
QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes:
# Create copy of data folder # Create copy of data folder
data_folder_path = AppLocation.get_data_path() data_folder_path = str(AppLocation.get_data_path())
timestamp = time.strftime("%Y%m%d-%H%M%S") timestamp = time.strftime("%Y%m%d-%H%M%S")
data_folder_backup_path = data_folder_path + '-' + timestamp data_folder_backup_path = data_folder_path + '-' + timestamp
try: try:
@ -338,6 +337,8 @@ def parse_options(args=None):
parser.add_argument('-d', '--dev-version', dest='dev_version', action='store_true', parser.add_argument('-d', '--dev-version', dest='dev_version', action='store_true',
help='Ignore the version file and pull the version directly from Bazaar') help='Ignore the version file and pull the version directly from Bazaar')
parser.add_argument('-s', '--style', dest='style', help='Set the Qt5 style (passed directly to Qt5).') parser.add_argument('-s', '--style', dest='style', help='Set the Qt5 style (passed directly to Qt5).')
parser.add_argument('-w', '--no-web-server', dest='no_web_server', action='store_false',
help='Turn off the Web and Socket Server ')
parser.add_argument('rargs', nargs='?', default=[]) parser.add_argument('rargs', nargs='?', default=[])
# Parse command line options and deal with them. Use args supplied pragmatically if possible. # Parse command line options and deal with them. Use args supplied pragmatically if possible.
return parser.parse_args(args) if args else parser.parse_args() return parser.parse_args(args) if args else parser.parse_args()
@ -347,15 +348,17 @@ def set_up_logging(log_path):
""" """
Setup our logging using log_path Setup our logging using log_path
:param log_path: the path :param openlp.core.common.path.Path log_path: The file to save the log to.
:rtype: None
""" """
check_directory_exists(log_path, True) check_directory_exists(log_path, True)
filename = os.path.join(log_path, 'openlp.log') file_path = log_path / 'openlp.log'
logfile = logging.FileHandler(filename, 'w', encoding="UTF-8") # TODO: FileHandler accepts a Path object in Py3.6
logfile = logging.FileHandler(str(file_path), 'w', encoding='UTF-8')
logfile.setFormatter(logging.Formatter('%(asctime)s %(name)-55s %(levelname)-8s %(message)s')) logfile.setFormatter(logging.Formatter('%(asctime)s %(name)-55s %(levelname)-8s %(message)s'))
log.addHandler(logfile) log.addHandler(logfile)
if log.isEnabledFor(logging.DEBUG): if log.isEnabledFor(logging.DEBUG):
print('Logging to: {name}'.format(name=filename)) print('Logging to: {name}'.format(name=file_path))
def main(args=None): def main(args=None):
@ -391,16 +394,16 @@ def main(args=None):
application.setApplicationName('OpenLPPortable') application.setApplicationName('OpenLPPortable')
Settings.setDefaultFormat(Settings.IniFormat) Settings.setDefaultFormat(Settings.IniFormat)
# Get location OpenLPPortable.ini # Get location OpenLPPortable.ini
application_path = AppLocation.get_directory(AppLocation.AppDir) portable_path = (AppLocation.get_directory(AppLocation.AppDir) / '..' / '..').resolve()
set_up_logging(os.path.abspath(os.path.join(application_path, '..', '..', 'Other'))) data_path = portable_path / 'Data'
set_up_logging(portable_path / 'Other')
log.info('Running portable') log.info('Running portable')
portable_settings_file = os.path.abspath(os.path.join(application_path, '..', '..', 'Data', 'OpenLP.ini')) portable_settings_path = data_path / 'OpenLP.ini'
# Make this our settings file # Make this our settings file
log.info('INI file: {name}'.format(name=portable_settings_file)) log.info('INI file: {name}'.format(name=portable_settings_path))
Settings.set_filename(portable_settings_file) Settings.set_filename(str(portable_settings_path))
portable_settings = Settings() portable_settings = Settings()
# Set our data path # Set our data path
data_path = os.path.abspath(os.path.join(application_path, '..', '..', 'Data',))
log.info('Data path: {name}'.format(name=data_path)) log.info('Data path: {name}'.format(name=data_path))
# Point to our data path # Point to our data path
portable_settings.setValue('advanced/data path', data_path) portable_settings.setValue('advanced/data path', data_path)
@ -411,6 +414,7 @@ def main(args=None):
set_up_logging(AppLocation.get_directory(AppLocation.CacheDir)) set_up_logging(AppLocation.get_directory(AppLocation.CacheDir))
Registry.create() Registry.create()
Registry().register('application', application) Registry().register('application', application)
Registry().set_flag('no_web_server', args.no_web_server)
application.setApplicationVersion(get_application_version()['version']) application.setApplicationVersion(get_application_version()['version'])
# Check if an instance of OpenLP is already running. Quit if there is a running instance and the user only wants one # Check if an instance of OpenLP is already running. Quit if there is a running instance and the user only wants one
if application.is_already_running(): if application.is_already_running():
@ -419,8 +423,21 @@ def main(args=None):
if application.is_data_path_missing(): if application.is_data_path_missing():
application.shared_memory.detach() application.shared_memory.detach()
sys.exit() sys.exit()
# Remove/convert obsolete settings. # Upgrade settings.
Settings().remove_obsolete_settings() settings = Settings()
if settings.can_upgrade():
now = datetime.now()
# Only back up if OpenLP has previously run.
if settings.value('core/has run wizard'):
back_up_path = AppLocation.get_data_path() / (now.strftime('%Y-%m-%d %H-%M') + '.conf')
log.info('Settings about to be upgraded. Existing settings are being backed up to {back_up_path}'
.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))
settings.export(back_up_path)
settings.upgrade_settings()
# First time checks in settings # First time checks in settings
if not Settings().value('core/has run wizard'): if not Settings().value('core/has run wizard'):
if not FirstTimeLanguageForm().exec(): if not FirstTimeLanguageForm().exec():

View File

@ -1,6 +1,6 @@
<!DOCTYPE html> # -*- coding: utf-8 -*-
<html> # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
<!--
############################################################################### ###############################################################################
# OpenLP - Open Source Lyrics Projection # # OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
@ -19,16 +19,10 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
-->
<head> from openlp.core.api.http.endpoint import Endpoint
<meta charset="utf-8" /> from openlp.core.api.http import register_endpoint, requires_auth
<title>${live_title}</title> from openlp.core.api.tab import ApiTab
<link rel="stylesheet" href="/css/main.css" /> from openlp.core.api.poll import Poller
<link rel="shortcut icon" type="image/x-icon" href="/images/favicon.ico">
<script type="text/javascript" src="/assets/jquery.min.js"></script> __all__ = ['Endpoint', 'ApiTab', 'register_endpoint', 'requires_auth']
<script type="text/javascript" src="/js/main.js"></script>
</head>
<body>
<img id="image" class="size"/>
</body>
</html>

View File

@ -19,9 +19,7 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
"""
from .remotetab import RemoteTab The Endpoint class, which provides plugins with a way to serve their own portion of the API
from .httprouter import HttpRouter """
from .httpserver import OpenLPServer from .pluginhelpers import search, live, service
__all__ = ['RemoteTab', 'OpenLPServer', 'HttpRouter']

View File

@ -0,0 +1,143 @@
# -*- 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
import os
import urllib.request
import urllib.error
import json
from openlp.core.api.http.endpoint import Endpoint
from openlp.core.api.http import requires_auth
from openlp.core.common import Registry, AppLocation, Settings
from openlp.core.lib import ItemCapabilities, create_thumb
log = logging.getLogger(__name__)
controller_endpoint = Endpoint('controller')
api_controller_endpoint = Endpoint('api')
@api_controller_endpoint.route('controller/live/text')
@controller_endpoint.route('live/text')
def controller_text(request):
"""
Perform an action on the slide controller.
:param request: the http request - not used
"""
log.debug("controller_text ")
live_controller = Registry().get('live_controller')
current_item = live_controller.service_item
data = []
if current_item:
for index, frame in enumerate(current_item.get_frames()):
item = {}
# Handle text (songs, custom, bibles)
if current_item.is_text():
if frame['verseTag']:
item['tag'] = str(frame['verseTag'])
else:
item['tag'] = str(index + 1)
item['chords_text'] = str(frame['chords_text'])
item['text'] = str(frame['text'])
item['html'] = str(frame['html'])
# Handle images, unless a custom thumbnail is given or if thumbnails is disabled
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)
# 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)
item['text'] = str(frame['title'])
item['html'] = str(frame['title'])
else:
# Handle presentation etc.
item['tag'] = str(index + 1)
if current_item.is_capable(ItemCapabilities.HasDisplayTitle):
item['title'] = str(frame['display_title'])
if current_item.is_capable(ItemCapabilities.HasNotes):
item['slide_notes'] = str(frame['notes'])
if current_item.is_capable(ItemCapabilities.HasThumbnails) and Settings().value('api/thumbnails'):
# If the file is under our app directory tree send the portion after the match
data_path = str(AppLocation.get_data_path())
if frame['image'][0:len(data_path)] == data_path:
item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):])
Registry().get('image_manager').add_image(frame['image'], frame['title'], None, 88, 88)
item['text'] = str(frame['title'])
item['html'] = str(frame['title'])
item['selected'] = (live_controller.selected_row == index)
data.append(item)
json_data = {'results': {'slides': data}}
if current_item:
json_data['results']['item'] = live_controller.service_item.unique_identifier
return json_data
@api_controller_endpoint.route('controller/live/set')
@controller_endpoint.route('live/set')
@requires_auth
def controller_set(request):
"""
Perform an action on the slide controller.
:param request: The action to perform.
"""
event = getattr(Registry().get('live_controller'), 'slidecontroller_live_set')
try:
json_data = request.GET.get('data')
data = int(json.loads(json_data)['request']['id'])
event.emit([data])
except KeyError:
log.error("Endpoint controller/live/set request id not found")
return {'results': {'success': True}}
@controller_endpoint.route('{action:next|previous}')
@requires_auth
def controller_direction(request, controller, action):
"""
Handles requests for setting service items in the slide controller
11
:param request: The http request object.
:param controller: the controller slides forward or backward.
:param action: the controller slides forward or backward.
"""
event = getattr(Registry().get('live_controller'), 'slidecontroller_{controller}_{action}'.
format(controller=controller, action=action))
event.emit()
@api_controller_endpoint.route('controller/{controller}/{action:next|previous}')
@requires_auth
def controller_direction_api(request, controller, action):
"""
Handles requests for setting service items in the slide controller
11
:param request: The http request object.
:param controller: the controller slides forward or backward.
:param action: the controller slides forward or backward.
"""
controller_direction(request, controller, action)
return {'results': {'success': True}}

View File

@ -0,0 +1,182 @@
# -*- 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
import os
from openlp.core.api.http.endpoint import Endpoint
from openlp.core.api.http import requires_auth
from openlp.core.common import Registry, UiStrings, translate
from openlp.core.lib import image_to_byte, PluginStatus, StringContent
template_dir = 'templates'
static_dir = 'static'
blank_dir = os.path.join(static_dir, 'index')
log = logging.getLogger(__name__)
chords_endpoint = Endpoint('chords', template_dir=template_dir, static_dir=static_dir)
stage_endpoint = Endpoint('stage', template_dir=template_dir, static_dir=static_dir)
main_endpoint = Endpoint('main', template_dir=template_dir, static_dir=static_dir)
blank_endpoint = Endpoint('', template_dir=template_dir, static_dir=blank_dir)
FILE_TYPES = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.ico': 'image/x-icon',
'.png': 'image/png'
}
remote = translate('RemotePlugin.Mobile', 'Remote')
stage = translate('RemotePlugin.Mobile', 'Stage View')
live = translate('RemotePlugin.Mobile', 'Live View')
chords = translate('RemotePlugin.Mobile', 'Chords View')
TRANSLATED_STRINGS = {
'app_title': "{main} {remote}".format(main=UiStrings().OpenLP, remote=remote),
'stage_title': "{main} {stage}".format(main=UiStrings().OpenLP, stage=stage),
'live_title': "{main} {live}".format(main=UiStrings().OpenLP, live=live),
'chords_title': "{main} {chords}".format(main=UiStrings().OpenLP, chords=chords),
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
'alerts': translate('RemotePlugin.Mobile', 'Alerts'),
'search': translate('RemotePlugin.Mobile', 'Search'),
'home': translate('RemotePlugin.Mobile', 'Home'),
'refresh': translate('RemotePlugin.Mobile', 'Refresh'),
'blank': translate('RemotePlugin.Mobile', 'Blank'),
'theme': translate('RemotePlugin.Mobile', 'Theme'),
'desktop': translate('RemotePlugin.Mobile', 'Desktop'),
'show': translate('RemotePlugin.Mobile', 'Show'),
'prev': translate('RemotePlugin.Mobile', 'Prev'),
'next': translate('RemotePlugin.Mobile', 'Next'),
'text': translate('RemotePlugin.Mobile', 'Text'),
'show_alert': translate('RemotePlugin.Mobile', 'Show Alert'),
'go_live': translate('RemotePlugin.Mobile', 'Go Live'),
'add_to_service': translate('RemotePlugin.Mobile', 'Add to Service'),
'add_and_go_to_service': translate('RemotePlugin.Mobile', 'Add &amp; Go to Service'),
'no_results': translate('RemotePlugin.Mobile', 'No Results'),
'options': translate('RemotePlugin.Mobile', 'Options'),
'service': translate('RemotePlugin.Mobile', 'Service'),
'slides': translate('RemotePlugin.Mobile', 'Slides'),
'settings': translate('RemotePlugin.Mobile', 'Settings'),
}
@stage_endpoint.route('')
def stage_index(request):
"""
Deliver the page for the /stage url
"""
return stage_endpoint.render_template('stage.mako', **TRANSLATED_STRINGS)
@chords_endpoint.route('')
def chords_index(request):
"""
Deliver the page for the /chords url
"""
return chords_endpoint.render_template('chords.mako', **TRANSLATED_STRINGS)
@main_endpoint.route('')
def main_index(request):
"""
Deliver the page for the /main url
"""
return main_endpoint.render_template('main.mako', **TRANSLATED_STRINGS)
@blank_endpoint.route('')
def index(request):
"""
Deliver the page for the / url
:param request:
"""
return blank_endpoint.render_template('index.mako', **TRANSLATED_STRINGS)
@blank_endpoint.route('api/poll')
@blank_endpoint.route('poll')
def poll(request):
"""
Deliver the page for the /poll url
:param request:
"""
return Registry().get('poller').poll()
@blank_endpoint.route('api/display/{display:hide|show|blank|theme|desktop}')
@blank_endpoint.route('display/{display:hide|show|blank|theme|desktop}')
@requires_auth
def toggle_display(request, display):
"""
Deliver the functions for the /display url
:param request: the http request - not used
:param display: the display function to be triggered
"""
Registry().get('live_controller').slidecontroller_toggle_display.emit(display)
return {'results': {'success': True}}
@blank_endpoint.route('api/plugin/search')
@blank_endpoint.route('plugin/search')
def plugin_search_list(request):
"""
Deliver a list of active plugins that support search
:param request: the http request - not used
"""
searches = []
for plugin in Registry().get('plugin_manager').plugins:
if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
searches.append([plugin.name, str(plugin.text_strings[StringContent.Name]['plural'])])
return {'results': {'items': searches}}
@main_endpoint.route('image')
def main_image(request):
"""
Return the latest display image as a byte stream.
:param request: base path of the URL. Not used but passed by caller
:return:
"""
live_controller = Registry().get('live_controller')
result = {
'slide_image': 'data:image/png;base64,' + str(image_to_byte(live_controller.slide_image))
}
return {'results': result}
def get_content_type(file_name):
"""
Examines the extension of the file and determines what the content_type should be, defaults to text/plain
Returns the extension and the content_type
:param file_name: name of file
"""
ext = os.path.splitext(file_name)[1]
content_type = FILE_TYPES.get(ext, 'text/plain')
return ext, content_type

View File

@ -0,0 +1,135 @@
# -*- 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 os
import json
import re
import urllib
from urllib.parse import urlparse
from webob import Response
from openlp.core.api.http.errors import NotFound
from openlp.core.common import Registry, AppLocation
from openlp.core.lib import PluginStatus, image_to_byte
def search(request, plugin_name, log):
"""
Handles requests for searching the plugins
:param request: The http request object.
:param plugin_name: The plugin name.
:param log: The class log object.
"""
try:
json_data = request.GET.get('data')
text = json.loads(json_data)['request']['text']
except KeyError:
log.error("Endpoint {text} search request text not found".format(text=plugin_name))
text = ""
text = urllib.parse.unquote(text)
plugin = Registry().get('plugin_manager').get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
results = plugin.media_item.search(text, False)
return {'results': {'items': results}}
else:
raise NotFound()
def live(request, plugin_name, log):
"""
Handles requests for making live of the plugins
:param request: The http request object.
:param plugin_name: The plugin name.
:param log: The class log object.
"""
try:
json_data = request.GET.get('data')
request_id = json.loads(json_data)['request']['id']
except KeyError:
log.error("Endpoint {text} search request text not found".format(text=plugin_name))
return []
plugin = Registry().get('plugin_manager').get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item:
getattr(plugin.media_item, '{name}_go_live'.format(name=plugin_name)).emit([request_id, True])
def service(request, plugin_name, log):
"""
Handles requests for adding to a service of the plugins
:param request: The http request object.
:param plugin_name: The plugin name.
:param log: The class log object.
"""
try:
json_data = request.GET.get('data')
request_id = json.loads(json_data)['request']['id']
except KeyError:
log.error("Endpoint {plugin} search request text not found".format(plugin=plugin_name))
return []
plugin = Registry().get('plugin_manager').get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item:
item_id = plugin.media_item.create_item_from_id(request_id)
getattr(plugin.media_item, '{name}_add_to_service'.format(name=plugin_name)).emit([item_id, True])
def display_thumbnails(request, controller_name, log, dimensions, file_name, slide=None):
"""
Handles requests for adding a song to the service
Return an image to a web page based on a URL
:param request: Request object
: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 slide: the individual image name
:return:
"""
log.debug('serve thumbnail {cname}/thumbnails{dim}/{fname}/{slide}'.format(cname=controller_name,
dim=dimensions,
fname=file_name,
slide=slide))
# -1 means use the default dimension in ImageManager
width = -1
height = -1
image = None
if dimensions:
match = re.search('(\d+)x(\d+)', dimensions)
if match:
# let's make sure that the dimensions are within reason
width = sorted([10, int(match.group(1)), 1000])[1]
height = sorted([10, int(match.group(2)), 1000])[1]
if controller_name and file_name:
file_name = urllib.parse.unquote(file_name)
if '..' not in file_name: # no hacking please
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)
return Response(body=image_to_byte(image, False), status=200, content_type='image/png', charset='utf8')

View File

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-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
import json
from openlp.core.api.http.endpoint import Endpoint
from openlp.core.api.http import register_endpoint, requires_auth
from openlp.core.common import Registry
log = logging.getLogger(__name__)
service_endpoint = Endpoint('service')
api_service_endpoint = Endpoint('api/service')
@api_service_endpoint.route('list')
@service_endpoint.route('list')
def list_service(request):
"""
Handles requests for service items in the service manager
:param request: The http request object.
"""
return {'results': {'items': get_service_items()}}
@api_service_endpoint.route('set')
@service_endpoint.route('set')
@requires_auth
def service_set(request):
"""
Handles requests for setting service items in the service manager
:param request: The http request object.
"""
event = getattr(Registry().get('service_manager'), 'servicemanager_set_item')
try:
json_data = request.GET.get('data')
data = int(json.loads(json_data)['request']['id'])
event.emit(data)
except KeyError:
log.error("Endpoint service/set request id not found")
return {'results': {'success': True}}
@api_service_endpoint.route('{action:next|previous}')
@service_endpoint.route('{action:next|previous}')
@requires_auth
def service_direction(request, action):
"""
Handles requests for setting service items in the service manager
:param request: The http request object.
:param action: the the service slides forward or backward.
"""
event = getattr(Registry().get('service_manager'), 'servicemanager_{action}_item'.format(action=action))
event.emit()
return {'results': {'success': True}}
def get_service_items():
"""
Read the service item in use and return the data as a json object
"""
live_controller = Registry().get('live_controller')
service_items = []
if live_controller.service_item:
current_unique_identifier = live_controller.service_item.unique_identifier
else:
current_unique_identifier = None
for item in Registry().get('service_manager').service_items:
service_item = item['service_item']
service_items.append({
'id': str(service_item.unique_identifier),
'title': str(service_item.get_display_title()),
'plugin': str(service_item.name),
'notes': str(service_item.notes),
'selected': (service_item.unique_identifier == current_unique_identifier)
})
return service_items

View File

@ -0,0 +1,110 @@
# -*- 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 base64
from functools import wraps
from webob import Response
from openlp.core.common.settings import Settings
from openlp.core.api.http.wsgiapp import WSGIApplication
from .errors import NotFound, ServerError, HttpError
application = WSGIApplication('api')
def _route_from_url(url_prefix, url):
"""
Create a route from the URL
"""
url_prefix = '/{prefix}/'.format(prefix=url_prefix.strip('/'))
if not url:
url = url_prefix[:-1]
else:
url = url_prefix + url
url = url.replace('//', '/')
return url
def register_endpoint(end_point):
"""
Register an endpoint with the app
"""
for url, view_func, method in end_point.routes:
# Set the view functions
route = _route_from_url(end_point.url_prefix, url)
application.add_route(route, view_func, method)
# Add a static route if necessary
if end_point.static_dir:
static_route = _route_from_url(end_point.url_prefix, 'static')
static_route += '(.*)'
application.add_static_route(static_route, end_point.static_dir)
def check_auth(auth):
"""
This function is called to check if a username password combination is valid.
:param auth: the authorisation object which needs to be tested
:return Whether authentication have been successful
"""
auth_code = "{user}:{password}".format(user=Settings().value('api/user id'),
password=Settings().value('api/password'))
try:
auth_base = base64.b64encode(auth_code)
except TypeError:
auth_base = base64.b64encode(auth_code.encode()).decode()
if auth[1] == auth_base:
return True
else:
return False
def authenticate():
"""
Sends a 401 response that enables basic auth to be triggered
"""
resp = Response(status=401)
resp.www_authenticate = 'Basic realm="OpenLP Login Required"'
return resp
def requires_auth(f):
"""
Decorates a function which needs to be authenticated before it can be used from the remote.
:param f: The function which has been wrapped
:return: the called function or a request to authenticate
"""
@wraps(f)
def decorated(*args, **kwargs):
if not Settings().value('api/authentication enabled'):
return f(*args, **kwargs)
req = args[0]
if not hasattr(req, 'authorization'):
return authenticate()
else:
auth = req.authorization
if auth and check_auth(auth):
return f(*args, **kwargs)
else:
return authenticate()
return decorated

View File

@ -0,0 +1,79 @@
# -*- 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 Endpoint class, which provides plugins with a way to serve their own portion of the API
"""
import os
from openlp.core.common import AppLocation
from mako.template import Template
class Endpoint(object):
"""
This is an endpoint for the HTTP API
"""
def __init__(self, url_prefix, template_dir=None, static_dir=None, assets_dir=None):
"""
Create an endpoint with a URL prefix
"""
self.url_prefix = url_prefix
self.static_dir = static_dir
self.template_dir = template_dir
if assets_dir:
self.assets_dir = assets_dir
else:
self.assets_dir = None
self.routes = []
def add_url_route(self, url, view_func, method):
"""
Add a url route to the list of routes
"""
self.routes.append((url, view_func, method))
def route(self, rule, method='GET'):
"""
Set up a URL route
"""
def decorator(func):
"""
Make this a decorator
"""
self.add_url_route(rule, func, method)
return func
return decorator
def render_template(self, filename, **kwargs):
"""
Render a mako template
"""
root = str(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)
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)

View File

@ -0,0 +1,65 @@
# -*- 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 #
###############################################################################
"""
HTTP Error classes
"""
class HttpError(Exception):
"""
A base HTTP error (aka status code)
"""
def __init__(self, status, message):
"""
Initialise the exception
"""
super(HttpError, self).__init__(message)
self.status = status
self.message = message
def to_response(self):
"""
Convert this exception to a Response object
"""
return self.message, self.status
class NotFound(HttpError):
"""
A 404
"""
def __init__(self):
"""
Make this a 404
"""
super(NotFound, self).__init__(404, 'Not Found')
class ServerError(HttpError):
"""
A 500
"""
def __init__(self):
"""
Make this a 500
"""
super(ServerError, self).__init__(500, 'Server Error')

View File

@ -0,0 +1,97 @@
# -*- 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:`http` module contains the API web server. This is a lightweight web server used by remotes to interact
with OpenLP. It uses JSON to communicate with the remotes.
"""
import logging
from PyQt5 import QtCore
from waitress import serve
from openlp.core.api.http import register_endpoint
from openlp.core.api.http import application
from openlp.core.common import RegistryMixin, RegistryProperties, OpenLPMixin, Settings, Registry
from openlp.core.api.poll import Poller
from openlp.core.api.endpoint.controller import controller_endpoint, api_controller_endpoint
from openlp.core.api.endpoint.core import chords_endpoint, stage_endpoint, blank_endpoint, main_endpoint
from openlp.core.api.endpoint.service import service_endpoint, api_service_endpoint
log = logging.getLogger(__name__)
class HttpWorker(QtCore.QObject):
"""
A special Qt thread class to allow the HTTP server to run at the same time as the UI.
"""
def __init__(self):
"""
Constructor for the thread class.
:param server: The http server class.
"""
super(HttpWorker, self).__init__()
def run(self):
"""
Run the thread.
"""
address = Settings().value('api/ip address')
port = Settings().value('api/port')
serve(application, host=address, port=port)
def stop(self):
pass
class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin):
"""
Wrapper round a server instance
"""
def __init__(self, parent=None):
"""
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()
def bootstrap_post_set_up(self):
"""
Register the poll return service and start the servers.
"""
self.poller = Poller()
Registry().register('poller', self.poller)
application.initialise()
register_endpoint(controller_endpoint)
register_endpoint(api_controller_endpoint)
register_endpoint(chords_endpoint)
register_endpoint(stage_endpoint)
register_endpoint(blank_endpoint)
register_endpoint(main_endpoint)
register_endpoint(service_endpoint)
register_endpoint(api_service_endpoint)

View File

@ -0,0 +1,185 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
# pylint: disable=logging-format-interpolation
###############################################################################
# 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 #
###############################################################################
"""
App stuff
"""
import json
import logging
import os
import re
from webob import Request, Response
from webob.static import DirectoryApp
from openlp.core.common import AppLocation
from openlp.core.api.http.errors import HttpError, NotFound, ServerError
ARGS_REGEX = re.compile(r'''\{(\w+)(?::([^}]+))?\}''', re.VERBOSE)
log = logging.getLogger(__name__)
def _route_to_regex(route):
"""
Convert a route to a regular expression
For example:
'songs/{song_id}' becomes 'songs/(?P<song_id>[^/]+)'
and
'songs/{song_id:\d+}' becomes 'songs/(?P<song_id>\d+)'
"""
route_regex = ''
last_pos = 0
for match in ARGS_REGEX.finditer(route):
route_regex += re.escape(route[last_pos:match.start()])
arg_name = match.group(1)
expr = match.group(2) or '[^/]+'
expr = '(?P<%s>%s)' % (arg_name, expr)
route_regex += expr
last_pos = match.end()
route_regex += re.escape(route[last_pos:])
route_regex = '^%s$' % route_regex
return route_regex
def _make_response(view_result):
"""
Create a Response object from response
"""
if isinstance(view_result, Response):
return view_result
elif isinstance(view_result, tuple):
content_type = 'text/html'
body = view_result[0]
if isinstance(body, dict):
content_type = 'application/json'
body = json.dumps(body)
response = Response(body=body, status=view_result[1],
content_type=content_type, charset='utf8')
if len(view_result) >= 3:
response.headers.update(view_result[2])
return response
elif isinstance(view_result, dict):
return Response(body=json.dumps(view_result), status=200,
content_type='application/json', charset='utf8')
elif isinstance(view_result, str):
return Response(body=view_result, status=200,
content_type='text/html', charset='utf8')
def _handle_exception(error):
"""
Handle exceptions
"""
log.exception(error)
if isinstance(error, HttpError):
return error.to_response()
else:
return ServerError().to_response()
class WSGIApplication(object):
"""
This is the core of the API, the WSGI app
"""
def __init__(self, name):
"""
Create the app object
"""
self.name = name
self.static_routes = {}
self.route_map = {}
def initialise(self):
"""
Set up generic roots for the whole application
:return: None
"""
self.add_static_route('/assets(.*)', '')
self.add_static_route('/images(.*)', '')
pass
def add_route(self, route, view_func, method):
"""
Add a route
"""
route_regex = _route_to_regex(route)
if route_regex not in self.route_map:
self.route_map[route_regex] = {}
self.route_map[route_regex][method.upper()] = view_func
def add_static_route(self, route, static_dir):
"""
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):
log.error('Static path "%s" does not exist. Skipping creating static route/', static_path)
return
self.static_routes[route] = DirectoryApp(static_path)
def dispatch(self, request):
"""
Find the appropriate URL and run the view function
"""
# If not a static route, try the views
for route, views in self.route_map.items():
match = re.match(route, request.path)
if match and request.method.upper() in views:
kwargs = match.groupdict()
log.debug('Found {method} {url}'.format(method=request.method, url=request.path))
view_func = views[request.method.upper()]
return _make_response(view_func(request, **kwargs))
# Look to see if this is a static file request
for route, static_app in self.static_routes.items():
if re.match(route, request.path):
return request.get_response(static_app)
log.error('URL {url} - Not found'.format(url=request.path))
raise NotFound()
def wsgi_app(self, environ, start_response):
"""
The actual WSGI application.
"""
request = Request(environ)
try:
response = self.dispatch(request)
except Exception as e:
response = _make_response(_handle_exception(e))
response.headers.add("cache-control", "no-cache, no-store, must-revalidate")
response.headers.add("pragma", "no-cache")
response.headers.add("expires", "0")
return response(environ, start_response)
def __call__(self, environ, start_response):
"""
Shortcut for wsgi_app.
"""
return self.wsgi_app(environ, start_response)

130
openlp/core/api/poll.py Normal file
View File

@ -0,0 +1,130 @@
# -*- 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 json
from openlp.core.common import RegistryProperties, Settings
from openlp.core.common.httputils import get_web_page
class Poller(RegistryProperties):
"""
Accessed by the web layer to get status type information from the application
"""
def __init__(self):
"""
Constructor for the poll builder class.
"""
super(Poller, self).__init__()
self.live_cache = None
self.stage_cache = None
self.chords_cache = None
def raw_poll(self):
return {
'service': self.service_manager.service_id,
'slide': self.live_controller.selected_row or 0,
'item': self.live_controller.service_item.unique_identifier if self.live_controller.service_item else '',
'twelve': Settings().value('api/twelve hour'),
'blank': self.live_controller.blank_screen.isChecked(),
'theme': self.live_controller.theme_screen.isChecked(),
'display': self.live_controller.desktop_screen.isChecked(),
'version': 3,
'isSecure': Settings().value('api/authentication enabled'),
'isAuthorised': False,
'chordNotation': Settings().value('songs/chord notation'),
'isStagedActive': self.is_stage_active(),
'isLiveActive': self.is_live_active(),
'isChordsActive': self.is_chords_active()
}
def poll(self):
"""
Poll OpenLP to determine the current slide number and item name.
"""
return {'results': self.raw_poll()}
def main_poll(self):
"""
Poll OpenLP to determine the current slide count.
"""
result = {
'slide_count': self.live_controller.slide_count
}
return json.dumps({'results': result}).encode()
def reset_cache(self):
"""
Reset the caches as the web has changed
:return:
"""
self.stage_cache = None
self.live_cache = None
self.chords.cache = None
def is_stage_active(self):
"""
Is stage active - call it and see but only once
:return: if stage is active or not
"""
if self.stage_cache is None:
try:
page = get_web_page("http://localhost:4316/stage")
except:
page = None
if page:
self.stage_cache = True
else:
self.stage_cache = False
return self.stage_cache
def is_live_active(self):
"""
Is main active - call it and see but only once
:return: if live is active or not
"""
if self.live_cache is None:
try:
page = get_web_page("http://localhost:4316/main")
except:
page = None
if page:
self.live_cache = True
else:
self.live_cache = False
return self.live_cache
def is_chords_active(self):
"""
Is chords active - call it and see but only once
:return: if live is active or not
"""
if self.chords_cache is None:
try:
page = get_web_page("http://localhost:4316/chords")
except:
page = None
if page:
self.chords_cache = True
else:
self.chords_cache = False
return self.chords_cache

View File

@ -20,26 +20,28 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
import os.path
from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets
from openlp.core.common import AppLocation, Settings, translate from openlp.core.common import UiStrings, Registry, Settings, translate
from openlp.core.lib import SettingsTab, build_icon from openlp.core.lib import SettingsTab
ZERO_URL = '0.0.0.0' ZERO_URL = '0.0.0.0'
class RemoteTab(SettingsTab): class ApiTab(SettingsTab):
""" """
RemoteTab is the Remotes settings tab in the settings dialog. RemoteTab is the Remotes settings tab in the settings dialog.
""" """
def __init__(self, parent, title, visible_title, icon_path): def __init__(self, parent):
super(RemoteTab, self).__init__(parent, title, visible_title, icon_path) self.icon_path = ':/plugins/plugin_remote.png'
advanced_translated = translate('OpenLP.AdvancedTab', 'Advanced')
super(ApiTab, self).__init__(parent, 'api', advanced_translated)
self.define_main_window_icon()
self.generate_icon()
def setupUi(self): def setupUi(self):
self.setObjectName('RemoteTab') self.setObjectName('ApiTab')
super(RemoteTab, self).setupUi() super(ApiTab, self).setupUi()
self.server_settings_group_box = QtWidgets.QGroupBox(self.left_column) self.server_settings_group_box = QtWidgets.QGroupBox(self.left_column)
self.server_settings_group_box.setObjectName('server_settings_group_box') self.server_settings_group_box.setObjectName('server_settings_group_box')
self.server_settings_layout = QtWidgets.QFormLayout(self.server_settings_group_box) self.server_settings_layout = QtWidgets.QFormLayout(self.server_settings_group_box)
@ -65,8 +67,7 @@ class RemoteTab(SettingsTab):
self.http_setting_layout.setObjectName('http_setting_layout') self.http_setting_layout.setObjectName('http_setting_layout')
self.port_label = QtWidgets.QLabel(self.http_settings_group_box) self.port_label = QtWidgets.QLabel(self.http_settings_group_box)
self.port_label.setObjectName('port_label') self.port_label.setObjectName('port_label')
self.port_spin_box = QtWidgets.QSpinBox(self.http_settings_group_box) self.port_spin_box = QtWidgets.QLabel(self.http_settings_group_box)
self.port_spin_box.setMaximum(32767)
self.port_spin_box.setObjectName('port_spin_box') self.port_spin_box.setObjectName('port_spin_box')
self.http_setting_layout.addRow(self.port_label, self.port_spin_box) self.http_setting_layout.addRow(self.port_label, self.port_spin_box)
self.remote_url_label = QtWidgets.QLabel(self.http_settings_group_box) self.remote_url_label = QtWidgets.QLabel(self.http_settings_group_box)
@ -111,6 +112,23 @@ class RemoteTab(SettingsTab):
self.password.setObjectName('password') self.password.setObjectName('password')
self.user_login_layout.addRow(self.password_label, self.password) self.user_login_layout.addRow(self.password_label, self.password)
self.left_layout.addWidget(self.user_login_group_box) self.left_layout.addWidget(self.user_login_group_box)
self.update_site_group_box = QtWidgets.QGroupBox(self.left_column)
self.update_site_group_box.setCheckable(True)
self.update_site_group_box.setChecked(False)
self.update_site_group_box.setObjectName('update_site_group_box')
self.update_site_layout = QtWidgets.QFormLayout(self.update_site_group_box)
self.update_site_layout.setObjectName('update_site_layout')
self.current_version_label = QtWidgets.QLabel(self.update_site_group_box)
self.current_version_label.setObjectName('current_version_label')
self.current_version_value = QtWidgets.QLabel(self.update_site_group_box)
self.current_version_value.setObjectName('current_version_value')
self.update_site_layout.addRow(self.current_version_label, self.current_version_value)
self.master_version_label = QtWidgets.QLabel(self.update_site_group_box)
self.master_version_label.setObjectName('master_version_label')
self.master_version_value = QtWidgets.QLabel(self.update_site_group_box)
self.master_version_value.setObjectName('master_version_value')
self.update_site_layout.addRow(self.master_version_label, self.master_version_value)
self.left_layout.addWidget(self.update_site_group_box)
self.android_app_group_box = QtWidgets.QGroupBox(self.right_column) self.android_app_group_box = QtWidgets.QGroupBox(self.right_column)
self.android_app_group_box.setObjectName('android_app_group_box') self.android_app_group_box.setObjectName('android_app_group_box')
self.right_layout.addWidget(self.android_app_group_box) self.right_layout.addWidget(self.android_app_group_box)
@ -146,16 +164,34 @@ class RemoteTab(SettingsTab):
self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed) self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed)
self.thumbnails_check_box.stateChanged.connect(self.on_thumbnails_check_box_changed) self.thumbnails_check_box.stateChanged.connect(self.on_thumbnails_check_box_changed)
self.address_edit.textChanged.connect(self.set_urls) self.address_edit.textChanged.connect(self.set_urls)
self.port_spin_box.valueChanged.connect(self.set_urls)
def define_main_window_icon(self):
"""
Define an icon on the main window to show the state of the server
:return:
"""
self.remote_server_icon = QtWidgets.QLabel(self.main_window.status_bar)
size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
size_policy.setHorizontalStretch(0)
size_policy.setVerticalStretch(0)
size_policy.setHeightForWidth(self.remote_server_icon.sizePolicy().hasHeightForWidth())
self.remote_server_icon.setSizePolicy(size_policy)
self.remote_server_icon.setFrameShadow(QtWidgets.QFrame.Plain)
self.remote_server_icon.setLineWidth(1)
self.remote_server_icon.setScaledContents(True)
self.remote_server_icon.setFixedSize(20, 20)
self.remote_server_icon.setObjectName('remote_server_icon')
self.main_window.status_bar.insertPermanentWidget(2, self.remote_server_icon)
def retranslateUi(self): def retranslateUi(self):
self.tab_title_visible = translate('RemotePlugin.RemoteTab', 'Remote Interface')
self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings')) self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings'))
self.address_label.setText(translate('RemotePlugin.RemoteTab', 'Serve on IP address:')) self.address_label.setText(translate('RemotePlugin.RemoteTab', 'Serve on IP address:'))
self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:')) self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:')) self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:')) self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
self.chords_url_label.setText(translate('RemotePlugin.RemoteTab', 'Chords view URL:'))
self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:')) self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
self.chords_url_label.setText(translate('RemotePlugin.RemoteTab', 'Chords view URL:'))
self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format')) self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab', self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab',
'Show thumbnails of non-text slides in remote and stage view.')) 'Show thumbnails of non-text slides in remote and stage view.'))
@ -170,22 +206,27 @@ class RemoteTab(SettingsTab):
'Scan the QR code or click <a href="{qr}">download</a> to install the iOS app from the App ' 'Scan the QR code or click <a href="{qr}">download</a> to install the iOS app from the App '
'Store.').format(qr='https://itunes.apple.com/app/id1096218725')) 'Store.').format(qr='https://itunes.apple.com/app/id1096218725'))
self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication')) self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication'))
self.aa = UiStrings()
self.update_site_group_box.setTitle(UiStrings().WebDownloadText)
self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:')) self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:'))
self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:')) self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:'))
self.current_version_label.setText(translate('RemotePlugin.RemoteTab', 'Current Version number:'))
self.master_version_label.setText(translate('RemotePlugin.RemoteTab', 'Latest Version number:'))
def set_urls(self): def set_urls(self):
""" """
Update the display based on the data input on the screen Update the display based on the data input on the screen
""" """
ip_address = self.get_ip_address(self.address_edit.text()) ip_address = self.get_ip_address(self.address_edit.text())
http_url = 'http://{url}:{text}/'.format(url=ip_address, text=self.port_spin_box.value()) http_url = 'http://{url}:{text}/'.format(url=ip_address, text=self.port_spin_box.text())
self.remote_url.setText('<a href="{url}">{url}</a>'.format(url=http_url)) self.remote_url.setText('<a href="{url}">{url}</a>'.format(url=http_url))
http_url_temp = http_url + 'stage' http_url_temp = http_url + 'stage'
self.stage_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp)) self.stage_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
http_url_temp = http_url + 'main' http_url_temp = http_url + 'main'
self.live_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp)) self.live_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
def get_ip_address(self, ip_address): @staticmethod
def get_ip_address(ip_address):
""" """
returns the IP address in dependency of the passed address returns the IP address in dependency of the passed address
ip_address == 0.0.0.0: return the IP address of the first valid interface ip_address == 0.0.0.0: return the IP address of the first valid interface
@ -209,7 +250,7 @@ class RemoteTab(SettingsTab):
""" """
Load the configuration and update the server configuration if necessary Load the configuration and update the server configuration if necessary
""" """
self.port_spin_box.setValue(Settings().value(self.settings_section + '/port')) self.port_spin_box.setText(str(Settings().value(self.settings_section + '/port')))
self.address_edit.setText(Settings().value(self.settings_section + '/ip address')) self.address_edit.setText(Settings().value(self.settings_section + '/ip address'))
self.twelve_hour = Settings().value(self.settings_section + '/twelve hour') self.twelve_hour = Settings().value(self.settings_section + '/twelve hour')
self.twelve_hour_check_box.setChecked(self.twelve_hour) self.twelve_hour_check_box.setChecked(self.twelve_hour)
@ -218,16 +259,18 @@ class RemoteTab(SettingsTab):
self.user_login_group_box.setChecked(Settings().value(self.settings_section + '/authentication enabled')) self.user_login_group_box.setChecked(Settings().value(self.settings_section + '/authentication enabled'))
self.user_id.setText(Settings().value(self.settings_section + '/user id')) self.user_id.setText(Settings().value(self.settings_section + '/user id'))
self.password.setText(Settings().value(self.settings_section + '/password')) self.password.setText(Settings().value(self.settings_section + '/password'))
self.current_version_value.setText(Settings().value('remotes/download version'))
self.master_version_value.setText(Registry().get_flag('website_version'))
if self.master_version_value.text() == self.current_version_value.text():
self.update_site_group_box.setEnabled(False)
self.set_urls() self.set_urls()
def save(self): def save(self):
""" """
Save the configuration and update the server configuration if necessary Save the configuration and update the server configuration if necessary
""" """
if Settings().value(self.settings_section + '/ip address') != self.address_edit.text() or \ if Settings().value(self.settings_section + '/ip address') != self.address_edit.text():
Settings().value(self.settings_section + '/port') != self.port_spin_box.value():
self.settings_form.register_post_process('remotes_config_updated') self.settings_form.register_post_process('remotes_config_updated')
Settings().setValue(self.settings_section + '/port', self.port_spin_box.value())
Settings().setValue(self.settings_section + '/ip address', self.address_edit.text()) Settings().setValue(self.settings_section + '/ip address', self.address_edit.text())
Settings().setValue(self.settings_section + '/twelve hour', self.twelve_hour) Settings().setValue(self.settings_section + '/twelve hour', self.twelve_hour)
Settings().setValue(self.settings_section + '/thumbnails', self.thumbnails) Settings().setValue(self.settings_section + '/thumbnails', self.thumbnails)
@ -235,6 +278,8 @@ class RemoteTab(SettingsTab):
Settings().setValue(self.settings_section + '/user id', self.user_id.text()) Settings().setValue(self.settings_section + '/user id', self.user_id.text())
Settings().setValue(self.settings_section + '/password', self.password.text()) Settings().setValue(self.settings_section + '/password', self.password.text())
self.generate_icon() self.generate_icon()
if self.update_site_group_box.isChecked():
self.settings_form.register_post_process('download_website')
def on_twelve_hour_check_box_changed(self, check_state): def on_twelve_hour_check_box_changed(self, check_state):
""" """

View File

@ -0,0 +1,142 @@
# -*- 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:`http` module contains the API web server. This is a lightweight web server used by remotes to interact
with OpenLP. It uses JSON to communicate with the remotes.
"""
import asyncio
import websockets
import json
import logging
import time
from PyQt5 import QtCore
from openlp.core.common import Settings, RegistryProperties, OpenLPMixin, Registry
log = logging.getLogger(__name__)
class WebSocketWorker(QtCore.QObject):
"""
A special Qt thread class to allow the WebSockets server to run at the same time as the UI.
"""
def __init__(self, server):
"""
Constructor for the thread class.
:param server: The http server class.
"""
self.ws_server = server
super(WebSocketWorker, self).__init__()
def run(self):
"""
Run the thread.
"""
self.ws_server.start_server()
def stop(self):
self.ws_server.stop = True
class WebSocketServer(RegistryProperties, OpenLPMixin):
"""
Wrapper round a server instance
"""
def __init__(self):
"""
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()
def start_server(self):
"""
Start the correct server and save the handler
"""
address = Settings().value(self.settings_section + '/ip address')
port = Settings().value(self.settings_section + '/websocket port')
self.start_websocket_instance(address, port)
# If web socket server start listening
if hasattr(self, 'ws_server') and self.ws_server:
event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(event_loop)
event_loop.run_until_complete(self.ws_server)
event_loop.run_forever()
else:
log.debug('Failed to start ws server on port {port}'.format(port=port))
def start_websocket_instance(self, address, port):
"""
Start the server
:param address: The server address
:param port: The run port
"""
loop = 1
while loop < 4:
try:
self.ws_server = websockets.serve(self.handle_websocket, address, port)
log.debug("Web Socket Server started for class {address} {port}".format(address=address, port=port))
break
except Exception as e:
log.error('Failed to start ws server {why}'.format(why=e))
loop += 1
time.sleep(0.1)
@staticmethod
async def handle_websocket(request, path):
"""
Handle web socket requests and return the poll information.
Check ever 0.2 seconds to get the latest position and send if changed.
Only gets triggered when 1st client attaches
:param request: request from client
:param path: determines the endpoints supported
:return:
"""
log.debug("web socket handler registered with client")
previous_poll = None
previous_main_poll = None
poller = Registry().get('poller')
if path == '/state':
while True:
current_poll = poller.poll()
if current_poll != previous_poll:
await request.send(json.dumps(current_poll).encode())
previous_poll = current_poll
await asyncio.sleep(0.2)
elif path == '/live_changed':
while True:
main_poll = poller.main_poll()
if main_poll != previous_main_poll:
await request.send(main_poll)
previous_main_poll = main_poll
await asyncio.sleep(0.2)

View File

@ -32,7 +32,6 @@ import sys
import traceback import traceback
from chardet.universaldetector import UniversalDetector from chardet.universaldetector import UniversalDetector
from ipaddress import IPv4Address, IPv6Address, AddressValueError from ipaddress import IPv4Address, IPv6Address, AddressValueError
from pathlib import Path
from shutil import which from shutil import which
from subprocess import check_output, CalledProcessError, STDOUT from subprocess import check_output, CalledProcessError, STDOUT
@ -65,17 +64,18 @@ def trace_error_handler(logger):
def check_directory_exists(directory, do_not_log=False): def check_directory_exists(directory, do_not_log=False):
""" """
Check a theme directory exists and if not create it Check a directory exists and if not create it
:param directory: The directory to make sure exists :param openlp.core.common.path.Path directory: The directory to make sure exists
:param do_not_log: To not log anything. This is need for the start up, when the log isn't ready. :param bool do_not_log: To not log anything. This is need for the start up, when the log isn't ready.
:rtype: None
""" """
if not do_not_log: if not do_not_log:
log.debug('check_directory_exists {text}'.format(text=directory)) log.debug('check_directory_exists {text}'.format(text=directory))
try: try:
if not os.path.exists(directory): if not directory.exists():
os.makedirs(directory) directory.mkdir(parents=True)
except IOError as e: except IOError:
if not do_not_log: if not do_not_log:
log.exception('failed to check if directory exists or create directory') log.exception('failed to check if directory exists or create directory')
@ -85,17 +85,12 @@ def extension_loader(glob_pattern, excluded_files=[]):
A utility function to find and load OpenLP extensions, such as plugins, presentation and media controllers and A utility function to find and load OpenLP extensions, such as plugins, presentation and media controllers and
importers. importers.
:param glob_pattern: A glob pattern used to find the extension(s) to be imported. Should be relative to the :param str glob_pattern: A glob pattern used to find the extension(s) to be imported. Should be relative to the
application directory. i.e. openlp/plugins/*/*plugin.py application directory. i.e. plugins/*/*plugin.py
:type glob_pattern: str :param list[str] excluded_files: A list of file names to exclude that the glob pattern may find.
:param excluded_files: A list of file names to exclude that the glob pattern may find.
:type excluded_files: list of strings
:return: None
:rtype: None :rtype: None
""" """
app_dir = Path(AppLocation.get_directory(AppLocation.AppDir)).parent app_dir = AppLocation.get_directory(AppLocation.AppDir)
for extension_path in app_dir.glob(glob_pattern): for extension_path in app_dir.glob(glob_pattern):
extension_path = extension_path.relative_to(app_dir) extension_path = extension_path.relative_to(app_dir)
if extension_path.name in excluded_files: if extension_path.name in excluded_files:
@ -106,21 +101,19 @@ def extension_loader(glob_pattern, excluded_files=[]):
except (ImportError, OSError): except (ImportError, OSError):
# On some platforms importing vlc.py might cause OSError exceptions. (e.g. Mac OS X) # On some platforms importing vlc.py might cause OSError exceptions. (e.g. Mac OS X)
log.warning('Failed to import {module_name} on path {extension_path}' log.warning('Failed to import {module_name} on path {extension_path}'
.format(module_name=module_name, extension_path=str(extension_path))) .format(module_name=module_name, extension_path=extension_path))
def path_to_module(path): def path_to_module(path):
""" """
Convert a path to a module name (i.e openlp.core.common) Convert a path to a module name (i.e openlp.core.common)
:param path: The path to convert to a module name. :param openlp.core.common.path.Path path: The path to convert to a module name.
:type path: Path
:return: The module name. :return: The module name.
:rtype: str :rtype: str
""" """
module_path = path.with_suffix('') module_path = path.with_suffix('')
return '.'.join(module_path.parts) return 'openlp.' + '.'.join(module_path.parts)
def get_frozen_path(frozen_option, non_frozen_option): def get_frozen_path(frozen_option, non_frozen_option):
@ -378,20 +371,22 @@ def split_filename(path):
return os.path.split(path) return os.path.split(path)
def delete_file(file_path_name): def delete_file(file_path):
""" """
Deletes a file from the system. Deletes a file from the system.
:param file_path_name: The file, including path, to delete. :param openlp.core.common.path.Path file_path: The file, including path, to delete.
:return: True if the deletion was successful, or the file never existed. False otherwise.
:rtype: bool
""" """
if not file_path_name: if not file_path:
return False return False
try: try:
if os.path.exists(file_path_name): if file_path.exists():
os.remove(file_path_name) file_path.unlink()
return True return True
except (IOError, OSError): except (IOError, OSError):
log.exception("Unable to delete file {text}".format(text=file_path_name)) log.exception('Unable to delete file {file_path}'.format(file_path=file_path))
return False return False
@ -411,18 +406,19 @@ def get_images_filter():
return IMAGES_FILTER return IMAGES_FILTER
def is_not_image_file(file_name): def is_not_image_file(file_path):
""" """
Validate that the file is not an image file. Validate that the file is not an image file.
:param file_name: File name to be checked. :param openlp.core.common.path.Path file_path: The file to be checked.
:return: If the file is not an image
:rtype: bool
""" """
if not file_name: if not (file_path and file_path.exists()):
return True return True
else: else:
formats = [bytes(fmt).decode().lower() for fmt in QtGui.QImageReader.supportedImageFormats()] formats = [bytes(fmt).decode().lower() for fmt in QtGui.QImageReader.supportedImageFormats()]
file_part, file_extension = os.path.splitext(str(file_name)) if file_path.suffix[1:].lower() in formats:
if file_extension[1:].lower() in formats and os.path.exists(file_name):
return False return False
return True return True
@ -431,10 +427,10 @@ def clean_filename(filename):
""" """
Removes invalid characters from the given ``filename``. Removes invalid characters from the given ``filename``.
:param filename: The "dirty" file name to clean. :param str filename: The "dirty" file name to clean.
:return: The cleaned string
:rtype: str
""" """
if not isinstance(filename, str):
filename = str(filename, 'utf-8')
return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename)) return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename))
@ -442,8 +438,9 @@ def check_binary_exists(program_path):
""" """
Function that checks whether a binary exists. Function that checks whether a binary exists.
:param program_path: The full path to the binary to check. :param openlp.core.common.path.Path program_path: The full path to the binary to check.
:return: program output to be parsed :return: program output to be parsed
:rtype: bytes
""" """
log.debug('testing program_path: {text}'.format(text=program_path)) log.debug('testing program_path: {text}'.format(text=program_path))
try: try:
@ -453,26 +450,27 @@ def check_binary_exists(program_path):
startupinfo.dwFlags |= STARTF_USESHOWWINDOW startupinfo.dwFlags |= STARTF_USESHOWWINDOW
else: else:
startupinfo = None startupinfo = None
runlog = check_output([program_path, '--help'], stderr=STDOUT, startupinfo=startupinfo) run_log = check_output([str(program_path), '--help'], stderr=STDOUT, startupinfo=startupinfo)
except CalledProcessError as e: except CalledProcessError as e:
runlog = e.output run_log = e.output
except Exception: except Exception:
trace_error_handler(log) trace_error_handler(log)
runlog = '' run_log = ''
log.debug('check_output returned: {text}'.format(text=runlog)) log.debug('check_output returned: {text}'.format(text=run_log))
return runlog return run_log
def get_file_encoding(filename): def get_file_encoding(file_path):
""" """
Utility function to incrementally detect the file encoding. Utility function to incrementally detect the file encoding.
:param filename: Filename for the file to determine the encoding for. Str :param openlp.core.common.path.Path file_path: Filename for the file to determine the encoding for.
:return: A dict with the keys 'encoding' and 'confidence' :return: A dict with the keys 'encoding' and 'confidence'
:rtype: dict[str, float]
""" """
detector = UniversalDetector() detector = UniversalDetector()
try: try:
with open(filename, 'rb') as detect_file: with file_path.open('rb') as detect_file:
while not detector.done: while not detector.done:
chunk = detect_file.read(1024) chunk = detect_file.read(1024)
if not chunk: if not chunk:

View File

@ -27,6 +27,7 @@ import os
import sys import sys
from openlp.core.common import Settings, is_win, is_macosx from openlp.core.common import Settings, is_win, is_macosx
from openlp.core.common.path import Path
if not is_win() and not is_macosx(): if not is_win() and not is_macosx():
@ -42,6 +43,9 @@ from openlp.core.common import check_directory_exists, get_frozen_path
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
FROZEN_APP_PATH = Path(sys.argv[0]).parent
APP_PATH = Path(openlp.__file__).parent
class AppLocation(object): class AppLocation(object):
""" """
@ -54,29 +58,21 @@ class AppLocation(object):
CacheDir = 5 CacheDir = 5
LanguageDir = 6 LanguageDir = 6
# Base path where data/config/cache dir is located
BaseDir = None
@staticmethod @staticmethod
def get_directory(dir_type=AppDir): def get_directory(dir_type=AppDir):
""" """
Return the appropriate directory according to the directory type. Return the appropriate directory according to the directory type.
:param dir_type: The directory type you want, for instance the data directory. Default *AppLocation.AppDir* :param dir_type: The directory type you want, for instance the data directory. Default *AppLocation.AppDir*
:return: The requested path
:rtype: openlp.core.common.path.Path
""" """
if dir_type == AppLocation.AppDir: if dir_type == AppLocation.AppDir or dir_type == AppLocation.VersionDir:
return get_frozen_path(os.path.abspath(os.path.dirname(sys.argv[0])), os.path.dirname(openlp.__file__)) return get_frozen_path(FROZEN_APP_PATH, APP_PATH)
elif dir_type == AppLocation.PluginsDir: elif dir_type == AppLocation.PluginsDir:
app_path = os.path.abspath(os.path.dirname(sys.argv[0])) return get_frozen_path(FROZEN_APP_PATH, APP_PATH) / 'plugins'
return get_frozen_path(os.path.join(app_path, 'plugins'),
os.path.join(os.path.dirname(openlp.__file__), 'plugins'))
elif dir_type == AppLocation.VersionDir:
return get_frozen_path(os.path.abspath(os.path.dirname(sys.argv[0])), os.path.dirname(openlp.__file__))
elif dir_type == AppLocation.LanguageDir: elif dir_type == AppLocation.LanguageDir:
app_path = get_frozen_path(os.path.abspath(os.path.dirname(sys.argv[0])), _get_os_dir_path(dir_type)) return get_frozen_path(FROZEN_APP_PATH, _get_os_dir_path(dir_type)) / 'i18n'
return os.path.join(app_path, 'i18n')
elif dir_type == AppLocation.DataDir and AppLocation.BaseDir:
return os.path.join(AppLocation.BaseDir, 'data')
else: else:
return _get_os_dir_path(dir_type) return _get_os_dir_path(dir_type)
@ -84,6 +80,9 @@ class AppLocation(object):
def get_data_path(): def get_data_path():
""" """
Return the path OpenLP stores all its data under. Return the path OpenLP stores all its data under.
:return: The data path to use.
:rtype: openlp.core.common.path.Path
""" """
# Check if we have a different data location. # Check if we have a different data location.
if Settings().contains('advanced/data path'): if Settings().contains('advanced/data path'):
@ -91,40 +90,38 @@ class AppLocation(object):
else: else:
path = AppLocation.get_directory(AppLocation.DataDir) path = AppLocation.get_directory(AppLocation.DataDir)
check_directory_exists(path) check_directory_exists(path)
return os.path.normpath(path) return path
@staticmethod @staticmethod
def get_files(section=None, extension=None): def get_files(section=None, extension=''):
""" """
Get a list of files from the data files path. Get a list of files from the data files path.
:param section: Defaults to *None*. The section of code getting the files - used to load from a section's :param None | str section: Defaults to *None*. The section of code getting the files - used to load from a
data subdirectory. section's data subdirectory.
:param extension: :param str extension: Defaults to ''. The extension to search for. For example::
Defaults to *None*. The extension to search for. For example:: '.png'
:return: List of files found.
'.png' :rtype: list[openlp.core.common.path.Path]
""" """
path = AppLocation.get_data_path() path = AppLocation.get_data_path()
if section: if section:
path = os.path.join(path, section) path = path / section
try: try:
files = os.listdir(path) file_paths = path.glob('*' + extension)
return [file_path.relative_to(path) for file_path in file_paths]
except OSError: except OSError:
return [] return []
if extension:
return [filename for filename in files if extension == os.path.splitext(filename)[1]]
else:
# no filtering required
return files
@staticmethod @staticmethod
def get_section_data_path(section): def get_section_data_path(section):
""" """
Return the path a particular module stores its data under. Return the path a particular module stores its data under.
:param str section:
:rtype: openlp.core.common.path.Path
""" """
data_path = AppLocation.get_data_path() path = AppLocation.get_data_path() / section
path = os.path.join(data_path, section)
check_directory_exists(path) check_directory_exists(path)
return path return path
@ -132,36 +129,41 @@ class AppLocation(object):
def _get_os_dir_path(dir_type): def _get_os_dir_path(dir_type):
""" """
Return a path based on which OS and environment we are running in. Return a path based on which OS and environment we are running in.
:param dir_type: AppLocation Enum of the requested path type
:return: The requested path
:rtype: openlp.core.common.path.Path
""" """
# If running from source, return the language directory from the source directory # If running from source, return the language directory from the source directory
if dir_type == AppLocation.LanguageDir: if dir_type == AppLocation.LanguageDir:
directory = os.path.abspath(os.path.join(os.path.dirname(openlp.__file__), '..', 'resources')) directory = Path(openlp.__file__, '..', '..').resolve() / 'resources'
if os.path.exists(directory): if directory.exists():
return directory return directory
if is_win(): if is_win():
openlp_folder_path = Path(os.getenv('APPDATA'), 'openlp')
if dir_type == AppLocation.DataDir: if dir_type == AppLocation.DataDir:
return os.path.join(str(os.getenv('APPDATA')), 'openlp', 'data') return openlp_folder_path / 'data'
elif dir_type == AppLocation.LanguageDir: elif dir_type == AppLocation.LanguageDir:
return os.path.dirname(openlp.__file__) return Path(openlp.__file__).parent
return os.path.join(str(os.getenv('APPDATA')), 'openlp') return openlp_folder_path
elif is_macosx(): elif is_macosx():
openlp_folder_path = Path(os.getenv('HOME'), 'Library', 'Application Support', 'openlp')
if dir_type == AppLocation.DataDir: if dir_type == AppLocation.DataDir:
return os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp', 'Data') return openlp_folder_path / 'Data'
elif dir_type == AppLocation.LanguageDir: elif dir_type == AppLocation.LanguageDir:
return os.path.dirname(openlp.__file__) return Path(openlp.__file__).parent
return os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp') return openlp_folder_path
else: else:
if dir_type == AppLocation.LanguageDir: if dir_type == AppLocation.LanguageDir:
for prefix in ['/usr/local', '/usr']: directory = Path('/usr', 'local', 'share', 'openlp')
directory = os.path.join(prefix, 'share', 'openlp') if directory.exists():
if os.path.exists(directory): return directory
return directory return Path('/usr', 'share', 'openlp')
return os.path.join('/usr', 'share', 'openlp')
if XDG_BASE_AVAILABLE: if XDG_BASE_AVAILABLE:
if dir_type == AppLocation.DataDir: if dir_type == AppLocation.DataDir or dir_type == AppLocation.CacheDir:
return os.path.join(str(BaseDirectory.xdg_data_home), 'openlp') return Path(BaseDirectory.xdg_data_home, 'openlp')
elif dir_type == AppLocation.CacheDir: elif dir_type == AppLocation.CacheDir:
return os.path.join(str(BaseDirectory.xdg_cache_home), 'openlp') return Path(BaseDirectory.xdg_cache_home, 'openlp')
if dir_type == AppLocation.DataDir: if dir_type == AppLocation.DataDir:
return os.path.join(str(os.getenv('HOME')), '.openlp', 'data') return Path(os.getenv('HOME'), '.openlp', 'data')
return os.path.join(str(os.getenv('HOME')), '.openlp') return Path(os.getenv('HOME'), '.openlp')

View File

@ -25,8 +25,10 @@ The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP.
import hashlib import hashlib
import logging import logging
import os import os
import platform
import socket import socket
import sys import sys
import subprocess
import time import time
import urllib.error import urllib.error
import urllib.parse import urllib.parse
@ -215,6 +217,7 @@ def url_get_file(callback, url, f_path, sha256=None):
block_count = 0 block_count = 0
block_size = 4096 block_size = 4096
retries = 0 retries = 0
log.debug("url_get_file: " + url)
while True: while True:
try: try:
filename = open(f_path, "wb") filename = open(f_path, "wb")
@ -253,4 +256,17 @@ def url_get_file(callback, url, f_path, sha256=None):
return True return True
def ping(host):
"""
Returns True if host responds to a ping request
"""
# Ping parameters as function of OS
ping_str = "-n 1" if platform.system().lower() == "windows" else "-c 1"
args = "ping " + " " + ping_str + " " + host
need_sh = False if platform.system().lower() == "windows" else True
# Ping
return subprocess.call(args, shell=need_sh) == 0
__all__ = ['get_web_page'] __all__ = ['get_web_page']

View File

@ -0,0 +1,86 @@
# -*- 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 json import JSONDecoder, JSONEncoder
from openlp.core.common.path import Path
class OpenLPJsonDecoder(JSONDecoder):
"""
Implement a custom JSONDecoder to handle Path objects
Example Usage:
object = json.loads(json_string, cls=OpenLPJsonDecoder)
"""
def __init__(self, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, strict=True,
object_pairs_hook=None, **kwargs):
"""
Re-implement __init__ so that we can pass in our object_hook method. Any additional kwargs, are stored in the
instance and are passed to custom objects upon encoding or decoding.
"""
self.kwargs = kwargs
if object_hook is None:
object_hook = self.custom_object_hook
super().__init__(object_hook, parse_float, parse_int, parse_constant, strict, object_pairs_hook)
def custom_object_hook(self, obj):
"""
Implement a custom Path object decoder.
:param dict obj: A decoded JSON object
:return: The original object literal, or a Path object if the object literal contains a key '__Path__'
:rtype: dict | openlp.core.common.path.Path
"""
if '__Path__' in obj:
obj = Path.encode_json(obj, **self.kwargs)
return obj
class OpenLPJsonEncoder(JSONEncoder):
"""
Implement a custom JSONEncoder to handle Path objects
Example Usage:
json_string = json.dumps(object, cls=OpenLPJsonEncoder)
"""
def __init__(self, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False,
indent=None, separators=None, default=None, **kwargs):
"""
Re-implement __init__ so that we can pass in additional kwargs, which are stored in the instance and are passed
to custom objects upon encoding or decoding.
"""
self.kwargs = kwargs
if default is None:
default = self.custom_default
super().__init__(skipkeys, ensure_ascii, check_circular, allow_nan, sort_keys, indent, separators, default)
def custom_default(self, obj):
"""
Convert any Path objects into a dictionary object which can be serialized.
:param object obj: The object to convert
:return: The serializable object
:rtype: dict
"""
if isinstance(obj, Path):
return obj.json_object(**self.kwargs)
return super().default(obj)

View File

@ -53,7 +53,7 @@ class LanguageManager(object):
""" """
if LanguageManager.auto_language: if LanguageManager.auto_language:
language = QtCore.QLocale.system().name() language = QtCore.QLocale.system().name()
lang_path = AppLocation.get_directory(AppLocation.LanguageDir) lang_path = str(AppLocation.get_directory(AppLocation.LanguageDir))
app_translator = QtCore.QTranslator() app_translator = QtCore.QTranslator()
app_translator.load(language, lang_path) app_translator.load(language, lang_path)
# A translator for buttons and other default strings provided by Qt. # A translator for buttons and other default strings provided by Qt.
@ -72,7 +72,7 @@ class LanguageManager(object):
Find all available language files in this OpenLP install Find all available language files in this OpenLP install
""" """
log.debug('Translation files: {files}'.format(files=AppLocation.get_directory(AppLocation.LanguageDir))) log.debug('Translation files: {files}'.format(files=AppLocation.get_directory(AppLocation.LanguageDir)))
trans_dir = QtCore.QDir(AppLocation.get_directory(AppLocation.LanguageDir)) trans_dir = QtCore.QDir(str(AppLocation.get_directory(AppLocation.LanguageDir)))
file_names = trans_dir.entryList(['*.qm'], QtCore.QDir.Files, QtCore.QDir.Name) file_names = trans_dir.entryList(['*.qm'], QtCore.QDir.Files, QtCore.QDir.Name)
# Remove qm files from the list which start with "qt". # Remove qm files from the list which start with "qt".
file_names = [file_ for file_ in file_names if not file_.startswith('qt')] file_names = [file_ for file_ in file_names if not file_.startswith('qt')]
@ -140,8 +140,8 @@ class LanguageManager(object):
reg_ex = QtCore.QRegExp("^.*i18n/(.*).qm") reg_ex = QtCore.QRegExp("^.*i18n/(.*).qm")
if reg_ex.exactMatch(qmf): if reg_ex.exactMatch(qmf):
name = '{regex}'.format(regex=reg_ex.cap(1)) name = '{regex}'.format(regex=reg_ex.cap(1))
# TODO: Test before converting to python3 string format LanguageManager.__qm_list__[
LanguageManager.__qm_list__['%#2i %s' % (counter + 1, LanguageManager.language_name(qmf))] = name '{count:>2i} {name}'.format(count=counter + 1, name=LanguageManager.language_name(qmf))] = name
@staticmethod @staticmethod
def get_qm_list(): def get_qm_list():

100
openlp/core/common/path.py Normal file
View File

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-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 contextlib import suppress
from openlp.core.common import is_win
if is_win():
from pathlib import WindowsPath as PathVariant
else:
from pathlib import PosixPath as PathVariant
def path_to_str(path=None):
"""
A utility function to convert a Path object or NoneType to a string equivalent.
:param openlp.core.common.path.Path | None path: The value to convert to a string
:return: An empty string if :param:`path` is None, else a string representation of the :param:`path`
:rtype: str
"""
if not isinstance(path, Path) and path is not None:
raise TypeError('parameter \'path\' must be of type Path or NoneType')
if path is None:
return ''
else:
return str(path)
def str_to_path(string):
"""
A utility function to convert a str object to a Path or NoneType.
This function is of particular use because initating a Path object with an empty string causes the Path object to
point to the current working directory.
:param str string: The string to convert
:return: None if :param:`string` is empty, or a Path object representation of :param:`string`
:rtype: openlp.core.common.path.Path | None
"""
if not isinstance(string, str):
raise TypeError('parameter \'string\' must be of type str')
if string == '':
return None
return Path(string)
class Path(PathVariant):
"""
Subclass pathlib.Path, so we can add json conversion methods
"""
@staticmethod
def encode_json(obj, base_path=None, **kwargs):
"""
Create a Path object from a dictionary representation. The dictionary has been constructed by JSON encoding of
a JSON reprensation of a Path object.
:param dict[str] obj: The dictionary representation
:param openlp.core.common.path.Path base_path: If specified, an absolute path to base the relative path off of.
:param kwargs: Contains any extra parameters. Not used!
:return: The reconstructed Path object
:rtype: openlp.core.common.path.Path
"""
path = Path(*obj['__Path__'])
if base_path and not path.is_absolute():
return base_path / path
return path
def json_object(self, base_path=None, **kwargs):
"""
Create a dictionary that can be JSON decoded.
:param openlp.core.common.path.Path base_path: If specified, an absolute path to make a relative path from.
:param kwargs: Contains any extra parameters. Not used!
:return: The dictionary representation of this Path object.
:rtype: dict[tuple]
"""
path = self
if base_path:
with suppress(ValueError):
path = path.relative_to(base_path)
return {'__Path__': path.parts}

View File

@ -24,15 +24,19 @@ This class contains the core default settings.
""" """
import datetime import datetime
import logging import logging
import json
import os import os
from tempfile import gettempdir
from PyQt5 import QtCore, QtGui from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import ThemeLevel, SlideLimits, UiStrings, is_win, is_linux
from openlp.core.common import SlideLimits, ThemeLevel, UiStrings, is_linux, is_win, translate
from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder
from openlp.core.common.path import Path, str_to_path
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
__version__ = 2
# Fix for bug #1014422. # Fix for bug #1014422.
X11_BYPASS_DEFAULT = True X11_BYPASS_DEFAULT = True
@ -44,21 +48,6 @@ if is_linux():
X11_BYPASS_DEFAULT = False X11_BYPASS_DEFAULT = False
def recent_files_conv(value):
"""
If the value is not a list convert it to a list
:param value: Value to convert
:return: value as a List
"""
if isinstance(value, list):
return value
elif isinstance(value, str):
return [value]
elif isinstance(value, bytes):
return [value.decode()]
return []
def media_players_conv(string): def media_players_conv(string):
""" """
If phonon is in the setting string replace it with system If phonon is in the setting string replace it with system
@ -73,14 +62,25 @@ def media_players_conv(string):
return string return string
def file_names_conv(file_names):
"""
Convert a list of file names in to a list of file paths.
:param list[str] file_names: The list of file names to convert.
:return: The list converted to file paths
:rtype: openlp.core.common.path.Path
"""
if file_names:
return [str_to_path(file_name) for file_name in file_names]
class Settings(QtCore.QSettings): class Settings(QtCore.QSettings):
""" """
Class to wrap QSettings. Class to wrap QSettings.
* Exposes all the methods of QSettings. * Exposes all the methods of QSettings.
* Adds functionality for OpenLP Portable. If the ``defaultFormat`` is set to * Adds functionality for OpenLP Portable. If the ``defaultFormat`` is set to ``IniFormat``, and the path to the Ini
``IniFormat``, and the path to the Ini file is set using ``set_filename``, file is set using ``set_filename``, then the Settings constructor (without any arguments) will create a Settings
then the Settings constructor (without any arguments) will create a Settings
object for accessing settings stored in that Ini file. object for accessing settings stored in that Ini file.
``__default_settings__`` ``__default_settings__``
@ -91,7 +91,7 @@ class Settings(QtCore.QSettings):
('general/enable slide loop', 'advanced/slide limits', [(SlideLimits.Wrap, True), (SlideLimits.End, False)]) ('general/enable slide loop', 'advanced/slide limits', [(SlideLimits.Wrap, True), (SlideLimits.End, False)])
The first entry is the *old key*; it will be removed. The first entry is the *old key*; if it is different from the *new key* it will be removed.
The second entry is the *new key*; we will add it to the config. If this is just an empty string, we just remove The second entry is the *new key*; we will add it to the config. If this is just an empty string, we just remove
the old key. The last entry is a list containing two-pair tuples. If the list is empty, no conversion is made. the old key. The last entry is a list containing two-pair tuples. If the list is empty, no conversion is made.
@ -105,11 +105,12 @@ class Settings(QtCore.QSettings):
So, if the type of the old value is bool, then there must be two rules. So, if the type of the old value is bool, then there must be two rules.
""" """
__default_settings__ = { __default_settings__ = {
'settings/version': 0,
'advanced/add page break': False, 'advanced/add page break': False,
'advanced/alternate rows': not is_win(), 'advanced/alternate rows': not is_win(),
'advanced/autoscrolling': {'dist': 1, 'pos': 0}, 'advanced/autoscrolling': {'dist': 1, 'pos': 0},
'advanced/current media plugin': -1, 'advanced/current media plugin': -1,
'advanced/data path': '', 'advanced/data path': None,
# 7 stands for now, 0 to 6 is Monday to Sunday. # 7 stands for now, 0 to 6 is Monday to Sunday.
'advanced/default service day': 7, 'advanced/default service day': 7,
'advanced/default service enabled': True, 'advanced/default service enabled': True,
@ -121,6 +122,7 @@ class Settings(QtCore.QSettings):
'advanced/enable exit confirmation': True, 'advanced/enable exit confirmation': True,
'advanced/expand service item': False, 'advanced/expand service item': False,
'advanced/hide mouse': True, 'advanced/hide mouse': True,
'advanced/ignore aspect ratio': False,
'advanced/is portable': False, 'advanced/is portable': False,
'advanced/max recent files': 20, 'advanced/max recent files': 20,
'advanced/print file meta data': False, 'advanced/print file meta data': False,
@ -134,7 +136,15 @@ class Settings(QtCore.QSettings):
'advanced/single click service preview': False, 'advanced/single click service preview': False,
'advanced/x11 bypass wm': X11_BYPASS_DEFAULT, 'advanced/x11 bypass wm': X11_BYPASS_DEFAULT,
'advanced/search as type': True, 'advanced/search as type': True,
'crashreport/last directory': '', 'api/twelve hour': True,
'api/port': 4316,
'api/websocket port': 4317,
'api/user id': 'openlp',
'api/password': 'password',
'api/authentication enabled': False,
'api/ip address': '0.0.0.0',
'api/thumbnails': True,
'crashreport/last directory': None,
'formattingTags/html_tags': '', 'formattingTags/html_tags': '',
'core/audio repeat list': False, 'core/audio repeat list': False,
'core/auto open': False, 'core/auto open': False,
@ -153,7 +163,7 @@ class Settings(QtCore.QSettings):
'core/screen blank': False, 'core/screen blank': False,
'core/show splash': True, 'core/show splash': True,
'core/logo background color': '#ffffff', 'core/logo background color': '#ffffff',
'core/logo file': ':/graphics/openlp-splash-screen.png', 'core/logo file': Path(':/graphics/openlp-splash-screen.png'),
'core/logo hide on startup': False, 'core/logo hide on startup': False,
'core/songselect password': '', 'core/songselect password': '',
'core/songselect username': '', 'core/songselect username': '',
@ -168,17 +178,17 @@ class Settings(QtCore.QSettings):
'media/players': 'system,webkit', 'media/players': 'system,webkit',
'media/override player': QtCore.Qt.Unchecked, 'media/override player': QtCore.Qt.Unchecked,
'players/background color': '#000000', 'players/background color': '#000000',
'servicemanager/last directory': '', 'servicemanager/last directory': None,
'servicemanager/last file': '', 'servicemanager/last file': None,
'servicemanager/service theme': '', 'servicemanager/service theme': None,
'SettingsImport/file_date_created': datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), 'SettingsImport/file_date_created': datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
'SettingsImport/Make_Changes': 'At_Own_RISK', 'SettingsImport/Make_Changes': 'At_Own_RISK',
'SettingsImport/type': 'OpenLP_settings_export', 'SettingsImport/type': 'OpenLP_settings_export',
'SettingsImport/version': '', 'SettingsImport/version': '',
'themes/global theme': '', 'themes/global theme': '',
'themes/last directory': '', 'themes/last directory': None,
'themes/last directory export': '', 'themes/last directory export': None,
'themes/last directory import': '', 'themes/last directory import': None,
'themes/theme level': ThemeLevel.Song, 'themes/theme level': ThemeLevel.Song,
'themes/wrap footer': False, 'themes/wrap footer': False,
'user interface/live panel': True, 'user interface/live panel': True,
@ -199,27 +209,60 @@ class Settings(QtCore.QSettings):
'projector/db database': '', 'projector/db database': '',
'projector/enable': True, 'projector/enable': True,
'projector/connect on start': False, 'projector/connect on start': False,
'projector/last directory import': '', 'projector/last directory import': None,
'projector/last directory export': '', 'projector/last directory export': None,
'projector/poll time': 20, # PJLink timeout is 30 seconds 'projector/poll time': 20, # PJLink timeout is 30 seconds
'projector/socket timeout': 5, # 5 second socket timeout 'projector/socket timeout': 5, # 5 second socket timeout
'projector/source dialog type': 0 # Source select dialog box type 'projector/source dialog type': 0 # Source select dialog box type
} }
__file_path__ = '' __file_path__ = ''
__obsolete_settings__ = [ __setting_upgrade_1__ = [
# Changed during 2.2.x development. # Changed during 2.2.x development.
# ('advanced/stylesheet fix', '', []),
# ('general/recent files', 'core/recent files', [(recent_files_conv, None)]),
('songs/search as type', 'advanced/search as type', []), ('songs/search as type', 'advanced/search as type', []),
('media/players', 'media/players_temp', [(media_players_conv, None)]), # Convert phonon to system ('media/players', 'media/players_temp', [(media_players_conv, None)]), # Convert phonon to system
('media/players_temp', 'media/players', []), # Move temp setting from above to correct setting ('media/players_temp', 'media/players', []), # Move temp setting from above to correct setting
('advanced/default color', 'core/logo background color', []), # Default image renamed + moved to general > 2.4. ('advanced/default color', 'core/logo background color', []), # Default image renamed + moved to general > 2.4.
('advanced/default image', 'core/logo file', []), # Default image renamed + moved to general after 2.4. ('advanced/default image', 'core/logo file', []), # Default image renamed + moved to general after 2.4.
('remotes/https enabled', '', []),
('remotes/https port', '', []),
('remotes/twelve hour', 'api/twelve hour', []),
('remotes/port', 'api/port', []),
('remotes/websocket port', 'api/websocket port', []),
('remotes/user id', 'api/user id', []),
('remotes/password', 'api/password', []),
('remotes/authentication enabled', 'api/authentication enabled', []),
('remotes/ip address', 'api/ip address', []),
('remotes/thumbnails', 'api/thumbnails', []),
('shortcuts/escapeItem', 'shortcuts/desktopScreenEnable', []), # Escape item was removed in 2.6. ('shortcuts/escapeItem', 'shortcuts/desktopScreenEnable', []), # Escape item was removed in 2.6.
('shortcuts/offlineHelpItem', 'shortcuts/userManualItem', []), # Online and Offline help were combined in 2.6. ('shortcuts/offlineHelpItem', 'shortcuts/userManualItem', []), # Online and Offline help were combined in 2.6.
('shortcuts/onlineHelpItem', 'shortcuts/userManualItem', []), # Online and Offline help were combined in 2.6. ('shortcuts/onlineHelpItem', 'shortcuts/userManualItem', []), # Online and Offline help were combined in 2.6.
('bibles/advanced bible', '', []), # Common bible search widgets combined in 2.6 ('bibles/advanced bible', '', []), # Common bible search widgets combined in 2.6
('bibles/quick bible', 'bibles/primary bible', []) # Common bible search widgets combined in 2.6 ('bibles/quick bible', 'bibles/primary bible', []), # Common bible search widgets combined in 2.6
# Last search type was renamed to last used search type in 2.6 since Bible search value type changed in 2.6.
('songs/last search type', 'songs/last used search type', []),
('bibles/last search type', '', []),
('custom/last search type', 'custom/last used search type', [])]
__setting_upgrade_2__ = [
# The following changes are being made for the conversion to using Path objects made in 2.6 development
('advanced/data path', 'advanced/data path', [(str_to_path, None)]),
('crashreport/last directory', 'crashreport/last directory', [(str_to_path, None)]),
('servicemanager/last directory', 'servicemanager/last directory', [(str_to_path, None)]),
('servicemanager/last file', 'servicemanager/last file', [(str_to_path, None)]),
('themes/last directory', 'themes/last directory', [(str_to_path, None)]),
('themes/last directory export', 'themes/last directory export', [(str_to_path, None)]),
('themes/last directory import', 'themes/last directory import', [(str_to_path, None)]),
('projector/last directory import', 'projector/last directory import', [(str_to_path, None)]),
('projector/last directory export', 'projector/last directory export', [(str_to_path, None)]),
('bibles/last directory import', 'bibles/last directory import', [(str_to_path, None)]),
('presentations/pdf_program', 'presentations/pdf_program', [(str_to_path, None)]),
('songs/last directory import', 'songs/last directory import', [(str_to_path, None)]),
('songs/last directory export', 'songs/last directory export', [(str_to_path, None)]),
('songusage/last directory export', 'songusage/last directory export', [(str_to_path, None)]),
('core/recent files', 'core/recent files', [(file_names_conv, None)]),
('media/media files', 'media/media files', [(file_names_conv, None)]),
('presentations/presentations files', 'presentations/presentations files', [(file_names_conv, None)]),
('core/logo file', 'core/logo file', [(str_to_path, None)])
] ]
@staticmethod @staticmethod
@ -232,13 +275,16 @@ class Settings(QtCore.QSettings):
Settings.__default_settings__.update(default_values) Settings.__default_settings__.update(default_values)
@staticmethod @staticmethod
def set_filename(ini_file): def set_filename(ini_path):
""" """
Sets the complete path to an Ini file to be used by Settings objects. Sets the complete path to an Ini file to be used by Settings objects.
Does not affect existing Settings objects. Does not affect existing Settings objects.
:param openlp.core.common.path.Path ini_path: ini file path
:rtype: None
""" """
Settings.__file_path__ = ini_file Settings.__file_path__ = str(ini_path)
@staticmethod @staticmethod
def set_up_default_values(): def set_up_default_values():
@ -407,14 +453,28 @@ class Settings(QtCore.QSettings):
key = self.group() + '/' + key key = self.group() + '/' + key
return Settings.__default_settings__[key] return Settings.__default_settings__[key]
def remove_obsolete_settings(self): def can_upgrade(self):
"""
Can / should the settings be upgraded
:rtype: bool
"""
return __version__ != self.value('settings/version')
def upgrade_settings(self):
""" """
This method is only called to clean up the config. It removes old settings and it renames settings. See This method is only called to clean up the config. It removes old settings and it renames settings. See
``__obsolete_settings__`` for more details. ``__obsolete_settings__`` for more details.
""" """
for old_key, new_key, rules in Settings.__obsolete_settings__: current_version = self.value('settings/version')
# Once removed we don't have to do this again. for version in range(current_version, __version__):
if self.contains(old_key): version += 1
upgrade_list = getattr(self, '__setting_upgrade_{version}__'.format(version=version))
for old_key, 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):
continue
if new_key: if new_key:
# Get the value of the old_key. # Get the value of the old_key.
old_value = super(Settings, self).value(old_key) old_value = super(Settings, self).value(old_key)
@ -433,14 +493,17 @@ class Settings(QtCore.QSettings):
old_value = new old_value = new
break break
self.setValue(new_key, old_value) self.setValue(new_key, old_value)
self.remove(old_key) if new_key != old_key:
self.remove(old_key)
self.setValue('settings/version', version)
def value(self, key): def value(self, key):
""" """
Returns the value for the given ``key``. The returned ``value`` is of the same type as the default value in the Returns the value for the given ``key``. The returned ``value`` is of the same type as the default value in the
*Settings.__default_settings__* dict. *Settings.__default_settings__* dict.
:param key: The key to return the value from. :param str key: The key to return the value from.
:return: The value stored by the setting.
""" """
# if group() is not empty the group has not been specified together with the key. # if group() is not empty the group has not been specified together with the key.
if self.group(): if self.group():
@ -450,6 +513,18 @@ class Settings(QtCore.QSettings):
setting = super(Settings, self).value(key, default_value) setting = super(Settings, self).value(key, default_value)
return self._convert_value(setting, default_value) return self._convert_value(setting, default_value)
def setValue(self, key, value):
"""
Reimplement the setValue method to handle Path objects.
:param str key: The key of the setting to save
:param value: The value to save
:rtype: None
"""
if isinstance(value, Path) or (isinstance(value, list) and value and isinstance(value[0], Path)):
value = json.dumps(value, cls=OpenLPJsonEncoder)
super().setValue(key, value)
def _convert_value(self, setting, default_value): def _convert_value(self, setting, default_value):
""" """
This converts the given ``setting`` to the type of the given ``default_value``. This converts the given ``setting`` to the type of the given ``default_value``.
@ -467,8 +542,11 @@ class Settings(QtCore.QSettings):
if isinstance(default_value, str): if isinstance(default_value, str):
return '' return ''
# An empty list saved to the settings results in a None type being returned. # An empty list saved to the settings results in a None type being returned.
else: elif isinstance(default_value, list):
return [] return []
elif isinstance(setting, str):
if '__Path__' in setting:
return json.loads(setting, cls=OpenLPJsonDecoder)
# Convert the setting to the correct type. # Convert the setting to the correct type.
if isinstance(default_value, bool): if isinstance(default_value, bool):
if isinstance(setting, bool): if isinstance(setting, bool):
@ -479,30 +557,58 @@ class Settings(QtCore.QSettings):
return int(setting) return int(setting)
return setting return setting
def get_files_from_config(self, plugin): def export(self, dest_path):
""" """
This removes the settings needed for old way we saved files (e. g. the image paths for the image plugin). A list Export the settings to file.
of file paths are returned.
**Note**: Only a list of paths is returned; this does not convert anything! :param openlp.core.common.path.Path dest_path: The file path to create the export file.
:return: Success
:param plugin: The Plugin object.The caller has to convert/save the list himself; o :rtype: bool
""" """
files_list = [] temp_path = Path(gettempdir(), 'openlp', 'exportConf.tmp')
# We need QSettings instead of Settings here to bypass our central settings dict. # Delete old files if found.
# Do NOT do this anywhere else! if temp_path.exists():
settings = QtCore.QSettings(self.fileName(), Settings.IniFormat) temp_path.unlink()
settings.beginGroup(plugin.settings_section) if dest_path.exists():
if settings.contains('{name} count'.format(name=plugin.name)): dest_path.unlink()
# Get the count. self.remove('SettingsImport')
list_count = int(settings.value('{name} count'.format(name=plugin.name), 0)) # Get the settings.
if list_count: keys = self.allKeys()
for counter in range(list_count): export_settings = QtCore.QSettings(str(temp_path), Settings.IniFormat)
# The keys were named e. g.: "image 0" # Add a header section.
item = settings.value('{name} {counter:d}'.format(name=plugin.name, counter=counter), '') # This is to insure it's our conf file for import.
if item: now = datetime.datetime.now()
files_list.append(item) # Write INI format using QSettings.
settings.remove('{name} {counter:d}'.format(name=plugin.name, counter=counter)) # Write our header.
settings.remove('{name} count'.format(name=plugin.name)) export_settings.beginGroup('SettingsImport')
settings.endGroup() export_settings.setValue('Make_Changes', 'At_Own_RISK')
return files_list export_settings.setValue('type', 'OpenLP_settings_export')
export_settings.setValue('file_date_created', now.strftime("%Y-%m-%d %H:%M"))
export_settings.endGroup()
# Write all the sections and keys.
for section_key in keys:
# FIXME: We are conflicting with the standard "General" section.
if 'eneral' in section_key:
section_key = section_key.lower()
key_value = super().value(section_key)
if key_value is not None:
export_settings.setValue(section_key, key_value)
export_settings.sync()
# Temp CONF file has been written. Blanks in keys are now '%20'.
# Read the temp file and output the user's CONF file with blanks to
# make it more readable.
try:
with dest_path.open('w') as export_conf_file, temp_path.open('r') as temp_conf:
for file_record in temp_conf:
# Get rid of any invalid entries.
if file_record.find('@Invalid()') == -1:
file_record = file_record.replace('%20', ' ')
export_conf_file.write(file_record)
except OSError as ose:
QtWidgets.QMessageBox.critical(self, translate('OpenLP.MainWindow', 'Export setting error'),
translate('OpenLP.MainWindow',
'An error occurred while exporting the settings: {err}'
).format(err=ose.strerror),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok))
finally:
temp_path.unlink()

View File

@ -120,9 +120,8 @@ class UiStrings(object):
self.NISs = translate('OpenLP.Ui', 'No Item Selected', 'Singular') self.NISs = translate('OpenLP.Ui', 'No Item Selected', 'Singular')
self.NISp = translate('OpenLP.Ui', 'No Items Selected', 'Plural') self.NISp = translate('OpenLP.Ui', 'No Items Selected', 'Plural')
self.NoResults = translate('OpenLP.Ui', 'No Search Results') self.NoResults = translate('OpenLP.Ui', 'No Search Results')
self.OLP = translate('OpenLP.Ui', 'OpenLP') self.OpenLP = translate('OpenLP.Ui', 'OpenLP')
self.OLPV2 = "{name} {version}".format(name=self.OLP, version="2") self.OpenLPv2AndUp = translate('OpenLP.Ui', 'OpenLP 2.0 and up')
self.OLPV2x = "{name} {version}".format(name=self.OLP, version="2.4")
self.OpenLPStart = translate('OpenLP.Ui', 'OpenLP is already running. Do you wish to continue?') self.OpenLPStart = translate('OpenLP.Ui', 'OpenLP is already running. Do you wish to continue?')
self.OpenService = translate('OpenLP.Ui', 'Open service.') self.OpenService = translate('OpenLP.Ui', 'Open service.')
self.OptionalShowInFooter = translate('OpenLP.Ui', 'Optional, this will be displayed in footer.') self.OptionalShowInFooter = translate('OpenLP.Ui', 'Optional, this will be displayed in footer.')
@ -154,8 +153,7 @@ class UiStrings(object):
self.Split = translate('OpenLP.Ui', 'Optional &Split') self.Split = translate('OpenLP.Ui', 'Optional &Split')
self.SplitToolTip = translate('OpenLP.Ui', self.SplitToolTip = translate('OpenLP.Ui',
'Split a slide into two only if it does not fit on the screen as one slide.') 'Split a slide into two only if it does not fit on the screen as one slide.')
# TODO: WHERE is this used at? cannot find where it's used at in code. self.StartingImport = translate('OpenLP.Ui', 'Starting import...')
self.StartTimeCode = translate('OpenLP.Ui', 'Start {code}')
self.StopPlaySlidesInLoop = translate('OpenLP.Ui', 'Stop Play Slides in Loop') self.StopPlaySlidesInLoop = translate('OpenLP.Ui', 'Stop Play Slides in Loop')
self.StopPlaySlidesToEnd = translate('OpenLP.Ui', 'Stop Play Slides to End') self.StopPlaySlidesToEnd = translate('OpenLP.Ui', 'Stop Play Slides to End')
self.Theme = translate('OpenLP.Ui', 'Theme', 'Singular') self.Theme = translate('OpenLP.Ui', 'Theme', 'Singular')
@ -169,6 +167,7 @@ class UiStrings(object):
self.View = translate('OpenLP.Ui', 'View') self.View = translate('OpenLP.Ui', 'View')
self.ViewMode = translate('OpenLP.Ui', 'View Mode') self.ViewMode = translate('OpenLP.Ui', 'View Mode')
self.Video = translate('OpenLP.Ui', 'Video') self.Video = translate('OpenLP.Ui', 'Video')
self.WebDownloadText = translate('OpenLP.Ui', 'Web Interface, Download and Install latest Version')
book_chapter = translate('OpenLP.Ui', 'Book Chapter') book_chapter = translate('OpenLP.Ui', 'Book Chapter')
chapter = translate('OpenLP.Ui', 'Chapter') chapter = translate('OpenLP.Ui', 'Chapter')
verse = translate('OpenLP.Ui', 'Verse') verse = translate('OpenLP.Ui', 'Verse')

View File

@ -1,3 +1,27 @@
# -*- 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.common` module downloads the version details for OpenLP.
"""
import logging import logging
import os import os
import platform import platform
@ -12,7 +36,8 @@ from subprocess import Popen, PIPE
from PyQt5 import QtCore from PyQt5 import QtCore
from openlp.core.common import AppLocation, Settings from openlp.core.common import AppLocation, Registry, Settings
from openlp.core.common.httputils import ping
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -42,12 +67,18 @@ class VersionThread(QtCore.QThread):
""" """
self.sleep(1) self.sleep(1)
log.debug('Version thread - run') log.debug('Version thread - run')
app_version = get_application_version() found = ping("openlp.io")
version = check_latest_version(app_version) Registry().set_flag('internet_present', found)
log.debug("Versions {version1} and {version2} ".format(version1=LooseVersion(str(version)), update_check = Settings().value('core/update check')
version2=LooseVersion(str(app_version['full'])))) if found:
if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])): Registry().execute('get_website_version')
self.main_window.openlp_version_check.emit('{version}'.format(version=version)) if update_check:
app_version = get_application_version()
version = check_latest_version(app_version)
log.debug("Versions {version1} and {version2} ".format(version1=LooseVersion(str(version)),
version2=LooseVersion(str(app_version['full']))))
if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])):
self.main_window.openlp_version_check.emit('{version}'.format(version=version))
def get_application_version(): def get_application_version():
@ -95,7 +126,7 @@ def get_application_version():
full_version = '{tag}-bzr{tree}'.format(tag=tag_version.strip(), tree=tree_revision.strip()) full_version = '{tag}-bzr{tree}'.format(tag=tag_version.strip(), tree=tree_revision.strip())
else: else:
# We're not running the development version, let's use the file. # We're not running the development version, let's use the file.
file_path = AppLocation.get_directory(AppLocation.VersionDir) file_path = str(AppLocation.get_directory(AppLocation.VersionDir))
file_path = os.path.join(file_path, '.version') file_path = os.path.join(file_path, '.version')
version_file = None version_file = None
try: try:

View File

@ -83,30 +83,28 @@ class ServiceItemAction(object):
Next = 3 Next = 3
def get_text_file_string(text_file): def get_text_file_string(text_file_path):
""" """
Open a file and return its content as unicode string. If the supplied file name is not a file then the function Open a file and return its content as a string. If the supplied file path is not a file then the function
returns False. If there is an error loading the file or the content can't be decoded then the function will return returns False. If there is an error loading the file or the content can't be decoded then the function will return
None. None.
:param text_file: The name of the file. :param openlp.core.common.path.Path text_file_path: The path to the file.
:return: The file as a single string :return: The contents of the file, False if the file does not exist, or None if there is an Error reading or
decoding the file.
:rtype: str | False | None
""" """
if not os.path.isfile(text_file): if not text_file_path.is_file():
return False return False
file_handle = None
content = None content = None
try: try:
file_handle = open(text_file, 'r', encoding='utf-8') with text_file_path.open('r', encoding='utf-8') as file_handle:
if file_handle.read(3) != '\xEF\xBB\xBF': if file_handle.read(3) != '\xEF\xBB\xBF':
# no BOM was found # no BOM was found
file_handle.seek(0) file_handle.seek(0)
content = file_handle.read() content = file_handle.read()
except (IOError, UnicodeError): except (IOError, UnicodeError):
log.exception('Failed to open text file {text}'.format(text=text_file)) log.exception('Failed to open text file {text}'.format(text=text_file_path))
finally:
if file_handle:
file_handle.close()
return content return content
@ -230,7 +228,7 @@ def validate_thumb(file_path, thumb_path):
return image_date <= thumb_date return image_date <= thumb_date
def resize_image(image_path, width, height, background='#000000'): def resize_image(image_path, width, height, background='#000000', ignore_aspect_ratio=False):
""" """
Resize an image to fit on the current screen. Resize an image to fit on the current screen.
@ -247,7 +245,7 @@ def resize_image(image_path, width, height, background='#000000'):
image_ratio = reader.size().width() / reader.size().height() image_ratio = reader.size().width() / reader.size().height()
resize_ratio = width / height resize_ratio = width / height
# Figure out the size we want to resize the image to (keep aspect ratio). # Figure out the size we want to resize the image to (keep aspect ratio).
if image_ratio == resize_ratio: if image_ratio == resize_ratio or ignore_aspect_ratio:
size = QtCore.QSize(width, height) size = QtCore.QSize(width, height)
elif image_ratio < resize_ratio: elif image_ratio < resize_ratio:
# Use the image's height as reference for the new size. # Use the image's height as reference for the new size.
@ -608,8 +606,36 @@ def create_separated_list(string_list):
return list_to_string return list_to_string
def replace_params(args, kwargs, params):
"""
Apply a transformation function to the specified args or kwargs
:param tuple args: Positional arguments
:param dict kwargs: Key Word arguments
:param params: A tuple of tuples with the position and the key word to replace.
:return: The modified positional and keyword arguments
:rtype: tuple[tuple, dict]
Usage:
Take a method with the following signature, and assume we which to apply the str function to arg2:
def method(arg1=None, arg2=None, arg3=None)
As arg2 can be specified postitionally as the second argument (1 with a zero index) or as a keyword, the we
would call this function as follows:
replace_params(args, kwargs, ((1, 'arg2', str),))
"""
args = list(args)
for position, key_word, transform in params:
if len(args) > position:
args[position] = transform(args[position])
elif key_word in kwargs:
kwargs[key_word] = transform(kwargs[key_word])
return tuple(args), kwargs
from .exceptions import ValidationError from .exceptions import ValidationError
from .filedialog import FileDialog
from .screen import ScreenList from .screen import ScreenList
from .formattingtags import FormattingTags from .formattingtags import FormattingTags
from .plugin import PluginStatus, StringContent, Plugin from .plugin import PluginStatus, StringContent, Plugin
@ -621,5 +647,5 @@ from .imagemanager import ImageManager
from .renderer import Renderer from .renderer import Renderer
from .mediamanageritem import MediaManagerItem from .mediamanageritem import MediaManagerItem
from .projector.db import ProjectorDB, Projector from .projector.db import ProjectorDB, Projector
from .projector.pjlink1 import PJLink from .projector.pjlink import PJLink
from .projector.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING from .projector.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING

View File

@ -25,12 +25,15 @@ The :mod:`db` module provides the core database functionality for OpenLP
""" """
import logging import logging
import os import os
from copy import copy
from urllib.parse import quote_plus as urlquote from urllib.parse import quote_plus as urlquote
from sqlalchemy import Table, MetaData, Column, types, create_engine from sqlalchemy import Table, MetaData, Column, types, create_engine
from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, DBAPIError, OperationalError from sqlalchemy.engine.url import make_url
from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, DBAPIError, OperationalError, ProgrammingError
from sqlalchemy.orm import scoped_session, sessionmaker, mapper from sqlalchemy.orm import scoped_session, sessionmaker, mapper
from sqlalchemy.pool import NullPool from sqlalchemy.pool import NullPool
from alembic.migration import MigrationContext from alembic.migration import MigrationContext
from alembic.operations import Operations from alembic.operations import Operations
@ -40,6 +43,66 @@ from openlp.core.lib.ui import critical_error_message_box
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def database_exists(url):
"""Check if a database exists.
:param url: A SQLAlchemy engine URL.
Performs backend-specific testing to quickly determine if a database
exists on the server. ::
database_exists('postgres://postgres@localhost/name') #=> False
create_database('postgres://postgres@localhost/name')
database_exists('postgres://postgres@localhost/name') #=> True
Supports checking against a constructed URL as well. ::
engine = create_engine('postgres://postgres@localhost/name')
database_exists(engine.url) #=> False
create_database(engine.url)
database_exists(engine.url) #=> True
Borrowed from SQLAlchemy_Utils (v0.32.14 )since we only need this one function.
"""
url = copy(make_url(url))
database = url.database
if url.drivername.startswith('postgresql'):
url.database = 'template1'
else:
url.database = None
engine = create_engine(url)
if engine.dialect.name == 'postgresql':
text = "SELECT 1 FROM pg_database WHERE datname='{db}'".format(db=database)
return bool(engine.execute(text).scalar())
elif engine.dialect.name == 'mysql':
text = ("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA "
"WHERE SCHEMA_NAME = '{db}'".format(db=database))
return bool(engine.execute(text).scalar())
elif engine.dialect.name == 'sqlite':
if database:
return database == ':memory:' or os.path.exists(database)
else:
# The default SQLAlchemy database is in memory,
# and :memory is not required, thus we should support that use-case
return True
else:
text = 'SELECT 1'
try:
url.database = database
engine = create_engine(url)
engine.execute(text)
return True
except (ProgrammingError, OperationalError):
return False
def init_db(url, auto_flush=True, auto_commit=False, base=None): def init_db(url, auto_flush=True, auto_commit=False, base=None):
""" """
Initialise and return the session and metadata for a database Initialise and return the session and metadata for a database
@ -144,6 +207,12 @@ def upgrade_db(url, upgrade):
:param url: The url of the database to upgrade. :param url: The url of the database to upgrade.
:param upgrade: The python module that contains the upgrade instructions. :param upgrade: The python module that contains the upgrade instructions.
""" """
if not database_exists(url):
log.warn("Database {db} doesn't exist - skipping upgrade checks".format(db=url))
return (0, 0)
log.debug('Checking upgrades for DB {db}'.format(db=url))
session, metadata = init_db(url) session, metadata = init_db(url)
class Metadata(BaseModel): class Metadata(BaseModel):
@ -160,17 +229,15 @@ def upgrade_db(url, upgrade):
metadata_table.create(checkfirst=True) metadata_table.create(checkfirst=True)
mapper(Metadata, metadata_table) mapper(Metadata, metadata_table)
version_meta = session.query(Metadata).get('version') version_meta = session.query(Metadata).get('version')
if version_meta is None: if version_meta:
# Tables have just been created - fill the version field with the most recent version version = int(version_meta.value)
if session.query(Metadata).get('dbversion'): else:
version = 0 # Due to issues with other checks, if the version is not set in the DB then default to 0
else: # and let the upgrade function handle the checks
version = upgrade.__version__ version = 0
version_meta = Metadata.populate(key='version', value=version) version_meta = Metadata.populate(key='version', value=version)
session.add(version_meta) session.add(version_meta)
session.commit() session.commit()
else:
version = int(version_meta.value)
if version > upgrade.__version__: if version > upgrade.__version__:
session.remove() session.remove()
return version, upgrade.__version__ return version, upgrade.__version__
@ -207,9 +274,9 @@ def delete_database(plugin_name, db_file_name=None):
:param db_file_name: The database file name. Defaults to None resulting in the plugin_name being used. :param db_file_name: The database file name. Defaults to None resulting in the plugin_name being used.
""" """
if db_file_name: if db_file_name:
db_file_path = os.path.join(AppLocation.get_section_data_path(plugin_name), db_file_name) db_file_path = AppLocation.get_section_data_path(plugin_name) / db_file_name
else: else:
db_file_path = os.path.join(AppLocation.get_section_data_path(plugin_name), plugin_name) db_file_path = AppLocation.get_section_data_path(plugin_name) / plugin_name
return delete_file(db_file_path) return delete_file(db_file_path)

View File

@ -24,7 +24,6 @@ The :mod:`~openlp.core.lib.exceptions` module contains custom exceptions
""" """
# TODO: Test __init__ & __str__
class ValidationError(Exception): class ValidationError(Exception):
""" """
The :class:`~openlp.core.lib.exceptions.ValidationError` exception provides a custom exception for validating The :class:`~openlp.core.lib.exceptions.ValidationError` exception provides a custom exception for validating

View File

@ -31,7 +31,7 @@ import queue
from PyQt5 import QtCore from PyQt5 import QtCore
from openlp.core.common import Registry from openlp.core.common import Registry, Settings
from openlp.core.lib import ScreenList, resize_image, image_to_byte from openlp.core.lib import ScreenList, resize_image, image_to_byte
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -56,7 +56,7 @@ class ImageThread(QtCore.QThread):
""" """
Run the thread. Run the thread.
""" """
self.image_manager._process() self.image_manager.process()
class Priority(object): class Priority(object):
@ -110,6 +110,8 @@ class Image(object):
:param width: The width of the image, defaults to -1 meaning that the screen width will be used. :param width: The width of the image, defaults to -1 meaning that the screen width will be used.
:param height: The height of the image, defaults to -1 meaning that the screen height will be used. :param height: The height of the image, defaults to -1 meaning that the screen height will be used.
""" """
if not os.path.exists(path):
raise FileNotFoundError('{path} not found'.format(path=path))
self.path = path self.path = path
self.image = None self.image = None
self.image_bytes = None self.image_bytes = None
@ -119,9 +121,7 @@ class Image(object):
self.timestamp = 0 self.timestamp = 0
self.width = width self.width = width
self.height = height self.height = height
# FIXME: We assume that the path exist. The caller has to take care that it exists! self.timestamp = os.stat(path).st_mtime
if os.path.exists(path):
self.timestamp = os.stat(path).st_mtime
self.secondary_priority = Image.secondary_priority self.secondary_priority = Image.secondary_priority
Image.secondary_priority += 1 Image.secondary_priority += 1
@ -235,8 +235,15 @@ class ImageManager(QtCore.QObject):
def get_image(self, path, source, width=-1, height=-1): def get_image(self, path, source, width=-1, height=-1):
""" """
Return the ``QImage`` from the cache. If not present wait for the background thread to process it. Return the ``QImage`` from the cache. If not present wait for the background thread to process it.
:param: path: The image path
:param: source: The source of the image
:param: background: The image background colour
:param: width: The processed image width
:param: height: The processed image height
""" """
log.debug('getImage {path}'.format(path=path)) log.debug('get_image {path} {source} {width} {height}'.format(path=path, source=source,
width=width, height=height))
image = self._cache[(path, source, width, height)] image = self._cache[(path, source, width, height)]
if image.image is None: if image.image is None:
self._conversion_queue.modify_priority(image, Priority.High) self._conversion_queue.modify_priority(image, Priority.High)
@ -255,8 +262,15 @@ class ImageManager(QtCore.QObject):
def get_image_bytes(self, path, source, width=-1, height=-1): def get_image_bytes(self, path, source, width=-1, height=-1):
""" """
Returns the byte string for an image. If not present wait for the background thread to process it. Returns the byte string for an image. If not present wait for the background thread to process it.
:param: path: The image path
:param: source: The source of the image
:param: background: The image background colour
:param: width: The processed image width
:param: height: The processed image height
""" """
log.debug('get_image_bytes {path}'.format(path=path)) log.debug('get_image_bytes {path} {source} {width} {height}'.format(path=path, source=source,
width=width, height=height))
image = self._cache[(path, source, width, height)] image = self._cache[(path, source, width, height)]
if image.image_bytes is None: if image.image_bytes is None:
self._conversion_queue.modify_priority(image, Priority.Urgent) self._conversion_queue.modify_priority(image, Priority.Urgent)
@ -270,9 +284,16 @@ class ImageManager(QtCore.QObject):
def add_image(self, path, source, background, width=-1, height=-1): def add_image(self, path, source, background, width=-1, height=-1):
""" """
Add image to cache if it is not already there. Add image to cache if it is not already there.
:param: path: The image path
:param: source: The source of the image
:param: background: The image background colour
:param: width: The processed image width
:param: height: The processed image height
""" """
log.debug('add_image {path}'.format(path=path)) log.debug('add_image {path} {source} {width} {height}'.format(path=path, source=source,
if (path, source, width, height) not in self._cache: width=width, height=height))
if not (path, source, width, height) in self._cache:
image = Image(path, source, background, width, height) image = Image(path, source, background, width, height)
self._cache[(path, source, width, height)] = image self._cache[(path, source, width, height)] = image
self._conversion_queue.put((image.priority, image.secondary_priority, image)) self._conversion_queue.put((image.priority, image.secondary_priority, image))
@ -286,11 +307,11 @@ class ImageManager(QtCore.QObject):
if not self.image_thread.isRunning(): if not self.image_thread.isRunning():
self.image_thread.start() self.image_thread.start()
def _process(self): def process(self):
""" """
Controls the processing called from a ``QtCore.QThread``. Controls the processing called from a ``QtCore.QThread``.
""" """
log.debug('_process - started') log.debug('process - started')
while not self._conversion_queue.empty() and not self.stop_manager: while not self._conversion_queue.empty() and not self.stop_manager:
self._process_cache() self._process_cache()
log.debug('_process - ended') log.debug('_process - ended')
@ -306,7 +327,8 @@ class ImageManager(QtCore.QObject):
# Let's see if the image was requested with specific dimensions # Let's see if the image was requested with specific dimensions
width = self.width if image.width == -1 else image.width width = self.width if image.width == -1 else image.width
height = self.height if image.height == -1 else image.height height = self.height if image.height == -1 else image.height
image.image = resize_image(image.path, width, height, image.background) image.image = resize_image(image.path, width, height, image.background,
Settings().value('advanced/ignore aspect ratio'))
# Set the priority to Lowest and stop here as we need to process more important images first. # Set the priority to Lowest and stop here as we need to process more important images first.
if image.priority == Priority.Normal: if image.priority == Priority.Normal:
self._conversion_queue.modify_priority(image, Priority.Lowest) self._conversion_queue.modify_priority(image, Priority.Lowest)

View File

@ -26,12 +26,14 @@ import logging
import os import os
import re import re
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate
from openlp.core.lib import FileDialog, ServiceItem, StringContent, ServiceItemContext from openlp.core.common.path import Path, path_to_str, str_to_path
from openlp.core.lib import ServiceItem, StringContent, ServiceItemContext
from openlp.core.lib.searchedit import SearchEdit from openlp.core.lib.searchedit import SearchEdit
from openlp.core.lib.ui import create_widget_action, critical_error_message_box from openlp.core.lib.ui import create_widget_action, critical_error_message_box
from openlp.core.ui.lib.filedialog import FileDialog
from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD
from openlp.core.ui.lib.toolbar import OpenLPToolbar from openlp.core.ui.lib.toolbar import OpenLPToolbar
@ -309,13 +311,14 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
""" """
Add a file to the list widget to make it available for showing Add a file to the list widget to make it available for showing
""" """
files = FileDialog.getOpenFileNames(self, self.on_new_prompt, file_paths, selected_filter = FileDialog.getOpenFileNames(
Settings().value(self.settings_section + '/last directory'), self, self.on_new_prompt,
self.on_new_file_masks) Settings().value(self.settings_section + '/last directory'),
log.info('New files(s) {files}'.format(files=files)) self.on_new_file_masks)
if files: log.info('New files(s) {file_paths}'.format(file_paths=file_paths))
if file_paths:
self.application.set_busy_cursor() self.application.set_busy_cursor()
self.validate_and_load(files) self.validate_and_load([path_to_str(path) for path in file_paths])
self.application.set_normal_cursor() self.application.set_normal_cursor()
def load_file(self, data): def load_file(self, data):
@ -374,9 +377,8 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
self.list_view.clear() self.list_view.clear()
self.load_list(full_list, target_group) self.load_list(full_list, target_group)
last_dir = os.path.split(files[0])[0] last_dir = os.path.split(files[0])[0]
Settings().setValue(self.settings_section + '/last directory', last_dir) Settings().setValue(self.settings_section + '/last directory', Path(last_dir))
Settings().setValue('{section}/{section} files'.format(section=self.settings_section), Settings().setValue('{section}/{section} files'.format(section=self.settings_section), self.get_file_list())
self.get_file_list())
if duplicates_found: if duplicates_found:
critical_error_message_box(UiStrings().Duplicate, critical_error_message_box(UiStrings().Duplicate,
translate('OpenLP.MediaManagerItem', translate('OpenLP.MediaManagerItem',
@ -397,13 +399,15 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
def get_file_list(self): def get_file_list(self):
""" """
Return the current list of files Return the current list of files
:rtype: list[openlp.core.common.path.Path]
""" """
file_list = [] file_paths = []
for index in range(self.list_view.count()): for index in range(self.list_view.count()):
list_item = self.list_view.item(index) list_item = self.list_view.item(index)
filename = list_item.data(QtCore.Qt.UserRole) filename = list_item.data(QtCore.Qt.UserRole)
file_list.append(filename) file_paths.append(str_to_path(filename))
return file_list return file_paths
def load_list(self, load_list, target_group): def load_list(self, load_list, target_group):
""" """

View File

@ -150,7 +150,7 @@ class Plugin(QtCore.QObject, RegistryProperties):
self.status = PluginStatus.Inactive self.status = PluginStatus.Inactive
# Add the default status to the default settings. # Add the default status to the default settings.
default_settings[name + '/status'] = PluginStatus.Inactive default_settings[name + '/status'] = PluginStatus.Inactive
default_settings[name + '/last directory'] = '' default_settings[name + '/last directory'] = None
# Append a setting for files in the mediamanager (note not all plugins # Append a setting for files in the mediamanager (note not all plugins
# which have a mediamanager need this). # which have a mediamanager need this).
if media_item_class is not None: if media_item_class is not None:

View File

@ -40,7 +40,7 @@ class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
""" """
super(PluginManager, self).__init__(parent) super(PluginManager, self).__init__(parent)
self.log_info('Plugin manager Initialising') self.log_info('Plugin manager Initialising')
self.base_path = os.path.abspath(AppLocation.get_directory(AppLocation.PluginsDir)) 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=self.base_path))
self.plugins = [] self.plugins = []
self.log_info('Plugin manager Initialised') self.log_info('Plugin manager Initialised')
@ -69,7 +69,7 @@ class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
""" """
Scan a directory for objects inheriting from the ``Plugin`` class. Scan a directory for objects inheriting from the ``Plugin`` class.
""" """
glob_pattern = os.path.join('openlp', 'plugins', '*', '*plugin.py') glob_pattern = os.path.join('plugins', '*', '*plugin.py')
extension_loader(glob_pattern) extension_loader(glob_pattern)
plugin_classes = Plugin.__subclasses__() plugin_classes = Plugin.__subclasses__()
plugin_objects = [] plugin_objects = []

View File

@ -46,7 +46,7 @@ __all__ = ['S_OK', 'E_GENERAL', 'E_NOT_CONNECTED', 'E_FAN', 'E_LAMP', 'E_TEMP',
'S_NOT_CONNECTED', 'S_CONNECTING', 'S_CONNECTED', 'S_NOT_CONNECTED', 'S_CONNECTING', 'S_CONNECTED',
'S_STATUS', 'S_OFF', 'S_INITIALIZE', 'S_STANDBY', 'S_WARMUP', 'S_ON', 'S_COOLDOWN', 'S_STATUS', 'S_OFF', 'S_INITIALIZE', 'S_STANDBY', 'S_WARMUP', 'S_ON', 'S_COOLDOWN',
'S_INFO', 'S_NETWORK_SENDING', 'S_NETWORK_RECEIVED', 'S_INFO', 'S_NETWORK_SENDING', 'S_NETWORK_RECEIVED',
'ERROR_STRING', 'CR', 'LF', 'PJLINK_ERST_STATUS', 'PJLINK_POWR_STATUS', 'ERROR_STRING', 'CR', 'LF', 'PJLINK_ERST_DATA', 'PJLINK_ERST_STATUS', 'PJLINK_POWR_STATUS',
'PJLINK_PORT', 'PJLINK_MAX_PACKET', 'TIMEOUT', 'ERROR_MSG', 'PJLINK_ERRORS', 'PJLINK_PORT', 'PJLINK_MAX_PACKET', 'TIMEOUT', 'ERROR_MSG', 'PJLINK_ERRORS',
'STATUS_STRING', 'PJLINK_VALID_CMD', 'CONNECTION_ERRORS', 'STATUS_STRING', 'PJLINK_VALID_CMD', 'CONNECTION_ERRORS',
'PJLINK_DEFAULT_SOURCES', 'PJLINK_DEFAULT_CODES', 'PJLINK_DEFAULT_ITEMS'] 'PJLINK_DEFAULT_SOURCES', 'PJLINK_DEFAULT_CODES', 'PJLINK_DEFAULT_ITEMS']
@ -57,35 +57,115 @@ LF = chr(0x0A) # \n
PJLINK_PORT = 4352 PJLINK_PORT = 4352
TIMEOUT = 30.0 TIMEOUT = 30.0
PJLINK_MAX_PACKET = 136 PJLINK_MAX_PACKET = 136
# NOTE: Change format to account for some commands are both class 1 and 2 # NOTE: Changed format to account for some commands are both class 1 and 2
PJLINK_VALID_CMD = { PJLINK_VALID_CMD = {
'ACKN': ['2', ], # UDP Reply to 'SRCH' 'ACKN': {'version': ['2', ],
'AVMT': ['1', ], # Shutter option 'description': translate('OpenLP.PJLinkConstants',
'CLSS': ['1', ], # PJLink class support query 'Acknowledge a PJLink SRCH command - returns MAC address.')
'ERST': ['1', '2'], # Error status option },
'FILT': ['2', ], # Get current filter usage time 'AVMT': {'version': ['1', ],
'FREZ': ['2', ], # Set freeze/unfreeze picture being projected 'description': translate('OpenLP.PJLinkConstants',
'INF1': ['1', ], # Manufacturer name query 'Blank/unblank video and/or mute audio.')
'INF2': ['1', ], # Product name query },
'INFO': ['1', ], # Other information query 'CLSS': {'version': ['1', ],
'INNM': ['2', ], # Get Video source input terminal name 'description': translate('OpenLP.PJLinkConstants',
'INPT': ['1', ], # Video sources option 'Query projector PJLink class support.')
'INST': ['1', ], # Input sources available query },
'IRES': ['2', ], # Get Video source resolution 'ERST': {'version': ['1', '2'],
'LAMP': ['1', ], # Lamp(s) query (Includes fans) 'description': translate('OpenLP.PJLinkConstants',
'LKUP': ['2', ], # UPD Linkup status notification 'Query error status from projector. '
'MVOL': ['2', ], # Set microphone volume 'Returns fan/lamp/temp/cover/filter/other error status.')
'NAME': ['1', ], # Projector name query },
'PJLINK': ['1', ], # Initial connection 'FILT': {'version': ['2', ],
'POWR': ['1', ], # Power option 'description': translate('OpenLP.PJLinkConstants',
'RFIL': ['2', ], # Get replacement air filter model number 'Query number of hours on filter.')
'RLMP': ['2', ], # Get lamp replacement model number },
'RRES': ['2', ], # Get projector recommended video resolution 'FREZ': {'version': ['2', ],
'SNUM': ['2', ], # Get projector serial number 'description': translate('OpenLP.PJLinkConstants',
'SRCH': ['2', ], # UDP broadcast search for available projectors on local network 'Freeze or unfreeze current image being projected.')
'SVER': ['2', ], # Get projector software version },
'SVOL': ['2', ] # Set speaker volume 'INF1': {'version': ['1', ],
'description': translate('OpenLP.PJLinkConstants',
'Query projector manufacturer name.')
},
'INF2': {'version': ['1', ],
'description': translate('OpenLP.PJLinkConstants',
'Query projector product name.')
},
'INFO': {'version': ['1', ],
'description': translate('OpenLP.PJLinkConstants',
'Query projector for other information set by manufacturer.')
},
'INNM': {'version': ['2', ],
'description': translate('OpenLP.PJLinkConstants',
'Query specified input source name')
},
'INPT': {'version': ['1', ],
'description': translate('OpenLP.PJLinkConstants',
'Switch to specified video source.')
},
'INST': {'version': ['1', ],
'description': translate('OpenLP.PJLinkConstants',
'Query available input sources.')
},
'IRES': {'version:': ['2', ],
'description': translate('OpenLP.PJLinkConstants',
'Query current input resolution.')
},
'LAMP': {'version': ['1', ],
'description': translate('OpenLP.PJLinkConstants',
'Query lamp time and on/off status. Multiple lamps supported.')
},
'LKUP': {'version': ['2', ],
'description': translate('OpenLP.PJLinkConstants',
'UDP Status - Projector is now available on network. Includes MAC address.')
},
'MVOL': {'version': ['2', ],
'description': translate('OpenLP.PJLinkConstants',
'Adjust microphone volume by 1 step.')
},
'NAME': {'version': ['1', ],
'description': translate('OpenLP.PJLinkConstants',
'Query customer-set projector name.')
},
'PJLINK': {'version': ['1', ],
'description': translate('OpenLP.PJLinkConstants',
'Initial connection with authentication/no authentication request.')
},
'POWR': {'version': ['1', ],
'description': translate('OpenLP.PJLinkConstants',
'Turn lamp on or off/standby.')
},
'RFIL': {'version': ['2', ],
'description': translate('OpenLP.PJLinkConstants',
'Query replacement air filter model number.')
},
'RLMP': {'version': ['2', ],
'description': translate('OpenLP.PJLinkConstants',
'Query replacement lamp model number.')
},
'RRES': {'version': ['2', ],
'description': translate('OpenLP.PJLinkConstants',
'Query recommended resolution.')
},
'SNUM': {'version': ['2', ],
'description': translate('OpenLP.PJLinkConstants',
'Query projector serial number.')
},
'SRCH': {'version': ['2', ],
'description': translate('OpenLP.PJLinkConstants',
'UDP broadcast search request for available projectors. Reply is ACKN.')
},
'SVER': {'version': ['2', ],
'description': translate('OpenLP.PJLinkConstants',
'Query projector software version number.')
},
'SVOL': {'version': ['2', ],
'description': translate('OpenLP.PJLinkConstants',
'Adjust speaker volume by 1 step.')
}
} }
# Error and status codes # Error and status codes
S_OK = E_OK = 0 # E_OK included since I sometimes forget S_OK = E_OK = 0 # E_OK included since I sometimes forget
# Error codes. Start at 200 so we don't duplicate system error codes. # Error codes. Start at 200 so we don't duplicate system error codes.
@ -313,11 +393,32 @@ ERROR_MSG = {
S_NETWORK_RECEIVED: translate('OpenLP.ProjectorConstants', 'Received data') S_NETWORK_RECEIVED: translate('OpenLP.ProjectorConstants', 'Received data')
} }
# Map ERST return code positions to equipment
PJLINK_ERST_DATA = {
'DATA_LENGTH': 6,
0: 'FAN',
1: 'LAMP',
2: 'TEMP',
3: 'COVER',
4: 'FILTER',
5: 'OTHER',
'FAN': 0,
'LAMP': 1,
'TEMP': 2,
'COVER': 3,
'FILTER': 4,
'OTHER': 5
}
# Map for ERST return codes to string # Map for ERST return codes to string
PJLINK_ERST_STATUS = { PJLINK_ERST_STATUS = {
'0': ERROR_STRING[E_OK], '0': 'OK',
'1': ERROR_STRING[E_WARN], '1': ERROR_STRING[E_WARN],
'2': ERROR_STRING[E_ERROR] '2': ERROR_STRING[E_ERROR],
'OK': '0',
E_OK: '0',
E_WARN: '1',
E_ERROR: '2'
} }
# Map for POWR return codes to status code # Map for POWR return codes to status code

View File

@ -44,6 +44,7 @@ from sqlalchemy.orm import relationship
from openlp.core.lib.db import Manager, init_db, init_url from openlp.core.lib.db import Manager, init_db, init_url
from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES
from openlp.core.lib.projector import upgrade
Base = declarative_base(MetaData()) Base = declarative_base(MetaData())
@ -166,13 +167,14 @@ class Projector(CommonBase, Base):
""" """
Return basic representation of Source table entry. Return basic representation of Source table entry.
""" """
return '< Projector(id="{data}", ip="{ip}", port="{port}", pin="{pin}", name="{name}", ' \ return '< Projector(id="{data}", ip="{ip}", port="{port}", mac_adx="{mac}", pin="{pin}", name="{name}", ' \
'location="{location}", notes="{notes}", pjlink_name="{pjlink_name}", ' \ 'location="{location}", notes="{notes}", pjlink_name="{pjlink_name}", ' \
'manufacturer="{manufacturer}", model="{model}", serial_no="{serial}", other="{other}", ' \ 'manufacturer="{manufacturer}", model="{model}", serial_no="{serial}", other="{other}", ' \
'sources="{sources}", source_list="{source_list}", model_filter="{mfilter}", ' \ 'sources="{sources}", source_list="{source_list}", model_filter="{mfilter}", ' \
'model_lamp="{mlamp}", sw_version="{sw_ver}") >'.format(data=self.id, 'model_lamp="{mlamp}", sw_version="{sw_ver}") >'.format(data=self.id,
ip=self.ip, ip=self.ip,
port=self.port, port=self.port,
mac=self.mac_adx,
pin=self.pin, pin=self.pin,
name=self.name, name=self.name,
location=self.location, location=self.location,
@ -189,6 +191,7 @@ class Projector(CommonBase, Base):
sw_ver=self.sw_version) sw_ver=self.sw_version)
ip = Column(String(100)) ip = Column(String(100))
port = Column(String(8)) port = Column(String(8))
mac_adx = Column(String(18))
pin = Column(String(20)) pin = Column(String(20))
name = Column(String(20)) name = Column(String(20))
location = Column(String(30)) location = Column(String(30))
@ -243,7 +246,9 @@ class ProjectorDB(Manager):
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
log.debug('ProjectorDB().__init__(args="{arg}", kwargs="{kwarg}")'.format(arg=args, kwarg=kwargs)) log.debug('ProjectorDB().__init__(args="{arg}", kwargs="{kwarg}")'.format(arg=args, kwarg=kwargs))
super().__init__(plugin_name='projector', init_schema=self.init_schema) super().__init__(plugin_name='projector',
init_schema=self.init_schema,
upgrade_mod=upgrade)
log.debug('ProjectorDB() Initialized using db url {db}'.format(db=self.db_url)) log.debug('ProjectorDB() Initialized using db url {db}'.format(db=self.db_url))
log.debug('Session: {session}'.format(session=self.session)) log.debug('Session: {session}'.format(session=self.session))
@ -298,7 +303,7 @@ class ProjectorDB(Manager):
:param ip: Host IP/Name :param ip: Host IP/Name
:returns: Projector() instance :returns: Projector() instance
""" """
log.debug('get_projector_by_ip(ip="%s")' % ip) log.debug('get_projector_by_ip(ip="{ip}")'.format(ip=ip))
projector = self.get_object_filtered(Projector, Projector.ip == ip) projector = self.get_object_filtered(Projector, Projector.ip == ip)
if projector is None: if projector is None:
# Not found # Not found

View File

@ -0,0 +1,85 @@
# -*- 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 #
###############################################################################
"""
:mod:`openlp.core.lib.projector.pjlink2` module provides the PJLink Class 2
updates from PJLink Class 1.
This module only handles the UDP socket functionality. Command/query/status
change messages will still be processed by the PJLink 1 module.
Currently, the only variance is the addition of a UDP "search" command to
query the local network for Class 2 capable projectors,
and UDP "notify" messages from projectors to connected software of status
changes (i.e., power change, input change, error changes).
Differences between Class 1 and Class 2 PJLink specifications are as follows.
New Functionality:
* Search - UDP Query local network for Class 2 capabable projector(s).
* Status - UDP Status change with connected projector(s). Status change
messages consist of:
* Initial projector power up when network communication becomes available
* Lamp off/standby to warmup or on
* Lamp on to cooldown or off/standby
* Input source select change completed
* Error status change (i.e., fan/lamp/temp/cover open/filter/other error(s))
New Commands:
* Query serial number of projector
* Query version number of projector software
* Query model number of replacement lamp
* Query model number of replacement air filter
* Query current projector screen resolution
* Query recommended screen resolution
* Query name of specific input terminal (video source)
* Adjust projector microphone in 1-step increments
* Adjust projector speacker in 1-step increments
Extended Commands:
* Addition of INTERNAL terminal (video source) for a total of 6 types of terminals.
* Number of terminals (video source) has been expanded from [1-9]
to [1-9a-z] (Addition of 26 terminals for each type of input).
See PJLink Class 2 Specifications for details.
http://pjlink.jbmia.or.jp/english/dl_class2.html
Section 5-1 PJLink Specifications
Section 5-5 Guidelines for Input Terminals
"""
import logging
log = logging.getLogger(__name__)
log.debug('pjlink2 loaded')
from PyQt5 import QtNetwork
class PJLinkUDP(QtNetwork.QUdpSocket):
"""
Socket service for handling datagram (UDP) sockets.
"""
log.debug('PJLinkUDP loaded')
# Class varialbe for projector list. Should be replaced by ProjectorManager's
# projector list after being loaded there.
projector_list = None
projectors_found = None # UDP search found list

View File

@ -0,0 +1,73 @@
# -*- 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:`upgrade` module provides a way for the database and schema that is the
backend for the projector setup.
"""
import logging
from sqlalchemy import Table, Column, types
from sqlalchemy.sql.expression import null
from openlp.core.lib.db import get_upgrade_op
log = logging.getLogger(__name__)
# Initial projector DB was unversioned
__version__ = 2
log.debug('Projector DB upgrade module loading')
def upgrade_1(session, metadata):
"""
Version 1 upgrade - old db might/might not be versioned.
"""
log.debug('Skipping projector DB upgrade to version 1 - not used')
def upgrade_2(session, metadata):
"""
Version 2 upgrade.
Update Projector() table to include new data defined in PJLink version 2 changes
mac_adx: Column(String(18))
serial_no: Column(String(30))
sw_version: Column(String(30))
model_filter: Column(String(30))
model_lamp: Column(String(30))
:param session: DB session instance
:param metadata: Metadata of current DB
"""
log.debug('Checking projector DB upgrade to version 2')
projector_table = Table('projector', metadata, autoload=True)
upgrade_db = 'mac_adx' not in [col.name for col in projector_table.c.values()]
if upgrade_db:
new_op = get_upgrade_op(session)
new_op.add_column('projector', Column('mac_adx', types.String(18), server_default=null()))
new_op.add_column('projector', Column('serial_no', types.String(30), server_default=null()))
new_op.add_column('projector', Column('sw_version', types.String(30), server_default=null()))
new_op.add_column('projector', Column('model_filter', types.String(30), server_default=null()))
new_op.add_column('projector', Column('model_lamp', types.String(30), server_default=null()))
log.debug('{status} projector DB upgrade to version 2'.format(status='Updated' if upgrade_db else 'Skipping'))

View File

@ -105,7 +105,7 @@ class SearchEdit(QtWidgets.QLineEdit):
self.setPlaceholderText(action.placeholder_text) self.setPlaceholderText(action.placeholder_text)
self.menu_button.setDefaultAction(action) self.menu_button.setDefaultAction(action)
self._current_search_type = identifier self._current_search_type = identifier
Settings().setValue('{section}/last search type'.format(section=self.settings_section), identifier) Settings().setValue('{section}/last used search type'.format(section=self.settings_section), identifier)
self.searchTypeChanged.emit(identifier) self.searchTypeChanged.emit(identifier)
return True return True
@ -141,7 +141,7 @@ class SearchEdit(QtWidgets.QLineEdit):
self.menu_button.resize(QtCore.QSize(28, 18)) self.menu_button.resize(QtCore.QSize(28, 18))
self.menu_button.setMenu(menu) self.menu_button.setMenu(menu)
self.set_current_search_type( self.set_current_search_type(
Settings().value('{section}/last search type'.format(section=self.settings_section))) Settings().value('{section}/last used search type'.format(section=self.settings_section)))
self.menu_button.show() self.menu_button.show()
self._update_style_sheet() self._update_style_sheet()

View File

@ -335,7 +335,7 @@ class ServiceItem(RegistryProperties):
if image and not self.has_original_files and self.name == 'presentations': if image and not self.has_original_files and self.name == 'presentations':
file_location = os.path.join(path, file_name) file_location = os.path.join(path, file_name)
file_location_hash = md5_hash(file_location.encode('utf-8')) file_location_hash = md5_hash(file_location.encode('utf-8'))
image = os.path.join(AppLocation.get_section_data_path(self.name), 'thumbnails', image = os.path.join(str(AppLocation.get_section_data_path(self.name)), 'thumbnails',
file_location_hash, ntpath.basename(image)) file_location_hash, ntpath.basename(image))
self._raw_frames.append({'title': file_name, 'image': image, 'path': path, self._raw_frames.append({'title': file_name, 'image': image, 'path': path,
'display_title': display_title, 'notes': notes}) 'display_title': display_title, 'notes': notes})

View File

@ -158,9 +158,8 @@ class Theme(object):
Initialise the theme object. Initialise the theme object.
""" """
# basic theme object with defaults # basic theme object with defaults
json_dir = os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'core', 'lib', 'json') json_path = AppLocation.get_directory(AppLocation.AppDir) / 'core' / 'lib' / 'json' / 'theme.json'
json_file = os.path.join(json_dir, 'theme.json') jsn = get_text_file_string(json_path)
jsn = get_text_file_string(json_file)
jsn = json.loads(jsn) jsn = json.loads(jsn)
self.expand_json(jsn) self.expand_json(jsn)
self.background_filename = '' self.background_filename = ''

View File

@ -49,6 +49,7 @@ def add_welcome_page(parent, image):
parent.title_label = QtWidgets.QLabel(parent.welcome_page) parent.title_label = QtWidgets.QLabel(parent.welcome_page)
parent.title_label.setObjectName('title_label') parent.title_label.setObjectName('title_label')
parent.welcome_layout.addWidget(parent.title_label) parent.welcome_layout.addWidget(parent.title_label)
parent.title_label.setWordWrap(True)
parent.welcome_layout.addSpacing(40) parent.welcome_layout.addSpacing(40)
parent.information_label = QtWidgets.QLabel(parent.welcome_page) parent.information_label = QtWidgets.QLabel(parent.welcome_page)
parent.information_label.setWordWrap(True) parent.information_label.setWordWrap(True)

View File

@ -99,7 +99,7 @@ from .themelayoutform import ThemeLayoutForm
from .themeform import ThemeForm from .themeform import ThemeForm
from .filerenameform import FileRenameForm from .filerenameform import FileRenameForm
from .starttimeform import StartTimeForm from .starttimeform import StartTimeForm
from .maindisplay import MainDisplay, Display from .maindisplay import MainDisplay, Display, AudioPlayer
from .servicenoteform import ServiceNoteForm from .servicenoteform import ServiceNoteForm
from .serviceitemeditform import ServiceItemEditForm from .serviceitemeditform import ServiceItemEditForm
from .slidecontroller import SlideController, DisplayController, PreviewController, LiveController from .slidecontroller import SlideController, DisplayController, PreviewController, LiveController
@ -120,8 +120,8 @@ from .projector.tab import ProjectorTab
from .projector.editform import ProjectorEditForm from .projector.editform import ProjectorEditForm
__all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', 'ThemeForm', __all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', 'ThemeForm',
'ThemeManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', 'ThemeManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', 'Display', 'AudioPlayer',
'Display', 'ServiceNoteForm', 'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay', 'ServiceNoteForm', 'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay',
'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm', 'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm',
'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'SingleColumnTableWidget', 'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'SingleColumnTableWidget',
'ProjectorManager', 'ProjectorTab', 'ProjectorEditForm'] 'ProjectorManager', 'ProjectorTab', 'ProjectorEditForm']

View File

@ -40,7 +40,8 @@ class AboutForm(QtWidgets.QDialog, UiAboutDialog):
""" """
Do some initialisation stuff Do some initialisation stuff
""" """
super(AboutForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) super(AboutForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self._setup() self._setup()
def _setup(self): def _setup(self):

View File

@ -30,6 +30,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import AppLocation, Settings, SlideLimits, UiStrings, translate from openlp.core.common import AppLocation, Settings, SlideLimits, UiStrings, translate
from openlp.core.common.languagemanager import format_time from openlp.core.common.languagemanager import format_time
from openlp.core.common.path import path_to_str
from openlp.core.lib import SettingsTab, build_icon from openlp.core.lib import SettingsTab, build_icon
from openlp.core.ui.lib import PathEdit, PathType from openlp.core.ui.lib import PathEdit, PathType
@ -207,6 +208,9 @@ class AdvancedTab(SettingsTab):
self.display_workaround_group_box.setObjectName('display_workaround_group_box') self.display_workaround_group_box.setObjectName('display_workaround_group_box')
self.display_workaround_layout = QtWidgets.QVBoxLayout(self.display_workaround_group_box) self.display_workaround_layout = QtWidgets.QVBoxLayout(self.display_workaround_group_box)
self.display_workaround_layout.setObjectName('display_workaround_layout') self.display_workaround_layout.setObjectName('display_workaround_layout')
self.ignore_aspect_ratio_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box)
self.ignore_aspect_ratio_check_box.setObjectName('ignore_aspect_ratio_check_box')
self.display_workaround_layout.addWidget(self.ignore_aspect_ratio_check_box)
self.x11_bypass_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box) self.x11_bypass_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box)
self.x11_bypass_check_box.setObjectName('x11_bypass_check_box') self.x11_bypass_check_box.setObjectName('x11_bypass_check_box')
self.display_workaround_layout.addWidget(self.x11_bypass_check_box) self.display_workaround_layout.addWidget(self.x11_bypass_check_box)
@ -310,6 +314,7 @@ class AdvancedTab(SettingsTab):
translate('OpenLP.AdvancedTab', '<strong>WARNING:</strong> New data directory location contains ' translate('OpenLP.AdvancedTab', '<strong>WARNING:</strong> New data directory location contains '
'OpenLP data files. These files WILL be replaced during a copy.')) 'OpenLP data files. These files WILL be replaced during a copy.'))
self.display_workaround_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Display Workarounds')) self.display_workaround_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Display Workarounds'))
self.ignore_aspect_ratio_check_box.setText(translate('OpenLP.AdvancedTab', 'Ignore Aspect Ratio'))
self.x11_bypass_check_box.setText(translate('OpenLP.AdvancedTab', 'Bypass X11 Window Manager')) self.x11_bypass_check_box.setText(translate('OpenLP.AdvancedTab', 'Bypass X11 Window Manager'))
self.alternate_rows_check_box.setText(translate('OpenLP.AdvancedTab', 'Use alternating row colours in lists')) self.alternate_rows_check_box.setText(translate('OpenLP.AdvancedTab', 'Use alternating row colours in lists'))
# Slide Limits # Slide Limits
@ -354,6 +359,7 @@ class AdvancedTab(SettingsTab):
default_service_enabled = settings.value('default service enabled') default_service_enabled = settings.value('default service enabled')
self.service_name_check_box.setChecked(default_service_enabled) self.service_name_check_box.setChecked(default_service_enabled)
self.service_name_check_box_toggled(default_service_enabled) self.service_name_check_box_toggled(default_service_enabled)
self.ignore_aspect_ratio_check_box.setChecked(settings.value('ignore aspect ratio'))
self.x11_bypass_check_box.setChecked(settings.value('x11 bypass wm')) self.x11_bypass_check_box.setChecked(settings.value('x11 bypass wm'))
self.slide_limits = settings.value('slide limits') self.slide_limits = settings.value('slide limits')
self.is_search_as_you_type_enabled = settings.value('search as type') self.is_search_as_you_type_enabled = settings.value('search as type')
@ -409,6 +415,7 @@ class AdvancedTab(SettingsTab):
settings.setValue('hide mouse', self.hide_mouse_check_box.isChecked()) settings.setValue('hide mouse', self.hide_mouse_check_box.isChecked())
settings.setValue('alternate rows', self.alternate_rows_check_box.isChecked()) settings.setValue('alternate rows', self.alternate_rows_check_box.isChecked())
settings.setValue('slide limits', self.slide_limits) settings.setValue('slide limits', self.slide_limits)
settings.setValue('ignore aspect ratio', self.ignore_aspect_ratio_check_box.isChecked())
if self.x11_bypass_check_box.isChecked() != settings.value('x11 bypass wm'): if self.x11_bypass_check_box.isChecked() != settings.value('x11 bypass wm'):
settings.setValue('x11 bypass wm', self.x11_bypass_check_box.isChecked()) settings.setValue('x11 bypass wm', self.x11_bypass_check_box.isChecked())
self.settings_form.register_post_process('config_screen_changed') self.settings_form.register_post_process('config_screen_changed')
@ -495,16 +502,14 @@ class AdvancedTab(SettingsTab):
'location of the OpenLP data directory to:\n\n{path}' 'location of the OpenLP data directory to:\n\n{path}'
'\n\nThe data directory will be changed when OpenLP is ' '\n\nThe data directory will be changed when OpenLP is '
'closed.').format(path=new_data_path), 'closed.').format(path=new_data_path),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | defaultButton=QtWidgets.QMessageBox.No)
QtWidgets.QMessageBox.No),
QtWidgets.QMessageBox.No)
if answer != QtWidgets.QMessageBox.Yes: if answer != QtWidgets.QMessageBox.Yes:
self.data_directory_path_edit.path = AppLocation.get_data_path() self.data_directory_path_edit.path = AppLocation.get_data_path()
return return
# Check if data already exists here. # Check if data already exists here.
self.check_data_overwrite(new_data_path) self.check_data_overwrite(path_to_str(new_data_path))
# Save the new location. # Save the new location.
self.main_window.set_new_data_path(new_data_path) self.main_window.set_new_data_path(path_to_str(new_data_path))
self.data_directory_cancel_button.show() self.data_directory_cancel_button.show()
def on_data_directory_copy_check_box_toggled(self): def on_data_directory_copy_check_box_toggled(self):

View File

@ -70,9 +70,9 @@ try:
except ImportError: except ImportError:
VLC_VERSION = '-' VLC_VERSION = '-'
from openlp.core.common import Settings, UiStrings, translate from openlp.core.common import RegistryProperties, Settings, UiStrings, is_linux, translate
from openlp.core.common.versionchecker import get_application_version from openlp.core.common.versionchecker import get_application_version
from openlp.core.common import RegistryProperties, is_linux from openlp.core.ui.lib.filedialog import FileDialog
from .exceptiondialog import Ui_ExceptionDialog from .exceptiondialog import Ui_ExceptionDialog
@ -139,17 +139,17 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
""" """
Saving exception log and system information to a file. Saving exception log and system information to a file.
""" """
filename = QtWidgets.QFileDialog.getSaveFileName( file_path, filter_used = FileDialog.getSaveFileName(
self, self,
translate('OpenLP.ExceptionForm', 'Save Crash Report'), translate('OpenLP.ExceptionForm', 'Save Crash Report'),
Settings().value(self.settings_section + '/last directory'), Settings().value(self.settings_section + '/last directory'),
translate('OpenLP.ExceptionForm', 'Text files (*.txt *.log *.text)'))[0] translate('OpenLP.ExceptionForm', 'Text files (*.txt *.log *.text)'))
if filename: if file_path:
filename = str(filename).replace('/', os.path.sep) Settings().setValue(self.settings_section + '/last directory', file_path.parent)
Settings().setValue(self.settings_section + '/last directory', os.path.dirname(filename))
opts = self._create_report() opts = self._create_report()
report_text = self.report_text.format(version=opts['version'], description=opts['description'], report_text = self.report_text.format(version=opts['version'], description=opts['description'],
traceback=opts['traceback'], libs=opts['libs'], system=opts['system']) traceback=opts['traceback'], libs=opts['libs'], system=opts['system'])
filename = str(file_path)
try: try:
report_file = open(filename, 'w') report_file = open(filename, 'w')
try: try:
@ -212,17 +212,16 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
def on_attach_file_button_clicked(self): def on_attach_file_button_clicked(self):
""" """
Attache files to the bug report e-mail. Attach files to the bug report e-mail.
""" """
files, filter_used = QtWidgets.QFileDialog.getOpenFileName(self, file_path, filter_used = \
translate('ImagePlugin.ExceptionDialog', FileDialog.getOpenFileName(self,
'Select Attachment'), translate('ImagePlugin.ExceptionDialog', 'Select Attachment'),
Settings().value(self.settings_section + Settings().value(self.settings_section + '/last directory'),
'/last directory'), '{text} (*)'.format(text=UiStrings().AllFiles))
'{text} (*)'.format(text=UiStrings().AllFiles)) log.info('New file {file}'.format(file=file_path))
log.info('New files(s) {files}'.format(files=str(files))) if file_path:
if files: self.file_attachment = str(file_path)
self.file_attachment = str(files)
def __button_state(self, state): def __button_state(self, state):
""" """

View File

@ -38,8 +38,8 @@ class FileRenameForm(QtWidgets.QDialog, Ui_FileRenameDialog, RegistryProperties)
""" """
Constructor Constructor
""" """
super(FileRenameForm, self).__init__(Registry().get('main_window'), super(FileRenameForm, self).__init__(Registry().get('main_window'), QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
self._setup() self._setup()
def _setup(self): def _setup(self):

View File

@ -29,13 +29,14 @@ import time
import urllib.request import urllib.request
import urllib.parse import urllib.parse
import urllib.error import urllib.error
from configparser import ConfigParser, MissingSectionHeaderError, NoOptionError, NoSectionError
from tempfile import gettempdir from tempfile import gettempdir
from configparser import ConfigParser, MissingSectionHeaderError, NoSectionError, NoOptionError
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, check_directory_exists, \ from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, check_directory_exists, \
translate, clean_button_text, trace_error_handler translate, clean_button_text, trace_error_handler
from openlp.core.common.path import Path
from openlp.core.lib import PluginStatus, build_icon from openlp.core.lib import PluginStatus, build_icon
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box
from openlp.core.common.httputils import get_web_page, get_url_file_size, url_get_file, CONNECTION_TIMEOUT from openlp.core.common.httputils import get_web_page, get_url_file_size, url_get_file, CONNECTION_TIMEOUT
@ -202,11 +203,10 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self.themes_url = self.web + self.config.get('themes', 'directory') + '/' self.themes_url = self.web + self.config.get('themes', 'directory') + '/'
self.web_access = True self.web_access = True
except (NoSectionError, NoOptionError, MissingSectionHeaderError): except (NoSectionError, NoOptionError, MissingSectionHeaderError):
log.debug('A problem occured while parsing the downloaded config file') log.debug('A problem occurred while parsing the downloaded config file')
trace_error_handler(log) trace_error_handler(log)
self.update_screen_list_combo() self.update_screen_list_combo()
self.application.process_events() self.application.process_events()
# TODO: Tested at home
self.downloading = translate('OpenLP.FirstTimeWizard', 'Downloading {name}...') self.downloading = translate('OpenLP.FirstTimeWizard', 'Downloading {name}...')
if self.has_run_wizard: if self.has_run_wizard:
self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active()) self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active())
@ -214,7 +214,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self.presentation_check_box.setChecked(self.plugin_manager.get_plugin_by_name('presentations').is_active()) self.presentation_check_box.setChecked(self.plugin_manager.get_plugin_by_name('presentations').is_active())
self.image_check_box.setChecked(self.plugin_manager.get_plugin_by_name('images').is_active()) self.image_check_box.setChecked(self.plugin_manager.get_plugin_by_name('images').is_active())
self.media_check_box.setChecked(self.plugin_manager.get_plugin_by_name('media').is_active()) self.media_check_box.setChecked(self.plugin_manager.get_plugin_by_name('media').is_active())
self.remote_check_box.setChecked(self.plugin_manager.get_plugin_by_name('remotes').is_active())
self.custom_check_box.setChecked(self.plugin_manager.get_plugin_by_name('custom').is_active()) self.custom_check_box.setChecked(self.plugin_manager.get_plugin_by_name('custom').is_active())
self.song_usage_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songusage').is_active()) self.song_usage_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songusage').is_active())
self.alert_check_box.setChecked(self.plugin_manager.get_plugin_by_name('alerts').is_active()) self.alert_check_box.setChecked(self.plugin_manager.get_plugin_by_name('alerts').is_active())
@ -284,7 +283,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self.no_internet_cancel_button.setVisible(False) self.no_internet_cancel_button.setVisible(False)
# Check if this is a re-run of the wizard. # Check if this is a re-run of the wizard.
self.has_run_wizard = Settings().value('core/has run wizard') self.has_run_wizard = Settings().value('core/has run wizard')
check_directory_exists(os.path.join(gettempdir(), 'openlp')) check_directory_exists(Path(gettempdir(), 'openlp'))
def update_screen_list_combo(self): def update_screen_list_combo(self):
""" """
@ -531,7 +530,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self._set_plugin_status(self.presentation_check_box, 'presentations/status') self._set_plugin_status(self.presentation_check_box, 'presentations/status')
self._set_plugin_status(self.image_check_box, 'images/status') self._set_plugin_status(self.image_check_box, 'images/status')
self._set_plugin_status(self.media_check_box, 'media/status') self._set_plugin_status(self.media_check_box, 'media/status')
self._set_plugin_status(self.remote_check_box, 'remotes/status')
self._set_plugin_status(self.custom_check_box, 'custom/status') self._set_plugin_status(self.custom_check_box, 'custom/status')
self._set_plugin_status(self.song_usage_check_box, 'songusage/status') self._set_plugin_status(self.song_usage_check_box, 'songusage/status')
self._set_plugin_status(self.alert_check_box, 'alerts/status') self._set_plugin_status(self.alert_check_box, 'alerts/status')
@ -555,15 +553,14 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
""" """
# Build directories for downloads # Build directories for downloads
songs_destination = os.path.join(gettempdir(), 'openlp') songs_destination = os.path.join(gettempdir(), 'openlp')
bibles_destination = AppLocation.get_section_data_path('bibles') bibles_destination = str(AppLocation.get_section_data_path('bibles'))
themes_destination = AppLocation.get_section_data_path('themes') themes_destination = str(AppLocation.get_section_data_path('themes'))
missed_files = [] missed_files = []
# Download songs # Download songs
for i in range(self.songs_list_widget.count()): for i in range(self.songs_list_widget.count()):
item = self.songs_list_widget.item(i) item = self.songs_list_widget.item(i)
if item.checkState() == QtCore.Qt.Checked: if item.checkState() == QtCore.Qt.Checked:
filename, sha256 = item.data(QtCore.Qt.UserRole) filename, sha256 = item.data(QtCore.Qt.UserRole)
# TODO: Tested at home
self._increment_progress_bar(self.downloading.format(name=filename), 0) self._increment_progress_bar(self.downloading.format(name=filename), 0)
self.previous_size = 0 self.previous_size = 0
destination = os.path.join(songs_destination, str(filename)) destination = os.path.join(songs_destination, str(filename))
@ -576,7 +573,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
item = bibles_iterator.value() item = bibles_iterator.value()
if item.parent() and item.checkState(0) == QtCore.Qt.Checked: if item.parent() and item.checkState(0) == QtCore.Qt.Checked:
bible, sha256 = item.data(0, QtCore.Qt.UserRole) bible, sha256 = item.data(0, QtCore.Qt.UserRole)
# TODO: Tested at home
self._increment_progress_bar(self.downloading.format(name=bible), 0) self._increment_progress_bar(self.downloading.format(name=bible), 0)
self.previous_size = 0 self.previous_size = 0
if not url_get_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible), if not url_get_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible),
@ -589,7 +585,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
item = self.themes_list_widget.item(i) item = self.themes_list_widget.item(i)
if item.checkState() == QtCore.Qt.Checked: if item.checkState() == QtCore.Qt.Checked:
theme, sha256 = item.data(QtCore.Qt.UserRole) theme, sha256 = item.data(QtCore.Qt.UserRole)
# TODO: Tested at home
self._increment_progress_bar(self.downloading.format(name=theme), 0) self._increment_progress_bar(self.downloading.format(name=theme), 0)
self.previous_size = 0 self.previous_size = 0
if not url_get_file(self, '{path}{name}'.format(path=self.themes_url, name=theme), if not url_get_file(self, '{path}{name}'.format(path=self.themes_url, name=theme),

View File

@ -37,7 +37,8 @@ class FirstTimeLanguageForm(QtWidgets.QDialog, Ui_FirstTimeLanguageDialog):
""" """
Constructor Constructor
""" """
super(FirstTimeLanguageForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) super(FirstTimeLanguageForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
self.setupUi(self) self.setupUi(self)
self.qm_list = LanguageManager.get_qm_list() self.qm_list = LanguageManager.get_qm_list()
self.language_combo_box.addItem('Autodetect') self.language_combo_box.addItem('Autodetect')

View File

@ -24,6 +24,7 @@ The UI widgets for the first time wizard.
""" """
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common.uistrings import UiStrings
from openlp.core.common import translate, is_macosx, clean_button_text, Settings from openlp.core.common import translate, is_macosx, clean_button_text, Settings
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
from openlp.core.lib.ui import add_welcome_page from openlp.core.lib.ui import add_welcome_page
@ -254,8 +255,7 @@ class UiFirstTimeWizard(object):
self.presentation_check_box.setText(translate('OpenLP.FirstTimeWizard', self.presentation_check_box.setText(translate('OpenLP.FirstTimeWizard',
'Presentations Show .ppt, .odp and .pdf files')) 'Presentations Show .ppt, .odp and .pdf files'))
self.media_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Media Playback of Audio and Video files')) self.media_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Media Playback of Audio and Video files'))
self.remote_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Remote Control OpenLP via browser or smart' self.remote_check_box.setText(str(UiStrings().WebDownloadText))
'phone app'))
self.song_usage_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Song Usage Monitor')) self.song_usage_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Song Usage Monitor'))
self.alert_check_box.setText(translate('OpenLP.FirstTimeWizard', self.alert_check_box.setText(translate('OpenLP.FirstTimeWizard',
'Alerts Display informative messages while showing other slides')) 'Alerts Display informative messages while showing other slides'))

View File

@ -130,8 +130,7 @@ class FormattingTagController(object):
elif not match.group('empty'): elif not match.group('empty'):
end_tags.append(tag) end_tags.append(tag)
match = self.html_tag_regex.search(start_html, match.end()) match = self.html_tag_regex.search(start_html, match.end())
# TODO: Verify format() works with lambda return ''.join(map(lambda tag: '</{tag}>'.format(tag=tag), reversed(end_tags)))
return ''.join(map(lambda tag: '</%s>' % tag, reversed(end_tags)))
def start_tag_changed(self, start_html, end_html): def start_tag_changed(self, start_html, end_html):
""" """

View File

@ -51,7 +51,8 @@ class FormattingTagForm(QtWidgets.QDialog, Ui_FormattingTagDialog, FormattingTag
""" """
Constructor Constructor
""" """
super(FormattingTagForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) super(FormattingTagForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.setupUi(self) self.setupUi(self)
self._setup() self._setup()
@ -122,8 +123,7 @@ class FormattingTagForm(QtWidgets.QDialog, Ui_FormattingTagDialog, FormattingTag
self.tag_table_widget.item(count, 2).text(), self.tag_table_widget.item(count, 2).text(),
self.tag_table_widget.item(count, 3).text()) self.tag_table_widget.item(count, 3).text())
if error: if error:
QtWidgets.QMessageBox.warning(self, translate('OpenLP.FormattingTagForm', 'Validation Error'), error, QtWidgets.QMessageBox.warning(self, translate('OpenLP.FormattingTagForm', 'Validation Error'), error)
QtWidgets.QMessageBox.Ok)
self.tag_table_widget.selectRow(count) self.tag_table_widget.selectRow(count)
return return
count += 1 count += 1
@ -198,6 +198,5 @@ class FormattingTagForm(QtWidgets.QDialog, Ui_FormattingTagDialog, FormattingTag
if tag: if tag:
self.tag_table_widget.setItem(pre_row, 3, QtWidgets.QTableWidgetItem(tag)) self.tag_table_widget.setItem(pre_row, 3, QtWidgets.QTableWidgetItem(tag))
if errors: if errors:
QtWidgets.QMessageBox.warning(self, translate('OpenLP.FormattingTagForm', 'Validation Error'), errors, QtWidgets.QMessageBox.warning(self, translate('OpenLP.FormattingTagForm', 'Validation Error'), errors)
QtWidgets.QMessageBox.Ok)
self.tag_table_widget.resizeRowsToContents() self.tag_table_widget.resizeRowsToContents()

View File

@ -27,6 +27,7 @@ import logging
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import Registry, Settings, UiStrings, translate, get_images_filter from openlp.core.common import Registry, Settings, UiStrings, translate, get_images_filter
from openlp.core.common.path import Path, path_to_str, str_to_path
from openlp.core.lib import SettingsTab, ScreenList from openlp.core.lib import SettingsTab, ScreenList
from openlp.core.ui.lib import ColorButton, PathEdit from openlp.core.ui.lib import ColorButton, PathEdit
@ -172,7 +173,8 @@ class GeneralTab(SettingsTab):
self.logo_layout.setObjectName('logo_layout') self.logo_layout.setObjectName('logo_layout')
self.logo_file_label = QtWidgets.QLabel(self.logo_group_box) self.logo_file_label = QtWidgets.QLabel(self.logo_group_box)
self.logo_file_label.setObjectName('logo_file_label') self.logo_file_label.setObjectName('logo_file_label')
self.logo_file_path_edit = PathEdit(self.logo_group_box, default_path=':/graphics/openlp-splash-screen.png') self.logo_file_path_edit = PathEdit(self.logo_group_box,
default_path=Path(':/graphics/openlp-splash-screen.png'))
self.logo_layout.addRow(self.logo_file_label, self.logo_file_path_edit) self.logo_layout.addRow(self.logo_file_label, self.logo_file_path_edit)
self.logo_color_label = QtWidgets.QLabel(self.logo_group_box) self.logo_color_label = QtWidgets.QLabel(self.logo_group_box)
self.logo_color_label.setObjectName('logo_color_label') self.logo_color_label.setObjectName('logo_color_label')
@ -266,7 +268,7 @@ class GeneralTab(SettingsTab):
self.audio_group_box.setTitle(translate('OpenLP.GeneralTab', 'Background Audio')) self.audio_group_box.setTitle(translate('OpenLP.GeneralTab', 'Background Audio'))
self.start_paused_check_box.setText(translate('OpenLP.GeneralTab', 'Start background audio paused')) self.start_paused_check_box.setText(translate('OpenLP.GeneralTab', 'Start background audio paused'))
self.repeat_list_check_box.setText(translate('OpenLP.GeneralTab', 'Repeat track list')) self.repeat_list_check_box.setText(translate('OpenLP.GeneralTab', 'Repeat track list'))
self.logo_file_path_edit.dialog_caption = dialog_caption = translate('OpenLP.AdvancedTab', 'Select Logo File') self.logo_file_path_edit.dialog_caption = translate('OpenLP.AdvancedTab', 'Select Logo File')
self.logo_file_path_edit.filters = '{text};;{names} (*)'.format( self.logo_file_path_edit.filters = '{text};;{names} (*)'.format(
text=get_images_filter(), names=UiStrings().AllFiles) text=get_images_filter(), names=UiStrings().AllFiles)

111
openlp/core/ui/lib/filedialog.py Executable file
View File

@ -0,0 +1,111 @@
# -*- 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 #
###############################################################################
""" Patch the QFileDialog so it accepts and returns Path objects"""
from PyQt5 import QtWidgets
from openlp.core.common.path import Path, path_to_str, str_to_path
from openlp.core.lib import replace_params
class FileDialog(QtWidgets.QFileDialog):
@classmethod
def getExistingDirectory(cls, *args, **kwargs):
"""
Wraps `getExistingDirectory` so that it can be called with, and return Path objects
:type parent: QtWidgets.QWidget or None
:type caption: str
:type directory: openlp.core.common.path.Path
:type options: QtWidgets.QFileDialog.Options
:rtype: tuple[Path, str]
"""
args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
return_value = super().getExistingDirectory(*args, **kwargs)
# getExistingDirectory returns a str that represents the path. The string is empty if the user cancels the
# dialog.
return str_to_path(return_value)
@classmethod
def getOpenFileName(cls, *args, **kwargs):
"""
Wraps `getOpenFileName` so that it can be called with, and return Path objects
:type parent: QtWidgets.QWidget or None
:type caption: str
:type directory: openlp.core.common.path.Path
:type filter: str
:type initialFilter: str
:type options: QtWidgets.QFileDialog.Options
:rtype: tuple[Path, str]
"""
args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
file_name, selected_filter = super().getOpenFileName(*args, **kwargs)
# getOpenFileName returns a tuple. The first item is a str that represents the path. The string is empty if
# the user cancels the dialog.
return str_to_path(file_name), selected_filter
@classmethod
def getOpenFileNames(cls, *args, **kwargs):
"""
Wraps `getOpenFileNames` so that it can be called with, and return Path objects
:type parent: QtWidgets.QWidget or None
:type caption: str
:type directory: openlp.core.common.path.Path
:type filter: str
:type initialFilter: str
:type options: QtWidgets.QFileDialog.Options
:rtype: tuple[list[Path], str]
"""
args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
file_names, selected_filter = super().getOpenFileNames(*args, **kwargs)
# getSaveFileName returns a tuple. The first item is a list of str's that represents the path. The list is
# empty if the user cancels the dialog.
paths = [str_to_path(path) for path in file_names]
return paths, selected_filter
@classmethod
def getSaveFileName(cls, *args, **kwargs):
"""
Wraps `getSaveFileName` so that it can be called with, and return Path objects
:type parent: QtWidgets.QWidget or None
:type caption: str
:type directory: openlp.core.common.path.Path
:type filter: str
:type initialFilter: str
:type options: QtWidgets.QFileDialog.Options
:rtype: tuple[Path or None, str]
"""
args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
file_name, selected_filter = super().getSaveFileName(*args, **kwargs)
# getSaveFileName returns a tuple. The first item represents the path as a str. The string is empty if the user
# cancels the dialog.
return str_to_path(file_name), selected_filter

401
openlp/core/ui/lib/pathedit.py Executable file → Normal file
View File

@ -1,205 +1,196 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
############################################################################### ###############################################################################
# OpenLP - Open Source Lyrics Projection # # OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 OpenLP Developers # # Copyright (c) 2008-2017 OpenLP Developers #
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it # # 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 # # under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. # # Software Foundation; version 2 of the License. #
# # # #
# This program is distributed in the hope that it will be useful, but WITHOUT # # This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. # # more details. #
# # # #
# You should have received a copy of the GNU General Public License along # # 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 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
from enum import Enum from enum import Enum
import os.path
from PyQt5 import QtCore, QtWidgets
from PyQt5 import QtCore, QtWidgets
from openlp.core.common import UiStrings, translate
from openlp.core.common 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.lib import build_icon
from openlp.core.ui.lib.filedialog import FileDialog
class PathType(Enum):
Files = 1 class PathType(Enum):
Directories = 2 Files = 1
Directories = 2
class PathEdit(QtWidgets.QWidget):
""" 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. The :class:`~openlp.core.ui.lib.pathedit.PathEdit` class subclasses QWidget to create a custom widget for use when
""" a file or directory needs to be selected.
pathChanged = QtCore.pyqtSignal(str) """
pathChanged = QtCore.pyqtSignal(Path)
def __init__(self, parent=None, path_type=PathType.Files, default_path=None, dialog_caption=None, show_revert=True):
""" def __init__(self, parent=None, path_type=PathType.Files, default_path=None, dialog_caption=None, show_revert=True):
Initalise the PathEdit widget """
Initialise the PathEdit widget
:param parent: The parent of the widget. This is just passed to the super method.
:type parent: QWidget or None :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 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
:param dialog_caption: str button is clicked
:param bool show_revert: Used to determine if the 'revert button' should be visible.
:param default_path: The default path. This is set as the path when the revert button is clicked :rtype: None
:type default_path: str """
super().__init__(parent)
:param show_revert: Used to determin if the 'revert button' should be visible. self.default_path = default_path
:type show_revert: bool self.dialog_caption = dialog_caption
self._path_type = path_type
:return: None self._path = None
:rtype: None self.filters = '{all_files} (*)'.format(all_files=UiStrings().AllFiles)
""" self._setup(show_revert)
super().__init__(parent)
self.default_path = default_path def _setup(self, show_revert):
self.dialog_caption = dialog_caption """
self._path_type = path_type Set up the widget
self._path = None :param bool show_revert: Show or hide the revert button
self.filters = '{all_files} (*)'.format(all_files=UiStrings().AllFiles) :rtype: None
self._setup(show_revert) """
widget_layout = QtWidgets.QHBoxLayout()
def _setup(self, show_revert): widget_layout.setContentsMargins(0, 0, 0, 0)
""" self.line_edit = QtWidgets.QLineEdit(self)
Set up the widget widget_layout.addWidget(self.line_edit)
:param show_revert: Show or hide the revert button self.browse_button = QtWidgets.QToolButton(self)
:type show_revert: bool self.browse_button.setIcon(build_icon(':/general/general_open.png'))
widget_layout.addWidget(self.browse_button)
:return: None self.revert_button = QtWidgets.QToolButton(self)
:rtype: None self.revert_button.setIcon(build_icon(':/general/general_revert.png'))
""" self.revert_button.setVisible(show_revert)
widget_layout = QtWidgets.QHBoxLayout() widget_layout.addWidget(self.revert_button)
widget_layout.setContentsMargins(0, 0, 0, 0) self.setLayout(widget_layout)
self.line_edit = QtWidgets.QLineEdit(self) # Signals and Slots
self.line_edit.setText(self._path) self.browse_button.clicked.connect(self.on_browse_button_clicked)
widget_layout.addWidget(self.line_edit) self.revert_button.clicked.connect(self.on_revert_button_clicked)
self.browse_button = QtWidgets.QToolButton(self) self.line_edit.editingFinished.connect(self.on_line_edit_editing_finished)
self.browse_button.setIcon(build_icon(':/general/general_open.png')) self.update_button_tool_tips()
widget_layout.addWidget(self.browse_button)
self.revert_button = QtWidgets.QToolButton(self) @property
self.revert_button.setIcon(build_icon(':/general/general_revert.png')) def path(self):
self.revert_button.setVisible(show_revert) """
widget_layout.addWidget(self.revert_button) A property getter method to return the selected path.
self.setLayout(widget_layout)
# Signals and Slots :return: The selected path
self.browse_button.clicked.connect(self.on_browse_button_clicked) :rtype: openlp.core.common.path.Path
self.revert_button.clicked.connect(self.on_revert_button_clicked) """
self.line_edit.editingFinished.connect(self.on_line_edit_editing_finished) return self._path
self.update_button_tool_tips()
@path.setter
@property def path(self, path):
def path(self): """
""" A Property setter method to set the selected path
A property getter method to return the selected path.
:param openlp.core.common.path.Path path: The path to set the widget to
:return: The selected path :rtype: None
:rtype: str """
""" self._path = path
return self._path text = path_to_str(path)
self.line_edit.setText(text)
@path.setter self.line_edit.setToolTip(text)
def path(self, path):
""" @property
A Property setter method to set the selected path def path_type(self):
"""
:param path: The path to set the widget to A property getter method to return the path_type. Path type allows you to sepecify if the user is restricted to
:type path: str selecting a file or directory.
"""
self._path = path :return: The type selected
self.line_edit.setText(path) :rtype: PathType
self.line_edit.setToolTip(path) """
return self._path_type
@property
def path_type(self): @path_type.setter
""" def path_type(self, path_type):
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. A Property setter method to set the path type
:return: The type selected :param PathType path_type: The type of path to select
:rtype: Enum of PathEdit :rtype: None
""" """
return self._path_type self._path_type = path_type
self.update_button_tool_tips()
@path_type.setter
def path_type(self, path_type): def update_button_tool_tips(self):
""" """
A Property setter method to set the path type Called to update the tooltips on the buttons. This is changing path types, and when the widget is initalised
:param path: The type of path to select :rtype: None
:type path: Enum of PathEdit """
""" if self._path_type == PathType.Directories:
self._path_type = path_type self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for directory.'))
self.update_button_tool_tips() self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default directory.'))
else:
def update_button_tool_tips(self): self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for file.'))
""" self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default file.'))
Called to update the tooltips on the buttons. This is changing path types, and when the widget is initalised
:return: None def on_browse_button_clicked(self):
""" """
if self._path_type == PathType.Directories: A handler to handle a click on the browse button.
self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for directory.'))
self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default directory.')) Show the QFileDialog and process the input from the user
else:
self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for file.')) :rtype: None
self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default file.')) """
caption = self.dialog_caption
def on_browse_button_clicked(self): path = None
""" if self._path_type == PathType.Directories:
A handler to handle a click on the browse button. if not caption:
caption = translate('OpenLP.PathEdit', 'Select Directory')
Show the QFileDialog and process the input from the user path = FileDialog.getExistingDirectory(self, caption, self._path, FileDialog.ShowDirsOnly)
:return: None elif self._path_type == PathType.Files:
""" if not caption:
caption = self.dialog_caption caption = self.dialog_caption = translate('OpenLP.PathEdit', 'Select File')
path = '' path, filter_used = FileDialog.getOpenFileName(self, caption, self._path, self.filters)
if self._path_type == PathType.Directories: if path:
if not caption: self.on_new_path(path)
caption = translate('OpenLP.PathEdit', 'Select Directory')
path = QtWidgets.QFileDialog.getExistingDirectory(self, caption, def on_revert_button_clicked(self):
self._path, QtWidgets.QFileDialog.ShowDirsOnly) """
elif self._path_type == PathType.Files: A handler to handle a click on the revert button.
if not caption:
caption = self.dialog_caption = translate('OpenLP.PathEdit', 'Select File') Set the new path to the value of the default_path instance variable.
path, filter_used = QtWidgets.QFileDialog.getOpenFileName(self, caption, self._path, self.filters)
if path: :rtype: None
path = os.path.normpath(path) """
self.on_new_path(path) self.on_new_path(self.default_path)
def on_revert_button_clicked(self): def on_line_edit_editing_finished(self):
""" """
A handler to handle a click on the revert button. A handler to handle when the line edit has finished being edited.
Set the new path to the value of the default_path instance variable. :rtype: None
:return: None """
""" path = str_to_path(self.line_edit.text())
self.on_new_path(self.default_path) self.on_new_path(path)
def on_line_edit_editing_finished(self): def on_new_path(self, path):
""" """
A handler to handle when the line edit has finished being edited. A method called to validate and set a new path.
:return: None
""" Emits the pathChanged Signal
self.on_new_path(self.line_edit.text())
:param openlp.core.common.path.Path path: The new path
def on_new_path(self, path): :rtype: None
""" """
A method called to validate and set a new path. if self._path != path:
self.path = path
Emits the pathChanged Signal self.pathChanged.emit(path)
:param path: The new path
:type path: str
:return: None
"""
if self._path != path:
self.path = path
self.pathChanged.emit(path)

View File

@ -25,11 +25,12 @@ The :mod:``wizard`` module provides generic wizard tools for OpenLP.
import logging import logging
import os import os
from PyQt5 import QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate, is_macosx from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate, is_macosx
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
from openlp.core.lib.ui import add_welcome_page from openlp.core.lib.ui import add_welcome_page
from openlp.core.ui.lib.filedialog import FileDialog
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -50,13 +51,13 @@ class WizardStrings(object):
# These strings should need a good reason to be retranslated elsewhere. # These strings should need a good reason to be retranslated elsewhere.
FinishedImport = translate('OpenLP.Ui', 'Finished import.') FinishedImport = translate('OpenLP.Ui', 'Finished import.')
FormatLabel = translate('OpenLP.Ui', 'Format:') FormatLabel = translate('OpenLP.Ui', 'Format:')
HeaderStyle = '<span style="font-size:14pt; font-weight:600;">%s</span>' HeaderStyle = '<span style="font-size:14pt; font-weight:600;">{text}</span>'
Importing = translate('OpenLP.Ui', 'Importing') Importing = translate('OpenLP.Ui', 'Importing')
ImportingType = translate('OpenLP.Ui', 'Importing "%s"...') ImportingType = translate('OpenLP.Ui', 'Importing "{source}"...')
ImportSelect = translate('OpenLP.Ui', 'Select Import Source') ImportSelect = translate('OpenLP.Ui', 'Select Import Source')
ImportSelectLong = translate('OpenLP.Ui', 'Select the import format and the location to import from.') ImportSelectLong = translate('OpenLP.Ui', 'Select the import format and the location to import from.')
OpenTypeFile = translate('OpenLP.Ui', 'Open %s File') OpenTypeFile = translate('OpenLP.Ui', 'Open {file_type} File')
OpenTypeFolder = translate('OpenLP.Ui', 'Open %s Folder') OpenTypeFolder = translate('OpenLP.Ui', 'Open {folder_name} Folder')
PercentSymbolFormat = translate('OpenLP.Ui', '%p%') PercentSymbolFormat = translate('OpenLP.Ui', '%p%')
Ready = translate('OpenLP.Ui', 'Ready.') Ready = translate('OpenLP.Ui', 'Ready.')
StartingImport = translate('OpenLP.Ui', 'Starting import...') StartingImport = translate('OpenLP.Ui', 'Starting import...')
@ -93,7 +94,10 @@ class OpenLPWizard(QtWidgets.QWizard, RegistryProperties):
""" """
Constructor Constructor
""" """
super(OpenLPWizard, self).__init__(parent) # QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint remove the "?" buttons from windows,
# QtCore.Qt.WindowCloseButtonHint enables the "x" button to close these windows.
super(OpenLPWizard, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.plugin = plugin self.plugin = plugin
self.with_progress_page = add_progress_page self.with_progress_page = add_progress_page
self.setFixedWidth(640) self.setFixedWidth(640)
@ -275,37 +279,38 @@ class OpenLPWizard(QtWidgets.QWizard, RegistryProperties):
def get_file_name(self, title, editbox, setting_name, filters=''): def get_file_name(self, title, editbox, setting_name, filters=''):
""" """
Opens a QFileDialog and saves the filename to the given editbox. Opens a FileDialog and saves the filename to the given editbox.
:param title: The title of the dialog (unicode). :param str title: The title of the dialog.
:param editbox: An editbox (QLineEdit). :param QtWidgets.QLineEdit editbox: An QLineEdit.
:param setting_name: The place where to save the last opened directory. :param str setting_name: The place where to save the last opened directory.
:param filters: The file extension filters. It should contain the file description :param str filters: The file extension filters. It should contain the file description
as well as the file extension. For example:: as well as the file extension. For example::
'OpenLP 2 Databases (*.sqlite)' 'OpenLP 2 Databases (*.sqlite)'
:rtype: None
""" """
if filters: if filters:
filters += ';;' filters += ';;'
filters += '%s (*)' % UiStrings().AllFiles filters += '%s (*)' % UiStrings().AllFiles
filename, filter_used = QtWidgets.QFileDialog.getOpenFileName( file_path, filter_used = FileDialog.getOpenFileName(
self, title, os.path.dirname(Settings().value(self.plugin.settings_section + '/' + setting_name)), self, title, Settings().value(self.plugin.settings_section + '/' + setting_name), filters)
filters) if file_path:
if filename: editbox.setText(str(file_path))
editbox.setText(filename) Settings().setValue(self.plugin.settings_section + '/' + setting_name, file_path.parent)
Settings().setValue(self.plugin.settings_section + '/' + setting_name, filename)
def get_folder(self, title, editbox, setting_name): def get_folder(self, title, editbox, setting_name):
""" """
Opens a QFileDialog and saves the selected folder to the given editbox. Opens a FileDialog and saves the selected folder to the given editbox.
:param title: The title of the dialog (unicode). :param str title: The title of the dialog.
:param editbox: An editbox (QLineEdit). :param QtWidgets.QLineEdit editbox: An QLineEditbox.
:param setting_name: The place where to save the last opened directory. :param str setting_name: The place where to save the last opened directory.
:rtype: None
""" """
folder = QtWidgets.QFileDialog.getExistingDirectory( folder_path = FileDialog.getExistingDirectory(
self, title, Settings().value(self.plugin.settings_section + '/' + setting_name), self, title, Settings().value(self.plugin.settings_section + '/' + setting_name),
QtWidgets.QFileDialog.ShowDirsOnly) QtWidgets.QFileDialog.ShowDirsOnly)
if folder: if folder_path:
editbox.setText(folder) editbox.setText(str(folder_path))
Settings().setValue(self.plugin.settings_section + '/' + setting_name, folder) Settings().setValue(self.plugin.settings_section + '/' + setting_name, folder_path)

View File

@ -37,6 +37,7 @@ from PyQt5 import QtCore, QtWidgets, QtWebKit, QtWebKitWidgets, QtGui, QtMultime
from openlp.core.common import AppLocation, Registry, RegistryProperties, OpenLPMixin, Settings, translate,\ from openlp.core.common import AppLocation, Registry, RegistryProperties, OpenLPMixin, Settings, translate,\
is_macosx, is_win is_macosx, is_win
from openlp.core.common.path import path_to_str
from openlp.core.lib import ServiceItem, ImageSource, ScreenList, build_html, expand_tags, image_to_byte from openlp.core.lib import ServiceItem, ImageSource, ScreenList, build_html, expand_tags, image_to_byte
from openlp.core.lib.theme import BackgroundType from openlp.core.lib.theme import BackgroundType
from openlp.core.ui import HideMode, AlertLocation, DisplayControllerType from openlp.core.ui import HideMode, AlertLocation, DisplayControllerType
@ -157,7 +158,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
# platforms. For OpenLP 2.0 keep it only for OS X to not cause any # platforms. For OpenLP 2.0 keep it only for OS X to not cause any
# regressions on other platforms. # regressions on other platforms.
if is_macosx(): if is_macosx():
window_flags = QtCore.Qt.FramelessWindowHint | QtCore.Qt.Window window_flags = QtCore.Qt.FramelessWindowHint | QtCore.Qt.Window | QtCore.Qt.NoDropShadowWindowHint
self.setWindowFlags(window_flags) self.setWindowFlags(window_flags)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.set_transparency(False) self.set_transparency(False)
@ -259,7 +260,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
background_color.setNamedColor(Settings().value('core/logo background color')) background_color.setNamedColor(Settings().value('core/logo background color'))
if not background_color.isValid(): if not background_color.isValid():
background_color = QtCore.Qt.white background_color = QtCore.Qt.white
image_file = Settings().value('core/logo file') image_file = path_to_str(Settings().value('core/logo file'))
splash_image = QtGui.QImage(image_file) splash_image = QtGui.QImage(image_file)
self.initial_fame = QtGui.QImage( self.initial_fame = QtGui.QImage(
self.screen['size'].width(), self.screen['size'].width(),
@ -484,7 +485,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
service_item = ServiceItem() service_item = ServiceItem()
service_item.title = 'webkit' service_item.title = 'webkit'
service_item.processor = 'webkit' service_item.processor = 'webkit'
path = os.path.join(AppLocation.get_section_data_path('themes'), path = os.path.join(str(AppLocation.get_section_data_path('themes')),
self.service_item.theme_data.theme_name) self.service_item.theme_data.theme_name)
service_item.add_from_command(path, service_item.add_from_command(path,
self.service_item.theme_data.background_filename, self.service_item.theme_data.background_filename,
@ -689,7 +690,7 @@ class AudioPlayer(OpenLPMixin, QtCore.QObject):
""" """
Skip forward to the next track in the list Skip forward to the next track in the list
""" """
self.playerlist.next() self.playlist.next()
def go_to(self, index): def go_to(self, index):
""" """

View File

@ -34,12 +34,15 @@ from tempfile import gettempdir
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, \ from openlp.core.api import websockets
from openlp.core.api.http import server
from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, UiStrings, \
check_directory_exists, translate, is_win, is_macosx, add_actions check_directory_exists, translate, is_win, is_macosx, add_actions
from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.actions import ActionList, CategoryOrder
from openlp.core.common.path import Path, path_to_str, str_to_path
from openlp.core.common.versionchecker import get_application_version from openlp.core.common.versionchecker import get_application_version
from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, ScreenList, build_icon from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, ScreenList, build_icon
from openlp.core.lib.ui import UiStrings, create_action from openlp.core.lib.ui import create_action
from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \ from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \
ShortcutListForm, FormattingTagForm, PreviewController ShortcutListForm, FormattingTagForm, PreviewController
from openlp.core.ui.firsttimeform import FirstTimeForm from openlp.core.ui.firsttimeform import FirstTimeForm
@ -47,8 +50,10 @@ from openlp.core.ui.media import MediaController
from openlp.core.ui.printserviceform import PrintServiceForm from openlp.core.ui.printserviceform import PrintServiceForm
from openlp.core.ui.projector.manager import ProjectorManager from openlp.core.ui.projector.manager import ProjectorManager
from openlp.core.ui.lib.dockwidget import OpenLPDockWidget 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.ui.lib.mediadockmanager import MediaDockManager
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
MEDIA_MANAGER_STYLE = """ MEDIA_MANAGER_STYLE = """
@ -305,9 +310,9 @@ class Ui_MainWindow(object):
# Give QT Extra Hint that this is an About Menu Item # Give QT Extra Hint that this is an About Menu Item
self.about_item.setMenuRole(QtWidgets.QAction.AboutRole) self.about_item.setMenuRole(QtWidgets.QAction.AboutRole)
if is_win(): if is_win():
self.local_help_file = os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'OpenLP.chm') self.local_help_file = os.path.join(str(AppLocation.get_directory(AppLocation.AppDir)), 'OpenLP.chm')
elif is_macosx(): elif is_macosx():
self.local_help_file = os.path.join(AppLocation.get_directory(AppLocation.AppDir), self.local_help_file = os.path.join(str(AppLocation.get_directory(AppLocation.AppDir)),
'..', 'Resources', 'OpenLP.help') '..', 'Resources', 'OpenLP.help')
self.user_manual_item = create_action(main_window, 'userManualItem', icon=':/system/system_help_contents.png', self.user_manual_item = create_action(main_window, 'userManualItem', icon=':/system/system_help_contents.png',
can_shortcuts=True, category=UiStrings().Help, can_shortcuts=True, category=UiStrings().Help,
@ -370,7 +375,7 @@ class Ui_MainWindow(object):
""" """
Set up the translation system Set up the translation system
""" """
main_window.setWindowTitle(UiStrings().OLP) main_window.setWindowTitle(UiStrings().OpenLP)
self.file_menu.setTitle(translate('OpenLP.MainWindow', '&File')) self.file_menu.setTitle(translate('OpenLP.MainWindow', '&File'))
self.file_import_menu.setTitle(translate('OpenLP.MainWindow', '&Import')) self.file_import_menu.setTitle(translate('OpenLP.MainWindow', '&Import'))
self.file_export_menu.setTitle(translate('OpenLP.MainWindow', '&Export')) self.file_export_menu.setTitle(translate('OpenLP.MainWindow', '&Export'))
@ -513,6 +518,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
Settings().set_up_default_values() Settings().set_up_default_values()
self.about_form = AboutForm(self) self.about_form = AboutForm(self)
MediaController() MediaController()
if Registry().get_flag('no_web_server'):
websockets.WebSocketServer()
server.HttpServer()
SettingsForm(self) SettingsForm(self)
self.formatting_tag_form = FormattingTagForm(self) self.formatting_tag_form = FormattingTagForm(self)
self.shortcut_form = ShortcutListForm(self) self.shortcut_form = ShortcutListForm(self)
@ -540,7 +548,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
self.tools_first_time_wizard.triggered.connect(self.on_first_time_wizard_clicked) self.tools_first_time_wizard.triggered.connect(self.on_first_time_wizard_clicked)
self.update_theme_images.triggered.connect(self.on_update_theme_images) self.update_theme_images.triggered.connect(self.on_update_theme_images)
self.formatting_tag_item.triggered.connect(self.on_formatting_tag_item_clicked) self.formatting_tag_item.triggered.connect(self.on_formatting_tag_item_clicked)
self.settings_configure_item.triggered.connect(self.on_settings_configure_iem_clicked) self.settings_configure_item.triggered.connect(self.on_settings_configure_item_clicked)
self.settings_shortcuts_item.triggered.connect(self.on_settings_shortcuts_item_clicked) self.settings_shortcuts_item.triggered.connect(self.on_settings_shortcuts_item_clicked)
self.settings_import_item.triggered.connect(self.on_settings_import_item_clicked) self.settings_import_item.triggered.connect(self.on_settings_import_item_clicked)
self.settings_export_item.triggered.connect(self.on_settings_export_item_clicked) self.settings_export_item.triggered.connect(self.on_settings_export_item_clicked)
@ -788,7 +796,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
""" """
Open data folder Open data folder
""" """
path = AppLocation.get_data_path() path = str(AppLocation.get_data_path())
QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(path)) QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(path))
def on_update_theme_images(self): def on_update_theme_images(self):
@ -803,7 +811,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
""" """
self.formatting_tag_form.exec() self.formatting_tag_form.exec()
def on_settings_configure_iem_clicked(self): def on_settings_configure_item_clicked(self):
""" """
Show the Settings dialog Show the Settings dialog
""" """
@ -864,16 +872,17 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
setting_sections.extend([plugin.name for plugin in self.plugin_manager.plugins]) setting_sections.extend([plugin.name for plugin in self.plugin_manager.plugins])
# Copy the settings file to the tmp dir, because we do not want to change the original one. # Copy the settings file to the tmp dir, because we do not want to change the original one.
temp_directory = os.path.join(str(gettempdir()), 'openlp') temp_directory = os.path.join(str(gettempdir()), 'openlp')
check_directory_exists(temp_directory) check_directory_exists(Path(temp_directory))
temp_config = os.path.join(temp_directory, os.path.basename(import_file_name)) temp_config = os.path.join(temp_directory, os.path.basename(import_file_name))
shutil.copyfile(import_file_name, temp_config) shutil.copyfile(import_file_name, temp_config)
settings = Settings() settings = Settings()
import_settings = Settings(temp_config, Settings.IniFormat) import_settings = Settings(temp_config, Settings.IniFormat)
# Convert image files
log.info('hook upgrade_plugin_settings') log.info('hook upgrade_plugin_settings')
self.plugin_manager.hook_upgrade_plugin_settings(import_settings) self.plugin_manager.hook_upgrade_plugin_settings(import_settings)
# Remove/rename old settings to prepare the import. # Upgrade settings to prepare the import.
import_settings.remove_obsolete_settings() if import_settings.can_upgrade():
import_settings.upgrade_settings()
# Lets do a basic sanity check. If it contains this string we can assume it was created by OpenLP and so we'll # Lets do a basic sanity check. If it contains this string we can assume it was created by OpenLP and so we'll
# load what we can from it, and just silently ignore anything we don't recognise. # load what we can from it, and just silently ignore anything we don't recognise.
if import_settings.value('SettingsImport/type') != 'OpenLP_settings_export': if import_settings.value('SettingsImport/type') != 'OpenLP_settings_export':
@ -920,8 +929,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
QtWidgets.QMessageBox.information(self, translate('OpenLP.MainWindow', 'Import settings'), QtWidgets.QMessageBox.information(self, translate('OpenLP.MainWindow', 'Import settings'),
translate('OpenLP.MainWindow', translate('OpenLP.MainWindow',
'OpenLP will now close. Imported settings will ' 'OpenLP will now close. Imported settings will '
'be applied the next time you start OpenLP.'), 'be applied the next time you start OpenLP.'))
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok))
self.settings_imported = True self.settings_imported = True
self.clean_up() self.clean_up()
QtCore.QCoreApplication.exit() QtCore.QCoreApplication.exit()
@ -930,89 +938,17 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
""" """
Export settings to a .conf file in INI format Export settings to a .conf file in INI format
""" """
export_file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName( export_file_path, filter_used = FileDialog.getSaveFileName(
self, self,
translate('OpenLP.MainWindow', 'Export Settings File'), translate('OpenLP.MainWindow', 'Export Settings File'),
'', None,
translate('OpenLP.MainWindow', 'OpenLP Settings (*.conf)')) translate('OpenLP.MainWindow', 'OpenLP Settings (*.conf)'))
if not export_file_name: if not export_file_path:
return return
# Make sure it's a .conf file. # Make sure it's a .conf file.
if not export_file_name.endswith('conf'): export_file_path = export_file_path.with_suffix('.conf')
export_file_name += '.conf'
temp_file = os.path.join(gettempdir(), 'openlp', 'exportConf.tmp')
self.save_settings() self.save_settings()
setting_sections = [] Settings().export(export_file_path)
# Add main sections.
setting_sections.extend([self.general_settings_section])
setting_sections.extend([self.advanced_settings_section])
setting_sections.extend([self.ui_settings_section])
setting_sections.extend([self.shortcuts_settings_section])
setting_sections.extend([self.service_manager_settings_section])
setting_sections.extend([self.themes_settings_section])
setting_sections.extend([self.display_tags_section])
# Add plugin sections.
for plugin in self.plugin_manager.plugins:
setting_sections.extend([plugin.name])
# Delete old files if found.
if os.path.exists(temp_file):
os.remove(temp_file)
if os.path.exists(export_file_name):
os.remove(export_file_name)
settings = Settings()
settings.remove(self.header_section)
# Get the settings.
keys = settings.allKeys()
export_settings = Settings(temp_file, Settings.IniFormat)
# Add a header section.
# This is to insure it's our conf file for import.
now = datetime.now()
application_version = get_application_version()
# Write INI format using Qsettings.
# Write our header.
export_settings.beginGroup(self.header_section)
export_settings.setValue('Make_Changes', 'At_Own_RISK')
export_settings.setValue('type', 'OpenLP_settings_export')
export_settings.setValue('file_date_created', now.strftime("%Y-%m-%d %H:%M"))
export_settings.setValue('version', application_version['full'])
export_settings.endGroup()
# Write all the sections and keys.
for section_key in keys:
# FIXME: We are conflicting with the standard "General" section.
if 'eneral' in section_key:
section_key = section_key.lower()
try:
key_value = settings.value(section_key)
except KeyError:
QtWidgets.QMessageBox.critical(self, translate('OpenLP.MainWindow', 'Export setting error'),
translate('OpenLP.MainWindow', 'The key "{key}" does not have a default '
'value so it will be skipped in this '
'export.').format(key=section_key),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok))
key_value = None
if key_value is not None:
export_settings.setValue(section_key, key_value)
export_settings.sync()
# Temp CONF file has been written. Blanks in keys are now '%20'.
# Read the temp file and output the user's CONF file with blanks to
# make it more readable.
temp_conf = open(temp_file, 'r')
try:
export_conf = open(export_file_name, 'w')
for file_record in temp_conf:
# Get rid of any invalid entries.
if file_record.find('@Invalid()') == -1:
file_record = file_record.replace('%20', ' ')
export_conf.write(file_record)
temp_conf.close()
export_conf.close()
os.remove(temp_file)
except OSError as ose:
QtWidgets.QMessageBox.critical(self, translate('OpenLP.MainWindow', 'Export setting error'),
translate('OpenLP.MainWindow',
'An error occurred while exporting the '
'settings: {err}').format(err=ose.strerror),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok))
def on_mode_default_item_clicked(self): def on_mode_default_item_clicked(self):
""" """
@ -1151,9 +1087,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
:param file_name: The file name of the service file. :param file_name: The file name of the service file.
""" """
if modified: if modified:
title = '{title} - {name}*'.format(title=UiStrings().OLP, name=file_name) title = '{title} - {name}*'.format(title=UiStrings().OpenLP, name=file_name)
else: else:
title = '{title} - {name}'.format(title=UiStrings().OLP, name=file_name) title = '{title} - {name}'.format(title=UiStrings().OpenLP, name=file_name)
self.setWindowTitle(title) self.setWindowTitle(title)
def show_status_message(self, message): def show_status_message(self, message):
@ -1271,7 +1207,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
settings.remove('custom slide') settings.remove('custom slide')
settings.remove('service') settings.remove('service')
settings.beginGroup(self.general_settings_section) settings.beginGroup(self.general_settings_section)
self.recent_files = settings.value('recent files') self.recent_files = [path_to_str(file_path) for file_path in settings.value('recent files')]
settings.endGroup() settings.endGroup()
settings.beginGroup(self.ui_settings_section) settings.beginGroup(self.ui_settings_section)
self.move(settings.value('main window position')) self.move(settings.value('main window position'))
@ -1295,7 +1231,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
log.debug('Saving QSettings') log.debug('Saving QSettings')
settings = Settings() settings = Settings()
settings.beginGroup(self.general_settings_section) settings.beginGroup(self.general_settings_section)
settings.setValue('recent files', self.recent_files) settings.setValue('recent files', [str_to_path(file) for file in self.recent_files])
settings.endGroup() settings.endGroup()
settings.beginGroup(self.ui_settings_section) settings.beginGroup(self.ui_settings_section)
settings.setValue('main window position', self.pos()) settings.setValue('main window position', self.pos())
@ -1316,7 +1252,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
self.recent_files_menu.clear() self.recent_files_menu.clear()
for file_id, filename in enumerate(recent_files_to_display): for file_id, filename in enumerate(recent_files_to_display):
log.debug('Recent file name: {name}'.format(name=filename)) log.debug('Recent file name: {name}'.format(name=filename))
# TODO: Should be good
action = create_action(self, '', action = create_action(self, '',
text='&{n} {name}'.format(n=file_id + 1, text='&{n} {name}'.format(n=file_id + 1,
name=os.path.splitext(os.path.basename(str(filename)))[0]), name=os.path.splitext(os.path.basename(str(filename)))[0]),
@ -1438,9 +1373,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
log.info('No data copy requested') log.info('No data copy requested')
# Change the location of data directory in config file. # Change the location of data directory in config file.
settings = QtCore.QSettings() settings = QtCore.QSettings()
settings.setValue('advanced/data path', self.new_data_path) settings.setValue('advanced/data path', Path(self.new_data_path))
# Check if the new data path is our default. # Check if the new data path is our default.
if self.new_data_path == AppLocation.get_directory(AppLocation.DataDir): if self.new_data_path == str(AppLocation.get_directory(AppLocation.DataDir)):
settings.remove('advanced/data path') settings.remove('advanced/data path')
self.application.set_normal_cursor() self.application.set_normal_cursor()

View File

@ -146,5 +146,6 @@ def format_milliseconds(milliseconds):
from .mediacontroller import MediaController from .mediacontroller import MediaController
from .playertab import PlayerTab from .playertab import PlayerTab
from .endpoint import media_endpoint
__all__ = ['MediaController', 'PlayerTab'] __all__ = ['MediaController', 'PlayerTab']

View File

@ -19,40 +19,54 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
"""
Provide a work around for a bug in QFileDialog <https://bugs.launchpad.net/openlp/+bug/1209515>
"""
import logging import logging
import os
from urllib import parse
from PyQt5 import QtWidgets from openlp.core.api.http.endpoint import Endpoint
from openlp.core.api.http import requires_auth
from openlp.core.common import Registry
from openlp.core.common import UiStrings
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
media_endpoint = Endpoint('media')
class FileDialog(QtWidgets.QFileDialog):
@media_endpoint.route('play')
@requires_auth
def media_play(request):
""" """
Subclass QFileDialog to work round a bug Handles requests for playing media
:param request: The http request object.
""" """
@staticmethod media = Registry().get('media_controller')
def getOpenFileNames(parent, *args, **kwargs): live = Registry().get('live_controller')
""" status = media.media_play(live, False)
Reimplement getOpenFileNames to fix the way it returns some file names that url encoded when selecting multiple return {'results': {'success': status}}
files
"""
files, filter_used = QtWidgets.QFileDialog.getOpenFileNames(parent, *args, **kwargs) @media_endpoint.route('pause')
file_list = [] @requires_auth
for file in files: def media_pause(request):
if not os.path.exists(file): """
log.info('File not found. Attempting to unquote.') Handles requests for pausing media
file = parse.unquote(file)
if not os.path.exists(file): :param request: The http request object.
log.error('File {text} not found.'.format(text=file)) """
QtWidgets.QMessageBox.information(parent, UiStrings().FileNotFound, media = Registry().get('media_controller')
UiStrings().FileNotFoundMessage.format(name=file)) live = Registry().get('live_controller')
continue status = media.media_pause(live)
file_list.append(file) return {'results': {'success': status}}
return file_list
@media_endpoint.route('stop')
@requires_auth
def media_stop(request):
"""
Handles requests for stopping
:param request: The http request object.
"""
event = getattr(Registry().get('live_controller'), 'mediacontroller_live_stop')
event.emit()
return {'results': {'success': True}}

View File

@ -28,12 +28,13 @@ import os
import datetime import datetime
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.api.http import register_endpoint
from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, \ from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, \
extension_loader, translate extension_loader, translate
from openlp.core.lib import ItemCapabilities from openlp.core.lib import ItemCapabilities
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box
from openlp.core.common import AppLocation
from openlp.core.ui import DisplayControllerType from openlp.core.ui import DisplayControllerType
from openlp.core.ui.media.endpoint import media_endpoint
from openlp.core.ui.media.vendor.mediainfoWrapper import MediaInfoWrapper from openlp.core.ui.media.vendor.mediainfoWrapper import MediaInfoWrapper
from openlp.core.ui.media.mediaplayer import MediaPlayer from openlp.core.ui.media.mediaplayer import MediaPlayer
from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players,\ from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players,\
@ -127,9 +128,11 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
Registry().register_function('media_unblank', self.media_unblank) Registry().register_function('media_unblank', self.media_unblank)
# Signals for background video # Signals for background video
Registry().register_function('songs_hide', self.media_hide) Registry().register_function('songs_hide', self.media_hide)
Registry().register_function('songs_blank', self.media_blank)
Registry().register_function('songs_unblank', self.media_unblank) Registry().register_function('songs_unblank', self.media_unblank)
Registry().register_function('mediaitem_media_rebuild', self._set_active_players) Registry().register_function('mediaitem_media_rebuild', self._set_active_players)
Registry().register_function('mediaitem_suffixes', self._generate_extensions_lists) Registry().register_function('mediaitem_suffixes', self._generate_extensions_lists)
register_endpoint(media_endpoint)
def _set_active_players(self): def _set_active_players(self):
""" """
@ -174,7 +177,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
Check to see if we have any media Player's available. Check to see if we have any media Player's available.
""" """
log.debug('_check_available_media_players') log.debug('_check_available_media_players')
controller_dir = os.path.join('openlp', 'core', 'ui', 'media') controller_dir = os.path.join('core', 'ui', 'media')
glob_pattern = os.path.join(controller_dir, '*player.py') glob_pattern = os.path.join(controller_dir, '*player.py')
extension_loader(glob_pattern, ['mediaplayer.py']) extension_loader(glob_pattern, ['mediaplayer.py'])
player_classes = MediaPlayer.__subclasses__() player_classes = MediaPlayer.__subclasses__()
@ -294,7 +297,9 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
triggers=controller.send_to_plugins) triggers=controller.send_to_plugins)
controller.position_label = QtWidgets.QLabel() controller.position_label = QtWidgets.QLabel()
controller.position_label.setText(' 00:00 / 00:00') controller.position_label.setText(' 00:00 / 00:00')
controller.position_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
controller.position_label.setToolTip(translate('OpenLP.SlideController', 'Video timer.')) controller.position_label.setToolTip(translate('OpenLP.SlideController', 'Video timer.'))
controller.position_label.setMinimumSize(90, 0)
controller.position_label.setObjectName('position_label') controller.position_label.setObjectName('position_label')
controller.mediabar.add_toolbar_widget(controller.position_label) controller.mediabar.add_toolbar_widget(controller.position_label)
# Build the seek_slider. # Build the seek_slider.
@ -430,7 +435,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
log.debug('video mediatype: ' + str(controller.media_info.media_type)) log.debug('video mediatype: ' + str(controller.media_info.media_type))
# dont care about actual theme, set a black background # dont care about actual theme, set a black background
if controller.is_live and not controller.media_info.is_background: if controller.is_live and not controller.media_info.is_background:
display.frame.evaluateJavaScript('show_video( "setBackBoard", null, null, null,"visible");') display.frame.evaluateJavaScript('show_video("setBackBoard", null, null,"visible");')
# now start playing - Preview is autoplay! # now start playing - Preview is autoplay!
autoplay = False autoplay = False
# Preview requested # Preview requested
@ -466,9 +471,10 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
player = self.media_players[used_players[0]] player = self.media_players[used_players[0]]
if suffix not in player.video_extensions_list and suffix not in player.audio_extensions_list: if suffix not in player.video_extensions_list and suffix not in player.audio_extensions_list:
# Media could not be loaded correctly # Media could not be loaded correctly
critical_error_message_box(translate('MediaPlugin.MediaItem', 'Unsupported Media File'), critical_error_message_box(
translate('MediaPlugin.MediaItem', 'File %s not supported using player %s') % translate('MediaPlugin.MediaItem', 'Unsupported Media File'),
(service_item.get_frame_path(), used_players[0])) translate('MediaPlugin.MediaItem', 'File {file_path} not supported using player {player_name}'
).format(file_path=service_item.get_frame_path(), player_name=used_players[0]))
return False return False
media_data = MediaInfoWrapper.parse(service_item.get_frame_path()) media_data = MediaInfoWrapper.parse(service_item.get_frame_path())
# duration returns in milli seconds # duration returns in milli seconds
@ -610,6 +616,14 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
""" """
self.media_play(msg[0], status) self.media_play(msg[0], status)
def on_media_play(self):
"""
Responds to the request to play a loaded video from the web.
:param msg: First element is the controller which should be used
"""
self.media_play(Registry().get('live_controller'), False)
def media_play(self, controller, first_time=True): def media_play(self, controller, first_time=True):
""" """
Responds to the request to play a loaded video Responds to the request to play a loaded video
@ -684,6 +698,14 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
""" """
self.media_pause(msg[0]) self.media_pause(msg[0])
def on_media_pause(self):
"""
Responds to the request to pause a loaded video from the web.
:param msg: First element is the controller which should be used
"""
self.media_pause(Registry().get('live_controller'))
def media_pause(self, controller): def media_pause(self, controller):
""" """
Responds to the request to pause a loaded video Responds to the request to pause a loaded video
@ -724,6 +746,14 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
""" """
self.media_stop(msg[0]) self.media_stop(msg[0])
def on_media_stop(self):
"""
Responds to the request to stop a loaded video from the web.
:param msg: First element is the controller which should be used
"""
self.media_stop(Registry().get('live_controller'))
def media_stop(self, controller, looping_background=False): def media_stop(self, controller, looping_background=False):
""" """
Responds to the request to stop a loaded video Responds to the request to stop a loaded video
@ -738,6 +768,11 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
self.current_media_players[controller.controller_type].stop(display) self.current_media_players[controller.controller_type].stop(display)
self.current_media_players[controller.controller_type].set_visible(display, False) self.current_media_players[controller.controller_type].set_visible(display, False)
controller.seek_slider.setSliderPosition(0) controller.seek_slider.setSliderPosition(0)
total_seconds = controller.media_info.length // 1000
total_minutes = total_seconds // 60
total_seconds %= 60
controller.position_label.setText(' %02d:%02d / %02d:%02d' %
(0, 0, total_minutes, total_seconds))
controller.mediabar.actions['playbackPlay'].setVisible(True) controller.mediabar.actions['playbackPlay'].setVisible(True)
controller.mediabar.actions['playbackStop'].setDisabled(True) controller.mediabar.actions['playbackStop'].setDisabled(True)
controller.mediabar.actions['playbackPause'].setVisible(False) controller.mediabar.actions['playbackPause'].setVisible(False)
@ -800,7 +835,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
display.override = {} display.override = {}
self.current_media_players[controller.controller_type].reset(display) self.current_media_players[controller.controller_type].reset(display)
self.current_media_players[controller.controller_type].set_visible(display, False) self.current_media_players[controller.controller_type].set_visible(display, False)
display.frame.evaluateJavaScript('show_video( "setBackBoard", null, null, null,"hidden");') display.frame.evaluateJavaScript('show_video("setBackBoard", null, null, "hidden");')
del self.current_media_players[controller.controller_type] del self.current_media_players[controller.controller_type]
def media_hide(self, msg): def media_hide(self, msg):
@ -815,7 +850,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
display = self._define_display(self.live_controller) display = self._define_display(self.live_controller)
if self.live_controller.controller_type in self.current_media_players and \ if self.live_controller.controller_type in self.current_media_players and \
self.current_media_players[self.live_controller.controller_type].get_live_state() == MediaState.Playing: self.current_media_players[self.live_controller.controller_type].get_live_state() == MediaState.Playing:
self.current_media_players[self.live_controller.controller_type].pause(display) self.media_pause(display.controller)
self.current_media_players[self.live_controller.controller_type].set_visible(display, False) self.current_media_players[self.live_controller.controller_type].set_visible(display, False)
def media_blank(self, msg): def media_blank(self, msg):
@ -833,7 +868,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
display = self._define_display(self.live_controller) display = self._define_display(self.live_controller)
if self.live_controller.controller_type in self.current_media_players and \ if self.live_controller.controller_type in self.current_media_players and \
self.current_media_players[self.live_controller.controller_type].get_live_state() == MediaState.Playing: self.current_media_players[self.live_controller.controller_type].get_live_state() == MediaState.Playing:
self.current_media_players[self.live_controller.controller_type].pause(display) self.media_pause(display.controller)
self.current_media_players[self.live_controller.controller_type].set_visible(display, False) self.current_media_players[self.live_controller.controller_type].set_visible(display, False)
def media_unblank(self, msg): def media_unblank(self, msg):
@ -851,7 +886,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
if self.live_controller.controller_type in self.current_media_players and \ if self.live_controller.controller_type in self.current_media_players and \
self.current_media_players[self.live_controller.controller_type].get_live_state() != \ self.current_media_players[self.live_controller.controller_type].get_live_state() != \
MediaState.Playing: MediaState.Playing:
if self.current_media_players[self.live_controller.controller_type].play(display): if self.media_play(display.controller):
self.current_media_players[self.live_controller.controller_type].set_visible(display, True) self.current_media_players[self.live_controller.controller_type].set_visible(display, True)
# Start Timer for ui updates # Start Timer for ui updates
if not self.live_timer.isActive(): if not self.live_timer.isActive():

View File

@ -258,7 +258,7 @@ class SystemPlayer(MediaPlayer):
:param display: The display where the media is :param display: The display where the media is
""" """
if display.media_player.state() == QtMultimedia.QMediaPlayer.PausedState and self.state != MediaState.Paused: if display.media_player.state() == QtMultimedia.QMediaPlayer.PausedState and self.state != MediaState.Paused:
self.stop(display) self.pause(display)
controller = display.controller controller = display.controller
if controller.media_info.end_time > 0: if controller.media_info.end_time > 0:
if display.media_player.position() > controller.media_info.end_time: if display.media_player.position() > controller.media_info.end_time:

View File

@ -41,7 +41,8 @@ class PluginForm(QtWidgets.QDialog, Ui_PluginViewDialog, RegistryProperties):
""" """
Constructor Constructor
""" """
super(PluginForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) super(PluginForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.active_plugin = None self.active_plugin = None
self.programatic_change = False self.programatic_change = False
self.setupUi(self) self.setupUi(self)
@ -60,7 +61,6 @@ class PluginForm(QtWidgets.QDialog, Ui_PluginViewDialog, RegistryProperties):
self._clear_details() self._clear_details()
self.programatic_change = True self.programatic_change = True
plugin_list_width = 0 plugin_list_width = 0
# TODO: Tested at home
for plugin in self.plugin_manager.plugins: for plugin in self.plugin_manager.plugins:
item = QtWidgets.QListWidgetItem(self.plugin_list_widget) item = QtWidgets.QListWidgetItem(self.plugin_list_widget)
# We do this just to make 100% sure the status is an integer as # We do this just to make 100% sure the status is an integer as
@ -137,7 +137,6 @@ class PluginForm(QtWidgets.QDialog, Ui_PluginViewDialog, RegistryProperties):
self.active_plugin.app_startup() self.active_plugin.app_startup()
else: else:
self.active_plugin.toggle_status(PluginStatus.Inactive) self.active_plugin.toggle_status(PluginStatus.Inactive)
# TODO: Tested at home
status_text = translate('OpenLP.PluginForm', '{name} (Inactive)') status_text = translate('OpenLP.PluginForm', '{name} (Inactive)')
if self.active_plugin.status == PluginStatus.Active: if self.active_plugin.status == PluginStatus.Active:
status_text = translate('OpenLP.PluginForm', '{name} (Active)') status_text = translate('OpenLP.PluginForm', '{name} (Active)')

View File

@ -125,8 +125,8 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
""" """
Constructor Constructor
""" """
super(PrintServiceForm, self).__init__(Registry().get('main_window'), super(PrintServiceForm, self).__init__(Registry().get('main_window'), QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
self.printer = QtPrintSupport.QPrinter() self.printer = QtPrintSupport.QPrinter()
self.print_dialog = QtPrintSupport.QPrintDialog(self.printer, self) self.print_dialog = QtPrintSupport.QPrintDialog(self.printer, self)
self.document = QtGui.QTextDocument() self.document = QtGui.QTextDocument()
@ -176,7 +176,7 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
html_data = self._add_element('html') html_data = self._add_element('html')
self._add_element('head', parent=html_data) self._add_element('head', parent=html_data)
self._add_element('title', self.title_line_edit.text(), html_data.head) self._add_element('title', self.title_line_edit.text(), html_data.head)
css_path = os.path.join(AppLocation.get_data_path(), 'serviceprint', 'service_print.css') css_path = AppLocation.get_data_path() / 'serviceprint' / 'service_print.css'
custom_css = get_text_file_string(css_path) custom_css = get_text_file_string(css_path)
if not custom_css: if not custom_css:
custom_css = DEFAULT_CSS custom_css = DEFAULT_CSS

View File

@ -142,7 +142,8 @@ class ProjectorEditForm(QtWidgets.QDialog, Ui_ProjectorEditForm):
editProjector = QtCore.pyqtSignal(object) editProjector = QtCore.pyqtSignal(object)
def __init__(self, parent=None, projectordb=None): def __init__(self, parent=None, projectordb=None):
super(ProjectorEditForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) super(ProjectorEditForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.projectordb = projectordb self.projectordb = projectordb
self.setupUi(self) self.setupUi(self)
self.button_box.accepted.connect(self.accept_me) self.button_box.accepted.connect(self.accept_me)

View File

@ -38,7 +38,8 @@ from openlp.core.lib.projector.constants import ERROR_MSG, ERROR_STRING, E_AUTHE
E_NETWORK, E_NOT_CONNECTED, E_UNKNOWN_SOCKET_ERROR, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_COOLDOWN, \ E_NETWORK, E_NOT_CONNECTED, E_UNKNOWN_SOCKET_ERROR, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_COOLDOWN, \
S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP
from openlp.core.lib.projector.db import ProjectorDB from openlp.core.lib.projector.db import ProjectorDB
from openlp.core.lib.projector.pjlink1 import PJLink from openlp.core.lib.projector.pjlink import PJLink
from openlp.core.lib.projector.pjlink2 import PJLinkUDP
from openlp.core.ui.projector.editform import ProjectorEditForm from openlp.core.ui.projector.editform import ProjectorEditForm
from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle
@ -290,6 +291,8 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
self.settings_section = 'projector' self.settings_section = 'projector'
self.projectordb = projectordb self.projectordb = projectordb
self.projector_list = [] self.projector_list = []
self.pjlink_udp = PJLinkUDP()
self.pjlink_udp.projector_list = self.projector_list
self.source_select_form = None self.source_select_form = None
def bootstrap_initialise(self): def bootstrap_initialise(self):
@ -417,9 +420,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
:param opt: Needed by PyQt5 :param opt: Needed by PyQt5
""" """
try: try:
ip = opt.link.ip opt.link.set_shutter_closed()
projector = opt
projector.link.set_shutter_closed()
except AttributeError: except AttributeError:
for list_item in self.projector_list_widget.selectedItems(): for list_item in self.projector_list_widget.selectedItems():
if list_item is None: if list_item is None:
@ -452,9 +453,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
:param opt: Needed by PyQt5 :param opt: Needed by PyQt5
""" """
try: try:
ip = opt.link.ip opt.link.connect_to_host()
projector = opt
projector.link.connect_to_host()
except AttributeError: except AttributeError:
for list_item in self.projector_list_widget.selectedItems(): for list_item in self.projector_list_widget.selectedItems():
if list_item is None: if list_item is None:
@ -524,7 +523,8 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
self.projector_list = new_list self.projector_list = new_list
list_item = self.projector_list_widget.takeItem(self.projector_list_widget.currentRow()) list_item = self.projector_list_widget.takeItem(self.projector_list_widget.currentRow())
list_item = None list_item = None
deleted = self.projectordb.delete_projector(projector.db_item) if not self.projectordb.delete_projector(projector.db_item):
log.warning('Delete projector {item} failed'.format(item=projector.db_item))
for item in self.projector_list: for item in self.projector_list:
log.debug('New projector list - item: {ip} {name}'.format(ip=item.link.ip, name=item.link.name)) log.debug('New projector list - item: {ip} {name}'.format(ip=item.link.ip, name=item.link.name))
@ -535,9 +535,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
:param opt: Needed by PyQt5 :param opt: Needed by PyQt5
""" """
try: try:
ip = opt.link.ip opt.link.disconnect_from_host()
projector = opt
projector.link.disconnect_from_host()
except AttributeError: except AttributeError:
for list_item in self.projector_list_widget.selectedItems(): for list_item in self.projector_list_widget.selectedItems():
if list_item is None: if list_item is None:
@ -570,9 +568,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
:param opt: Needed by PyQt5 :param opt: Needed by PyQt5
""" """
try: try:
ip = opt.link.ip opt.link.set_power_off()
projector = opt
projector.link.set_power_off()
except AttributeError: except AttributeError:
for list_item in self.projector_list_widget.selectedItems(): for list_item in self.projector_list_widget.selectedItems():
if list_item is None: if list_item is None:
@ -590,9 +586,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
:param opt: Needed by PyQt5 :param opt: Needed by PyQt5
""" """
try: try:
ip = opt.link.ip opt.link.set_power_on()
projector = opt
projector.link.set_power_on()
except AttributeError: except AttributeError:
for list_item in self.projector_list_widget.selectedItems(): for list_item in self.projector_list_widget.selectedItems():
if list_item is None: if list_item is None:
@ -610,9 +604,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
:param opt: Needed by PyQt5 :param opt: Needed by PyQt5
""" """
try: try:
ip = opt.link.ip opt.link.set_shutter_open()
projector = opt
projector.link.set_shutter_open()
except AttributeError: except AttributeError:
for list_item in self.projector_list_widget.selectedItems(): for list_item in self.projector_list_widget.selectedItems():
if list_item is None: if list_item is None:
@ -659,19 +651,34 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
data=translate('OpenLP.ProjectorManager', 'Closed') data=translate('OpenLP.ProjectorManager', 'Closed')
if projector.link.shutter if projector.link.shutter
else translate('OpenLP', 'Open')) else translate('OpenLP', 'Open'))
message = '%s<b>%s</b>: %s<br />' % (message, message = '{msg}<b>{source}</b>: {selected}<br />'.format(msg=message,
translate('OpenLP.ProjectorManager', 'Current source input is'), source=translate('OpenLP.ProjectorManager',
projector.link.source) 'Current source input is'),
selected=projector.link.source)
if projector.link.pjlink_class == '2':
# Information only available for PJLink Class 2 projectors
message += '<b>{title}</b>: {data}<br /><br />'.format(title=translate('OpenLP.ProjectorManager',
'Serial Number'),
data=projector.serial_no)
message += '<b>{title}</b>: {data}<br /><br />'.format(title=translate('OpenLP.ProjectorManager',
'Software Version'),
data=projector.sw_version)
message += '<b>{title}</b>: {data}<br /><br />'.format(title=translate('OpenLP.ProjectorManager',
'Lamp type'),
data=projector.model_lamp)
message += '<b>{title}</b>: {data}<br /><br />'.format(title=translate('OpenLP.ProjectorManager',
'Filter type'),
data=projector.model_filter)
count = 1 count = 1
for item in projector.link.lamp: for item in projector.link.lamp:
message += '<b>{title} {count}</b> {status} '.format(title=translate('OpenLP.ProjectorManager', message += '<b>{title} {count}</b> {status} '.format(title=translate('OpenLP.ProjectorManager',
'Lamp'), 'Lamp'),
count=count, count=count,
status=translate('OpenLP.ProjectorManager', status=translate('OpenLP.ProjectorManager',
' is on') 'ON')
if item['On'] if item['On']
else translate('OpenLP.ProjectorManager', else translate('OpenLP.ProjectorManager',
'is off')) 'OFF'))
message += '<b>{title}</b>: {hours}<br />'.format(title=translate('OpenLP.ProjectorManager', 'Hours'), message += '<b>{title}</b>: {hours}<br />'.format(title=translate('OpenLP.ProjectorManager', 'Hours'),
hours=item['Hours']) hours=item['Hours'])
@ -973,7 +980,7 @@ class ProjectorItem(QtCore.QObject):
self.poll_time = None self.poll_time = None
self.socket_timeout = None self.socket_timeout = None
self.status = S_NOT_CONNECTED self.status = S_NOT_CONNECTED
super(ProjectorItem, self).__init__() super().__init__()
def not_implemented(function): def not_implemented(function):

View File

@ -233,7 +233,8 @@ class SourceSelectTabs(QtWidgets.QDialog):
:param projectordb: ProjectorDB session to use :param projectordb: ProjectorDB session to use
""" """
log.debug('Initializing SourceSelectTabs()') log.debug('Initializing SourceSelectTabs()')
super(SourceSelectTabs, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) super(SourceSelectTabs, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.setMinimumWidth(350) self.setMinimumWidth(350)
self.projectordb = projectordb self.projectordb = projectordb
self.edit = edit self.edit = edit
@ -388,12 +389,13 @@ class SourceSelectSingle(QtWidgets.QDialog):
""" """
log.debug('Initializing SourceSelectSingle()') log.debug('Initializing SourceSelectSingle()')
self.projectordb = projectordb self.projectordb = projectordb
super(SourceSelectSingle, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) super(SourceSelectSingle, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.edit = edit self.edit = edit
if self.edit: if self.edit:
title = translate('OpenLP.SourceSelectForm', 'Edit Projector Source Text') self.title = translate('OpenLP.SourceSelectForm', 'Edit Projector Source Text')
else: else:
title = translate('OpenLP.SourceSelectForm', 'Select Projector Source') self.title = translate('OpenLP.SourceSelectForm', 'Select Projector Source')
self.setObjectName('source_select_single') self.setObjectName('source_select_single')
self.setWindowIcon(build_icon(':/icon/openlp-log.svg')) self.setWindowIcon(build_icon(':/icon/openlp-log.svg'))
self.setModal(True) self.setModal(True)

View File

@ -37,8 +37,8 @@ class ServiceItemEditForm(QtWidgets.QDialog, Ui_ServiceItemEditDialog, RegistryP
""" """
Constructor Constructor
""" """
super(ServiceItemEditForm, self).__init__(Registry().get('main_window'), super(ServiceItemEditForm, self).__init__(Registry().get('main_window'), QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
self.setupUi(self) self.setupUi(self)
self.item_list = [] self.item_list = []
self.list_widget.currentRowChanged.connect(self.on_current_row_changed) self.list_widget.currentRowChanged.connect(self.on_current_row_changed)

View File

@ -35,11 +35,13 @@ from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, ThemeLevel, OpenLPMixin, \ from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, ThemeLevel, OpenLPMixin, \
RegistryMixin, check_directory_exists, UiStrings, translate, split_filename, delete_file RegistryMixin, check_directory_exists, UiStrings, translate, split_filename, delete_file
from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.actions import ActionList, CategoryOrder
from openlp.core.common.languagemanager import format_time
from openlp.core.common.path import Path, path_to_str, str_to_path
from openlp.core.lib import ServiceItem, ItemCapabilities, PluginStatus, build_icon from openlp.core.lib import ServiceItem, ItemCapabilities, PluginStatus, build_icon
from openlp.core.lib.ui import critical_error_message_box, create_widget_action, find_and_set_in_combo_box from openlp.core.lib.ui import critical_error_message_box, create_widget_action, find_and_set_in_combo_box
from openlp.core.ui import ServiceNoteForm, ServiceItemEditForm, StartTimeForm from openlp.core.ui import ServiceNoteForm, ServiceItemEditForm, StartTimeForm
from openlp.core.ui.lib import OpenLPToolbar from openlp.core.ui.lib import OpenLPToolbar
from openlp.core.common.languagemanager import format_time from openlp.core.ui.lib.filedialog import FileDialog
class ServiceManagerList(QtWidgets.QTreeWidget): class ServiceManagerList(QtWidgets.QTreeWidget):
@ -66,6 +68,12 @@ class ServiceManagerList(QtWidgets.QTreeWidget):
elif event.key() == QtCore.Qt.Key_Down: elif event.key() == QtCore.Qt.Key_Down:
self.service_manager.on_move_selection_down() self.service_manager.on_move_selection_down()
event.accept() event.accept()
elif event.key() == QtCore.Qt.Key_Right:
self.service_manager.on_expand_selection()
event.accept()
elif event.key() == QtCore.Qt.Key_Left:
self.service_manager.on_collapse_selection()
event.accept()
elif event.key() == QtCore.Qt.Key_Delete: elif event.key() == QtCore.Qt.Key_Delete:
self.service_manager.on_delete_from_service() self.service_manager.on_delete_from_service()
event.accept() event.accept()
@ -217,7 +225,7 @@ class Ui_ServiceManager(object):
self.service_manager_list.itemExpanded.connect(self.expanded) self.service_manager_list.itemExpanded.connect(self.expanded)
# Last little bits of setting up # Last little bits of setting up
self.service_theme = Settings().value(self.main_window.service_manager_settings_section + '/service theme') self.service_theme = Settings().value(self.main_window.service_manager_settings_section + '/service theme')
self.service_path = AppLocation.get_section_data_path('servicemanager') self.service_path = str(AppLocation.get_section_data_path('servicemanager'))
# build the drag and drop context menu # build the drag and drop context menu
self.dnd_menu = QtWidgets.QMenu() self.dnd_menu = QtWidgets.QMenu()
self.new_action = self.dnd_menu.addAction(translate('OpenLP.ServiceManager', '&Add New Item')) self.new_action = self.dnd_menu.addAction(translate('OpenLP.ServiceManager', '&Add New Item'))
@ -366,7 +374,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
""" """
self._file_name = str(file_name) self._file_name = str(file_name)
self.main_window.set_service_modified(self.is_modified(), self.short_file_name()) self.main_window.set_service_modified(self.is_modified(), self.short_file_name())
Settings().setValue('servicemanager/last file', file_name) Settings().setValue('servicemanager/last file', Path(file_name))
self._save_lite = self._file_name.endswith('.oszl') self._save_lite = self._file_name.endswith('.oszl')
def file_name(self): def file_name(self):
@ -428,18 +436,17 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
elif result == QtWidgets.QMessageBox.Save: elif result == QtWidgets.QMessageBox.Save:
self.decide_save_method() self.decide_save_method()
if not load_file: if not load_file:
file_name, filter_used = QtWidgets.QFileDialog.getOpenFileName( file_path, filter_used = FileDialog.getOpenFileName(
self.main_window, self.main_window,
translate('OpenLP.ServiceManager', 'Open File'), translate('OpenLP.ServiceManager', 'Open File'),
Settings().value(self.main_window.service_manager_settings_section + '/last directory'), Settings().value(self.main_window.service_manager_settings_section + '/last directory'),
translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz *.oszl)')) translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz *.oszl)'))
if not file_name: if not file_path:
return False return False
else: else:
file_name = load_file file_path = str_to_path(load_file)
Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', file_path.parent)
split_filename(file_name)[0]) self.load_file(str(file_path))
self.load_file(file_name)
def save_modified_service(self): def save_modified_service(self):
""" """
@ -470,7 +477,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
self.set_file_name('') self.set_file_name('')
self.service_id += 1 self.service_id += 1
self.set_modified(False) self.set_modified(False)
Settings().setValue('servicemanager/last file', '') Settings().setValue('servicemanager/last file', None)
self.plugin_manager.new_service_created() self.plugin_manager.new_service_created()
def create_basic_service(self): def create_basic_service(self):
@ -506,7 +513,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
base_name = os.path.splitext(file_name)[0] base_name = os.path.splitext(file_name)[0]
service_file_name = '{name}.osj'.format(name=base_name) service_file_name = '{name}.osj'.format(name=base_name)
self.log_debug('ServiceManager.save_file - {name}'.format(name=path_file_name)) self.log_debug('ServiceManager.save_file - {name}'.format(name=path_file_name))
Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', path) Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', Path(path))
service = self.create_basic_service() service = self.create_basic_service()
write_list = [] write_list = []
missing_list = [] missing_list = []
@ -581,7 +588,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
audio_from = os.path.join(self.service_path, audio_from) audio_from = os.path.join(self.service_path, audio_from)
save_file = os.path.join(self.service_path, audio_to) save_file = os.path.join(self.service_path, audio_to)
save_path = os.path.split(save_file)[0] save_path = os.path.split(save_file)[0]
check_directory_exists(save_path) check_directory_exists(Path(save_path))
if not os.path.exists(save_file): if not os.path.exists(save_file):
shutil.copy(audio_from, save_file) shutil.copy(audio_from, save_file)
zip_file.write(audio_from, audio_to) zip_file.write(audio_from, audio_to)
@ -608,7 +615,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
success = False success = False
self.main_window.add_recent_file(path_file_name) self.main_window.add_recent_file(path_file_name)
self.set_modified(False) self.set_modified(False)
delete_file(temp_file_name) delete_file(Path(temp_file_name))
return success return success
def save_local_file(self): def save_local_file(self):
@ -627,7 +634,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
base_name = os.path.splitext(file_name)[0] base_name = os.path.splitext(file_name)[0]
service_file_name = '{name}.osj'.format(name=base_name) service_file_name = '{name}.osj'.format(name=base_name)
self.log_debug('ServiceManager.save_file - {name}'.format(name=path_file_name)) self.log_debug('ServiceManager.save_file - {name}'.format(name=path_file_name))
Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', path) Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', Path(path))
service = self.create_basic_service() service = self.create_basic_service()
self.application.set_busy_cursor() self.application.set_busy_cursor()
# Number of items + 1 to zip it # Number of items + 1 to zip it
@ -663,7 +670,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
return self.save_file_as() return self.save_file_as()
self.main_window.add_recent_file(path_file_name) self.main_window.add_recent_file(path_file_name)
self.set_modified(False) self.set_modified(False)
delete_file(temp_file_name) delete_file(Path(temp_file_name))
return success return success
def save_file_as(self, field=None): def save_file_as(self, field=None):
@ -688,7 +695,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
default_file_name = format_time(default_pattern, local_time) default_file_name = format_time(default_pattern, local_time)
else: else:
default_file_name = '' default_file_name = ''
directory = Settings().value(self.main_window.service_manager_settings_section + '/last directory') directory = path_to_str(Settings().value(self.main_window.service_manager_settings_section + '/last directory'))
path = os.path.join(directory, default_file_name) path = os.path.join(directory, default_file_name)
# SaveAs from osz to oszl is not valid as the files will be deleted on exit which is not sensible or usable in # SaveAs from osz to oszl is not valid as the files will be deleted on exit which is not sensible or usable in
# the long term. # the long term.
@ -768,10 +775,10 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
self.set_file_name(file_name) self.set_file_name(file_name)
self.main_window.display_progress_bar(len(items)) self.main_window.display_progress_bar(len(items))
self.process_service_items(items) self.process_service_items(items)
delete_file(p_file) delete_file(Path(p_file))
self.main_window.add_recent_file(file_name) self.main_window.add_recent_file(file_name)
self.set_modified(False) self.set_modified(False)
Settings().setValue('servicemanager/last file', file_name) Settings().setValue('servicemanager/last file', Path(file_name))
else: else:
critical_error_message_box(message=translate('OpenLP.ServiceManager', 'File is not a valid service.')) critical_error_message_box(message=translate('OpenLP.ServiceManager', 'File is not a valid service.'))
self.log_error('File contains no service data') self.log_error('File contains no service data')
@ -836,7 +843,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
Load the last service item from the service manager when the service was last closed. Can be blank if there was Load the last service item from the service manager when the service was last closed. Can be blank if there was
no service present. no service present.
""" """
file_name = Settings().value('servicemanager/last file') file_name = str_to_path(Settings().value('servicemanager/last file'))
if file_name: if file_name:
self.load_file(file_name) self.load_file(file_name)
@ -1119,6 +1126,35 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
return return
self.service_manager_list.setCurrentItem(item_after) self.service_manager_list.setCurrentItem(item_after)
def on_expand_selection(self):
"""
Expands cursor selection on the window. Called by the right arrow
"""
item = self.service_manager_list.currentItem()
# Since we only have 2 levels we find them by checking for children
if item.childCount():
if not self.service_manager_list.isExpanded(self.service_manager_list.currentIndex()):
self.service_manager_list.expandItem(item)
self.service_manager.expanded(item)
# If not expanded, Expand it
self.service_manager_list.setCurrentItem(self.service_manager_list.itemBelow(item))
# Then move selection down to child whether it needed to be expanded or not
def on_collapse_selection(self):
"""
Collapses cursor selection on the window Called by the left arrow
"""
item = self.service_manager_list.currentItem()
# Since we only have 2 levels we find them by checking for children
if item.childCount():
if self.service_manager_list.isExpanded(self.service_manager_list.currentIndex()):
self.service_manager_list.collapseItem(item)
self.service_manager.collapsed(item)
else: # If selection is lower level
self.service_manager_list.collapseItem(item.parent())
self.service_manager.collapsed(item.parent())
self.service_manager_list.setCurrentItem(item.parent())
def on_collapse_all(self, field=None): def on_collapse_all(self, field=None):
""" """
Collapse all the service items. Collapse all the service items.
@ -1308,7 +1344,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
Empties the service_path of temporary files on system exit. Empties the service_path of temporary files on system exit.
""" """
for file_name in os.listdir(self.service_path): for file_name in os.listdir(self.service_path):
file_path = os.path.join(self.service_path, file_name) file_path = Path(self.service_path, file_name)
delete_file(file_path) delete_file(file_path)
if os.path.exists(os.path.join(self.service_path, 'audio')): if os.path.exists(os.path.join(self.service_path, 'audio')):
shutil.rmtree(os.path.join(self.service_path, 'audio'), True) shutil.rmtree(os.path.join(self.service_path, 'audio'), True)

View File

@ -37,8 +37,8 @@ class ServiceNoteForm(QtWidgets.QDialog, RegistryProperties):
""" """
Constructor Constructor
""" """
super(ServiceNoteForm, self).__init__(Registry().get('main_window'), super(ServiceNoteForm, self).__init__(Registry().get('main_window'), QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
self.setupUi() self.setupUi()
self.retranslateUi() self.retranslateUi()

View File

@ -26,6 +26,7 @@ import logging
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.api import ApiTab
from openlp.core.common import Registry, RegistryProperties from openlp.core.common import Registry, RegistryProperties
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
from openlp.core.ui import AdvancedTab, GeneralTab, ThemesTab from openlp.core.ui import AdvancedTab, GeneralTab, ThemesTab
@ -46,7 +47,8 @@ class SettingsForm(QtWidgets.QDialog, Ui_SettingsDialog, RegistryProperties):
""" """
Registry().register('settings_form', self) Registry().register('settings_form', self)
Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up) Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up)
super(SettingsForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) super(SettingsForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.processes = [] self.processes = []
self.setupUi(self) self.setupUi(self)
self.setting_list_widget.currentRowChanged.connect(self.list_item_changed) self.setting_list_widget.currentRowChanged.connect(self.list_item_changed)
@ -55,12 +57,13 @@ class SettingsForm(QtWidgets.QDialog, Ui_SettingsDialog, RegistryProperties):
self.projector_tab = None self.projector_tab = None
self.advanced_tab = None self.advanced_tab = None
self.player_tab = None self.player_tab = None
self.api_tab = None
def exec(self): def exec(self):
""" """
Execute the form Execute the form
""" """
# load all the # load all the widgets
self.setting_list_widget.blockSignals(True) self.setting_list_widget.blockSignals(True)
self.setting_list_widget.clear() self.setting_list_widget.clear()
while self.stacked_layout.count(): while self.stacked_layout.count():
@ -71,6 +74,7 @@ class SettingsForm(QtWidgets.QDialog, Ui_SettingsDialog, RegistryProperties):
self.insert_tab(self.advanced_tab) self.insert_tab(self.advanced_tab)
self.insert_tab(self.player_tab) self.insert_tab(self.player_tab)
self.insert_tab(self.projector_tab) self.insert_tab(self.projector_tab)
self.insert_tab(self.api_tab)
for plugin in self.plugin_manager.plugins: for plugin in self.plugin_manager.plugins:
if plugin.settings_tab: if plugin.settings_tab:
self.insert_tab(plugin.settings_tab, plugin.is_active()) self.insert_tab(plugin.settings_tab, plugin.is_active())
@ -92,6 +96,7 @@ class SettingsForm(QtWidgets.QDialog, Ui_SettingsDialog, RegistryProperties):
list_item = QtWidgets.QListWidgetItem(build_icon(tab_widget.icon_path), tab_widget.tab_title_visible) list_item = QtWidgets.QListWidgetItem(build_icon(tab_widget.icon_path), tab_widget.tab_title_visible)
list_item.setData(QtCore.Qt.UserRole, tab_widget.tab_title) list_item.setData(QtCore.Qt.UserRole, tab_widget.tab_title)
self.setting_list_widget.addItem(list_item) self.setting_list_widget.addItem(list_item)
tab_widget.load()
def accept(self): def accept(self):
""" """
@ -153,10 +158,13 @@ class SettingsForm(QtWidgets.QDialog, Ui_SettingsDialog, RegistryProperties):
self.advanced_tab = AdvancedTab(self) self.advanced_tab = AdvancedTab(self)
# Advanced tab # Advanced tab
self.player_tab = PlayerTab(self) self.player_tab = PlayerTab(self)
# Api tab
self.api_tab = ApiTab(self)
self.general_tab.post_set_up() self.general_tab.post_set_up()
self.themes_tab.post_set_up() self.themes_tab.post_set_up()
self.advanced_tab.post_set_up() self.advanced_tab.post_set_up()
self.player_tab.post_set_up() self.player_tab.post_set_up()
self.api_tab.post_set_up()
for plugin in self.plugin_manager.plugins: for plugin in self.plugin_manager.plugins:
if plugin.settings_tab: if plugin.settings_tab:
plugin.settings_tab.post_set_up() plugin.settings_tab.post_set_up()

View File

@ -44,7 +44,8 @@ class ShortcutListForm(QtWidgets.QDialog, Ui_ShortcutListDialog, RegistryPropert
""" """
Constructor Constructor
""" """
super(ShortcutListForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) super(ShortcutListForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.setupUi(self) self.setupUi(self)
self.changed_actions = {} self.changed_actions = {}
self.action_list = ActionList.get_instance() self.action_list = ActionList.get_instance()
@ -279,9 +280,7 @@ class ShortcutListForm(QtWidgets.QDialog, Ui_ShortcutListDialog, RegistryPropert
return return
if QtWidgets.QMessageBox.question(self, translate('OpenLP.ShortcutListDialog', 'Restore Default Shortcuts'), if QtWidgets.QMessageBox.question(self, translate('OpenLP.ShortcutListDialog', 'Restore Default Shortcuts'),
translate('OpenLP.ShortcutListDialog', 'Do you want to restore all ' translate('OpenLP.ShortcutListDialog', 'Do you want to restore all '
'shortcuts to their defaults?'), 'shortcuts to their defaults?')
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
QtWidgets.QMessageBox.No)
) == QtWidgets.QMessageBox.No: ) == QtWidgets.QMessageBox.No:
return return
self._adjust_button(self.primary_push_button, False, text='') self._adjust_button(self.primary_push_button, False, text='')

View File

@ -439,6 +439,10 @@ class SlideController(DisplayController, RegistryProperties):
# NOTE: {t} used to keep line length < maxline # NOTE: {t} used to keep line length < maxline
getattr(self, getattr(self,
'slidecontroller_{t}_previous'.format(t=self.type_prefix)).connect(self.on_slide_selected_previous) 'slidecontroller_{t}_previous'.format(t=self.type_prefix)).connect(self.on_slide_selected_previous)
if self.is_live:
getattr(self, 'mediacontroller_live_play').connect(self.media_controller.on_media_play)
getattr(self, 'mediacontroller_live_pause').connect(self.media_controller.on_media_pause)
getattr(self, 'mediacontroller_live_stop').connect(self.media_controller.on_media_stop)
def _slide_shortcut_activated(self): def _slide_shortcut_activated(self):
""" """
@ -1530,6 +1534,9 @@ class LiveController(RegistryMixin, OpenLPMixin, SlideController):
slidecontroller_live_next = QtCore.pyqtSignal() slidecontroller_live_next = QtCore.pyqtSignal()
slidecontroller_live_previous = QtCore.pyqtSignal() slidecontroller_live_previous = QtCore.pyqtSignal()
slidecontroller_toggle_display = QtCore.pyqtSignal(str) slidecontroller_toggle_display = QtCore.pyqtSignal(str)
mediacontroller_live_play = QtCore.pyqtSignal()
mediacontroller_live_pause = QtCore.pyqtSignal()
mediacontroller_live_stop = QtCore.pyqtSignal()
def __init__(self, parent): def __init__(self, parent):
""" """

View File

@ -38,8 +38,8 @@ class StartTimeForm(QtWidgets.QDialog, Ui_StartTimeDialog, RegistryProperties):
""" """
Constructor Constructor
""" """
super(StartTimeForm, self).__init__(Registry().get('main_window'), super(StartTimeForm, self).__init__(Registry().get('main_window'), QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
self.setupUi(self) self.setupUi(self)
def exec(self): def exec(self):

View File

@ -28,6 +28,7 @@ import os
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import Registry, RegistryProperties, UiStrings, translate, get_images_filter, is_not_image_file from openlp.core.common import Registry, RegistryProperties, UiStrings, translate, get_images_filter, is_not_image_file
from openlp.core.common.path import Path, path_to_str, str_to_path
from openlp.core.lib.theme import BackgroundType, BackgroundGradientType from openlp.core.lib.theme import BackgroundType, BackgroundGradientType
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui import ThemeLayoutForm from openlp.core.ui import ThemeLayoutForm
@ -187,7 +188,8 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
""" """
background_image = BackgroundType.to_string(BackgroundType.Image) background_image = BackgroundType.to_string(BackgroundType.Image)
if self.page(self.currentId()) == self.background_page and \ if self.page(self.currentId()) == self.background_page and \
self.theme.background_type == background_image and is_not_image_file(self.theme.background_filename): self.theme.background_type == background_image and \
is_not_image_file(Path(self.theme.background_filename)):
QtWidgets.QMessageBox.critical(self, translate('OpenLP.ThemeWizard', 'Background Image Empty'), QtWidgets.QMessageBox.critical(self, translate('OpenLP.ThemeWizard', 'Background Image Empty'),
translate('OpenLP.ThemeWizard', 'You have not selected a ' translate('OpenLP.ThemeWizard', 'You have not selected a '
'background image. Please select one before continuing.')) 'background image. Please select one before continuing.'))
@ -316,11 +318,11 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
self.setField('background_type', 1) self.setField('background_type', 1)
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image): elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image):
self.image_color_button.color = self.theme.background_border_color self.image_color_button.color = self.theme.background_border_color
self.image_path_edit.path = self.theme.background_filename self.image_path_edit.path = str_to_path(self.theme.background_filename)
self.setField('background_type', 2) self.setField('background_type', 2)
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Video): elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
self.video_color_button.color = self.theme.background_border_color self.video_color_button.color = self.theme.background_border_color
self.video_path_edit.path = self.theme.background_filename self.video_path_edit.path = str_to_path(self.theme.background_filename)
self.setField('background_type', 4) self.setField('background_type', 4)
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Transparent): elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Transparent):
self.setField('background_type', 3) self.setField('background_type', 3)
@ -448,18 +450,18 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
""" """
self.theme.background_end_color = color self.theme.background_end_color = color
def on_image_path_edit_path_changed(self, filename): def on_image_path_edit_path_changed(self, file_path):
""" """
Background Image button pushed. Background Image button pushed.
""" """
self.theme.background_filename = filename self.theme.background_filename = path_to_str(file_path)
self.set_background_page_values() self.set_background_page_values()
def on_video_path_edit_path_changed(self, filename): def on_video_path_edit_path_changed(self, file_path):
""" """
Background video button pushed. Background video button pushed.
""" """
self.theme.background_filename = filename self.theme.background_filename = path_to_str(file_path)
self.set_background_page_values() self.set_background_page_values()
def on_main_color_changed(self, color): def on_main_color_changed(self, color):

View File

@ -22,7 +22,6 @@
""" """
The Theme Manager manages adding, deleteing and modifying of themes. The Theme Manager manages adding, deleteing and modifying of themes.
""" """
import json
import os import os
import zipfile import zipfile
import shutil import shutil
@ -31,13 +30,15 @@ from xml.etree.ElementTree import ElementTree, XML
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, OpenLPMixin, RegistryMixin, \ from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, OpenLPMixin, RegistryMixin, \
check_directory_exists, UiStrings, translate, is_win, get_filesystem_encoding, delete_file UiStrings, check_directory_exists, translate, is_win, get_filesystem_encoding, delete_file
from openlp.core.lib import FileDialog, ImageSource, ValidationError, get_text_file_string, build_icon, \ from openlp.core.common.path import Path, path_to_str, str_to_path
from openlp.core.lib import ImageSource, ValidationError, get_text_file_string, build_icon, \
check_item_selected, create_thumb, validate_thumb check_item_selected, create_thumb, validate_thumb
from openlp.core.lib.theme import Theme, BackgroundType from openlp.core.lib.theme import Theme, BackgroundType
from openlp.core.lib.ui import critical_error_message_box, create_widget_action from openlp.core.lib.ui import critical_error_message_box, create_widget_action
from openlp.core.ui import FileRenameForm, ThemeForm from openlp.core.ui import FileRenameForm, ThemeForm
from openlp.core.ui.lib import OpenLPToolbar from openlp.core.ui.lib import OpenLPToolbar
from openlp.core.ui.lib.filedialog import FileDialog
from openlp.core.common.languagemanager import get_locale_key from openlp.core.common.languagemanager import get_locale_key
@ -159,10 +160,10 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
""" """
Set up the theme path variables Set up the theme path variables
""" """
self.path = AppLocation.get_section_data_path(self.settings_section) self.path = str(AppLocation.get_section_data_path(self.settings_section))
check_directory_exists(self.path) check_directory_exists(Path(self.path))
self.thumb_path = os.path.join(self.path, 'thumbnails') self.thumb_path = os.path.join(self.path, 'thumbnails')
check_directory_exists(self.thumb_path) check_directory_exists(Path(self.thumb_path))
def check_list_state(self, item, field=None): def check_list_state(self, item, field=None):
""" """
@ -257,10 +258,9 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
Renames an existing theme to a new name Renames an existing theme to a new name
:param field: :param field:
""" """
# TODO: Check for delayed format() conversions
if self._validate_theme_action(translate('OpenLP.ThemeManager', 'You must select a theme to rename.'), if self._validate_theme_action(translate('OpenLP.ThemeManager', 'You must select a theme to rename.'),
translate('OpenLP.ThemeManager', 'Rename Confirmation'), translate('OpenLP.ThemeManager', 'Rename Confirmation'),
translate('OpenLP.ThemeManager', 'Rename %s theme?'), False, False): translate('OpenLP.ThemeManager', 'Rename {theme_name} theme?'), False, False):
item = self.theme_list_widget.currentItem() item = self.theme_list_widget.currentItem()
old_theme_name = item.data(QtCore.Qt.UserRole) old_theme_name = item.data(QtCore.Qt.UserRole)
self.file_rename_form.file_name_edit.setText(old_theme_name) self.file_rename_form.file_name_edit.setText(old_theme_name)
@ -334,10 +334,9 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
Delete a theme triggered by the UI. Delete a theme triggered by the UI.
:param field: :param field:
""" """
# TODO: Verify delayed format() conversions
if self._validate_theme_action(translate('OpenLP.ThemeManager', 'You must select a theme to delete.'), if self._validate_theme_action(translate('OpenLP.ThemeManager', 'You must select a theme to delete.'),
translate('OpenLP.ThemeManager', 'Delete Confirmation'), translate('OpenLP.ThemeManager', 'Delete Confirmation'),
translate('OpenLP.ThemeManager', 'Delete %s theme?')): translate('OpenLP.ThemeManager', 'Delete {theme_name} theme?')):
item = self.theme_list_widget.currentItem() item = self.theme_list_widget.currentItem()
theme = item.text() theme = item.text()
row = self.theme_list_widget.row(item) row = self.theme_list_widget.row(item)
@ -356,8 +355,8 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
""" """
self.theme_list.remove(theme) self.theme_list.remove(theme)
thumb = '{name}.png'.format(name=theme) thumb = '{name}.png'.format(name=theme)
delete_file(os.path.join(self.path, thumb)) delete_file(Path(self.path, thumb))
delete_file(os.path.join(self.thumb_path, thumb)) delete_file(Path(self.thumb_path, thumb))
try: try:
# Windows is always unicode, so no need to encode filenames # Windows is always unicode, so no need to encode filenames
if is_win(): if is_win():
@ -379,16 +378,16 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.')) critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.'))
return return
theme = item.data(QtCore.Qt.UserRole) theme = item.data(QtCore.Qt.UserRole)
path, filter_used = \ export_path, filter_used = \
QtWidgets.QFileDialog.getSaveFileName(self.main_window, FileDialog.getSaveFileName(self.main_window,
translate('OpenLP.ThemeManager', 'Save Theme - ({name})'). translate('OpenLP.ThemeManager', 'Save Theme - ({name})').
format(name=theme), format(name=theme),
Settings().value(self.settings_section + '/last directory export'), Settings().value(self.settings_section + '/last directory export'),
translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)')) translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
self.application.set_busy_cursor() self.application.set_busy_cursor()
if path: if export_path:
Settings().setValue(self.settings_section + '/last directory export', path) Settings().setValue(self.settings_section + '/last directory export', export_path.parent)
if self._export_theme(path, theme): if self._export_theme(str(export_path), theme):
QtWidgets.QMessageBox.information(self, QtWidgets.QMessageBox.information(self,
translate('OpenLP.ThemeManager', 'Theme Exported'), translate('OpenLP.ThemeManager', 'Theme Exported'),
translate('OpenLP.ThemeManager', translate('OpenLP.ThemeManager',
@ -426,17 +425,18 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
those files. This process will only load version 2 themes. those files. This process will only load version 2 themes.
:param field: :param field:
""" """
files = FileDialog.getOpenFileNames(self, file_paths, selected_filter = FileDialog.getOpenFileNames(
translate('OpenLP.ThemeManager', 'Select Theme Import File'), self,
Settings().value(self.settings_section + '/last directory import'), translate('OpenLP.ThemeManager', 'Select Theme Import File'),
translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)')) Settings().value(self.settings_section + '/last directory import'),
self.log_info('New Themes {name}'.format(name=str(files))) translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
if not files: self.log_info('New Themes {file_paths}'.format(file_paths=file_paths))
if not file_paths:
return return
self.application.set_busy_cursor() self.application.set_busy_cursor()
for file_name in files: for file_path in file_paths:
Settings().setValue(self.settings_section + '/last directory import', str(file_name)) self.unzip_theme(path_to_str(file_path), self.path)
self.unzip_theme(file_name, self.path) Settings().setValue(self.settings_section + '/last directory import', file_path)
self.load_themes() self.load_themes()
self.application.set_normal_cursor() self.application.set_normal_cursor()
@ -447,9 +447,9 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
self.application.set_busy_cursor() self.application.set_busy_cursor()
files = AppLocation.get_files(self.settings_section, '.otz') files = AppLocation.get_files(self.settings_section, '.otz')
for theme_file in files: for theme_file in files:
theme_file = os.path.join(self.path, theme_file) theme_file = os.path.join(self.path, str(theme_file))
self.unzip_theme(theme_file, self.path) self.unzip_theme(theme_file, self.path)
delete_file(theme_file) delete_file(Path(theme_file))
files = AppLocation.get_files(self.settings_section, '.png') files = AppLocation.get_files(self.settings_section, '.png')
# No themes have been found so create one # No themes have been found so create one
if not files: if not files:
@ -472,6 +472,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
files.sort(key=lambda file_name: get_locale_key(str(file_name))) files.sort(key=lambda file_name: get_locale_key(str(file_name)))
# now process the file list of png files # now process the file list of png files
for name in files: for name in files:
name = str(name)
# check to see file is in theme root directory # check to see file is in theme root directory
theme = os.path.join(self.path, name) theme = os.path.join(self.path, name)
if os.path.exists(theme): if os.path.exists(theme):
@ -512,12 +513,12 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
:return: The theme object. :return: The theme object.
""" """
self.log_debug('get theme data for theme {name}'.format(name=theme_name)) self.log_debug('get theme data for theme {name}'.format(name=theme_name))
theme_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.json') theme_file_path = Path(self.path, str(theme_name), '{file_name}.json'.format(file_name=theme_name))
theme_data = get_text_file_string(theme_file) theme_data = get_text_file_string(theme_file_path)
jsn = True jsn = True
if not theme_data: if not theme_data:
theme_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.xml') theme_file_path = theme_file_path.with_suffix('.xml')
theme_data = get_text_file_string(theme_file) theme_data = get_text_file_string(theme_file_path)
jsn = False jsn = False
if not theme_data: if not theme_data:
self.log_debug('No theme data - using default theme') self.log_debug('No theme data - using default theme')
@ -539,9 +540,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
translate('OpenLP.ThemeManager', translate('OpenLP.ThemeManager',
'Theme {name} already exists. ' 'Theme {name} already exists. '
'Do you want to replace it?').format(name=theme_name), 'Do you want to replace it?').format(name=theme_name),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | defaultButton=QtWidgets.QMessageBox.No)
QtWidgets.QMessageBox.No),
QtWidgets.QMessageBox.No)
return ret == QtWidgets.QMessageBox.Yes return ret == QtWidgets.QMessageBox.Yes
def unzip_theme(self, file_name, directory): def unzip_theme(self, file_name, directory):
@ -592,7 +591,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
# is directory or preview file # is directory or preview file
continue continue
full_name = os.path.join(directory, out_name) full_name = os.path.join(directory, out_name)
check_directory_exists(os.path.dirname(full_name)) check_directory_exists(Path(os.path.dirname(full_name)))
if os.path.splitext(name)[1].lower() == '.xml' or os.path.splitext(name)[1].lower() == '.json': if os.path.splitext(name)[1].lower() == '.xml' or os.path.splitext(name)[1].lower() == '.json':
file_xml = str(theme_zip.read(name), 'utf-8') file_xml = str(theme_zip.read(name), 'utf-8')
out_file = open(full_name, 'w', encoding='utf-8') out_file = open(full_name, 'w', encoding='utf-8')
@ -670,10 +669,10 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
name = theme.theme_name name = theme.theme_name
theme_pretty = theme.export_theme() theme_pretty = theme.export_theme()
theme_dir = os.path.join(self.path, name) theme_dir = os.path.join(self.path, name)
check_directory_exists(theme_dir) check_directory_exists(Path(theme_dir))
theme_file = os.path.join(theme_dir, name + '.json') theme_file = os.path.join(theme_dir, name + '.json')
if self.old_background_image and image_to != self.old_background_image: if self.old_background_image and image_to != self.old_background_image:
delete_file(self.old_background_image) delete_file(Path(self.old_background_image))
out_file = None out_file = None
try: try:
out_file = open(theme_file, 'w', encoding='utf-8') out_file = open(theme_file, 'w', encoding='utf-8')
@ -785,9 +784,8 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
# confirm deletion # confirm deletion
if confirm: if confirm:
answer = QtWidgets.QMessageBox.question( answer = QtWidgets.QMessageBox.question(
self, confirm_title, confirm_text % theme, self, confirm_title, confirm_text.format(theme_name=theme),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No), defaultButton=QtWidgets.QMessageBox.No)
QtWidgets.QMessageBox.No)
if answer == QtWidgets.QMessageBox.No: if answer == QtWidgets.QMessageBox.No:
return False return False
# should be the same unless default # should be the same unless default

View File

@ -25,6 +25,7 @@ The Create/Edit theme wizard
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import UiStrings, translate, is_macosx from openlp.core.common import UiStrings, translate, is_macosx
from openlp.core.common.path import Path
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType
from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets

View File

@ -24,13 +24,15 @@ import logging
from PyQt5 import QtGui from PyQt5 import QtGui
from openlp.core.common import Settings, translate from openlp.core.api.http import register_endpoint
from openlp.core.common import Settings, UiStrings, translate
from openlp.core.common.actions import ActionList from openlp.core.common.actions import ActionList
from openlp.core.lib import Plugin, StringContent, build_icon from openlp.core.lib import Plugin, StringContent, build_icon
from openlp.core.lib.db import Manager from openlp.core.lib.db import Manager
from openlp.core.lib.theme import VerticalType from openlp.core.lib.theme import VerticalType
from openlp.core.lib.ui import create_action, UiStrings from openlp.core.lib.ui import create_action
from openlp.core.ui import AlertLocation from openlp.core.ui import AlertLocation
from openlp.plugins.alerts.endpoint import api_alerts_endpoint, alerts_endpoint
from openlp.plugins.alerts.forms import AlertForm from openlp.plugins.alerts.forms import AlertForm
from openlp.plugins.alerts.lib import AlertsManager, AlertsTab from openlp.plugins.alerts.lib import AlertsManager, AlertsTab
from openlp.plugins.alerts.lib.db import init_schema from openlp.plugins.alerts.lib.db import init_schema
@ -88,21 +90,20 @@ JAVASCRIPT = """
} }
} }
""" """
# TODO: Verify format() with variable templates
CSS = """ CSS = """
#alert { #alert {{
position: absolute; position: absolute;
left: 0px; left: 0px;
top: 0px; top: 0px;
z-index: 10; z-index: 10;
width: 100%%; width: 100%;
vertical-align: %s; vertical-align: {vertical_align};
font-family: %s; font-family: {font_family};
font-size: %spt; font-size: {font_size:d}pt;
color: %s; color: {color};
background-color: %s; background-color: {background_color};
word-wrap: break-word; word-wrap: break-word;
} }}
""" """
HTML = """ HTML = """
@ -141,6 +142,8 @@ class AlertsPlugin(Plugin):
AlertsManager(self) AlertsManager(self)
self.manager = Manager('alerts', init_schema) self.manager = Manager('alerts', init_schema)
self.alert_form = AlertForm(self) self.alert_form = AlertForm(self)
register_endpoint(alerts_endpoint)
register_endpoint(api_alerts_endpoint)
def add_tools_menu_item(self, tools_menu): def add_tools_menu_item(self, tools_menu):
""" """
@ -228,8 +231,11 @@ class AlertsPlugin(Plugin):
Add CSS to the main display. Add CSS to the main display.
""" """
align = VerticalType.Names[self.settings_tab.location] align = VerticalType.Names[self.settings_tab.location]
return CSS % (align, self.settings_tab.font_face, self.settings_tab.font_size, self.settings_tab.font_color, return CSS.format(vertical_align=align,
self.settings_tab.background_color) font_family=self.settings_tab.font_face,
font_size=self.settings_tab.font_size,
color=self.settings_tab.font_color,
background_color=self.settings_tab.background_color)
@staticmethod @staticmethod
def get_display_html(): def get_display_html():

View File

@ -0,0 +1,60 @@
# -*- 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
import json
import urllib
from urllib.parse import urlparse
from openlp.core.api.http.endpoint import Endpoint
from openlp.core.api.http import requires_auth
from openlp.core.common import Registry
from openlp.core.lib import PluginStatus
log = logging.getLogger(__name__)
alerts_endpoint = Endpoint('alert')
api_alerts_endpoint = Endpoint('api')
@alerts_endpoint.route('')
@api_alerts_endpoint.route('alert')
@requires_auth
def alert(request):
"""
Handles requests for setting service items in the service manager
:param request: The http request object.
"""
plugin = Registry().get('plugin_manager').get_plugin_by_name("alerts")
if plugin.status == PluginStatus.Active:
try:
json_data = request.GET.get('data')
text = json.loads(json_data)['request']['text']
except KeyError:
log.error("Endpoint alerts request text not found")
text = urllib.parse.unquote(text)
Registry().get('alerts_manager').alerts_text.emit([text])
success = True
else:
success = False
return {'results': {'success': success}}

View File

@ -36,8 +36,8 @@ class AlertForm(QtWidgets.QDialog, Ui_AlertDialog):
""" """
Initialise the alert form Initialise the alert form
""" """
super(AlertForm, self).__init__(Registry().get('main_window'), super(AlertForm, self).__init__(Registry().get('main_window'), QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
self.manager = plugin.manager self.manager = plugin.manager
self.plugin = plugin self.plugin = plugin
self.item_id = None self.item_id = None
@ -180,9 +180,7 @@ class AlertForm(QtWidgets.QDialog, Ui_AlertDialog):
translate('AlertsPlugin.AlertForm', 'No Parameter Found'), translate('AlertsPlugin.AlertForm', 'No Parameter Found'),
translate('AlertsPlugin.AlertForm', translate('AlertsPlugin.AlertForm',
'You have not entered a parameter to be replaced.\n' 'You have not entered a parameter to be replaced.\n'
'Do you want to continue anyway?'), 'Do you want to continue anyway?')
QtWidgets.QMessageBox.StandardButtons(
QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Yes)
) == QtWidgets.QMessageBox.No: ) == QtWidgets.QMessageBox.No:
self.parameter_edit.setFocus() self.parameter_edit.setFocus()
return False return False
@ -193,9 +191,7 @@ class AlertForm(QtWidgets.QDialog, Ui_AlertDialog):
translate('AlertsPlugin.AlertForm', 'No Placeholder Found'), translate('AlertsPlugin.AlertForm', 'No Placeholder Found'),
translate('AlertsPlugin.AlertForm', translate('AlertsPlugin.AlertForm',
'The alert text does not contain \'<>\'.\n' 'The alert text does not contain \'<>\'.\n'
'Do you want to continue anyway?'), 'Do you want to continue anyway?')
QtWidgets.QMessageBox.StandardButtons(
QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Yes)
) == QtWidgets.QMessageBox.No: ) == QtWidgets.QMessageBox.No:
self.parameter_edit.setFocus() self.parameter_edit.setFocus()
return False return False

View File

@ -105,7 +105,7 @@ class AlertsTab(SettingsTab):
self.timeout_label.setText(translate('AlertsPlugin.AlertsTab', 'Alert timeout:')) self.timeout_label.setText(translate('AlertsPlugin.AlertsTab', 'Alert timeout:'))
self.timeout_spin_box.setSuffix(' {unit}'.format(unit=UiStrings().Seconds)) self.timeout_spin_box.setSuffix(' {unit}'.format(unit=UiStrings().Seconds))
self.preview_group_box.setTitle(UiStrings().Preview) self.preview_group_box.setTitle(UiStrings().Preview)
self.font_preview.setText(UiStrings().OLPV2x) self.font_preview.setText(UiStrings().OpenLP)
def on_background_color_changed(self, color): def on_background_color_changed(self, color):
""" """

View File

@ -22,9 +22,12 @@
import logging import logging
from openlp.core.api.http import register_endpoint
from openlp.core.common import UiStrings
from openlp.core.common.actions import ActionList from openlp.core.common.actions import ActionList
from openlp.core.lib import Plugin, StringContent, build_icon, translate from openlp.core.lib import Plugin, StringContent, build_icon, translate
from openlp.core.lib.ui import UiStrings, create_action from openlp.plugins.bibles.endpoint import api_bibles_endpoint, bibles_endpoint
from openlp.core.lib.ui import create_action
from openlp.plugins.bibles.lib import BibleManager, BiblesTab, BibleMediaItem, LayoutStyle, DisplayStyle, \ from openlp.plugins.bibles.lib import BibleManager, BiblesTab, BibleMediaItem, LayoutStyle, DisplayStyle, \
LanguageSelection LanguageSelection
from openlp.plugins.bibles.lib.mediaitem import BibleSearch from openlp.plugins.bibles.lib.mediaitem import BibleSearch
@ -38,7 +41,7 @@ __default_settings__ = {
'bibles/db password': '', 'bibles/db password': '',
'bibles/db hostname': '', 'bibles/db hostname': '',
'bibles/db database': '', 'bibles/db database': '',
'bibles/last search type': BibleSearch.Combined, 'bibles/last used search type': BibleSearch.Combined,
'bibles/reset to combined quick search': True, 'bibles/reset to combined quick search': True,
'bibles/verse layout style': LayoutStyle.VersePerSlide, 'bibles/verse layout style': LayoutStyle.VersePerSlide,
'bibles/book name language': LanguageSelection.Bible, 'bibles/book name language': LanguageSelection.Bible,
@ -56,7 +59,7 @@ __default_settings__ = {
'bibles/range separator': '', 'bibles/range separator': '',
'bibles/list separator': '', 'bibles/list separator': '',
'bibles/end separator': '', 'bibles/end separator': '',
'bibles/last directory import': '', 'bibles/last directory import': None,
'bibles/hide combined quick error': False, 'bibles/hide combined quick error': False,
'bibles/is search while typing enabled': True 'bibles/is search while typing enabled': True
} }
@ -74,6 +77,8 @@ class BiblePlugin(Plugin):
self.icon_path = ':/plugins/plugin_bibles.png' self.icon_path = ':/plugins/plugin_bibles.png'
self.icon = build_icon(self.icon_path) self.icon = build_icon(self.icon_path)
self.manager = BibleManager(self) self.manager = BibleManager(self)
register_endpoint(bibles_endpoint)
register_endpoint(api_bibles_endpoint)
def initialise(self): def initialise(self):
""" """

View File

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-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 openlp.core.api.http.endpoint import Endpoint
from openlp.core.api.http.errors import NotFound
from openlp.core.api.endpoint.pluginhelpers import search, live, service
from openlp.core.api.http import requires_auth
log = logging.getLogger(__name__)
bibles_endpoint = Endpoint('bibles')
api_bibles_endpoint = Endpoint('api')
@bibles_endpoint.route('search')
def bibles_search(request):
"""
Handles requests for searching the bibles plugin
:param request: The http request object.
"""
return search(request, 'bibles', log)
@bibles_endpoint.route('live')
@requires_auth
def bibles_live(request):
"""
Handles requests for making a song live
:param request: The http request object.
"""
return live(request, 'bibles', log)
@bibles_endpoint.route('add')
@requires_auth
def bibles_service(request):
"""
Handles requests for adding a song to the service
:param request: The http request object.
"""
service(request, 'bibles', log)
@api_bibles_endpoint.route('bibles/search')
def bibles_search_api(request):
"""
Handles requests for searching the bibles plugin
:param request: The http request object.
"""
return search(request, 'bibles', log)
@api_bibles_endpoint.route('bibles/live')
@requires_auth
def bibles_live_api(request):
"""
Handles requests for making a song live
:param request: The http request object.
"""
return live(request, 'bibles', log)
@api_bibles_endpoint.route('bibles/add')
@requires_auth
def bibles_service_api(request):
"""
Handles requests for adding a song to the service
:param request: The http request object.
"""
try:
search(request, 'bibles', log)
except NotFound:
return {'results': {'items': []}}

View File

@ -421,8 +421,8 @@ class BibleImportForm(OpenLPWizard):
Allow for localisation of the bible import wizard. Allow for localisation of the bible import wizard.
""" """
self.setWindowTitle(translate('BiblesPlugin.ImportWizardForm', 'Bible Import Wizard')) self.setWindowTitle(translate('BiblesPlugin.ImportWizardForm', 'Bible Import Wizard'))
self.title_label.setText(WizardStrings.HeaderStyle % translate('OpenLP.Ui', self.title_label.setText(WizardStrings.HeaderStyle.format(text=translate('OpenLP.Ui',
'Welcome to the Bible Import Wizard')) 'Welcome to the Bible Import Wizard')))
self.information_label.setText( self.information_label.setText(
translate('BiblesPlugin.ImportWizardForm', translate('BiblesPlugin.ImportWizardForm',
'This wizard will help you to import Bibles from a variety of ' 'This wizard will help you to import Bibles from a variety of '
@ -584,7 +584,7 @@ class BibleImportForm(OpenLPWizard):
elif self.currentPage() == self.license_details_page: elif self.currentPage() == self.license_details_page:
license_version = self.field('license_version') license_version = self.field('license_version')
license_copyright = self.field('license_copyright') license_copyright = self.field('license_copyright')
path = AppLocation.get_section_data_path('bibles') path = str(AppLocation.get_section_data_path('bibles'))
if not license_version: if not license_version:
critical_error_message_box( critical_error_message_box(
UiStrings().EmptyField, UiStrings().EmptyField,

View File

@ -49,7 +49,8 @@ class BookNameForm(QDialog, Ui_BookNameDialog):
""" """
Constructor Constructor
""" """
super(BookNameForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) super(BookNameForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.setupUi(self) self.setupUi(self)
self.custom_signals() self.custom_signals()
self.book_names = BibleStrings().BookNames self.book_names = BibleStrings().BookNames

View File

@ -45,7 +45,8 @@ class EditBibleForm(QtWidgets.QDialog, Ui_EditBibleDialog, RegistryProperties):
""" """
Constructor Constructor
""" """
super(EditBibleForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) super(EditBibleForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.media_item = media_item self.media_item = media_item
self.book_names = BibleStrings().BookNames self.book_names = BibleStrings().BookNames
self.setupUi(self) self.setupUi(self)

View File

@ -47,7 +47,8 @@ class LanguageForm(QDialog, Ui_LanguageDialog):
""" """
Constructor Constructor
""" """
super(LanguageForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) super(LanguageForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.setupUi(self) self.setupUi(self)
def exec(self, bible_name): def exec(self, bible_name):

View File

@ -221,18 +221,16 @@ def update_reference_separators():
REFERENCE_SEPARATORS['sep_{role}'.format(role=role)] = '\s*(?:{source})\s*'.format(source=source_string) REFERENCE_SEPARATORS['sep_{role}'.format(role=role)] = '\s*(?:{source})\s*'.format(source=source_string)
REFERENCE_SEPARATORS['sep_{role}_default'.format(role=role)] = default_separators[index] REFERENCE_SEPARATORS['sep_{role}_default'.format(role=role)] = default_separators[index]
# verse range match: (<chapter>:)?<verse>(-((<chapter>:)?<verse>|end)?)? # verse range match: (<chapter>:)?<verse>(-((<chapter>:)?<verse>|end)?)?
# TODO: Check before converting this string range_regex = '(?:(?P<from_chapter>[0-9]+){sep_v})?' \
range_regex = '(?:(?P<from_chapter>[0-9]+)%(sep_v)s)?' \ '(?P<from_verse>[0-9]+)(?P<range_to>{sep_r}(?:(?:(?P<to_chapter>' \
'(?P<from_verse>[0-9]+)(?P<range_to>%(sep_r)s(?:(?:(?P<to_chapter>' \ '[0-9]+){sep_v})?(?P<to_verse>[0-9]+)|{sep_e})?)?'.format_map(REFERENCE_SEPARATORS)
'[0-9]+)%(sep_v)s)?(?P<to_verse>[0-9]+)|%(sep_e)s)?)?' % REFERENCE_SEPARATORS REFERENCE_MATCHES['range'] = re.compile(r'^\s*{range}\s*$'.format(range=range_regex), re.UNICODE)
# TODO: Test before converting re.compile strings
REFERENCE_MATCHES['range'] = re.compile('^\s*%s\s*$' % range_regex, re.UNICODE)
REFERENCE_MATCHES['range_separator'] = re.compile(REFERENCE_SEPARATORS['sep_l'], re.UNICODE) REFERENCE_MATCHES['range_separator'] = re.compile(REFERENCE_SEPARATORS['sep_l'], re.UNICODE)
# full reference match: <book>(<range>(,(?!$)|(?=$)))+ # full reference match: <book>(<range>(,(?!$)|(?=$)))+
REFERENCE_MATCHES['full'] = \ REFERENCE_MATCHES['full'] = \
re.compile('^\s*(?!\s)(?P<book>[\d]*[^\d\.]+)\.*(?<!\s)\s*' re.compile(r'^\s*(?!\s)(?P<book>[\d]*[.]?[^\d\.]+)\.*(?<!\s)\s*'
'(?P<ranges>(?:%(range_regex)s(?:%(sep_l)s(?!\s*$)|(?=\s*$)))+)\s*$' r'(?P<ranges>(?:{range_regex}(?:{sep_l}(?!\s*$)|(?=\s*$)))+)\s*$'.format(
% dict(list(REFERENCE_SEPARATORS.items()) + [('range_regex', range_regex)]), re.UNICODE) range_regex=range_regex, sep_l=REFERENCE_SEPARATORS['sep_l']), re.UNICODE)
def get_reference_separator(separator_type): def get_reference_separator(separator_type):
@ -326,7 +324,7 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False):
``^\s*(?!\s)(?P<book>[\d]*[^\d]+)(?<!\s)\s*`` ``^\s*(?!\s)(?P<book>[\d]*[^\d]+)(?<!\s)\s*``
The ``book`` group starts with the first non-whitespace character. There are optional leading digits followed by The ``book`` group starts with the first non-whitespace character. There are optional leading digits followed by
non-digits. The group ends before the whitspace, or a full stop in front of the next digit. non-digits. The group ends before the whitespace, or a full stop in front of the next digit.
``(?P<ranges>(?:%(range_regex)s(?:%(sep_l)s(?!\s*$)|(?=\s*$)))+)\s*$`` ``(?P<ranges>(?:%(range_regex)s(?:%(sep_l)s(?!\s*$)|(?=\s*$)))+)\s*$``
The second group contains all ``ranges``. This can be multiple declarations of range_regex separated by a list The second group contains all ``ranges``. This can be multiple declarations of range_regex separated by a list

View File

@ -306,9 +306,8 @@ class BibleDB(Manager):
book_escaped = book book_escaped = book
for character in RESERVED_CHARACTERS: for character in RESERVED_CHARACTERS:
book_escaped = book_escaped.replace(character, '\\' + character) book_escaped = book_escaped.replace(character, '\\' + character)
# TODO: Verify regex patters before using format() regex_book = re.compile('\\s*{book}\\s*'.format(book='\\s*'.join(book_escaped.split())),
regex_book = re.compile('\s*%s\s*' % '\s*'.join( re.UNICODE | re.IGNORECASE)
book_escaped.split()), re.UNICODE | re.IGNORECASE)
if language_selection == LanguageSelection.Bible: if language_selection == LanguageSelection.Bible:
db_book = self.get_book(book) db_book = self.get_book(book)
if db_book: if db_book:
@ -471,7 +470,7 @@ class BiblesResourcesDB(QtCore.QObject, Manager):
Return the cursor object. Instantiate one if it doesn't exist yet. Return the cursor object. Instantiate one if it doesn't exist yet.
""" """
if BiblesResourcesDB.cursor is None: if BiblesResourcesDB.cursor is None:
file_path = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), file_path = os.path.join(str(AppLocation.get_directory(AppLocation.PluginsDir)),
'bibles', 'resources', 'bibles_resources.sqlite') 'bibles', 'resources', 'bibles_resources.sqlite')
conn = sqlite3.connect(file_path) conn = sqlite3.connect(file_path)
BiblesResourcesDB.cursor = conn.cursor() BiblesResourcesDB.cursor = conn.cursor()
@ -760,7 +759,7 @@ class AlternativeBookNamesDB(QtCore.QObject, Manager):
""" """
if AlternativeBookNamesDB.cursor is None: if AlternativeBookNamesDB.cursor is None:
file_path = os.path.join( file_path = os.path.join(
AppLocation.get_directory(AppLocation.DataDir), 'bibles', 'alternative_book_names.sqlite') str(AppLocation.get_directory(AppLocation.DataDir)), 'bibles', 'alternative_book_names.sqlite')
if not os.path.exists(file_path): if not os.path.exists(file_path):
# create new DB, create table alternative_book_names # create new DB, create table alternative_book_names
AlternativeBookNamesDB.conn = sqlite3.connect(file_path) AlternativeBookNamesDB.conn = sqlite3.connect(file_path)

View File

@ -53,6 +53,7 @@ import csv
from collections import namedtuple from collections import namedtuple
from openlp.core.common import get_file_encoding, translate from openlp.core.common import get_file_encoding, translate
from openlp.core.common.path import Path
from openlp.core.lib.exceptions import ValidationError from openlp.core.lib.exceptions import ValidationError
from openlp.plugins.bibles.lib.bibleimport import BibleImport from openlp.plugins.bibles.lib.bibleimport import BibleImport
@ -100,7 +101,7 @@ class CSVBible(BibleImport):
:return: An iterable yielding namedtuples of type results_tuple :return: An iterable yielding namedtuples of type results_tuple
""" """
try: try:
encoding = get_file_encoding(filename)['encoding'] encoding = get_file_encoding(Path(filename))['encoding']
with open(filename, 'r', encoding=encoding, newline='') as csv_file: with open(filename, 'r', encoding=encoding, newline='') as csv_file:
csv_reader = csv.reader(csv_file, delimiter=',', quotechar='"') csv_reader = csv.reader(csv_file, delimiter=',', quotechar='"')
return [results_tuple(*line) for line in csv_reader] return [results_tuple(*line) for line in csv_reader]

View File

@ -90,6 +90,8 @@ class BGExtract(RegistryProperties):
""" """
Extract verses from BibleGateway Extract verses from BibleGateway
""" """
NAME = 'BibleGateway'
def __init__(self, proxy_url=None): def __init__(self, proxy_url=None):
log.debug('BGExtract.init("{url}")'.format(url=proxy_url)) log.debug('BGExtract.init("{url}")'.format(url=proxy_url))
self.proxy_url = proxy_url self.proxy_url = proxy_url
@ -253,7 +255,7 @@ class BGExtract(RegistryProperties):
chapter=chapter, chapter=chapter,
version=version) version=version)
soup = get_soup_for_bible_ref( soup = get_soup_for_bible_ref(
'http://biblegateway.com/passage/?{url}'.format(url=url_params), 'http://www.biblegateway.com/passage/?{url}'.format(url=url_params),
pre_parse_regex=r'<meta name.*?/>', pre_parse_substitute='') pre_parse_regex=r'<meta name.*?/>', pre_parse_substitute='')
if not soup: if not soup:
return None return None
@ -282,7 +284,7 @@ class BGExtract(RegistryProperties):
""" """
log.debug('BGExtract.get_books_from_http("{version}")'.format(version=version)) log.debug('BGExtract.get_books_from_http("{version}")'.format(version=version))
url_params = urllib.parse.urlencode({'action': 'getVersionInfo', 'vid': '{version}'.format(version=version)}) url_params = urllib.parse.urlencode({'action': 'getVersionInfo', 'vid': '{version}'.format(version=version)})
reference_url = 'http://biblegateway.com/versions/?{url}#books'.format(url=url_params) reference_url = 'http://www.biblegateway.com/versions/?{url}#books'.format(url=url_params)
page = get_web_page(reference_url) page = get_web_page(reference_url)
if not page: if not page:
send_error_message('download') send_error_message('download')
@ -323,7 +325,7 @@ class BGExtract(RegistryProperties):
returns a list in the form [(biblename, biblekey, language_code)] returns a list in the form [(biblename, biblekey, language_code)]
""" """
log.debug('BGExtract.get_bibles_from_http') log.debug('BGExtract.get_bibles_from_http')
bible_url = 'https://biblegateway.com/versions/' bible_url = 'https://www.biblegateway.com/versions/'
soup = get_soup_for_bible_ref(bible_url) soup = get_soup_for_bible_ref(bible_url)
if not soup: if not soup:
return None return None
@ -357,6 +359,8 @@ class BSExtract(RegistryProperties):
""" """
Extract verses from Bibleserver.com Extract verses from Bibleserver.com
""" """
NAME = 'BibleServer'
def __init__(self, proxy_url=None): def __init__(self, proxy_url=None):
log.debug('BSExtract.init("{url}")'.format(url=proxy_url)) log.debug('BSExtract.init("{url}")'.format(url=proxy_url))
self.proxy_url = proxy_url self.proxy_url = proxy_url
@ -458,6 +462,8 @@ class CWExtract(RegistryProperties):
""" """
Extract verses from CrossWalk/BibleStudyTools Extract verses from CrossWalk/BibleStudyTools
""" """
NAME = 'Crosswalk'
def __init__(self, proxy_url=None): def __init__(self, proxy_url=None):
log.debug('CWExtract.init("{url}")'.format(url=proxy_url)) log.debug('CWExtract.init("{url}")'.format(url=proxy_url))
self.proxy_url = proxy_url self.proxy_url = proxy_url
@ -767,7 +773,7 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre
return None return None
try: try:
page = get_web_page(reference_url, header, True) page = get_web_page(reference_url, header, True)
except: except Exception as e:
page = None page = None
if not page: if not page:
send_error_message('download') send_error_message('download')

View File

@ -21,9 +21,9 @@
############################################################################### ###############################################################################
import logging import logging
import os
from openlp.core.common import AppLocation, OpenLPMixin, RegistryProperties, Settings, translate, delete_file, UiStrings from openlp.core.common import AppLocation, OpenLPMixin, RegistryProperties, Settings, translate, delete_file, UiStrings
from openlp.core.common.path import Path
from openlp.plugins.bibles.lib import LanguageSelection, parse_reference from openlp.plugins.bibles.lib import LanguageSelection, parse_reference
from openlp.plugins.bibles.lib.db import BibleDB, BibleMeta from openlp.plugins.bibles.lib.db import BibleDB, BibleMeta
from .importers.csvbible import CSVBible from .importers.csvbible import CSVBible
@ -111,7 +111,7 @@ class BibleManager(OpenLPMixin, RegistryProperties):
self.settings_section = 'bibles' self.settings_section = 'bibles'
self.web = 'Web' self.web = 'Web'
self.db_cache = None self.db_cache = None
self.path = AppLocation.get_section_data_path(self.settings_section) self.path = str(AppLocation.get_section_data_path(self.settings_section))
self.proxy_name = Settings().value(self.settings_section + '/proxy name') self.proxy_name = Settings().value(self.settings_section + '/proxy name')
self.suffix = '.sqlite' self.suffix = '.sqlite'
self.import_wizard = None self.import_wizard = None
@ -124,7 +124,7 @@ class BibleManager(OpenLPMixin, RegistryProperties):
of HTTPBible is loaded instead of the BibleDB class. of HTTPBible is loaded instead of the BibleDB class.
""" """
log.debug('Reload bibles') log.debug('Reload bibles')
files = AppLocation.get_files(self.settings_section, self.suffix) files = [str(file) for file in AppLocation.get_files(self.settings_section, self.suffix)]
if 'alternative_book_names.sqlite' in files: if 'alternative_book_names.sqlite' in files:
files.remove('alternative_book_names.sqlite') files.remove('alternative_book_names.sqlite')
log.debug('Bible Files {text}'.format(text=files)) log.debug('Bible Files {text}'.format(text=files))
@ -137,7 +137,7 @@ class BibleManager(OpenLPMixin, RegistryProperties):
# Remove corrupted files. # Remove corrupted files.
if name is None: if name is None:
bible.session.close_all() bible.session.close_all()
delete_file(os.path.join(self.path, filename)) delete_file(Path(self.path, filename))
continue continue
log.debug('Bible Name: "{name}"'.format(name=name)) log.debug('Bible Name: "{name}"'.format(name=name))
self.db_cache[name] = bible self.db_cache[name] = bible
@ -185,7 +185,7 @@ class BibleManager(OpenLPMixin, RegistryProperties):
bible = self.db_cache[name] bible = self.db_cache[name]
bible.session.close_all() bible.session.close_all()
bible.session = None bible.session = None
return delete_file(os.path.join(bible.path, bible.file)) return delete_file(Path(bible.path, bible.file))
def get_bibles(self): def get_bibles(self):
""" """
@ -305,13 +305,10 @@ class BibleManager(OpenLPMixin, RegistryProperties):
""" """
Does a verse search for the given bible and text. Does a verse search for the given bible and text.
:param bible: The bible to search :param str bible: The bible to search
:type bible: str :param str text: The text to search for
:param text: The text to search for
:type text: str
:return: The search results if valid, or None if the search is invalid. :return: The search results if valid, or None if the search is invalid.
:rtype: None, list :rtype: None | list
""" """
log.debug('BibleManager.verse_search("{bible}", "{text}")'.format(bible=bible, text=text)) log.debug('BibleManager.verse_search("{bible}", "{text}")'.format(bible=bible, text=text))
if not text: if not text:

View File

@ -414,7 +414,9 @@ class BibleMediaItem(MediaManagerItem):
if self.bible: if self.bible:
book_data = self.get_common_books(self.bible, self.second_bible) book_data = self.get_common_books(self.bible, self.second_bible)
language_selection = self.plugin.manager.get_language_selection(self.bible.name) language_selection = self.plugin.manager.get_language_selection(self.bible.name)
books = [book.get_name(language_selection) for book in book_data] # Get book names + add a space to the end. Thus Psalm23 becomes Psalm 23
# when auto complete is used and user does not need to add the space manually.
books = [book.get_name(language_selection) + ' ' for book in book_data]
books.sort(key=get_locale_key) books.sort(key=get_locale_key)
set_case_insensitive_completer(books, self.search_edit) set_case_insensitive_completer(books, self.search_edit)
@ -463,8 +465,7 @@ class BibleMediaItem(MediaManagerItem):
""" """
Show the selected tab and set focus to it Show the selected tab and set focus to it
:param index: The tab selected :param int index: The tab selected
:type index: int
:return: None :return: None
""" """
if index == SearchTabs.Search or index == SearchTabs.Select: if index == SearchTabs.Search or index == SearchTabs.Select:
@ -481,7 +482,7 @@ class BibleMediaItem(MediaManagerItem):
Update list_widget with the contents of the selected list Update list_widget with the contents of the selected list
:param index: The index of the tab that has been changed to. (int) :param index: The index of the tab that has been changed to. (int)
:return: None :rtype: None
""" """
if index == ResultsTab.Saved: if index == ResultsTab.Saved:
self.add_built_results_to_list_widget(self.saved_results) self.add_built_results_to_list_widget(self.saved_results)

View File

@ -26,8 +26,10 @@ for the Custom Slides plugin.
import logging import logging
from openlp.core.api.http import register_endpoint
from openlp.core.lib import Plugin, StringContent, build_icon, translate from openlp.core.lib import Plugin, StringContent, build_icon, translate
from openlp.core.lib.db import Manager from openlp.core.lib.db import Manager
from openlp.plugins.custom.endpoint import api_custom_endpoint, custom_endpoint
from openlp.plugins.custom.lib import CustomMediaItem, CustomTab from openlp.plugins.custom.lib import CustomMediaItem, CustomTab
from openlp.plugins.custom.lib.db import CustomSlide, init_schema from openlp.plugins.custom.lib.db import CustomSlide, init_schema
from openlp.plugins.custom.lib.mediaitem import CustomSearch from openlp.plugins.custom.lib.mediaitem import CustomSearch
@ -40,7 +42,7 @@ __default_settings__ = {
'custom/db password': '', 'custom/db password': '',
'custom/db hostname': '', 'custom/db hostname': '',
'custom/db database': '', 'custom/db database': '',
'custom/last search type': CustomSearch.Titles, 'custom/last used search type': CustomSearch.Titles,
'custom/display footer': True, 'custom/display footer': True,
'custom/add custom from service': True 'custom/add custom from service': True
} }
@ -61,6 +63,8 @@ class CustomPlugin(Plugin):
self.db_manager = Manager('custom', init_schema) self.db_manager = Manager('custom', init_schema)
self.icon_path = ':/plugins/plugin_custom.png' self.icon_path = ':/plugins/plugin_custom.png'
self.icon = build_icon(self.icon_path) self.icon = build_icon(self.icon_path)
register_endpoint(custom_endpoint)
register_endpoint(api_custom_endpoint)
@staticmethod @staticmethod
def about(): def about():

View File

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-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 openlp.core.api.http.endpoint import Endpoint
from openlp.core.api.http.errors import NotFound
from openlp.core.api.endpoint.pluginhelpers import search, live, service
from openlp.core.api.http import requires_auth
log = logging.getLogger(__name__)
custom_endpoint = Endpoint('custom')
api_custom_endpoint = Endpoint('api')
@custom_endpoint.route('search')
def custom_search(request):
"""
Handles requests for searching the custom plugin
:param request: The http request object.
"""
return search(request, 'custom', log)
@custom_endpoint.route('live')
@requires_auth
def custom_live(request):
"""
Handles requests for making a song live
:param request: The http request object.
"""
return live(request, 'custom', log)
@custom_endpoint.route('add')
@requires_auth
def custom_service(request):
"""
Handles requests for adding a song to the service
:param request: The http request object.
"""
service(request, 'custom', log)
@api_custom_endpoint.route('custom/search')
def custom_search_api(request):
"""
Handles requests for searching the custom plugin
:param request: The http request object.
"""
return search(request, 'custom', log)
@api_custom_endpoint.route('custom/live')
@requires_auth
def custom_live_api(request):
"""
Handles requests for making a song live
:param request: The http request object.
"""
return live(request, 'custom', log)
@api_custom_endpoint.route('custom/add')
@requires_auth
def custom_service_api(request):
"""
Handles requests for adding a song to the service
:param request: The http request object.
"""
try:
search(request, 'custom', log)
except NotFound:
return {'results': {'items': []}}

View File

@ -44,7 +44,8 @@ class EditCustomForm(QtWidgets.QDialog, Ui_CustomEditDialog):
""" """
Constructor Constructor
""" """
super(EditCustomForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) super(EditCustomForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.manager = manager self.manager = manager
self.media_item = media_item self.media_item = media_item
self.setupUi(self) self.setupUi(self)

View File

@ -39,7 +39,8 @@ class EditCustomSlideForm(QtWidgets.QDialog, Ui_CustomSlideEditDialog):
""" """
Constructor Constructor
""" """
super(EditCustomSlideForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) super(EditCustomSlideForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.setupUi(self) self.setupUi(self)
# Connecting signals and slots # Connecting signals and slots
self.insert_button.clicked.connect(self.on_insert_button_clicked) self.insert_button.clicked.connect(self.on_insert_button_clicked)

View File

@ -190,9 +190,7 @@ class CustomMediaItem(MediaManagerItem):
translate('CustomPlugin.MediaItem', translate('CustomPlugin.MediaItem',
'Are you sure you want to delete the "{items:d}" ' 'Are you sure you want to delete the "{items:d}" '
'selected custom slide(s)?').format(items=len(items)), 'selected custom slide(s)?').format(items=len(items)),
QtWidgets.QMessageBox.StandardButtons( defaultButton=QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.No:
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No),
QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.No:
return return
row_list = [item.row() for item in self.list_view.selectedIndexes()] row_list = [item.row() for item in self.list_view.selectedIndexes()]
row_list.sort(reverse=True) row_list.sort(reverse=True)

View File

@ -0,0 +1,113 @@
# -*- 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 openlp.core.api.http.endpoint import Endpoint
from openlp.core.api.http.errors import NotFound
from openlp.core.api.endpoint.pluginhelpers import search, live, service, display_thumbnails
from openlp.core.api.http import requires_auth
log = logging.getLogger(__name__)
images_endpoint = Endpoint('images')
api_images_endpoint = Endpoint('api')
# images/thumbnails/320x240/1.jpg
@images_endpoint.route('thumbnails/{dimensions}/{file_name}')
def images_thumbnails(request, dimensions, file_name):
"""
Return an image to a web page based on a URL
:param request: Request object
:param dimensions: the image size eg 88x88
:param file_name: the individual image name
:return:
"""
return display_thumbnails(request, 'images', log, dimensions, file_name)
@images_endpoint.route('search')
def images_search(request):
"""
Handles requests for searching the images plugin
:param request: The http request object.
"""
return search(request, 'images', log)
@images_endpoint.route('live')
@requires_auth
def images_live(request):
"""
Handles requests for making a song live
:param request: The http request object.
"""
return live(request, 'images', log)
@images_endpoint.route('add')
@requires_auth
def images_service(request):
"""
Handles requests for adding a song to the service
:param request: The http request object.
"""
service(request, 'images', log)
@api_images_endpoint.route('images/search')
def images_search_api(request):
"""
Handles requests for searching the images plugin
:param request: The http request object.
"""
return search(request, 'images', log)
@api_images_endpoint.route('images/live')
@requires_auth
def images_live_api(request):
"""
Handles requests for making a song live
:param request: The http request object.
"""
return live(request, 'images', log)
@api_images_endpoint.route('images/add')
@requires_auth
def images_service_api(request):
"""
Handles requests for adding a song to the service
:param request: The http request object.
"""
try:
search(request, 'images', log)
except NotFound:
return {'results': {'items': []}}

View File

@ -35,7 +35,8 @@ class AddGroupForm(QtWidgets.QDialog, Ui_AddGroupDialog):
""" """
Constructor Constructor
""" """
super(AddGroupForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) super(AddGroupForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.setupUi(self) self.setupUi(self)
def exec(self, clear=True, show_top_level_group=False, selected_group=None): def exec(self, clear=True, show_top_level_group=False, selected_group=None):

View File

@ -33,7 +33,8 @@ class ChooseGroupForm(QtWidgets.QDialog, Ui_ChooseGroupDialog):
""" """
Constructor Constructor
""" """
super(ChooseGroupForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) super(ChooseGroupForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.setupUi(self) self.setupUi(self)
def exec(self, selected_group=None): def exec(self, selected_group=None):

View File

@ -24,9 +24,11 @@ from PyQt5 import QtGui
import logging import logging
from openlp.core.api.http import register_endpoint
from openlp.core.common import Settings, translate from openlp.core.common import Settings, translate
from openlp.core.lib import Plugin, StringContent, ImageSource, build_icon from openlp.core.lib import Plugin, StringContent, ImageSource, build_icon
from openlp.core.lib.db import Manager from openlp.core.lib.db import Manager
from openlp.plugins.images.endpoint import api_images_endpoint, images_endpoint
from openlp.plugins.images.lib import ImageMediaItem, ImageTab from openlp.plugins.images.lib import ImageMediaItem, ImageTab
from openlp.plugins.images.lib.db import init_schema from openlp.plugins.images.lib.db import init_schema
@ -51,6 +53,8 @@ class ImagePlugin(Plugin):
self.weight = -7 self.weight = -7
self.icon_path = ':/plugins/plugin_images.png' self.icon_path = ':/plugins/plugin_images.png'
self.icon = build_icon(self.icon_path) self.icon = build_icon(self.icon_path)
register_endpoint(images_endpoint)
register_endpoint(api_images_endpoint)
@staticmethod @staticmethod
def about(): def about():
@ -67,14 +71,6 @@ class ImagePlugin(Plugin):
'provided by the theme.') 'provided by the theme.')
return about_text return about_text
def upgrade_settings(self, settings):
"""
Upgrade the settings of this plugin.
:param settings: The Settings object containing the old settings.
"""
pass
def set_plugin_text_strings(self): def set_plugin_text_strings(self):
""" """
Called to define all translatable texts of the plugin. Called to define all translatable texts of the plugin.

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