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 sys
import time
from datetime import datetime
from traceback import format_exception
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManager, Settings, UiStrings, \
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.lib import ScreenList
from openlp.core.resources import qInitResources
@ -153,10 +155,8 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
self.processEvents()
if not has_run_wizard:
self.main_window.first_time()
# update_check = Settings().value('core/update check')
# if update_check:
# version = VersionThread(self.main_window)
# version.start()
version = VersionThread(self.main_window)
version.start()
self.main_window.is_display_blank()
self.main_window.app_startup()
return self.exec()
@ -181,7 +181,7 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
"""
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):
log.critical('Database was not found in: ' + data_folder_path)
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'),
translate('OpenLP', 'OpenLP has been upgraded, do you want to create\n'
'a backup of the old data folder?'),
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes:
defaultButton=QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes:
# 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")
data_folder_backup_path = data_folder_path + '-' + timestamp
try:
@ -338,6 +337,8 @@ def parse_options(args=None):
parser.add_argument('-d', '--dev-version', dest='dev_version', action='store_true',
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('-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=[])
# 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()
@ -347,15 +348,17 @@ def set_up_logging(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)
filename = os.path.join(log_path, 'openlp.log')
logfile = logging.FileHandler(filename, 'w', encoding="UTF-8")
file_path = log_path / 'openlp.log'
# 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'))
log.addHandler(logfile)
if log.isEnabledFor(logging.DEBUG):
print('Logging to: {name}'.format(name=filename))
print('Logging to: {name}'.format(name=file_path))
def main(args=None):
@ -391,16 +394,16 @@ def main(args=None):
application.setApplicationName('OpenLPPortable')
Settings.setDefaultFormat(Settings.IniFormat)
# Get location OpenLPPortable.ini
application_path = AppLocation.get_directory(AppLocation.AppDir)
set_up_logging(os.path.abspath(os.path.join(application_path, '..', '..', 'Other')))
portable_path = (AppLocation.get_directory(AppLocation.AppDir) / '..' / '..').resolve()
data_path = portable_path / 'Data'
set_up_logging(portable_path / 'Other')
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
log.info('INI file: {name}'.format(name=portable_settings_file))
Settings.set_filename(portable_settings_file)
log.info('INI file: {name}'.format(name=portable_settings_path))
Settings.set_filename(str(portable_settings_path))
portable_settings = Settings()
# Set our data path
data_path = os.path.abspath(os.path.join(application_path, '..', '..', 'Data',))
log.info('Data path: {name}'.format(name=data_path))
# Point to our 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))
Registry.create()
Registry().register('application', application)
Registry().set_flag('no_web_server', args.no_web_server)
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
if application.is_already_running():
@ -419,8 +423,21 @@ def main(args=None):
if application.is_data_path_missing():
application.shared_memory.detach()
sys.exit()
# Remove/convert obsolete settings.
Settings().remove_obsolete_settings()
# Upgrade 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
if not Settings().value('core/has run wizard'):
if not FirstTimeLanguageForm().exec():

View File

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

View File

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

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 #
###############################################################################
import os.path
from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets
from openlp.core.common import AppLocation, Settings, translate
from openlp.core.lib import SettingsTab, build_icon
from openlp.core.common import UiStrings, Registry, Settings, translate
from openlp.core.lib import SettingsTab
ZERO_URL = '0.0.0.0'
class RemoteTab(SettingsTab):
class ApiTab(SettingsTab):
"""
RemoteTab is the Remotes settings tab in the settings dialog.
"""
def __init__(self, parent, title, visible_title, icon_path):
super(RemoteTab, self).__init__(parent, title, visible_title, icon_path)
def __init__(self, parent):
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):
self.setObjectName('RemoteTab')
super(RemoteTab, self).setupUi()
self.setObjectName('ApiTab')
super(ApiTab, self).setupUi()
self.server_settings_group_box = QtWidgets.QGroupBox(self.left_column)
self.server_settings_group_box.setObjectName('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.port_label = QtWidgets.QLabel(self.http_settings_group_box)
self.port_label.setObjectName('port_label')
self.port_spin_box = QtWidgets.QSpinBox(self.http_settings_group_box)
self.port_spin_box.setMaximum(32767)
self.port_spin_box = QtWidgets.QLabel(self.http_settings_group_box)
self.port_spin_box.setObjectName('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)
@ -111,6 +112,23 @@ class RemoteTab(SettingsTab):
self.password.setObjectName('password')
self.user_login_layout.addRow(self.password_label, self.password)
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.setObjectName('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.thumbnails_check_box.stateChanged.connect(self.on_thumbnails_check_box_changed)
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):
self.tab_title_visible = translate('RemotePlugin.RemoteTab', 'Remote Interface')
self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings'))
self.address_label.setText(translate('RemotePlugin.RemoteTab', 'Serve on IP address:'))
self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote 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.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.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab',
'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 '
'Store.').format(qr='https://itunes.apple.com/app/id1096218725'))
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.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):
"""
Update the display based on the data input on the screen
"""
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))
http_url_temp = http_url + 'stage'
self.stage_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
http_url_temp = http_url + 'main'
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
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
"""
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.twelve_hour = Settings().value(self.settings_section + '/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_id.setText(Settings().value(self.settings_section + '/user id'))
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()
def save(self):
"""
Save the configuration and update the server configuration if necessary
"""
if Settings().value(self.settings_section + '/ip address') != self.address_edit.text() or \
Settings().value(self.settings_section + '/port') != self.port_spin_box.value():
if Settings().value(self.settings_section + '/ip address') != self.address_edit.text():
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 + '/twelve hour', self.twelve_hour)
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 + '/password', self.password.text())
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):
"""

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
from chardet.universaldetector import UniversalDetector
from ipaddress import IPv4Address, IPv6Address, AddressValueError
from pathlib import Path
from shutil import which
from subprocess import check_output, CalledProcessError, STDOUT
@ -65,17 +64,18 @@ def trace_error_handler(logger):
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 do_not_log: To not log anything. This is need for the start up, when the log isn't ready.
:param openlp.core.common.path.Path directory: The directory to make sure exists
: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:
log.debug('check_directory_exists {text}'.format(text=directory))
try:
if not os.path.exists(directory):
os.makedirs(directory)
except IOError as e:
if not directory.exists():
directory.mkdir(parents=True)
except IOError:
if not do_not_log:
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
importers.
:param glob_pattern: A glob pattern used to find the extension(s) to be imported. Should be relative to the
application directory. i.e. openlp/plugins/*/*plugin.py
:type glob_pattern: str
:param excluded_files: A list of file names to exclude that the glob pattern may find.
:type excluded_files: list of strings
:return: None
: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. plugins/*/*plugin.py
:param list[str] excluded_files: A list of file names to exclude that the glob pattern may find.
: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):
extension_path = extension_path.relative_to(app_dir)
if extension_path.name in excluded_files:
@ -106,21 +101,19 @@ def extension_loader(glob_pattern, excluded_files=[]):
except (ImportError, OSError):
# On some platforms importing vlc.py might cause OSError exceptions. (e.g. Mac OS X)
log.warning('Failed to import {module_name} on path {extension_path}'
.format(module_name=module_name, extension_path=str(extension_path)))
.format(module_name=module_name, extension_path=extension_path))
def path_to_module(path):
"""
Convert a path to a module name (i.e openlp.core.common)
:param path: The path to convert to a module name.
:type path: Path
:param openlp.core.common.path.Path path: The path to convert to a module name.
:return: The module name.
:rtype: str
"""
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):
@ -378,20 +371,22 @@ def split_filename(path):
return os.path.split(path)
def delete_file(file_path_name):
def delete_file(file_path):
"""
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
try:
if os.path.exists(file_path_name):
os.remove(file_path_name)
if file_path.exists():
file_path.unlink()
return True
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
@ -411,18 +406,19 @@ def get_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.
: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
else:
formats = [bytes(fmt).decode().lower() for fmt in QtGui.QImageReader.supportedImageFormats()]
file_part, file_extension = os.path.splitext(str(file_name))
if file_extension[1:].lower() in formats and os.path.exists(file_name):
if file_path.suffix[1:].lower() in formats:
return False
return True
@ -431,10 +427,10 @@ def clean_filename(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))
@ -442,8 +438,9 @@ def check_binary_exists(program_path):
"""
Function that checks whether a binary exists.
:param program_path: The full path to the binary to check.
:param openlp.core.common.path.Path program_path: The full path to the binary to check.
:return: program output to be parsed
:rtype: bytes
"""
log.debug('testing program_path: {text}'.format(text=program_path))
try:
@ -453,26 +450,27 @@ def check_binary_exists(program_path):
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
else:
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:
runlog = e.output
run_log = e.output
except Exception:
trace_error_handler(log)
runlog = ''
log.debug('check_output returned: {text}'.format(text=runlog))
return runlog
run_log = ''
log.debug('check_output returned: {text}'.format(text=run_log))
return run_log
def get_file_encoding(filename):
def get_file_encoding(file_path):
"""
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'
:rtype: dict[str, float]
"""
detector = UniversalDetector()
try:
with open(filename, 'rb') as detect_file:
with file_path.open('rb') as detect_file:
while not detector.done:
chunk = detect_file.read(1024)
if not chunk:

View File

@ -27,6 +27,7 @@ import os
import sys
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():
@ -42,6 +43,9 @@ from openlp.core.common import check_directory_exists, get_frozen_path
log = logging.getLogger(__name__)
FROZEN_APP_PATH = Path(sys.argv[0]).parent
APP_PATH = Path(openlp.__file__).parent
class AppLocation(object):
"""
@ -54,29 +58,21 @@ class AppLocation(object):
CacheDir = 5
LanguageDir = 6
# Base path where data/config/cache dir is located
BaseDir = None
@staticmethod
def get_directory(dir_type=AppDir):
"""
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*
:return: The requested path
:rtype: openlp.core.common.path.Path
"""
if dir_type == AppLocation.AppDir:
return get_frozen_path(os.path.abspath(os.path.dirname(sys.argv[0])), os.path.dirname(openlp.__file__))
if dir_type == AppLocation.AppDir or dir_type == AppLocation.VersionDir:
return get_frozen_path(FROZEN_APP_PATH, APP_PATH)
elif dir_type == AppLocation.PluginsDir:
app_path = os.path.abspath(os.path.dirname(sys.argv[0]))
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__))
return get_frozen_path(FROZEN_APP_PATH, APP_PATH) / 'plugins'
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 os.path.join(app_path, 'i18n')
elif dir_type == AppLocation.DataDir and AppLocation.BaseDir:
return os.path.join(AppLocation.BaseDir, 'data')
return get_frozen_path(FROZEN_APP_PATH, _get_os_dir_path(dir_type)) / 'i18n'
else:
return _get_os_dir_path(dir_type)
@ -84,6 +80,9 @@ class AppLocation(object):
def get_data_path():
"""
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.
if Settings().contains('advanced/data path'):
@ -91,40 +90,38 @@ class AppLocation(object):
else:
path = AppLocation.get_directory(AppLocation.DataDir)
check_directory_exists(path)
return os.path.normpath(path)
return path
@staticmethod
def get_files(section=None, extension=None):
def get_files(section=None, extension=''):
"""
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
data subdirectory.
:param extension:
Defaults to *None*. The extension to search for. For example::
'.png'
:param None | str section: Defaults to *None*. The section of code getting the files - used to load from a
section's data subdirectory.
:param str extension: Defaults to ''. The extension to search for. For example::
'.png'
:return: List of files found.
:rtype: list[openlp.core.common.path.Path]
"""
path = AppLocation.get_data_path()
if section:
path = os.path.join(path, section)
path = path / section
try:
files = os.listdir(path)
file_paths = path.glob('*' + extension)
return [file_path.relative_to(path) for file_path in file_paths]
except OSError:
return []
if extension:
return [filename for filename in files if extension == os.path.splitext(filename)[1]]
else:
# no filtering required
return files
@staticmethod
def get_section_data_path(section):
"""
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 = os.path.join(data_path, section)
path = AppLocation.get_data_path() / section
check_directory_exists(path)
return path
@ -132,36 +129,41 @@ class AppLocation(object):
def _get_os_dir_path(dir_type):
"""
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 dir_type == AppLocation.LanguageDir:
directory = os.path.abspath(os.path.join(os.path.dirname(openlp.__file__), '..', 'resources'))
if os.path.exists(directory):
directory = Path(openlp.__file__, '..', '..').resolve() / 'resources'
if directory.exists():
return directory
if is_win():
openlp_folder_path = Path(os.getenv('APPDATA'), 'openlp')
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:
return os.path.dirname(openlp.__file__)
return os.path.join(str(os.getenv('APPDATA')), 'openlp')
return Path(openlp.__file__).parent
return openlp_folder_path
elif is_macosx():
openlp_folder_path = Path(os.getenv('HOME'), 'Library', 'Application Support', 'openlp')
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:
return os.path.dirname(openlp.__file__)
return os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp')
return Path(openlp.__file__).parent
return openlp_folder_path
else:
if dir_type == AppLocation.LanguageDir:
for prefix in ['/usr/local', '/usr']:
directory = os.path.join(prefix, 'share', 'openlp')
if os.path.exists(directory):
return directory
return os.path.join('/usr', 'share', 'openlp')
directory = Path('/usr', 'local', 'share', 'openlp')
if directory.exists():
return directory
return Path('/usr', 'share', 'openlp')
if XDG_BASE_AVAILABLE:
if dir_type == AppLocation.DataDir:
return os.path.join(str(BaseDirectory.xdg_data_home), 'openlp')
if dir_type == AppLocation.DataDir or dir_type == AppLocation.CacheDir:
return Path(BaseDirectory.xdg_data_home, 'openlp')
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:
return os.path.join(str(os.getenv('HOME')), '.openlp', 'data')
return os.path.join(str(os.getenv('HOME')), '.openlp')
return Path(os.getenv('HOME'), '.openlp', 'data')
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 logging
import os
import platform
import socket
import sys
import subprocess
import time
import urllib.error
import urllib.parse
@ -215,6 +217,7 @@ def url_get_file(callback, url, f_path, sha256=None):
block_count = 0
block_size = 4096
retries = 0
log.debug("url_get_file: " + url)
while True:
try:
filename = open(f_path, "wb")
@ -253,4 +256,17 @@ def url_get_file(callback, url, f_path, sha256=None):
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']

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:
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.load(language, lang_path)
# 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
"""
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)
# Remove qm files from the list which start with "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")
if reg_ex.exactMatch(qmf):
name = '{regex}'.format(regex=reg_ex.cap(1))
# TODO: Test before converting to python3 string format
LanguageManager.__qm_list__['%#2i %s' % (counter + 1, LanguageManager.language_name(qmf))] = name
LanguageManager.__qm_list__[
'{count:>2i} {name}'.format(count=counter + 1, name=LanguageManager.language_name(qmf))] = name
@staticmethod
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 logging
import json
import os
from tempfile import gettempdir
from PyQt5 import QtCore, QtGui
from openlp.core.common import ThemeLevel, SlideLimits, UiStrings, is_win, is_linux
from PyQt5 import QtCore, QtGui, QtWidgets
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__)
__version__ = 2
# Fix for bug #1014422.
X11_BYPASS_DEFAULT = True
@ -44,21 +48,6 @@ if is_linux():
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):
"""
If phonon is in the setting string replace it with system
@ -73,14 +62,25 @@ def media_players_conv(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 to wrap QSettings.
* Exposes all the methods of QSettings.
* Adds functionality for OpenLP Portable. If the ``defaultFormat`` is set to
``IniFormat``, and the path to the Ini file is set using ``set_filename``,
then the Settings constructor (without any arguments) will create a Settings
* Adds functionality for OpenLP Portable. If the ``defaultFormat`` is set to ``IniFormat``, and the path to the Ini
file is set using ``set_filename``, then the Settings constructor (without any arguments) will create a Settings
object for accessing settings stored in that Ini file.
``__default_settings__``
@ -91,7 +91,7 @@ class Settings(QtCore.QSettings):
('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 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.
"""
__default_settings__ = {
'settings/version': 0,
'advanced/add page break': False,
'advanced/alternate rows': not is_win(),
'advanced/autoscrolling': {'dist': 1, 'pos': 0},
'advanced/current media plugin': -1,
'advanced/data path': '',
'advanced/data path': None,
# 7 stands for now, 0 to 6 is Monday to Sunday.
'advanced/default service day': 7,
'advanced/default service enabled': True,
@ -121,6 +122,7 @@ class Settings(QtCore.QSettings):
'advanced/enable exit confirmation': True,
'advanced/expand service item': False,
'advanced/hide mouse': True,
'advanced/ignore aspect ratio': False,
'advanced/is portable': False,
'advanced/max recent files': 20,
'advanced/print file meta data': False,
@ -134,7 +136,15 @@ class Settings(QtCore.QSettings):
'advanced/single click service preview': False,
'advanced/x11 bypass wm': X11_BYPASS_DEFAULT,
'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': '',
'core/audio repeat list': False,
'core/auto open': False,
@ -153,7 +163,7 @@ class Settings(QtCore.QSettings):
'core/screen blank': False,
'core/show splash': True,
'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/songselect password': '',
'core/songselect username': '',
@ -168,17 +178,17 @@ class Settings(QtCore.QSettings):
'media/players': 'system,webkit',
'media/override player': QtCore.Qt.Unchecked,
'players/background color': '#000000',
'servicemanager/last directory': '',
'servicemanager/last file': '',
'servicemanager/service theme': '',
'servicemanager/last directory': None,
'servicemanager/last file': None,
'servicemanager/service theme': None,
'SettingsImport/file_date_created': datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
'SettingsImport/Make_Changes': 'At_Own_RISK',
'SettingsImport/type': 'OpenLP_settings_export',
'SettingsImport/version': '',
'themes/global theme': '',
'themes/last directory': '',
'themes/last directory export': '',
'themes/last directory import': '',
'themes/last directory': None,
'themes/last directory export': None,
'themes/last directory import': None,
'themes/theme level': ThemeLevel.Song,
'themes/wrap footer': False,
'user interface/live panel': True,
@ -199,27 +209,60 @@ class Settings(QtCore.QSettings):
'projector/db database': '',
'projector/enable': True,
'projector/connect on start': False,
'projector/last directory import': '',
'projector/last directory export': '',
'projector/last directory import': None,
'projector/last directory export': None,
'projector/poll time': 20, # PJLink timeout is 30 seconds
'projector/socket timeout': 5, # 5 second socket timeout
'projector/source dialog type': 0 # Source select dialog box type
}
__file_path__ = ''
__obsolete_settings__ = [
__setting_upgrade_1__ = [
# 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', []),
('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
('advanced/default color', 'core/logo background color', []), # Default image renamed + moved to general > 2.4.
('advanced/default image', 'core/logo file', []), # Default image renamed + moved to general after 2.4.
('remotes/https enabled', '', []),
('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/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.
('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
@ -232,13 +275,16 @@ class Settings(QtCore.QSettings):
Settings.__default_settings__.update(default_values)
@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.
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
def set_up_default_values():
@ -407,14 +453,28 @@ class Settings(QtCore.QSettings):
key = self.group() + '/' + 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
``__obsolete_settings__`` for more details.
"""
for old_key, new_key, rules in Settings.__obsolete_settings__:
# Once removed we don't have to do this again.
if self.contains(old_key):
current_version = self.value('settings/version')
for version in range(current_version, __version__):
version += 1
upgrade_list = getattr(self, '__setting_upgrade_{version}__'.format(version=version))
for old_key, new_key, rules in upgrade_list:
# 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:
# Get the value of the old_key.
old_value = super(Settings, self).value(old_key)
@ -433,14 +493,17 @@ class Settings(QtCore.QSettings):
old_value = new
break
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):
"""
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.
: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 self.group():
@ -450,6 +513,18 @@ class Settings(QtCore.QSettings):
setting = super(Settings, self).value(key, 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):
"""
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):
return ''
# An empty list saved to the settings results in a None type being returned.
else:
elif isinstance(default_value, list):
return []
elif isinstance(setting, str):
if '__Path__' in setting:
return json.loads(setting, cls=OpenLPJsonDecoder)
# Convert the setting to the correct type.
if isinstance(default_value, bool):
if isinstance(setting, bool):
@ -479,30 +557,58 @@ class Settings(QtCore.QSettings):
return int(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
of file paths are returned.
Export the settings to file.
**Note**: Only a list of paths is returned; this does not convert anything!
:param plugin: The Plugin object.The caller has to convert/save the list himself; o
:param openlp.core.common.path.Path dest_path: The file path to create the export file.
:return: Success
:rtype: bool
"""
files_list = []
# We need QSettings instead of Settings here to bypass our central settings dict.
# Do NOT do this anywhere else!
settings = QtCore.QSettings(self.fileName(), Settings.IniFormat)
settings.beginGroup(plugin.settings_section)
if settings.contains('{name} count'.format(name=plugin.name)):
# Get the count.
list_count = int(settings.value('{name} count'.format(name=plugin.name), 0))
if list_count:
for counter in range(list_count):
# The keys were named e. g.: "image 0"
item = settings.value('{name} {counter:d}'.format(name=plugin.name, counter=counter), '')
if item:
files_list.append(item)
settings.remove('{name} {counter:d}'.format(name=plugin.name, counter=counter))
settings.remove('{name} count'.format(name=plugin.name))
settings.endGroup()
return files_list
temp_path = Path(gettempdir(), 'openlp', 'exportConf.tmp')
# Delete old files if found.
if temp_path.exists():
temp_path.unlink()
if dest_path.exists():
dest_path.unlink()
self.remove('SettingsImport')
# Get the settings.
keys = self.allKeys()
export_settings = QtCore.QSettings(str(temp_path), Settings.IniFormat)
# Add a header section.
# This is to insure it's our conf file for import.
now = datetime.datetime.now()
# Write INI format using QSettings.
# Write our header.
export_settings.beginGroup('SettingsImport')
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.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.NISp = translate('OpenLP.Ui', 'No Items Selected', 'Plural')
self.NoResults = translate('OpenLP.Ui', 'No Search Results')
self.OLP = translate('OpenLP.Ui', 'OpenLP')
self.OLPV2 = "{name} {version}".format(name=self.OLP, version="2")
self.OLPV2x = "{name} {version}".format(name=self.OLP, version="2.4")
self.OpenLP = translate('OpenLP.Ui', 'OpenLP')
self.OpenLPv2AndUp = translate('OpenLP.Ui', 'OpenLP 2.0 and up')
self.OpenLPStart = translate('OpenLP.Ui', 'OpenLP is already running. Do you wish to continue?')
self.OpenService = translate('OpenLP.Ui', 'Open service.')
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.SplitToolTip = translate('OpenLP.Ui',
'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.StartTimeCode = translate('OpenLP.Ui', 'Start {code}')
self.StartingImport = translate('OpenLP.Ui', 'Starting import...')
self.StopPlaySlidesInLoop = translate('OpenLP.Ui', 'Stop Play Slides in Loop')
self.StopPlaySlidesToEnd = translate('OpenLP.Ui', 'Stop Play Slides to End')
self.Theme = translate('OpenLP.Ui', 'Theme', 'Singular')
@ -169,6 +167,7 @@ class UiStrings(object):
self.View = translate('OpenLP.Ui', 'View')
self.ViewMode = translate('OpenLP.Ui', 'View Mode')
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')
chapter = translate('OpenLP.Ui', 'Chapter')
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 os
import platform
@ -12,7 +36,8 @@ from subprocess import Popen, PIPE
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__)
@ -42,12 +67,18 @@ class VersionThread(QtCore.QThread):
"""
self.sleep(1)
log.debug('Version thread - run')
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))
found = ping("openlp.io")
Registry().set_flag('internet_present', found)
update_check = Settings().value('core/update check')
if found:
Registry().execute('get_website_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():
@ -95,7 +126,7 @@ def get_application_version():
full_version = '{tag}-bzr{tree}'.format(tag=tag_version.strip(), tree=tree_revision.strip())
else:
# We're not running the development version, let's use the file.
file_path = AppLocation.get_directory(AppLocation.VersionDir)
file_path = str(AppLocation.get_directory(AppLocation.VersionDir))
file_path = os.path.join(file_path, '.version')
version_file = None
try:

View File

@ -83,30 +83,28 @@ class ServiceItemAction(object):
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
None.
:param text_file: The name of the file.
:return: The file as a single string
:param openlp.core.common.path.Path text_file_path: The path to the file.
: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
file_handle = None
content = None
try:
file_handle = open(text_file, 'r', encoding='utf-8')
if file_handle.read(3) != '\xEF\xBB\xBF':
# no BOM was found
file_handle.seek(0)
content = file_handle.read()
with text_file_path.open('r', encoding='utf-8') as file_handle:
if file_handle.read(3) != '\xEF\xBB\xBF':
# no BOM was found
file_handle.seek(0)
content = file_handle.read()
except (IOError, UnicodeError):
log.exception('Failed to open text file {text}'.format(text=text_file))
finally:
if file_handle:
file_handle.close()
log.exception('Failed to open text file {text}'.format(text=text_file_path))
return content
@ -230,7 +228,7 @@ def validate_thumb(file_path, thumb_path):
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.
@ -247,7 +245,7 @@ def resize_image(image_path, width, height, background='#000000'):
image_ratio = reader.size().width() / reader.size().height()
resize_ratio = width / height
# 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)
elif image_ratio < resize_ratio:
# 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
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 .filedialog import FileDialog
from .screen import ScreenList
from .formattingtags import FormattingTags
from .plugin import PluginStatus, StringContent, Plugin
@ -621,5 +647,5 @@ from .imagemanager import ImageManager
from .renderer import Renderer
from .mediamanageritem import MediaManagerItem
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

View File

@ -25,12 +25,15 @@ The :mod:`db` module provides the core database functionality for OpenLP
"""
import logging
import os
from copy import copy
from urllib.parse import quote_plus as urlquote
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.pool import NullPool
from alembic.migration import MigrationContext
from alembic.operations import Operations
@ -40,6 +43,66 @@ from openlp.core.lib.ui import critical_error_message_box
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):
"""
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 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)
class Metadata(BaseModel):
@ -160,17 +229,15 @@ def upgrade_db(url, upgrade):
metadata_table.create(checkfirst=True)
mapper(Metadata, metadata_table)
version_meta = session.query(Metadata).get('version')
if version_meta is None:
# Tables have just been created - fill the version field with the most recent version
if session.query(Metadata).get('dbversion'):
version = 0
else:
version = upgrade.__version__
if version_meta:
version = int(version_meta.value)
else:
# Due to issues with other checks, if the version is not set in the DB then default to 0
# and let the upgrade function handle the checks
version = 0
version_meta = Metadata.populate(key='version', value=version)
session.add(version_meta)
session.commit()
else:
version = int(version_meta.value)
if version > upgrade.__version__:
session.remove()
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.
"""
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:
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)

View File

@ -24,7 +24,6 @@ The :mod:`~openlp.core.lib.exceptions` module contains custom exceptions
"""
# TODO: Test __init__ & __str__
class ValidationError(Exception):
"""
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 openlp.core.common import Registry
from openlp.core.common import Registry, Settings
from openlp.core.lib import ScreenList, resize_image, image_to_byte
log = logging.getLogger(__name__)
@ -56,7 +56,7 @@ class ImageThread(QtCore.QThread):
"""
Run the thread.
"""
self.image_manager._process()
self.image_manager.process()
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 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.image = None
self.image_bytes = None
@ -119,9 +121,7 @@ class Image(object):
self.timestamp = 0
self.width = width
self.height = height
# FIXME: We assume that the path exist. The caller has to take care that it exists!
if os.path.exists(path):
self.timestamp = os.stat(path).st_mtime
self.timestamp = os.stat(path).st_mtime
self.secondary_priority = Image.secondary_priority
Image.secondary_priority += 1
@ -235,8 +235,15 @@ class ImageManager(QtCore.QObject):
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.
: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)]
if image.image is None:
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):
"""
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)]
if image.image_bytes is None:
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):
"""
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))
if (path, source, width, height) not in self._cache:
log.debug('add_image {path} {source} {width} {height}'.format(path=path, source=source,
width=width, height=height))
if not (path, source, width, height) in self._cache:
image = Image(path, source, background, width, height)
self._cache[(path, source, width, height)] = 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():
self.image_thread.start()
def _process(self):
def process(self):
"""
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:
self._process_cache()
log.debug('_process - ended')
@ -306,7 +327,8 @@ class ImageManager(QtCore.QObject):
# Let's see if the image was requested with specific dimensions
width = self.width if image.width == -1 else image.width
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.
if image.priority == Priority.Normal:
self._conversion_queue.modify_priority(image, Priority.Lowest)

View File

@ -26,12 +26,14 @@ import logging
import os
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.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.ui import create_widget_action, critical_error_message_box
from openlp.core.ui.lib.filedialog import FileDialog
from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD
from openlp.core.ui.lib.toolbar import OpenLPToolbar
@ -309,13 +311,14 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
"""
Add a file to the list widget to make it available for showing
"""
files = FileDialog.getOpenFileNames(self, self.on_new_prompt,
Settings().value(self.settings_section + '/last directory'),
self.on_new_file_masks)
log.info('New files(s) {files}'.format(files=files))
if files:
file_paths, selected_filter = FileDialog.getOpenFileNames(
self, self.on_new_prompt,
Settings().value(self.settings_section + '/last directory'),
self.on_new_file_masks)
log.info('New files(s) {file_paths}'.format(file_paths=file_paths))
if file_paths:
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()
def load_file(self, data):
@ -374,9 +377,8 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
self.list_view.clear()
self.load_list(full_list, target_group)
last_dir = os.path.split(files[0])[0]
Settings().setValue(self.settings_section + '/last directory', last_dir)
Settings().setValue('{section}/{section} files'.format(section=self.settings_section),
self.get_file_list())
Settings().setValue(self.settings_section + '/last directory', Path(last_dir))
Settings().setValue('{section}/{section} files'.format(section=self.settings_section), self.get_file_list())
if duplicates_found:
critical_error_message_box(UiStrings().Duplicate,
translate('OpenLP.MediaManagerItem',
@ -397,13 +399,15 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
def get_file_list(self):
"""
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()):
list_item = self.list_view.item(index)
filename = list_item.data(QtCore.Qt.UserRole)
file_list.append(filename)
return file_list
file_paths.append(str_to_path(filename))
return file_paths
def load_list(self, load_list, target_group):
"""

View File

@ -150,7 +150,7 @@ class Plugin(QtCore.QObject, RegistryProperties):
self.status = PluginStatus.Inactive
# Add the default status to the default settings.
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
# which have a mediamanager need this).
if media_item_class is not None:

View File

@ -40,7 +40,7 @@ class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
"""
super(PluginManager, self).__init__(parent)
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.plugins = []
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.
"""
glob_pattern = os.path.join('openlp', 'plugins', '*', '*plugin.py')
glob_pattern = os.path.join('plugins', '*', '*plugin.py')
extension_loader(glob_pattern)
plugin_classes = Plugin.__subclasses__()
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_STATUS', 'S_OFF', 'S_INITIALIZE', 'S_STANDBY', 'S_WARMUP', 'S_ON', 'S_COOLDOWN',
'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',
'STATUS_STRING', 'PJLINK_VALID_CMD', 'CONNECTION_ERRORS',
'PJLINK_DEFAULT_SOURCES', 'PJLINK_DEFAULT_CODES', 'PJLINK_DEFAULT_ITEMS']
@ -57,35 +57,115 @@ LF = chr(0x0A) # \n
PJLINK_PORT = 4352
TIMEOUT = 30.0
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 = {
'ACKN': ['2', ], # UDP Reply to 'SRCH'
'AVMT': ['1', ], # Shutter option
'CLSS': ['1', ], # PJLink class support query
'ERST': ['1', '2'], # Error status option
'FILT': ['2', ], # Get current filter usage time
'FREZ': ['2', ], # Set freeze/unfreeze picture being projected
'INF1': ['1', ], # Manufacturer name query
'INF2': ['1', ], # Product name query
'INFO': ['1', ], # Other information query
'INNM': ['2', ], # Get Video source input terminal name
'INPT': ['1', ], # Video sources option
'INST': ['1', ], # Input sources available query
'IRES': ['2', ], # Get Video source resolution
'LAMP': ['1', ], # Lamp(s) query (Includes fans)
'LKUP': ['2', ], # UPD Linkup status notification
'MVOL': ['2', ], # Set microphone volume
'NAME': ['1', ], # Projector name query
'PJLINK': ['1', ], # Initial connection
'POWR': ['1', ], # Power option
'RFIL': ['2', ], # Get replacement air filter model number
'RLMP': ['2', ], # Get lamp replacement model number
'RRES': ['2', ], # Get projector recommended video resolution
'SNUM': ['2', ], # Get projector serial number
'SRCH': ['2', ], # UDP broadcast search for available projectors on local network
'SVER': ['2', ], # Get projector software version
'SVOL': ['2', ] # Set speaker volume
'ACKN': {'version': ['2', ],
'description': translate('OpenLP.PJLinkConstants',
'Acknowledge a PJLink SRCH command - returns MAC address.')
},
'AVMT': {'version': ['1', ],
'description': translate('OpenLP.PJLinkConstants',
'Blank/unblank video and/or mute audio.')
},
'CLSS': {'version': ['1', ],
'description': translate('OpenLP.PJLinkConstants',
'Query projector PJLink class support.')
},
'ERST': {'version': ['1', '2'],
'description': translate('OpenLP.PJLinkConstants',
'Query error status from projector. '
'Returns fan/lamp/temp/cover/filter/other error status.')
},
'FILT': {'version': ['2', ],
'description': translate('OpenLP.PJLinkConstants',
'Query number of hours on filter.')
},
'FREZ': {'version': ['2', ],
'description': translate('OpenLP.PJLinkConstants',
'Freeze or unfreeze current image being projected.')
},
'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
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.
@ -313,11 +393,32 @@ ERROR_MSG = {
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
PJLINK_ERST_STATUS = {
'0': ERROR_STRING[E_OK],
'0': 'OK',
'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

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.projector.constants import PJLINK_DEFAULT_CODES
from openlp.core.lib.projector import upgrade
Base = declarative_base(MetaData())
@ -166,13 +167,14 @@ class Projector(CommonBase, Base):
"""
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}", ' \
'manufacturer="{manufacturer}", model="{model}", serial_no="{serial}", other="{other}", ' \
'sources="{sources}", source_list="{source_list}", model_filter="{mfilter}", ' \
'model_lamp="{mlamp}", sw_version="{sw_ver}") >'.format(data=self.id,
ip=self.ip,
port=self.port,
mac=self.mac_adx,
pin=self.pin,
name=self.name,
location=self.location,
@ -189,6 +191,7 @@ class Projector(CommonBase, Base):
sw_ver=self.sw_version)
ip = Column(String(100))
port = Column(String(8))
mac_adx = Column(String(18))
pin = Column(String(20))
name = Column(String(20))
location = Column(String(30))
@ -243,7 +246,9 @@ class ProjectorDB(Manager):
"""
def __init__(self, *args, **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('Session: {session}'.format(session=self.session))
@ -298,7 +303,7 @@ class ProjectorDB(Manager):
:param ip: Host IP/Name
: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)
if projector is None:
# 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.menu_button.setDefaultAction(action)
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)
return True
@ -141,7 +141,7 @@ class SearchEdit(QtWidgets.QLineEdit):
self.menu_button.resize(QtCore.QSize(28, 18))
self.menu_button.setMenu(menu)
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._update_style_sheet()

View File

@ -335,7 +335,7 @@ class ServiceItem(RegistryProperties):
if image and not self.has_original_files and self.name == 'presentations':
file_location = os.path.join(path, file_name)
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))
self._raw_frames.append({'title': file_name, 'image': image, 'path': path,
'display_title': display_title, 'notes': notes})

View File

@ -158,9 +158,8 @@ class Theme(object):
Initialise the theme object.
"""
# basic theme object with defaults
json_dir = os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'core', 'lib', 'json')
json_file = os.path.join(json_dir, 'theme.json')
jsn = get_text_file_string(json_file)
json_path = AppLocation.get_directory(AppLocation.AppDir) / 'core' / 'lib' / 'json' / 'theme.json'
jsn = get_text_file_string(json_path)
jsn = json.loads(jsn)
self.expand_json(jsn)
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.setObjectName('title_label')
parent.welcome_layout.addWidget(parent.title_label)
parent.title_label.setWordWrap(True)
parent.welcome_layout.addSpacing(40)
parent.information_label = QtWidgets.QLabel(parent.welcome_page)
parent.information_label.setWordWrap(True)

View File

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

View File

@ -40,7 +40,8 @@ class AboutForm(QtWidgets.QDialog, UiAboutDialog):
"""
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()
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.languagemanager import format_time
from openlp.core.common.path import path_to_str
from openlp.core.lib import SettingsTab, build_icon
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_layout = QtWidgets.QVBoxLayout(self.display_workaround_group_box)
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.setObjectName('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 '
'OpenLP data files. These files WILL be replaced during a copy.'))
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.alternate_rows_check_box.setText(translate('OpenLP.AdvancedTab', 'Use alternating row colours in lists'))
# Slide Limits
@ -354,6 +359,7 @@ class AdvancedTab(SettingsTab):
default_service_enabled = settings.value('default service enabled')
self.service_name_check_box.setChecked(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.slide_limits = settings.value('slide limits')
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('alternate rows', self.alternate_rows_check_box.isChecked())
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'):
settings.setValue('x11 bypass wm', self.x11_bypass_check_box.isChecked())
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}'
'\n\nThe data directory will be changed when OpenLP is '
'closed.').format(path=new_data_path),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
QtWidgets.QMessageBox.No),
QtWidgets.QMessageBox.No)
defaultButton=QtWidgets.QMessageBox.No)
if answer != QtWidgets.QMessageBox.Yes:
self.data_directory_path_edit.path = AppLocation.get_data_path()
return
# Check if data already exists here.
self.check_data_overwrite(new_data_path)
self.check_data_overwrite(path_to_str(new_data_path))
# 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()
def on_data_directory_copy_check_box_toggled(self):

View File

@ -70,9 +70,9 @@ try:
except ImportError:
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 import RegistryProperties, is_linux
from openlp.core.ui.lib.filedialog import FileDialog
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.
"""
filename = QtWidgets.QFileDialog.getSaveFileName(
file_path, filter_used = FileDialog.getSaveFileName(
self,
translate('OpenLP.ExceptionForm', 'Save Crash Report'),
Settings().value(self.settings_section + '/last directory'),
translate('OpenLP.ExceptionForm', 'Text files (*.txt *.log *.text)'))[0]
if filename:
filename = str(filename).replace('/', os.path.sep)
Settings().setValue(self.settings_section + '/last directory', os.path.dirname(filename))
translate('OpenLP.ExceptionForm', 'Text files (*.txt *.log *.text)'))
if file_path:
Settings().setValue(self.settings_section + '/last directory', file_path.parent)
opts = self._create_report()
report_text = self.report_text.format(version=opts['version'], description=opts['description'],
traceback=opts['traceback'], libs=opts['libs'], system=opts['system'])
filename = str(file_path)
try:
report_file = open(filename, 'w')
try:
@ -212,17 +212,16 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
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,
translate('ImagePlugin.ExceptionDialog',
'Select Attachment'),
Settings().value(self.settings_section +
'/last directory'),
'{text} (*)'.format(text=UiStrings().AllFiles))
log.info('New files(s) {files}'.format(files=str(files)))
if files:
self.file_attachment = str(files)
file_path, filter_used = \
FileDialog.getOpenFileName(self,
translate('ImagePlugin.ExceptionDialog', 'Select Attachment'),
Settings().value(self.settings_section + '/last directory'),
'{text} (*)'.format(text=UiStrings().AllFiles))
log.info('New file {file}'.format(file=file_path))
if file_path:
self.file_attachment = str(file_path)
def __button_state(self, state):
"""

View File

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

View File

@ -29,13 +29,14 @@ import time
import urllib.request
import urllib.parse
import urllib.error
from configparser import ConfigParser, MissingSectionHeaderError, NoOptionError, NoSectionError
from tempfile import gettempdir
from configparser import ConfigParser, MissingSectionHeaderError, NoSectionError, NoOptionError
from PyQt5 import QtCore, QtWidgets
from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, check_directory_exists, \
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.ui import critical_error_message_box
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.web_access = True
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)
self.update_screen_list_combo()
self.application.process_events()
# TODO: Tested at home
self.downloading = translate('OpenLP.FirstTimeWizard', 'Downloading {name}...')
if self.has_run_wizard:
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.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.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.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())
@ -284,7 +283,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self.no_internet_cancel_button.setVisible(False)
# Check if this is a re-run of the 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):
"""
@ -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.image_check_box, 'images/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.song_usage_check_box, 'songusage/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
songs_destination = os.path.join(gettempdir(), 'openlp')
bibles_destination = AppLocation.get_section_data_path('bibles')
themes_destination = AppLocation.get_section_data_path('themes')
bibles_destination = str(AppLocation.get_section_data_path('bibles'))
themes_destination = str(AppLocation.get_section_data_path('themes'))
missed_files = []
# Download songs
for i in range(self.songs_list_widget.count()):
item = self.songs_list_widget.item(i)
if item.checkState() == QtCore.Qt.Checked:
filename, sha256 = item.data(QtCore.Qt.UserRole)
# TODO: Tested at home
self._increment_progress_bar(self.downloading.format(name=filename), 0)
self.previous_size = 0
destination = os.path.join(songs_destination, str(filename))
@ -576,7 +573,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
item = bibles_iterator.value()
if item.parent() and item.checkState(0) == QtCore.Qt.Checked:
bible, sha256 = item.data(0, QtCore.Qt.UserRole)
# TODO: Tested at home
self._increment_progress_bar(self.downloading.format(name=bible), 0)
self.previous_size = 0
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)
if item.checkState() == QtCore.Qt.Checked:
theme, sha256 = item.data(QtCore.Qt.UserRole)
# TODO: Tested at home
self._increment_progress_bar(self.downloading.format(name=theme), 0)
self.previous_size = 0
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
"""
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.qm_list = LanguageManager.get_qm_list()
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 openlp.core.common.uistrings import UiStrings
from openlp.core.common import translate, is_macosx, clean_button_text, Settings
from openlp.core.lib import build_icon
from openlp.core.lib.ui import add_welcome_page
@ -254,8 +255,7 @@ class UiFirstTimeWizard(object):
self.presentation_check_box.setText(translate('OpenLP.FirstTimeWizard',
'Presentations Show .ppt, .odp and .pdf 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'
'phone app'))
self.remote_check_box.setText(str(UiStrings().WebDownloadText))
self.song_usage_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Song Usage Monitor'))
self.alert_check_box.setText(translate('OpenLP.FirstTimeWizard',
'Alerts Display informative messages while showing other slides'))

View File

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

View File

@ -51,7 +51,8 @@ class FormattingTagForm(QtWidgets.QDialog, Ui_FormattingTagDialog, FormattingTag
"""
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._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, 3).text())
if error:
QtWidgets.QMessageBox.warning(self, translate('OpenLP.FormattingTagForm', 'Validation Error'), error,
QtWidgets.QMessageBox.Ok)
QtWidgets.QMessageBox.warning(self, translate('OpenLP.FormattingTagForm', 'Validation Error'), error)
self.tag_table_widget.selectRow(count)
return
count += 1
@ -198,6 +198,5 @@ class FormattingTagForm(QtWidgets.QDialog, Ui_FormattingTagDialog, FormattingTag
if tag:
self.tag_table_widget.setItem(pre_row, 3, QtWidgets.QTableWidgetItem(tag))
if errors:
QtWidgets.QMessageBox.warning(self, translate('OpenLP.FormattingTagForm', 'Validation Error'), errors,
QtWidgets.QMessageBox.Ok)
QtWidgets.QMessageBox.warning(self, translate('OpenLP.FormattingTagForm', 'Validation Error'), errors)
self.tag_table_widget.resizeRowsToContents()

View File

@ -27,6 +27,7 @@ import logging
from PyQt5 import QtCore, QtGui, QtWidgets
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.ui.lib import ColorButton, PathEdit
@ -172,7 +173,8 @@ class GeneralTab(SettingsTab):
self.logo_layout.setObjectName('logo_layout')
self.logo_file_label = QtWidgets.QLabel(self.logo_group_box)
self.logo_file_label.setObjectName('logo_file_label')
self.logo_file_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_color_label = QtWidgets.QLabel(self.logo_group_box)
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.start_paused_check_box.setText(translate('OpenLP.GeneralTab', 'Start background audio paused'))
self.repeat_list_check_box.setText(translate('OpenLP.GeneralTab', 'Repeat track list'))
self.logo_file_path_edit.dialog_caption = dialog_caption = translate('OpenLP.AdvancedTab', 'Select Logo File')
self.logo_file_path_edit.dialog_caption = translate('OpenLP.AdvancedTab', 'Select Logo File')
self.logo_file_path_edit.filters = '{text};;{names} (*)'.format(
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

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

@ -20,12 +20,13 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
from enum import Enum
import os.path
from PyQt5 import QtCore, QtWidgets
from openlp.core.common import UiStrings, translate
from openlp.core.common.path import Path, path_to_str, str_to_path
from openlp.core.lib import build_icon
from openlp.core.ui.lib.filedialog import FileDialog
class PathType(Enum):
@ -38,25 +39,17 @@ class PathEdit(QtWidgets.QWidget):
The :class:`~openlp.core.ui.lib.pathedit.PathEdit` class subclasses QWidget to create a custom widget for use when
a file or directory needs to be selected.
"""
pathChanged = QtCore.pyqtSignal(str)
pathChanged = QtCore.pyqtSignal(Path)
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 dialog_caption: Used to customise the caption in the QFileDialog.
:param dialog_caption: str
:param default_path: The default path. This is set as the path when the revert button is clicked
:type default_path: str
:param show_revert: Used to determin if the 'revert button' should be visible.
:type show_revert: bool
:return: None
:param QtWidget.QWidget | None: The parent of the widget. This is just passed to the super method.
:param str dialog_caption: Used to customise the caption in the QFileDialog.
:param openlp.core.common.path.Path default_path: The default path. This is set as the path when the revert
button is clicked
:param bool show_revert: Used to determine if the 'revert button' should be visible.
:rtype: None
"""
super().__init__(parent)
@ -70,16 +63,12 @@ class PathEdit(QtWidgets.QWidget):
def _setup(self, show_revert):
"""
Set up the widget
:param show_revert: Show or hide the revert button
:type show_revert: bool
:return: None
:param bool show_revert: Show or hide the revert button
:rtype: None
"""
widget_layout = QtWidgets.QHBoxLayout()
widget_layout.setContentsMargins(0, 0, 0, 0)
self.line_edit = QtWidgets.QLineEdit(self)
self.line_edit.setText(self._path)
widget_layout.addWidget(self.line_edit)
self.browse_button = QtWidgets.QToolButton(self)
self.browse_button.setIcon(build_icon(':/general/general_open.png'))
@ -101,7 +90,7 @@ class PathEdit(QtWidgets.QWidget):
A property getter method to return the selected path.
:return: The selected path
:rtype: str
:rtype: openlp.core.common.path.Path
"""
return self._path
@ -110,12 +99,13 @@ class PathEdit(QtWidgets.QWidget):
"""
A Property setter method to set the selected path
:param path: The path to set the widget to
:type path: str
:param openlp.core.common.path.Path path: The path to set the widget to
:rtype: None
"""
self._path = path
self.line_edit.setText(path)
self.line_edit.setToolTip(path)
text = path_to_str(path)
self.line_edit.setText(text)
self.line_edit.setToolTip(text)
@property
def path_type(self):
@ -124,7 +114,7 @@ class PathEdit(QtWidgets.QWidget):
selecting a file or directory.
:return: The type selected
:rtype: Enum of PathEdit
:rtype: PathType
"""
return self._path_type
@ -133,8 +123,8 @@ class PathEdit(QtWidgets.QWidget):
"""
A Property setter method to set the path type
:param path: The type of path to select
:type path: Enum of PathEdit
:param PathType path_type: The type of path to select
:rtype: None
"""
self._path_type = path_type
self.update_button_tool_tips()
@ -142,7 +132,8 @@ class PathEdit(QtWidgets.QWidget):
def update_button_tool_tips(self):
"""
Called to update the tooltips on the buttons. This is changing path types, and when the widget is initalised
:return: None
:rtype: None
"""
if self._path_type == PathType.Directories:
self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for directory.'))
@ -156,21 +147,20 @@ class PathEdit(QtWidgets.QWidget):
A handler to handle a click on the browse button.
Show the QFileDialog and process the input from the user
:return: None
:rtype: None
"""
caption = self.dialog_caption
path = ''
path = None
if self._path_type == PathType.Directories:
if not caption:
caption = translate('OpenLP.PathEdit', 'Select Directory')
path = QtWidgets.QFileDialog.getExistingDirectory(self, caption,
self._path, QtWidgets.QFileDialog.ShowDirsOnly)
path = FileDialog.getExistingDirectory(self, caption, self._path, FileDialog.ShowDirsOnly)
elif self._path_type == PathType.Files:
if not caption:
caption = self.dialog_caption = translate('OpenLP.PathEdit', 'Select File')
path, filter_used = QtWidgets.QFileDialog.getOpenFileName(self, caption, self._path, self.filters)
path, filter_used = FileDialog.getOpenFileName(self, caption, self._path, self.filters)
if path:
path = os.path.normpath(path)
self.on_new_path(path)
def on_revert_button_clicked(self):
@ -178,16 +168,19 @@ class PathEdit(QtWidgets.QWidget):
A handler to handle a click on the revert button.
Set the new path to the value of the default_path instance variable.
:return: None
:rtype: None
"""
self.on_new_path(self.default_path)
def on_line_edit_editing_finished(self):
"""
A handler to handle when the line edit has finished being edited.
:return: None
:rtype: None
"""
self.on_new_path(self.line_edit.text())
path = str_to_path(self.line_edit.text())
self.on_new_path(path)
def on_new_path(self, path):
"""
@ -195,10 +188,8 @@ class PathEdit(QtWidgets.QWidget):
Emits the pathChanged Signal
:param path: The new path
:type path: str
:return: None
:param openlp.core.common.path.Path path: The new path
:rtype: None
"""
if self._path != path:
self.path = path

View File

@ -25,11 +25,12 @@ The :mod:``wizard`` module provides generic wizard tools for OpenLP.
import logging
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.lib import build_icon
from openlp.core.lib.ui import add_welcome_page
from openlp.core.ui.lib.filedialog import FileDialog
log = logging.getLogger(__name__)
@ -50,13 +51,13 @@ class WizardStrings(object):
# These strings should need a good reason to be retranslated elsewhere.
FinishedImport = translate('OpenLP.Ui', 'Finished import.')
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')
ImportingType = translate('OpenLP.Ui', 'Importing "%s"...')
ImportingType = translate('OpenLP.Ui', 'Importing "{source}"...')
ImportSelect = translate('OpenLP.Ui', 'Select Import Source')
ImportSelectLong = translate('OpenLP.Ui', 'Select the import format and the location to import from.')
OpenTypeFile = translate('OpenLP.Ui', 'Open %s File')
OpenTypeFolder = translate('OpenLP.Ui', 'Open %s Folder')
OpenTypeFile = translate('OpenLP.Ui', 'Open {file_type} File')
OpenTypeFolder = translate('OpenLP.Ui', 'Open {folder_name} Folder')
PercentSymbolFormat = translate('OpenLP.Ui', '%p%')
Ready = translate('OpenLP.Ui', 'Ready.')
StartingImport = translate('OpenLP.Ui', 'Starting import...')
@ -93,7 +94,10 @@ class OpenLPWizard(QtWidgets.QWizard, RegistryProperties):
"""
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.with_progress_page = add_progress_page
self.setFixedWidth(640)
@ -275,37 +279,38 @@ class OpenLPWizard(QtWidgets.QWizard, RegistryProperties):
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 editbox: An editbox (QLineEdit).
:param 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 title: The title of the dialog.
:param QtWidgets.QLineEdit editbox: An QLineEdit.
:param str setting_name: The place where to save the last opened directory.
:param str filters: The file extension filters. It should contain the file description
as well as the file extension. For example::
'OpenLP 2 Databases (*.sqlite)'
:rtype: None
"""
if filters:
filters += ';;'
filters += '%s (*)' % UiStrings().AllFiles
filename, filter_used = QtWidgets.QFileDialog.getOpenFileName(
self, title, os.path.dirname(Settings().value(self.plugin.settings_section + '/' + setting_name)),
filters)
if filename:
editbox.setText(filename)
Settings().setValue(self.plugin.settings_section + '/' + setting_name, filename)
file_path, filter_used = FileDialog.getOpenFileName(
self, title, Settings().value(self.plugin.settings_section + '/' + setting_name), filters)
if file_path:
editbox.setText(str(file_path))
Settings().setValue(self.plugin.settings_section + '/' + setting_name, file_path.parent)
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 editbox: An editbox (QLineEdit).
:param setting_name: The place where to save the last opened directory.
:param str title: The title of the dialog.
:param QtWidgets.QLineEdit editbox: An QLineEditbox.
: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),
QtWidgets.QFileDialog.ShowDirsOnly)
if folder:
editbox.setText(folder)
Settings().setValue(self.plugin.settings_section + '/' + setting_name, folder)
if folder_path:
editbox.setText(str(folder_path))
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,\
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.theme import BackgroundType
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
# regressions on other platforms.
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.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.set_transparency(False)
@ -259,7 +260,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
background_color.setNamedColor(Settings().value('core/logo background color'))
if not background_color.isValid():
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)
self.initial_fame = QtGui.QImage(
self.screen['size'].width(),
@ -484,7 +485,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
service_item = ServiceItem()
service_item.title = '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)
service_item.add_from_command(path,
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
"""
self.playerlist.next()
self.playlist.next()
def go_to(self, index):
"""

View File

@ -34,12 +34,15 @@ from tempfile import gettempdir
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
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.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, \
ShortcutListForm, FormattingTagForm, PreviewController
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.projector.manager import ProjectorManager
from openlp.core.ui.lib.dockwidget import OpenLPDockWidget
from openlp.core.ui.lib.filedialog import FileDialog
from openlp.core.ui.lib.mediadockmanager import MediaDockManager
log = logging.getLogger(__name__)
MEDIA_MANAGER_STYLE = """
@ -305,9 +310,9 @@ class Ui_MainWindow(object):
# Give QT Extra Hint that this is an About Menu Item
self.about_item.setMenuRole(QtWidgets.QAction.AboutRole)
if is_win():
self.local_help_file = os.path.join(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():
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')
self.user_manual_item = create_action(main_window, 'userManualItem', icon=':/system/system_help_contents.png',
can_shortcuts=True, category=UiStrings().Help,
@ -370,7 +375,7 @@ class Ui_MainWindow(object):
"""
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_import_menu.setTitle(translate('OpenLP.MainWindow', '&Import'))
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()
self.about_form = AboutForm(self)
MediaController()
if Registry().get_flag('no_web_server'):
websockets.WebSocketServer()
server.HttpServer()
SettingsForm(self)
self.formatting_tag_form = FormattingTagForm(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.update_theme_images.triggered.connect(self.on_update_theme_images)
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_import_item.triggered.connect(self.on_settings_import_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
"""
path = AppLocation.get_data_path()
path = str(AppLocation.get_data_path())
QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(path))
def on_update_theme_images(self):
@ -803,7 +811,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
"""
self.formatting_tag_form.exec()
def on_settings_configure_iem_clicked(self):
def on_settings_configure_item_clicked(self):
"""
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])
# 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')
check_directory_exists(temp_directory)
check_directory_exists(Path(temp_directory))
temp_config = os.path.join(temp_directory, os.path.basename(import_file_name))
shutil.copyfile(import_file_name, temp_config)
settings = Settings()
import_settings = Settings(temp_config, Settings.IniFormat)
# Convert image files
log.info('hook upgrade_plugin_settings')
self.plugin_manager.hook_upgrade_plugin_settings(import_settings)
# Remove/rename old settings to prepare the import.
import_settings.remove_obsolete_settings()
# Upgrade settings to prepare the import.
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
# load what we can from it, and just silently ignore anything we don't recognise.
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'),
translate('OpenLP.MainWindow',
'OpenLP will now close. Imported settings will '
'be applied the next time you start OpenLP.'),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok))
'be applied the next time you start OpenLP.'))
self.settings_imported = True
self.clean_up()
QtCore.QCoreApplication.exit()
@ -930,89 +938,17 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
"""
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,
translate('OpenLP.MainWindow', 'Export Settings File'),
'',
None,
translate('OpenLP.MainWindow', 'OpenLP Settings (*.conf)'))
if not export_file_name:
if not export_file_path:
return
# Make sure it's a .conf file.
if not export_file_name.endswith('conf'):
export_file_name += '.conf'
temp_file = os.path.join(gettempdir(), 'openlp', 'exportConf.tmp')
# Make sure it's a .conf file.
export_file_path = export_file_path.with_suffix('.conf')
self.save_settings()
setting_sections = []
# 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))
Settings().export(export_file_path)
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.
"""
if modified:
title = '{title} - {name}*'.format(title=UiStrings().OLP, name=file_name)
title = '{title} - {name}*'.format(title=UiStrings().OpenLP, name=file_name)
else:
title = '{title} - {name}'.format(title=UiStrings().OLP, name=file_name)
title = '{title} - {name}'.format(title=UiStrings().OpenLP, name=file_name)
self.setWindowTitle(title)
def show_status_message(self, message):
@ -1271,7 +1207,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
settings.remove('custom slide')
settings.remove('service')
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.beginGroup(self.ui_settings_section)
self.move(settings.value('main window position'))
@ -1295,7 +1231,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
log.debug('Saving QSettings')
settings = Settings()
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.beginGroup(self.ui_settings_section)
settings.setValue('main window position', self.pos())
@ -1316,7 +1252,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
self.recent_files_menu.clear()
for file_id, filename in enumerate(recent_files_to_display):
log.debug('Recent file name: {name}'.format(name=filename))
# TODO: Should be good
action = create_action(self, '',
text='&{n} {name}'.format(n=file_id + 1,
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')
# Change the location of data directory in config file.
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.
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')
self.application.set_normal_cursor()

View File

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

View File

@ -19,40 +19,54 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# 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 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__)
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
def getOpenFileNames(parent, *args, **kwargs):
"""
Reimplement getOpenFileNames to fix the way it returns some file names that url encoded when selecting multiple
files
"""
files, filter_used = QtWidgets.QFileDialog.getOpenFileNames(parent, *args, **kwargs)
file_list = []
for file in files:
if not os.path.exists(file):
log.info('File not found. Attempting to unquote.')
file = parse.unquote(file)
if not os.path.exists(file):
log.error('File {text} not found.'.format(text=file))
QtWidgets.QMessageBox.information(parent, UiStrings().FileNotFound,
UiStrings().FileNotFoundMessage.format(name=file))
continue
file_list.append(file)
return file_list
media = Registry().get('media_controller')
live = Registry().get('live_controller')
status = media.media_play(live, False)
return {'results': {'success': status}}
@media_endpoint.route('pause')
@requires_auth
def media_pause(request):
"""
Handles requests for pausing media
:param request: The http request object.
"""
media = Registry().get('media_controller')
live = Registry().get('live_controller')
status = media.media_pause(live)
return {'results': {'success': status}}
@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
from PyQt5 import QtCore, QtWidgets
from openlp.core.api.http import register_endpoint
from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, \
extension_loader, translate
from openlp.core.lib import ItemCapabilities
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.common import AppLocation
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.mediaplayer import MediaPlayer
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)
# Signals for background video
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('mediaitem_media_rebuild', self._set_active_players)
Registry().register_function('mediaitem_suffixes', self._generate_extensions_lists)
register_endpoint(media_endpoint)
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.
"""
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')
extension_loader(glob_pattern, ['mediaplayer.py'])
player_classes = MediaPlayer.__subclasses__()
@ -294,7 +297,9 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
triggers=controller.send_to_plugins)
controller.position_label = QtWidgets.QLabel()
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.setMinimumSize(90, 0)
controller.position_label.setObjectName('position_label')
controller.mediabar.add_toolbar_widget(controller.position_label)
# Build the seek_slider.
@ -430,7 +435,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
log.debug('video mediatype: ' + str(controller.media_info.media_type))
# dont care about actual theme, set a black 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!
autoplay = False
# Preview requested
@ -466,9 +471,10 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
player = self.media_players[used_players[0]]
if suffix not in player.video_extensions_list and suffix not in player.audio_extensions_list:
# Media could not be loaded correctly
critical_error_message_box(translate('MediaPlugin.MediaItem', 'Unsupported Media File'),
translate('MediaPlugin.MediaItem', 'File %s not supported using player %s') %
(service_item.get_frame_path(), used_players[0]))
critical_error_message_box(
translate('MediaPlugin.MediaItem', 'Unsupported Media File'),
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
media_data = MediaInfoWrapper.parse(service_item.get_frame_path())
# duration returns in milli seconds
@ -610,6 +616,14 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
"""
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):
"""
Responds to the request to play a loaded video
@ -684,6 +698,14 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
"""
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):
"""
Responds to the request to pause a loaded video
@ -724,6 +746,14 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
"""
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):
"""
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].set_visible(display, False)
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['playbackStop'].setDisabled(True)
controller.mediabar.actions['playbackPause'].setVisible(False)
@ -800,7 +835,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
display.override = {}
self.current_media_players[controller.controller_type].reset(display)
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]
def media_hide(self, msg):
@ -815,7 +850,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
display = self._define_display(self.live_controller)
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].pause(display)
self.media_pause(display.controller)
self.current_media_players[self.live_controller.controller_type].set_visible(display, False)
def media_blank(self, msg):
@ -833,7 +868,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
display = self._define_display(self.live_controller)
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].pause(display)
self.media_pause(display.controller)
self.current_media_players[self.live_controller.controller_type].set_visible(display, False)
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 \
self.current_media_players[self.live_controller.controller_type].get_live_state() != \
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)
# Start Timer for ui updates
if not self.live_timer.isActive():

View File

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

View File

@ -41,7 +41,8 @@ class PluginForm(QtWidgets.QDialog, Ui_PluginViewDialog, RegistryProperties):
"""
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.programatic_change = False
self.setupUi(self)
@ -60,7 +61,6 @@ class PluginForm(QtWidgets.QDialog, Ui_PluginViewDialog, RegistryProperties):
self._clear_details()
self.programatic_change = True
plugin_list_width = 0
# TODO: Tested at home
for plugin in self.plugin_manager.plugins:
item = QtWidgets.QListWidgetItem(self.plugin_list_widget)
# 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()
else:
self.active_plugin.toggle_status(PluginStatus.Inactive)
# TODO: Tested at home
status_text = translate('OpenLP.PluginForm', '{name} (Inactive)')
if self.active_plugin.status == PluginStatus.Active:
status_text = translate('OpenLP.PluginForm', '{name} (Active)')

View File

@ -125,8 +125,8 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
"""
Constructor
"""
super(PrintServiceForm, self).__init__(Registry().get('main_window'),
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
super(PrintServiceForm, self).__init__(Registry().get('main_window'), QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
self.printer = QtPrintSupport.QPrinter()
self.print_dialog = QtPrintSupport.QPrintDialog(self.printer, self)
self.document = QtGui.QTextDocument()
@ -176,7 +176,7 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
html_data = self._add_element('html')
self._add_element('head', parent=html_data)
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)
if not custom_css:
custom_css = DEFAULT_CSS

View File

@ -142,7 +142,8 @@ class ProjectorEditForm(QtWidgets.QDialog, Ui_ProjectorEditForm):
editProjector = QtCore.pyqtSignal(object)
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.setupUi(self)
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, \
S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP
from openlp.core.lib.projector.db import ProjectorDB
from openlp.core.lib.projector.pjlink1 import 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.sourceselectform import SourceSelectTabs, SourceSelectSingle
@ -290,6 +291,8 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
self.settings_section = 'projector'
self.projectordb = projectordb
self.projector_list = []
self.pjlink_udp = PJLinkUDP()
self.pjlink_udp.projector_list = self.projector_list
self.source_select_form = None
def bootstrap_initialise(self):
@ -417,9 +420,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
:param opt: Needed by PyQt5
"""
try:
ip = opt.link.ip
projector = opt
projector.link.set_shutter_closed()
opt.link.set_shutter_closed()
except AttributeError:
for list_item in self.projector_list_widget.selectedItems():
if list_item is None:
@ -452,9 +453,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
:param opt: Needed by PyQt5
"""
try:
ip = opt.link.ip
projector = opt
projector.link.connect_to_host()
opt.link.connect_to_host()
except AttributeError:
for list_item in self.projector_list_widget.selectedItems():
if list_item is None:
@ -524,7 +523,8 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
self.projector_list = new_list
list_item = self.projector_list_widget.takeItem(self.projector_list_widget.currentRow())
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:
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
"""
try:
ip = opt.link.ip
projector = opt
projector.link.disconnect_from_host()
opt.link.disconnect_from_host()
except AttributeError:
for list_item in self.projector_list_widget.selectedItems():
if list_item is None:
@ -570,9 +568,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
:param opt: Needed by PyQt5
"""
try:
ip = opt.link.ip
projector = opt
projector.link.set_power_off()
opt.link.set_power_off()
except AttributeError:
for list_item in self.projector_list_widget.selectedItems():
if list_item is None:
@ -590,9 +586,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
:param opt: Needed by PyQt5
"""
try:
ip = opt.link.ip
projector = opt
projector.link.set_power_on()
opt.link.set_power_on()
except AttributeError:
for list_item in self.projector_list_widget.selectedItems():
if list_item is None:
@ -610,9 +604,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
:param opt: Needed by PyQt5
"""
try:
ip = opt.link.ip
projector = opt
projector.link.set_shutter_open()
opt.link.set_shutter_open()
except AttributeError:
for list_item in self.projector_list_widget.selectedItems():
if list_item is None:
@ -659,19 +651,34 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
data=translate('OpenLP.ProjectorManager', 'Closed')
if projector.link.shutter
else translate('OpenLP', 'Open'))
message = '%s<b>%s</b>: %s<br />' % (message,
translate('OpenLP.ProjectorManager', 'Current source input is'),
projector.link.source)
message = '{msg}<b>{source}</b>: {selected}<br />'.format(msg=message,
source=translate('OpenLP.ProjectorManager',
'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
for item in projector.link.lamp:
message += '<b>{title} {count}</b> {status} '.format(title=translate('OpenLP.ProjectorManager',
'Lamp'),
count=count,
status=translate('OpenLP.ProjectorManager',
' is on')
'ON')
if item['On']
else translate('OpenLP.ProjectorManager',
'is off'))
'OFF'))
message += '<b>{title}</b>: {hours}<br />'.format(title=translate('OpenLP.ProjectorManager', 'Hours'),
hours=item['Hours'])
@ -973,7 +980,7 @@ class ProjectorItem(QtCore.QObject):
self.poll_time = None
self.socket_timeout = None
self.status = S_NOT_CONNECTED
super(ProjectorItem, self).__init__()
super().__init__()
def not_implemented(function):

View File

@ -233,7 +233,8 @@ class SourceSelectTabs(QtWidgets.QDialog):
:param projectordb: ProjectorDB session to use
"""
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.projectordb = projectordb
self.edit = edit
@ -388,12 +389,13 @@ class SourceSelectSingle(QtWidgets.QDialog):
"""
log.debug('Initializing SourceSelectSingle()')
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
if self.edit:
title = translate('OpenLP.SourceSelectForm', 'Edit Projector Source Text')
self.title = translate('OpenLP.SourceSelectForm', 'Edit Projector Source Text')
else:
title = translate('OpenLP.SourceSelectForm', 'Select Projector Source')
self.title = translate('OpenLP.SourceSelectForm', 'Select Projector Source')
self.setObjectName('source_select_single')
self.setWindowIcon(build_icon(':/icon/openlp-log.svg'))
self.setModal(True)

View File

@ -37,8 +37,8 @@ class ServiceItemEditForm(QtWidgets.QDialog, Ui_ServiceItemEditDialog, RegistryP
"""
Constructor
"""
super(ServiceItemEditForm, self).__init__(Registry().get('main_window'),
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
super(ServiceItemEditForm, self).__init__(Registry().get('main_window'), QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
self.setupUi(self)
self.item_list = []
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, \
RegistryMixin, check_directory_exists, UiStrings, translate, split_filename, delete_file
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.ui import critical_error_message_box, create_widget_action, find_and_set_in_combo_box
from openlp.core.ui import ServiceNoteForm, ServiceItemEditForm, StartTimeForm
from openlp.core.ui.lib import OpenLPToolbar
from openlp.core.common.languagemanager import format_time
from openlp.core.ui.lib.filedialog import FileDialog
class ServiceManagerList(QtWidgets.QTreeWidget):
@ -66,6 +68,12 @@ class ServiceManagerList(QtWidgets.QTreeWidget):
elif event.key() == QtCore.Qt.Key_Down:
self.service_manager.on_move_selection_down()
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:
self.service_manager.on_delete_from_service()
event.accept()
@ -217,7 +225,7 @@ class Ui_ServiceManager(object):
self.service_manager_list.itemExpanded.connect(self.expanded)
# Last little bits of setting up
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
self.dnd_menu = QtWidgets.QMenu()
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.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')
def file_name(self):
@ -428,18 +436,17 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
elif result == QtWidgets.QMessageBox.Save:
self.decide_save_method()
if not load_file:
file_name, filter_used = QtWidgets.QFileDialog.getOpenFileName(
file_path, filter_used = FileDialog.getOpenFileName(
self.main_window,
translate('OpenLP.ServiceManager', 'Open File'),
Settings().value(self.main_window.service_manager_settings_section + '/last directory'),
translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz *.oszl)'))
if not file_name:
if not file_path:
return False
else:
file_name = load_file
Settings().setValue(self.main_window.service_manager_settings_section + '/last directory',
split_filename(file_name)[0])
self.load_file(file_name)
file_path = str_to_path(load_file)
Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', file_path.parent)
self.load_file(str(file_path))
def save_modified_service(self):
"""
@ -470,7 +477,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
self.set_file_name('')
self.service_id += 1
self.set_modified(False)
Settings().setValue('servicemanager/last file', '')
Settings().setValue('servicemanager/last file', None)
self.plugin_manager.new_service_created()
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]
service_file_name = '{name}.osj'.format(name=base_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()
write_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)
save_file = os.path.join(self.service_path, audio_to)
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):
shutil.copy(audio_from, save_file)
zip_file.write(audio_from, audio_to)
@ -608,7 +615,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
success = False
self.main_window.add_recent_file(path_file_name)
self.set_modified(False)
delete_file(temp_file_name)
delete_file(Path(temp_file_name))
return success
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]
service_file_name = '{name}.osj'.format(name=base_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()
self.application.set_busy_cursor()
# Number of items + 1 to zip it
@ -663,7 +670,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
return self.save_file_as()
self.main_window.add_recent_file(path_file_name)
self.set_modified(False)
delete_file(temp_file_name)
delete_file(Path(temp_file_name))
return success
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)
else:
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)
# 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.
@ -768,10 +775,10 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
self.set_file_name(file_name)
self.main_window.display_progress_bar(len(items))
self.process_service_items(items)
delete_file(p_file)
delete_file(Path(p_file))
self.main_window.add_recent_file(file_name)
self.set_modified(False)
Settings().setValue('servicemanager/last file', file_name)
Settings().setValue('servicemanager/last file', Path(file_name))
else:
critical_error_message_box(message=translate('OpenLP.ServiceManager', 'File is not a valid service.'))
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
no service present.
"""
file_name = Settings().value('servicemanager/last file')
file_name = str_to_path(Settings().value('servicemanager/last file'))
if file_name:
self.load_file(file_name)
@ -1119,6 +1126,35 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
return
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):
"""
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.
"""
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)
if os.path.exists(os.path.join(self.service_path, 'audio')):
shutil.rmtree(os.path.join(self.service_path, 'audio'), True)

View File

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

View File

@ -26,6 +26,7 @@ import logging
from PyQt5 import QtCore, QtWidgets
from openlp.core.api import ApiTab
from openlp.core.common import Registry, RegistryProperties
from openlp.core.lib import build_icon
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_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.setupUi(self)
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.advanced_tab = None
self.player_tab = None
self.api_tab = None
def exec(self):
"""
Execute the form
"""
# load all the
# load all the widgets
self.setting_list_widget.blockSignals(True)
self.setting_list_widget.clear()
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.player_tab)
self.insert_tab(self.projector_tab)
self.insert_tab(self.api_tab)
for plugin in self.plugin_manager.plugins:
if plugin.settings_tab:
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.setData(QtCore.Qt.UserRole, tab_widget.tab_title)
self.setting_list_widget.addItem(list_item)
tab_widget.load()
def accept(self):
"""
@ -153,10 +158,13 @@ class SettingsForm(QtWidgets.QDialog, Ui_SettingsDialog, RegistryProperties):
self.advanced_tab = AdvancedTab(self)
# Advanced tab
self.player_tab = PlayerTab(self)
# Api tab
self.api_tab = ApiTab(self)
self.general_tab.post_set_up()
self.themes_tab.post_set_up()
self.advanced_tab.post_set_up()
self.player_tab.post_set_up()
self.api_tab.post_set_up()
for plugin in self.plugin_manager.plugins:
if plugin.settings_tab:
plugin.settings_tab.post_set_up()

View File

@ -44,7 +44,8 @@ class ShortcutListForm(QtWidgets.QDialog, Ui_ShortcutListDialog, RegistryPropert
"""
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.changed_actions = {}
self.action_list = ActionList.get_instance()
@ -279,9 +280,7 @@ class ShortcutListForm(QtWidgets.QDialog, Ui_ShortcutListDialog, RegistryPropert
return
if QtWidgets.QMessageBox.question(self, translate('OpenLP.ShortcutListDialog', 'Restore Default Shortcuts'),
translate('OpenLP.ShortcutListDialog', 'Do you want to restore all '
'shortcuts to their defaults?'),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
QtWidgets.QMessageBox.No)
'shortcuts to their defaults?')
) == QtWidgets.QMessageBox.No:
return
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
getattr(self,
'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):
"""
@ -1530,6 +1534,9 @@ class LiveController(RegistryMixin, OpenLPMixin, SlideController):
slidecontroller_live_next = QtCore.pyqtSignal()
slidecontroller_live_previous = QtCore.pyqtSignal()
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):
"""

View File

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

View File

@ -28,6 +28,7 @@ import os
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.path import Path, path_to_str, str_to_path
from openlp.core.lib.theme import BackgroundType, BackgroundGradientType
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui import ThemeLayoutForm
@ -187,7 +188,8 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
"""
background_image = BackgroundType.to_string(BackgroundType.Image)
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'),
translate('OpenLP.ThemeWizard', 'You have not selected a '
'background image. Please select one before continuing.'))
@ -316,11 +318,11 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
self.setField('background_type', 1)
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image):
self.image_color_button.color = self.theme.background_border_color
self.image_path_edit.path = self.theme.background_filename
self.image_path_edit.path = str_to_path(self.theme.background_filename)
self.setField('background_type', 2)
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
self.video_color_button.color = self.theme.background_border_color
self.video_path_edit.path = self.theme.background_filename
self.video_path_edit.path = str_to_path(self.theme.background_filename)
self.setField('background_type', 4)
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Transparent):
self.setField('background_type', 3)
@ -448,18 +450,18 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
"""
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.
"""
self.theme.background_filename = filename
self.theme.background_filename = path_to_str(file_path)
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.
"""
self.theme.background_filename = filename
self.theme.background_filename = path_to_str(file_path)
self.set_background_page_values()
def on_main_color_changed(self, color):

View File

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

View File

@ -25,6 +25,7 @@ The Create/Edit theme wizard
from PyQt5 import QtCore, QtGui, QtWidgets
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.theme import HorizontalType, BackgroundType, BackgroundGradientType
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 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.lib import Plugin, StringContent, build_icon
from openlp.core.lib.db import Manager
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.plugins.alerts.endpoint import api_alerts_endpoint, alerts_endpoint
from openlp.plugins.alerts.forms import AlertForm
from openlp.plugins.alerts.lib import AlertsManager, AlertsTab
from openlp.plugins.alerts.lib.db import init_schema
@ -88,21 +90,20 @@ JAVASCRIPT = """
}
}
"""
# TODO: Verify format() with variable templates
CSS = """
#alert {
#alert {{
position: absolute;
left: 0px;
top: 0px;
z-index: 10;
width: 100%%;
vertical-align: %s;
font-family: %s;
font-size: %spt;
color: %s;
background-color: %s;
width: 100%;
vertical-align: {vertical_align};
font-family: {font_family};
font-size: {font_size:d}pt;
color: {color};
background-color: {background_color};
word-wrap: break-word;
}
}}
"""
HTML = """
@ -141,6 +142,8 @@ class AlertsPlugin(Plugin):
AlertsManager(self)
self.manager = Manager('alerts', init_schema)
self.alert_form = AlertForm(self)
register_endpoint(alerts_endpoint)
register_endpoint(api_alerts_endpoint)
def add_tools_menu_item(self, tools_menu):
"""
@ -228,8 +231,11 @@ class AlertsPlugin(Plugin):
Add CSS to the main display.
"""
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,
self.settings_tab.background_color)
return CSS.format(vertical_align=align,
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
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
"""
super(AlertForm, self).__init__(Registry().get('main_window'),
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
super(AlertForm, self).__init__(Registry().get('main_window'), QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
self.manager = plugin.manager
self.plugin = plugin
self.item_id = None
@ -180,9 +180,7 @@ class AlertForm(QtWidgets.QDialog, Ui_AlertDialog):
translate('AlertsPlugin.AlertForm', 'No Parameter Found'),
translate('AlertsPlugin.AlertForm',
'You have not entered a parameter to be replaced.\n'
'Do you want to continue anyway?'),
QtWidgets.QMessageBox.StandardButtons(
QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Yes)
'Do you want to continue anyway?')
) == QtWidgets.QMessageBox.No:
self.parameter_edit.setFocus()
return False
@ -193,9 +191,7 @@ class AlertForm(QtWidgets.QDialog, Ui_AlertDialog):
translate('AlertsPlugin.AlertForm', 'No Placeholder Found'),
translate('AlertsPlugin.AlertForm',
'The alert text does not contain \'<>\'.\n'
'Do you want to continue anyway?'),
QtWidgets.QMessageBox.StandardButtons(
QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Yes)
'Do you want to continue anyway?')
) == QtWidgets.QMessageBox.No:
self.parameter_edit.setFocus()
return False

View File

@ -105,7 +105,7 @@ class AlertsTab(SettingsTab):
self.timeout_label.setText(translate('AlertsPlugin.AlertsTab', 'Alert timeout:'))
self.timeout_spin_box.setSuffix(' {unit}'.format(unit=UiStrings().Seconds))
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):
"""

View File

@ -22,9 +22,12 @@
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.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, \
LanguageSelection
from openlp.plugins.bibles.lib.mediaitem import BibleSearch
@ -38,7 +41,7 @@ __default_settings__ = {
'bibles/db password': '',
'bibles/db hostname': '',
'bibles/db database': '',
'bibles/last search type': BibleSearch.Combined,
'bibles/last used search type': BibleSearch.Combined,
'bibles/reset to combined quick search': True,
'bibles/verse layout style': LayoutStyle.VersePerSlide,
'bibles/book name language': LanguageSelection.Bible,
@ -56,7 +59,7 @@ __default_settings__ = {
'bibles/range separator': '',
'bibles/list separator': '',
'bibles/end separator': '',
'bibles/last directory import': '',
'bibles/last directory import': None,
'bibles/hide combined quick error': False,
'bibles/is search while typing enabled': True
}
@ -74,6 +77,8 @@ class BiblePlugin(Plugin):
self.icon_path = ':/plugins/plugin_bibles.png'
self.icon = build_icon(self.icon_path)
self.manager = BibleManager(self)
register_endpoint(bibles_endpoint)
register_endpoint(api_bibles_endpoint)
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.
"""
self.setWindowTitle(translate('BiblesPlugin.ImportWizardForm', 'Bible Import Wizard'))
self.title_label.setText(WizardStrings.HeaderStyle % translate('OpenLP.Ui',
'Welcome to the Bible Import Wizard'))
self.title_label.setText(WizardStrings.HeaderStyle.format(text=translate('OpenLP.Ui',
'Welcome to the Bible Import Wizard')))
self.information_label.setText(
translate('BiblesPlugin.ImportWizardForm',
'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:
license_version = self.field('license_version')
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:
critical_error_message_box(
UiStrings().EmptyField,

View File

@ -49,7 +49,8 @@ class BookNameForm(QDialog, Ui_BookNameDialog):
"""
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.custom_signals()
self.book_names = BibleStrings().BookNames

View File

@ -45,7 +45,8 @@ class EditBibleForm(QtWidgets.QDialog, Ui_EditBibleDialog, RegistryProperties):
"""
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.book_names = BibleStrings().BookNames
self.setupUi(self)

View File

@ -47,7 +47,8 @@ class LanguageForm(QDialog, Ui_LanguageDialog):
"""
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)
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}_default'.format(role=role)] = default_separators[index]
# verse range match: (<chapter>:)?<verse>(-((<chapter>:)?<verse>|end)?)?
# TODO: Check before converting this string
range_regex = '(?:(?P<from_chapter>[0-9]+)%(sep_v)s)?' \
'(?P<from_verse>[0-9]+)(?P<range_to>%(sep_r)s(?:(?:(?P<to_chapter>' \
'[0-9]+)%(sep_v)s)?(?P<to_verse>[0-9]+)|%(sep_e)s)?)?' % REFERENCE_SEPARATORS
# TODO: Test before converting re.compile strings
REFERENCE_MATCHES['range'] = re.compile('^\s*%s\s*$' % range_regex, re.UNICODE)
range_regex = '(?:(?P<from_chapter>[0-9]+){sep_v})?' \
'(?P<from_verse>[0-9]+)(?P<range_to>{sep_r}(?:(?:(?P<to_chapter>' \
'[0-9]+){sep_v})?(?P<to_verse>[0-9]+)|{sep_e})?)?'.format_map(REFERENCE_SEPARATORS)
REFERENCE_MATCHES['range'] = re.compile(r'^\s*{range}\s*$'.format(range=range_regex), re.UNICODE)
REFERENCE_MATCHES['range_separator'] = re.compile(REFERENCE_SEPARATORS['sep_l'], re.UNICODE)
# full reference match: <book>(<range>(,(?!$)|(?=$)))+
REFERENCE_MATCHES['full'] = \
re.compile('^\s*(?!\s)(?P<book>[\d]*[^\d\.]+)\.*(?<!\s)\s*'
'(?P<ranges>(?:%(range_regex)s(?:%(sep_l)s(?!\s*$)|(?=\s*$)))+)\s*$'
% dict(list(REFERENCE_SEPARATORS.items()) + [('range_regex', range_regex)]), re.UNICODE)
re.compile(r'^\s*(?!\s)(?P<book>[\d]*[.]?[^\d\.]+)\.*(?<!\s)\s*'
r'(?P<ranges>(?:{range_regex}(?:{sep_l}(?!\s*$)|(?=\s*$)))+)\s*$'.format(
range_regex=range_regex, sep_l=REFERENCE_SEPARATORS['sep_l']), re.UNICODE)
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*``
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*$``
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
for character in RESERVED_CHARACTERS:
book_escaped = book_escaped.replace(character, '\\' + character)
# TODO: Verify regex patters before using format()
regex_book = re.compile('\s*%s\s*' % '\s*'.join(
book_escaped.split()), re.UNICODE | re.IGNORECASE)
regex_book = re.compile('\\s*{book}\\s*'.format(book='\\s*'.join(book_escaped.split())),
re.UNICODE | re.IGNORECASE)
if language_selection == LanguageSelection.Bible:
db_book = self.get_book(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.
"""
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')
conn = sqlite3.connect(file_path)
BiblesResourcesDB.cursor = conn.cursor()
@ -760,7 +759,7 @@ class AlternativeBookNamesDB(QtCore.QObject, Manager):
"""
if AlternativeBookNamesDB.cursor is None:
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):
# create new DB, create table alternative_book_names
AlternativeBookNamesDB.conn = sqlite3.connect(file_path)

View File

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

View File

@ -90,6 +90,8 @@ class BGExtract(RegistryProperties):
"""
Extract verses from BibleGateway
"""
NAME = 'BibleGateway'
def __init__(self, proxy_url=None):
log.debug('BGExtract.init("{url}")'.format(url=proxy_url))
self.proxy_url = proxy_url
@ -253,7 +255,7 @@ class BGExtract(RegistryProperties):
chapter=chapter,
version=version)
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='')
if not soup:
return None
@ -282,7 +284,7 @@ class BGExtract(RegistryProperties):
"""
log.debug('BGExtract.get_books_from_http("{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)
if not page:
send_error_message('download')
@ -323,7 +325,7 @@ class BGExtract(RegistryProperties):
returns a list in the form [(biblename, biblekey, language_code)]
"""
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)
if not soup:
return None
@ -357,6 +359,8 @@ class BSExtract(RegistryProperties):
"""
Extract verses from Bibleserver.com
"""
NAME = 'BibleServer'
def __init__(self, proxy_url=None):
log.debug('BSExtract.init("{url}")'.format(url=proxy_url))
self.proxy_url = proxy_url
@ -458,6 +462,8 @@ class CWExtract(RegistryProperties):
"""
Extract verses from CrossWalk/BibleStudyTools
"""
NAME = 'Crosswalk'
def __init__(self, proxy_url=None):
log.debug('CWExtract.init("{url}")'.format(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
try:
page = get_web_page(reference_url, header, True)
except:
except Exception as e:
page = None
if not page:
send_error_message('download')

View File

@ -21,9 +21,9 @@
###############################################################################
import logging
import os
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.db import BibleDB, BibleMeta
from .importers.csvbible import CSVBible
@ -111,7 +111,7 @@ class BibleManager(OpenLPMixin, RegistryProperties):
self.settings_section = 'bibles'
self.web = 'Web'
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.suffix = '.sqlite'
self.import_wizard = None
@ -124,7 +124,7 @@ class BibleManager(OpenLPMixin, RegistryProperties):
of HTTPBible is loaded instead of the BibleDB class.
"""
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:
files.remove('alternative_book_names.sqlite')
log.debug('Bible Files {text}'.format(text=files))
@ -137,7 +137,7 @@ class BibleManager(OpenLPMixin, RegistryProperties):
# Remove corrupted files.
if name is None:
bible.session.close_all()
delete_file(os.path.join(self.path, filename))
delete_file(Path(self.path, filename))
continue
log.debug('Bible Name: "{name}"'.format(name=name))
self.db_cache[name] = bible
@ -185,7 +185,7 @@ class BibleManager(OpenLPMixin, RegistryProperties):
bible = self.db_cache[name]
bible.session.close_all()
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):
"""
@ -305,13 +305,10 @@ class BibleManager(OpenLPMixin, RegistryProperties):
"""
Does a verse search for the given bible and text.
:param bible: The bible to search
:type bible: str
:param text: The text to search for
:type text: str
:param str bible: The bible to search
:param str text: The text to search for
: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))
if not text:

View File

@ -414,7 +414,9 @@ class BibleMediaItem(MediaManagerItem):
if self.bible:
book_data = self.get_common_books(self.bible, self.second_bible)
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)
set_case_insensitive_completer(books, self.search_edit)
@ -463,8 +465,7 @@ class BibleMediaItem(MediaManagerItem):
"""
Show the selected tab and set focus to it
:param index: The tab selected
:type index: int
:param int index: The tab selected
:return: None
"""
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
:param index: The index of the tab that has been changed to. (int)
:return: None
:rtype: None
"""
if index == ResultsTab.Saved:
self.add_built_results_to_list_widget(self.saved_results)

View File

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

View File

@ -39,7 +39,8 @@ class EditCustomSlideForm(QtWidgets.QDialog, Ui_CustomSlideEditDialog):
"""
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)
# Connecting signals and slots
self.insert_button.clicked.connect(self.on_insert_button_clicked)

View File

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

View File

@ -24,9 +24,11 @@ from PyQt5 import QtGui
import logging
from openlp.core.api.http import register_endpoint
from openlp.core.common import Settings, translate
from openlp.core.lib import Plugin, StringContent, ImageSource, build_icon
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.db import init_schema
@ -51,6 +53,8 @@ class ImagePlugin(Plugin):
self.weight = -7
self.icon_path = ':/plugins/plugin_images.png'
self.icon = build_icon(self.icon_path)
register_endpoint(images_endpoint)
register_endpoint(api_images_endpoint)
@staticmethod
def about():
@ -67,14 +71,6 @@ class ImagePlugin(Plugin):
'provided by the theme.')
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):
"""
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