forked from openlp/openlp
Pulled in latest changes from trunk
This commit is contained in:
commit
14f53eef89
67
.bzrignore
67
.bzrignore
@ -1,57 +1,48 @@
|
||||
*.*~
|
||||
*.~\?~
|
||||
\#*\#
|
||||
build
|
||||
.cache
|
||||
cover
|
||||
.coverage
|
||||
coverage
|
||||
.directory
|
||||
.vscode
|
||||
dist
|
||||
*.dll
|
||||
documentation/build/doctrees
|
||||
documentation/build/html
|
||||
*.e4*
|
||||
*eric[1-9]project
|
||||
.git
|
||||
env
|
||||
# Git files
|
||||
.gitignore
|
||||
htmlcov
|
||||
.idea
|
||||
*.kate-swp
|
||||
*.kdev4
|
||||
.kdev4
|
||||
*.komodoproject
|
||||
.komodotools
|
||||
list
|
||||
*.log*
|
||||
*.nja
|
||||
openlp.cfg
|
||||
openlp/core/resources.py.old
|
||||
OpenLP.egg-info
|
||||
openlp.org 2.0.e4*
|
||||
openlp.pro
|
||||
openlp-test-projectordb.sqlite
|
||||
*.orig
|
||||
output
|
||||
*.pyc
|
||||
__pycache__
|
||||
.pylint.d
|
||||
.pytest_cache
|
||||
*.qm
|
||||
*.rej
|
||||
# Rejected diff's
|
||||
resources/innosetup/Output
|
||||
resources/windows/warnOpenLP.txt
|
||||
*.ropeproject
|
||||
tags
|
||||
output
|
||||
*.~\?~
|
||||
*eric[1-9]project
|
||||
.cache
|
||||
.coverage
|
||||
.directory
|
||||
.git
|
||||
.gitignore
|
||||
.idea
|
||||
.kdev4
|
||||
.komodotools
|
||||
.pylint.d
|
||||
.pytest_cache
|
||||
.vscode
|
||||
OpenLP.egg-info
|
||||
\#*\#
|
||||
__pycache__
|
||||
build
|
||||
cover
|
||||
coverage
|
||||
dist
|
||||
env
|
||||
htmlcov
|
||||
list
|
||||
node_modules
|
||||
openlp-test-projectordb.sqlite
|
||||
openlp.cfg
|
||||
openlp.pro
|
||||
openlp/core/resources.py.old
|
||||
openlp/plugins/presentations/lib/vendor/Pyro4
|
||||
openlp/plugins/presentations/lib/vendor/serpent.py
|
||||
output
|
||||
package-lock.json
|
||||
.cache
|
||||
tags
|
||||
test
|
||||
tests.kdev4
|
||||
|
27
nose2.cfg
27
nose2.cfg
@ -1,27 +0,0 @@
|
||||
[unittest]
|
||||
verbose = true
|
||||
plugins = nose2.plugins.mp
|
||||
|
||||
[log-capture]
|
||||
always-on = true
|
||||
clear-handlers = true
|
||||
filter = -nose
|
||||
log-level = ERROR
|
||||
|
||||
[test-result]
|
||||
always-on = true
|
||||
descriptions = true
|
||||
|
||||
[coverage]
|
||||
always-on = true
|
||||
coverage = openlp
|
||||
coverage-report = html
|
||||
|
||||
[multiprocess]
|
||||
always-on = false
|
||||
processes = 4
|
||||
|
||||
[output-buffer]
|
||||
always-on = true
|
||||
stderr = true
|
||||
stdout = true
|
@ -34,7 +34,7 @@ def deploy_zipfile(app_root_path, zip_name):
|
||||
Process the downloaded zip file and add to the correct directory
|
||||
|
||||
:param str zip_name: the zip file name to be processed
|
||||
:param openlp.core.common.path.Path app_root_path: The directory to expand the zip to
|
||||
:param pathlib.Path app_root_path: The directory to expand the zip to
|
||||
|
||||
:return: None
|
||||
"""
|
||||
|
@ -24,11 +24,11 @@ import logging
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
from openlp.core.api.http import requires_auth
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib import create_thumb
|
||||
|
@ -28,10 +28,12 @@ logging and a plugin framework are contained within the openlp.core module.
|
||||
"""
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from shutil import copytree
|
||||
from traceback import format_exception
|
||||
|
||||
from PyQt5 import QtCore, QtWebEngineWidgets, QtWidgets # noqa
|
||||
@ -41,7 +43,7 @@ from openlp.core.common import is_macosx, is_win
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.loader import loader
|
||||
from openlp.core.common.i18n import LanguageManager, UiStrings, translate
|
||||
from openlp.core.common.path import copytree, create_paths, Path
|
||||
from openlp.core.common.path import create_paths
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.display.screens import ScreenList
|
||||
@ -316,7 +318,7 @@ def set_up_logging(log_path):
|
||||
"""
|
||||
Setup our logging using log_path
|
||||
|
||||
:param openlp.core.common.path.Path log_path: The file to save the log to.
|
||||
:param Path log_path: The file to save the log to.
|
||||
:rtype: None
|
||||
"""
|
||||
create_paths(log_path, do_not_log=True)
|
||||
@ -383,6 +385,15 @@ def main():
|
||||
else:
|
||||
application.setApplicationName('OpenLP')
|
||||
set_up_logging(AppLocation.get_directory(AppLocation.CacheDir))
|
||||
# Set the libvlc environment variable if we're frozen
|
||||
if getattr(sys, 'frozen', False):
|
||||
if is_macosx():
|
||||
vlc_lib = 'libvlc.dylib'
|
||||
elif is_win():
|
||||
vlc_lib = 'libvlc.dll'
|
||||
os.environ['PYTHON_VLC_LIB_PATH'] = str(AppLocation.get_directory(AppLocation.AppDir) / vlc_lib)
|
||||
log.debug('VLC Path: {}'.format(os.environ['PYTHON_VLC_LIB_PATH']))
|
||||
# Initialise the Registry
|
||||
Registry.create()
|
||||
Registry().register('application', application)
|
||||
Registry().set_flag('no_web_server', args.no_web_server)
|
||||
|
@ -142,7 +142,7 @@ def path_to_module(path):
|
||||
"""
|
||||
Convert a path to a module name (i.e openlp.core.common)
|
||||
|
||||
:param openlp.core.common.path.Path path: The path to convert to a module name.
|
||||
:param pathlib.Path path: The path to convert to a module name.
|
||||
:return: The module name.
|
||||
:rtype: str
|
||||
"""
|
||||
@ -371,7 +371,7 @@ def delete_file(file_path):
|
||||
"""
|
||||
Deletes a file from the system.
|
||||
|
||||
:param openlp.core.common.path.Path file_path: The file, including path, to delete.
|
||||
:param pathlib.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
|
||||
"""
|
||||
@ -407,7 +407,7 @@ def is_not_image_file(file_path):
|
||||
"""
|
||||
Validate that the file is not an image file.
|
||||
|
||||
:param openlp.core.common.path.Path file_path: The file to be checked.
|
||||
:param pathlib.Path file_path: The file to be checked.
|
||||
:return: If the file is not an image
|
||||
:rtype: bool
|
||||
"""
|
||||
@ -435,7 +435,7 @@ def check_binary_exists(program_path):
|
||||
"""
|
||||
Function that checks whether a binary exists.
|
||||
|
||||
:param openlp.core.common.path.Path program_path: The full path to the binary to check.
|
||||
:param pathlib.Path program_path: The full path to the binary to check.
|
||||
:return: program output to be parsed
|
||||
:rtype: bytes
|
||||
"""
|
||||
@ -462,7 +462,7 @@ def get_file_encoding(file_path):
|
||||
"""
|
||||
Utility function to incrementally detect the file encoding.
|
||||
|
||||
:param openlp.core.common.path.Path file_path: Filename for the file to determine the encoding for.
|
||||
:param pathlib.Path file_path: Filename for the file to determine the encoding for.
|
||||
:return: The name of the encoding detected
|
||||
:rtype: str
|
||||
"""
|
||||
|
@ -25,12 +25,13 @@ The :mod:`openlp.core.common.applocation` module provides an utility for OpenLP
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import appdirs
|
||||
|
||||
import openlp
|
||||
from openlp.core.common import get_frozen_path, is_macosx, is_win
|
||||
from openlp.core.common.path import Path, create_paths
|
||||
from openlp.core.common.path import create_paths
|
||||
from openlp.core.common.settings import Settings
|
||||
|
||||
|
||||
@ -58,7 +59,7 @@ class AppLocation(object):
|
||||
|
||||
: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
|
||||
:rtype: Path
|
||||
"""
|
||||
if dir_type == AppLocation.AppDir or dir_type == AppLocation.VersionDir:
|
||||
return get_frozen_path(FROZEN_APP_PATH, APP_PATH)
|
||||
@ -75,7 +76,7 @@ class AppLocation(object):
|
||||
Return the path OpenLP stores all its data under.
|
||||
|
||||
:return: The data path to use.
|
||||
:rtype: openlp.core.common.path.Path
|
||||
:rtype: Path
|
||||
"""
|
||||
# Check if we have a different data location.
|
||||
if Settings().contains('advanced/data path'):
|
||||
@ -95,7 +96,7 @@ class AppLocation(object):
|
||||
: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]
|
||||
:rtype: list[Path]
|
||||
"""
|
||||
path = AppLocation.get_data_path()
|
||||
if section:
|
||||
@ -112,7 +113,7 @@ class AppLocation(object):
|
||||
Return the path a particular module stores its data under.
|
||||
|
||||
:param str section:
|
||||
:rtype: openlp.core.common.path.Path
|
||||
:rtype: Path
|
||||
"""
|
||||
path = AppLocation.get_data_path() / section
|
||||
create_paths(path)
|
||||
@ -125,7 +126,7 @@ def _get_os_dir_path(dir_type):
|
||||
|
||||
:param dir_type: AppLocation Enum of the requested path type
|
||||
:return: The requested path
|
||||
:rtype: openlp.core.common.path.Path
|
||||
:rtype: Path
|
||||
"""
|
||||
# If running from source, return the language directory from the source directory
|
||||
if dir_type == AppLocation.LanguageDir:
|
||||
|
@ -26,6 +26,7 @@ import hashlib
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from random import randint
|
||||
from tempfile import gettempdir
|
||||
|
||||
@ -33,7 +34,6 @@ import requests
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common import trace_error_handler
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import ProxyMode, Settings
|
||||
from openlp.core.threading import ThreadWorker
|
||||
|
@ -385,6 +385,7 @@ class UiStrings(object):
|
||||
self.Error = translate('OpenLP.Ui', 'Error')
|
||||
self.Export = translate('OpenLP.Ui', 'Export')
|
||||
self.File = translate('OpenLP.Ui', 'File')
|
||||
self.FileCorrupt = translate('OpenLP.Ui', 'File appears to be corrupt.')
|
||||
self.FontSizePtUnit = translate('OpenLP.Ui', 'pt', 'Abbreviated font point size unit')
|
||||
self.Help = translate('OpenLP.Ui', 'Help')
|
||||
self.Hours = translate('OpenLP.Ui', 'h', 'The abbreviated unit for hours')
|
||||
|
@ -19,14 +19,75 @@
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
from contextlib import suppress
|
||||
from json import JSONDecoder, JSONEncoder
|
||||
from pathlib import Path
|
||||
|
||||
from openlp.core.common.path import Path
|
||||
_registered_classes = {}
|
||||
|
||||
|
||||
class OpenLPJsonDecoder(JSONDecoder):
|
||||
class JSONMixin(object):
|
||||
"""
|
||||
Implement a custom JSONDecoder to handle Path objects
|
||||
:class:`JSONMixin` is a mixin class to simplify the serialization of a subclass to JSON.
|
||||
|
||||
:cvar:`_json_keys` is used to specify the attributes of the subclass that you wish to serialize.
|
||||
:vartype _json_keys: list[str]
|
||||
:cvar:`_name` set to override the the subclass name, useful if using a `proxy` class
|
||||
:vartype _name: str
|
||||
"""
|
||||
_json_keys = []
|
||||
_name = None
|
||||
_version = 1
|
||||
|
||||
def __init_subclass__(cls, register_names=None, **kwargs):
|
||||
"""
|
||||
Register the subclass.
|
||||
|
||||
:param collections.Iterable[str] register_names: Alternative names to register instead of the class name
|
||||
:param kwargs: Other args to pass to the super method
|
||||
:return None:
|
||||
"""
|
||||
super().__init_subclass__(**kwargs)
|
||||
for key in register_names or [cls.__name__]:
|
||||
_registered_classes[key] = cls
|
||||
|
||||
@classmethod
|
||||
def encode_json(cls, obj, **kwargs):
|
||||
"""
|
||||
Create a instance of the subclass from the dictionary that has been constructed by the JSON representation.
|
||||
Only use the keys specified in :cvar:`_json_keys`.
|
||||
|
||||
:param dict[str] obj: The dictionary representation of the subclass (deserailized from the JSON)
|
||||
:param kwargs: Contains any extra parameters. Not used!
|
||||
:return: The desrialized object
|
||||
"""
|
||||
return cls(**{key: obj[key] for key in cls._json_keys if obj.get(key) is not None})
|
||||
|
||||
@classmethod
|
||||
def attach_meta(cls, j_dict):
|
||||
"""
|
||||
Attach meta data to the serialized dictionary.
|
||||
|
||||
:param dict[str] j_dict: The dictionary to update with the meta data
|
||||
:return None:
|
||||
"""
|
||||
j_dict.update({'json_meta': {'class': cls._name or cls.__name__, 'version': cls._version}})
|
||||
|
||||
def json_object(self, **kwargs):
|
||||
"""
|
||||
Create a dictionary that can be JSON decoded.
|
||||
|
||||
:param kwargs: Contains any extra parameters. Not used!
|
||||
:return dict[str]: The dictionary representation of this Path object.
|
||||
"""
|
||||
j_dict = {key: self.__dict__[key] for key in self._json_keys if self.__dict__.get(key) is not None}
|
||||
self.attach_meta(j_dict)
|
||||
return j_dict
|
||||
|
||||
|
||||
class OpenLPJSONDecoder(JSONDecoder):
|
||||
"""
|
||||
Implement a custom JSONDecoder to extend compatibility to custom objects
|
||||
|
||||
Example Usage:
|
||||
object = json.loads(json_string, cls=OpenLPJsonDecoder)
|
||||
@ -45,23 +106,28 @@ class OpenLPJsonDecoder(JSONDecoder):
|
||||
|
||||
def custom_object_hook(self, obj):
|
||||
"""
|
||||
Implement a custom Path object decoder.
|
||||
Implement a custom 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
|
||||
:return: The custom object from the serialized data if the custom object is registered, else obj
|
||||
"""
|
||||
if '__Path__' in obj:
|
||||
obj = Path.encode_json(obj, **self.kwargs)
|
||||
return PathSerializer.encode_json(obj, **self.kwargs)
|
||||
try:
|
||||
key = obj['json_meta']['class']
|
||||
except KeyError:
|
||||
return obj
|
||||
if key in _registered_classes:
|
||||
return _registered_classes[key].encode_json(obj, **self.kwargs)
|
||||
return obj
|
||||
|
||||
|
||||
class OpenLPJsonEncoder(JSONEncoder):
|
||||
class OpenLPJSONEncoder(JSONEncoder):
|
||||
"""
|
||||
Implement a custom JSONEncoder to handle Path objects
|
||||
Implement a custom JSONEncoder to handle to extend compatibility to custom objects
|
||||
|
||||
Example Usage:
|
||||
json_string = json.dumps(object, cls=OpenLPJsonEncoder)
|
||||
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):
|
||||
@ -78,12 +144,64 @@ class OpenLPJsonEncoder(JSONEncoder):
|
||||
|
||||
def custom_default(self, obj):
|
||||
"""
|
||||
Convert any Path objects into a dictionary object which can be serialized.
|
||||
Convert any registered objects into a dictionary object which can be serialized.
|
||||
|
||||
:param object obj: The object to convert
|
||||
:return: The serializable object
|
||||
:rtype: dict
|
||||
:return dict: The serializable object
|
||||
"""
|
||||
if isinstance(obj, Path):
|
||||
return obj.json_object(**self.kwargs)
|
||||
return super().default(obj)
|
||||
if isinstance(obj, JSONMixin):
|
||||
return obj.json_object()
|
||||
elif obj.__class__.__name__ in _registered_classes:
|
||||
return _registered_classes[obj.__class__.__name__].json_object(obj, **self.kwargs)
|
||||
return super().default(obj, **self.kwargs)
|
||||
|
||||
|
||||
def is_serializable(obj):
|
||||
return obj.__class__.__name__ in _registered_classes
|
||||
|
||||
|
||||
class PathSerializer(JSONMixin, register_names=('Path', 'PosixPath', 'WindowsPath')):
|
||||
"""
|
||||
Implement a de/serializer for pathlib.Path objects
|
||||
"""
|
||||
_name = 'Path'
|
||||
|
||||
@staticmethod
|
||||
def encode_json(obj, base_path=None, **kwargs):
|
||||
"""
|
||||
Reimplement encode_json to create a Path object from a dictionary representation.
|
||||
|
||||
:param dict[str] obj: The dictionary representation
|
||||
:param Path base_path: If specified, an absolute path to base the relative path off of.
|
||||
:param kwargs: Contains any extra parameters. Not used!
|
||||
:return Path: The deserialized Path object
|
||||
"""
|
||||
if '__Path__' in obj:
|
||||
parts = obj['__Path__']
|
||||
else:
|
||||
parts = obj['parts']
|
||||
path = Path(*parts)
|
||||
if base_path and not path.is_absolute():
|
||||
return base_path / path
|
||||
return path
|
||||
|
||||
@classmethod
|
||||
def json_object(cls, obj, base_path=None, is_js=False, **kwargs):
|
||||
"""
|
||||
Create a dictionary that can be JSON decoded.
|
||||
|
||||
:param Path base_path: If specified, an absolute path to make a relative path from.
|
||||
:param bool is_js: Encode the path as a uri. For example for use in the js rendering code.
|
||||
:param kwargs: Contains any extra parameters. Not used!
|
||||
:return: The dictionary representation of this Path object.
|
||||
:rtype: dict[tuple]
|
||||
"""
|
||||
path = obj
|
||||
if base_path:
|
||||
with suppress(ValueError):
|
||||
path = path.relative_to(base_path)
|
||||
if is_js is True:
|
||||
return path.as_uri()
|
||||
json_dict = {'parts': path.parts}
|
||||
cls.attach_meta(json_dict)
|
||||
return json_dict
|
||||
|
@ -21,66 +21,11 @@
|
||||
##########################################################################
|
||||
import logging
|
||||
import shutil
|
||||
from contextlib import suppress
|
||||
|
||||
from openlp.core.common import is_win
|
||||
|
||||
|
||||
if is_win():
|
||||
from pathlib import WindowsPath as PathVariant # pragma: nocover
|
||||
else:
|
||||
from pathlib import PosixPath as PathVariant # pragma: nocover
|
||||
from pathlib import Path
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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}
|
||||
|
||||
def rmtree(self, ignore_errors=False, onerror=None):
|
||||
"""
|
||||
Provide an interface to :func:`shutil.rmtree`
|
||||
|
||||
:param bool ignore_errors: Ignore errors
|
||||
:param onerror: Handler function to handle any errors
|
||||
:rtype: None
|
||||
"""
|
||||
shutil.rmtree(self, ignore_errors, onerror)
|
||||
|
||||
|
||||
def replace_params(args, kwargs, params):
|
||||
"""
|
||||
Apply a transformation function to the specified args or kwargs
|
||||
@ -110,65 +55,11 @@ def replace_params(args, kwargs, params):
|
||||
return tuple(args), kwargs
|
||||
|
||||
|
||||
def copy(*args, **kwargs):
|
||||
"""
|
||||
Wraps :func:`shutil.copy` so that we can accept Path objects.
|
||||
|
||||
:param src openlp.core.common.path.Path: Takes a Path object which is then converted to a str object
|
||||
:param dst openlp.core.common.path.Path: Takes a Path object which is then converted to a str object
|
||||
:return: Converts the str object received from :func:`shutil.copy` to a Path or NoneType object
|
||||
:rtype: openlp.core.common.path.Path | None
|
||||
|
||||
See the following link for more information on the other parameters:
|
||||
https://docs.python.org/3/library/shutil.html#shutil.copy
|
||||
"""
|
||||
|
||||
args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str)))
|
||||
|
||||
return str_to_path(shutil.copy(*args, **kwargs))
|
||||
|
||||
|
||||
def copyfile(*args, **kwargs):
|
||||
"""
|
||||
Wraps :func:`shutil.copyfile` so that we can accept Path objects.
|
||||
|
||||
:param openlp.core.common.path.Path src: Takes a Path object which is then converted to a str object
|
||||
:param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object
|
||||
:return: Converts the str object received from :func:`shutil.copyfile` to a Path or NoneType object
|
||||
:rtype: openlp.core.common.path.Path | None
|
||||
|
||||
See the following link for more information on the other parameters:
|
||||
https://docs.python.org/3/library/shutil.html#shutil.copyfile
|
||||
"""
|
||||
|
||||
args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str)))
|
||||
|
||||
return str_to_path(shutil.copyfile(*args, **kwargs))
|
||||
|
||||
|
||||
def copytree(*args, **kwargs):
|
||||
"""
|
||||
Wraps :func:shutil.copytree` so that we can accept Path objects.
|
||||
|
||||
:param openlp.core.common.path.Path src : Takes a Path object which is then converted to a str object
|
||||
:param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object
|
||||
:return: Converts the str object received from :func:`shutil.copytree` to a Path or NoneType object
|
||||
:rtype: openlp.core.common.path.Path | None
|
||||
|
||||
See the following link for more information on the other parameters:
|
||||
https://docs.python.org/3/library/shutil.html#shutil.copytree
|
||||
"""
|
||||
|
||||
args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str)))
|
||||
|
||||
return str_to_path(shutil.copytree(*args, **kwargs))
|
||||
|
||||
|
||||
def which(*args, **kwargs):
|
||||
"""
|
||||
Wraps :func:shutil.which` so that it return a Path objects.
|
||||
|
||||
:rtype: openlp.core.common.Path
|
||||
:rtype: Path
|
||||
|
||||
See the following link for more information on the other parameters:
|
||||
https://docs.python.org/3/library/shutil.html#shutil.which
|
||||
@ -183,10 +74,12 @@ 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
|
||||
:param 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 isinstance(path, str):
|
||||
return path
|
||||
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:
|
||||
@ -204,7 +97,7 @@ def str_to_path(string):
|
||||
|
||||
: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
|
||||
:rtype: Path | None
|
||||
"""
|
||||
if not isinstance(string, str):
|
||||
log.error('parameter \'string\' must be of type str, got {} which is a {} instead'.format(string, type(string)))
|
||||
@ -218,7 +111,7 @@ def create_paths(*paths, **kwargs):
|
||||
"""
|
||||
Create one or more paths
|
||||
|
||||
:param openlp.core.common.path.Path paths: The paths to create
|
||||
:param Path paths: The paths to create
|
||||
:param bool do_not_log: To not log anything. This is need for the start up, when the log isn't ready.
|
||||
:rtype: None
|
||||
"""
|
||||
@ -239,7 +132,7 @@ def files_to_paths(file_names):
|
||||
|
||||
: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
|
||||
:rtype: Path
|
||||
"""
|
||||
if file_names:
|
||||
return [str_to_path(file_name) for file_name in file_names]
|
||||
|
@ -146,7 +146,7 @@ class Registry(object):
|
||||
try:
|
||||
log.debug('Running function {} for {}'.format(function, event))
|
||||
result = function(*args, **kwargs)
|
||||
if result:
|
||||
if result is not None:
|
||||
results.append(result)
|
||||
except TypeError:
|
||||
# Who has called me can help in debugging
|
||||
|
@ -27,13 +27,14 @@ import json
|
||||
import logging
|
||||
import os
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
|
||||
from PyQt5 import QtCore, QtGui
|
||||
|
||||
from openlp.core.common import SlideLimits, ThemeLevel, is_linux, is_win
|
||||
from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder
|
||||
from openlp.core.common.path import Path, files_to_paths, str_to_path
|
||||
from openlp.core.common.json import OpenLPJSONDecoder, OpenLPJSONEncoder, is_serializable
|
||||
from openlp.core.common.path import files_to_paths, str_to_path
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -129,9 +130,6 @@ class Settings(QtCore.QSettings):
|
||||
``advanced/slide limits`` to ``SlideLimits.Wrap``. **NOTE**, this means that the rules have to cover all cases!
|
||||
So, if the type of the old value is bool, then there must be two rules.
|
||||
"""
|
||||
on_monitor_default = True
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
on_monitor_default = False
|
||||
__default_settings__ = {
|
||||
'settings/version': 0,
|
||||
'advanced/add page break': False,
|
||||
@ -204,7 +202,7 @@ class Settings(QtCore.QSettings):
|
||||
'core/view mode': 'default',
|
||||
# The other display settings (display position and dimensions) are defined in the ScreenList class due to a
|
||||
# circular dependency.
|
||||
'core/display on monitor': on_monitor_default,
|
||||
'core/display on monitor': False,
|
||||
'core/override position': False,
|
||||
'core/monitor': {},
|
||||
'core/application version': '0.0',
|
||||
@ -212,6 +210,8 @@ class Settings(QtCore.QSettings):
|
||||
'media/media auto start': QtCore.Qt.Unchecked,
|
||||
'media/stream command': '',
|
||||
'media/vlc arguments': '',
|
||||
'media/video': '',
|
||||
'media/audio': '',
|
||||
'remotes/download version': '0.0',
|
||||
'players/background color': '#000000',
|
||||
'servicemanager/last directory': None,
|
||||
@ -337,7 +337,7 @@ class Settings(QtCore.QSettings):
|
||||
|
||||
Does not affect existing Settings objects.
|
||||
|
||||
:param openlp.core.common.path.Path ini_path: ini file path
|
||||
:param Path ini_path: ini file path
|
||||
:rtype: None
|
||||
"""
|
||||
Settings.__file_path__ = str(ini_path)
|
||||
@ -584,8 +584,9 @@ class Settings(QtCore.QSettings):
|
||||
:param value: The value to save
|
||||
:rtype: None
|
||||
"""
|
||||
if isinstance(value, (Path, dict)) or (isinstance(value, list) and value and isinstance(value[0], Path)):
|
||||
value = json.dumps(value, cls=OpenLPJsonEncoder)
|
||||
if is_serializable(value) or isinstance(value, dict) or \
|
||||
(isinstance(value, list) and value and is_serializable(value[0])):
|
||||
value = json.dumps(value, cls=OpenLPJSONEncoder)
|
||||
super().setValue(key, value)
|
||||
|
||||
def _convert_value(self, setting, default_value):
|
||||
@ -611,8 +612,8 @@ class Settings(QtCore.QSettings):
|
||||
elif isinstance(default_value, dict):
|
||||
return {}
|
||||
elif isinstance(setting, str):
|
||||
if '__Path__' in setting or setting.startswith('{'):
|
||||
return json.loads(setting, cls=OpenLPJsonDecoder)
|
||||
if 'json_meta' in setting or '__Path__' in setting or setting.startswith('{'):
|
||||
return json.loads(setting, cls=OpenLPJSONDecoder)
|
||||
# Convert the setting to the correct type.
|
||||
if isinstance(default_value, bool):
|
||||
if isinstance(setting, bool):
|
||||
@ -629,7 +630,7 @@ class Settings(QtCore.QSettings):
|
||||
"""
|
||||
Export the settings to file.
|
||||
|
||||
:param openlp.core.common.path.Path dest_path: The file path to create the export file.
|
||||
:param Path dest_path: The file path to create the export file.
|
||||
:return: Success
|
||||
:rtype: bool
|
||||
"""
|
||||
|
@ -161,20 +161,6 @@ function _prepareText(text) {
|
||||
return "<p>" + _nl2br(text) + "</p>";
|
||||
}
|
||||
|
||||
/**
|
||||
* The paths we get are JSON versions of Python Path objects, so let's just fix that.
|
||||
* @private
|
||||
* @param {object} path - The Path object
|
||||
* @returns {string} The actual file path
|
||||
*/
|
||||
function _pathToString(path) {
|
||||
var filename = path.__Path__.join("/").replace("//", "/");
|
||||
if (!filename.startsWith("/")) {
|
||||
filename = "/" + filename;
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* An audio player with a play list
|
||||
*/
|
||||
@ -903,13 +889,13 @@ var Display = {
|
||||
}
|
||||
break;
|
||||
case BackgroundType.Image:
|
||||
background_filename = _pathToString(theme.background_filename);
|
||||
backgroundStyle["background-image"] = "url('file://" + background_filename + "')";
|
||||
backgroundStyle["background-image"] = "url('" + theme.background_filename + "')";
|
||||
console.warn(backgroundStyle["background-image"]);
|
||||
break;
|
||||
case BackgroundType.Video:
|
||||
background_filename = _pathToString(theme.background_filename);
|
||||
backgroundStyle["background-color"] = theme.background_border_color;
|
||||
backgroundHtml = "<video loop autoplay muted><source src='file://" + background_filename + "'></video>";
|
||||
backgroundHtml = "<video loop autoplay muted><source src='" + theme.background_filename + "'></video>";
|
||||
console.warn(backgroundHtml);
|
||||
break;
|
||||
default:
|
||||
backgroundStyle["background"] = "#000";
|
||||
|
@ -24,6 +24,7 @@ The :mod:`~openlp.display.render` module contains functions for rendering.
|
||||
"""
|
||||
import html
|
||||
import logging
|
||||
import mako
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
@ -32,8 +33,10 @@ import time
|
||||
from PyQt5 import QtWidgets, QtGui
|
||||
|
||||
from openlp.core.common import ThemeLevel
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.mixins import LogMixin, RegistryProperties
|
||||
from openlp.core.common.registry import Registry, RegistryBase
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.display.screens import ScreenList
|
||||
from openlp.core.display.window import DisplayWindow
|
||||
from openlp.core.lib import ItemCapabilities
|
||||
@ -58,8 +61,10 @@ VERSE = 'The Lord said to {r}Noah{/r}: \n' \
|
||||
'{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}' \
|
||||
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
|
||||
VERSE_FOR_LINE_COUNT = '\n'.join(map(str, range(100)))
|
||||
TITLE = 'Arky Arky (Unknown)'
|
||||
FOOTER = ['Public Domain', 'CCLI 123456']
|
||||
TITLE = 'Arky Arky'
|
||||
AUTHOR = 'John Doe'
|
||||
FOOTER_COPYRIGHT = 'Public Domain'
|
||||
CCLI_NO = '123456'
|
||||
|
||||
|
||||
def remove_tags(text, can_remove_chords=False):
|
||||
@ -425,7 +430,7 @@ def get_start_tags(raw_text):
|
||||
return raw_text + ''.join(end_tags), ''.join(start_tags), ''.join(html_tags)
|
||||
|
||||
|
||||
class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
|
||||
class ThemePreviewRenderer(LogMixin, DisplayWindow):
|
||||
"""
|
||||
A virtual display used for rendering thumbnails and other offscreen tasks
|
||||
"""
|
||||
@ -435,24 +440,6 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.force_page = False
|
||||
for screen in ScreenList():
|
||||
if screen.is_display:
|
||||
self.setGeometry(screen.display_geometry.x(), screen.display_geometry.y(),
|
||||
screen.display_geometry.width(), screen.display_geometry.height())
|
||||
break
|
||||
# If the display is not show'ed and hidden like this webegine will not render
|
||||
self.show()
|
||||
self.hide()
|
||||
self.theme_height = 0
|
||||
self.theme_level = ThemeLevel.Global
|
||||
|
||||
def set_theme_level(self, theme_level):
|
||||
"""
|
||||
Sets the theme level.
|
||||
|
||||
:param theme_level: The theme level to be used.
|
||||
"""
|
||||
self.theme_level = theme_level
|
||||
|
||||
def calculate_line_count(self):
|
||||
"""
|
||||
@ -466,7 +453,30 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
|
||||
"""
|
||||
return self.run_javascript('Display.clearSlides();')
|
||||
|
||||
def generate_preview(self, theme_data, force_page=False):
|
||||
def generate_footer(self):
|
||||
"""
|
||||
"""
|
||||
footer_template = Settings().value('songs/footer template')
|
||||
# Keep this in sync with the list in songstab.py
|
||||
vars = {
|
||||
'title': TITLE,
|
||||
'authors_none_label': translate('OpenLP.Ui', 'Written by'),
|
||||
'authors_words_label': translate('SongsPlugin.AuthorType', 'Words',
|
||||
'Author who wrote the lyrics of a song'),
|
||||
'authors_words': [AUTHOR],
|
||||
'copyright': FOOTER_COPYRIGHT,
|
||||
'ccli_license': Settings().value('core/ccli number'),
|
||||
'ccli_license_label': translate('SongsPlugin.MediaItem', 'CCLI License'),
|
||||
'ccli_number': CCLI_NO,
|
||||
}
|
||||
try:
|
||||
footer_html = mako.template.Template(footer_template).render_unicode(**vars).replace('\n', '')
|
||||
except mako.exceptions.SyntaxException:
|
||||
log.error('Failed to render Song footer html:\n' + mako.exceptions.text_error_template().render())
|
||||
footer_html = 'Dummy footer text'
|
||||
return footer_html
|
||||
|
||||
def generate_preview(self, theme_data, force_page=False, generate_screenshot=True):
|
||||
"""
|
||||
Generate a preview of a theme.
|
||||
|
||||
@ -479,13 +489,15 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
|
||||
if not self.force_page:
|
||||
self.set_theme(theme_data)
|
||||
self.theme_height = theme_data.font_main_height
|
||||
slides = self.format_slide(render_tags(VERSE), None)
|
||||
slides = self.format_slide(VERSE, None)
|
||||
verses = dict()
|
||||
verses['title'] = TITLE
|
||||
verses['text'] = slides[0]
|
||||
verses['text'] = render_tags(slides[0])
|
||||
verses['verse'] = 'V1'
|
||||
verses['footer'] = self.generate_footer()
|
||||
self.load_verses([verses])
|
||||
self.force_page = False
|
||||
if generate_screenshot:
|
||||
return self.save_screenshot()
|
||||
self.force_page = False
|
||||
return None
|
||||
@ -515,7 +527,7 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
|
||||
if item and item.is_capable(ItemCapabilities.CanWordSplit):
|
||||
pages = self._paginate_slide_words(text.split('\n'), line_end)
|
||||
# Songs and Custom
|
||||
elif item is None or item.is_capable(ItemCapabilities.CanSoftBreak):
|
||||
elif item is None or (item and item.is_capable(ItemCapabilities.CanSoftBreak)):
|
||||
pages = []
|
||||
if '[---]' in text:
|
||||
# Remove Overflow split if at start of the text
|
||||
@ -722,7 +734,8 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
|
||||
:param text: The text to check. It may contain HTML tags.
|
||||
"""
|
||||
self.clear_slides()
|
||||
self.run_javascript('Display.addTextSlide("v1", "{text}", "Dummy Footer");'.format(text=text), is_sync=True)
|
||||
self.run_javascript('Display.addTextSlide("v1", "{text}", "Dummy Footer");'
|
||||
.format(text=text.replace('"', '\\"')), is_sync=True)
|
||||
does_text_fits = self.run_javascript('Display.doesContentFit();', is_sync=True)
|
||||
return does_text_fits
|
||||
|
||||
@ -745,3 +758,33 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
|
||||
pixmap.save(fname, ext)
|
||||
else:
|
||||
return pixmap
|
||||
|
||||
|
||||
class Renderer(RegistryBase, RegistryProperties, ThemePreviewRenderer):
|
||||
"""
|
||||
A virtual display used for rendering thumbnails and other offscreen tasks
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Constructor
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.force_page = False
|
||||
for screen in ScreenList():
|
||||
if screen.is_display:
|
||||
self.setGeometry(screen.display_geometry.x(), screen.display_geometry.y(),
|
||||
screen.display_geometry.width(), screen.display_geometry.height())
|
||||
break
|
||||
# If the display is not show'ed and hidden like this webegine will not render
|
||||
self.show()
|
||||
self.hide()
|
||||
self.theme_height = 0
|
||||
self.theme_level = ThemeLevel.Global
|
||||
|
||||
def set_theme_level(self, theme_level):
|
||||
"""
|
||||
Sets the theme level.
|
||||
|
||||
:param theme_level: The theme level to be used.
|
||||
"""
|
||||
self.theme_level = theme_level
|
||||
|
@ -249,18 +249,18 @@ class DisplayWindow(QtWidgets.QWidget):
|
||||
"""
|
||||
Set images in the display
|
||||
"""
|
||||
for image in images:
|
||||
if not image['path'].startswith('file://'):
|
||||
image['path'] = 'file://' + image['path']
|
||||
json_images = json.dumps(images)
|
||||
imagesr = copy.deepcopy(images)
|
||||
for image in imagesr:
|
||||
image['path'] = image['path'].as_uri()
|
||||
json_images = json.dumps(imagesr)
|
||||
self.run_javascript('Display.setImageSlides({images});'.format(images=json_images))
|
||||
|
||||
def load_video(self, video):
|
||||
"""
|
||||
Load video in the display
|
||||
"""
|
||||
if not video['path'].startswith('file://'):
|
||||
video['path'] = 'file://' + video['path']
|
||||
video = copy.deepcopy(video)
|
||||
video['path'] = video['path'].as_uri()
|
||||
json_video = json.dumps(video)
|
||||
self.run_javascript('Display.setVideo({video});'.format(video=json_video))
|
||||
|
||||
@ -332,9 +332,9 @@ class DisplayWindow(QtWidgets.QWidget):
|
||||
theme_copy = copy.deepcopy(theme)
|
||||
theme_copy.background_type = 'image'
|
||||
theme_copy.background_filename = self.checkerboard_path
|
||||
exported_theme = theme_copy.export_theme()
|
||||
exported_theme = theme_copy.export_theme(is_js=True)
|
||||
else:
|
||||
exported_theme = theme.export_theme()
|
||||
exported_theme = theme.export_theme(is_js=True)
|
||||
self.run_javascript('Display.setTheme({theme});'.format(theme=exported_theme))
|
||||
|
||||
def get_video_types(self):
|
||||
|
@ -24,15 +24,23 @@ The :mod:`lib` module contains most of the components and libraries that make
|
||||
OpenLP work.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.i18n import UiStrings, translate
|
||||
|
||||
log = logging.getLogger(__name__ + '.__init__')
|
||||
|
||||
|
||||
class DataType(IntEnum):
|
||||
U8 = 1
|
||||
U16 = 2
|
||||
U32 = 4
|
||||
|
||||
|
||||
class ServiceItemContext(object):
|
||||
"""
|
||||
The context in which a Service Item is being generated
|
||||
@ -173,6 +181,7 @@ class ItemCapabilities(object):
|
||||
HasNotes = 20
|
||||
HasThumbnails = 21
|
||||
HasMetaData = 22
|
||||
CanStream = 23
|
||||
|
||||
|
||||
def get_text_file_string(text_file_path):
|
||||
@ -181,7 +190,7 @@ def get_text_file_string(text_file_path):
|
||||
returns False. If there is an error loading the file or the content can't be decoded then the function will return
|
||||
None.
|
||||
|
||||
:param openlp.core.common.path.Path text_file_path: The path to the file.
|
||||
:param 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
|
||||
@ -263,8 +272,8 @@ def create_thumb(image_path, thumb_path, return_icon=True, size=None):
|
||||
"""
|
||||
Create a thumbnail from the given image path and depending on ``return_icon`` it returns an icon from this thumb.
|
||||
|
||||
:param openlp.core.common.path.Path image_path: The image file to create the icon from.
|
||||
:param openlp.core.common.path.Path thumb_path: The filename to save the thumbnail to.
|
||||
:param Path image_path: The image file to create the icon from.
|
||||
:param Path thumb_path: The filename to save the thumbnail to.
|
||||
:param return_icon: States if an icon should be build and returned from the thumb. Defaults to ``True``.
|
||||
:param size: Allows to state a own size (QtCore.QSize) to use. Defaults to ``None``, which means that a default
|
||||
height of 88 is used.
|
||||
@ -311,8 +320,8 @@ def validate_thumb(file_path, thumb_path):
|
||||
Validates whether an file's thumb still exists and if is up to date. **Note**, you must **not** call this function,
|
||||
before checking the existence of the file.
|
||||
|
||||
:param openlp.core.common.path.Path file_path: The path to the file. The file **must** exist!
|
||||
:param openlp.core.common.path.Path thumb_path: The path to the thumb.
|
||||
:param Path file_path: The path to the file. The file **must** exist!
|
||||
:param Path thumb_path: The path to the thumb.
|
||||
:return: Has the image changed since the thumb was created?
|
||||
:rtype: bool
|
||||
"""
|
||||
@ -396,3 +405,48 @@ def create_separated_list(string_list):
|
||||
else:
|
||||
list_to_string = ''
|
||||
return list_to_string
|
||||
|
||||
|
||||
def read_or_fail(file_object, length):
|
||||
"""
|
||||
Ensure that the data read is as the exact length requested. Otherwise raise an OSError.
|
||||
|
||||
:param io.IOBase file_object: The file-lke object ot read from.
|
||||
:param int length: The length of the data to read.
|
||||
:return: The data read.
|
||||
"""
|
||||
data = file_object.read(length)
|
||||
if len(data) != length:
|
||||
raise OSError(UiStrings().FileCorrupt)
|
||||
return data
|
||||
|
||||
|
||||
def read_int(file_object, data_type, endian='big'):
|
||||
"""
|
||||
Read the correct amount of data from a file-like object to decode it to the specified type.
|
||||
|
||||
:param io.IOBase file_object: The file-like object to read from.
|
||||
:param DataType data_type: A member from the :enum:`DataType`
|
||||
:param endian: The endianess of the data to be read
|
||||
:return int: The decoded int
|
||||
"""
|
||||
data = read_or_fail(file_object, data_type)
|
||||
return int.from_bytes(data, endian)
|
||||
|
||||
|
||||
def seek_or_fail(file_object, offset, how=os.SEEK_SET):
|
||||
"""
|
||||
See to a set position and return an error if the cursor has not moved to that position.
|
||||
|
||||
:param io.IOBase file_object: The file-like object to attempt to seek.
|
||||
:param int offset: The offset / position to seek by / to.
|
||||
:param [os.SEEK_CUR | os.SEEK_SET how: Currently only supports os.SEEK_CUR (0) or os.SEEK_SET (1)
|
||||
:return int: The new position in the file.
|
||||
"""
|
||||
if how not in (os.SEEK_CUR, os.SEEK_SET):
|
||||
raise NotImplementedError
|
||||
prev_pos = file_object.tell()
|
||||
new_pos = file_object.seek(offset, how)
|
||||
if how == os.SEEK_SET and new_pos != offset or how == os.SEEK_CUR and new_pos != prev_pos + offset:
|
||||
raise OSError(UiStrings().FileCorrupt)
|
||||
return new_pos
|
||||
|
@ -40,7 +40,7 @@ from sqlalchemy.pool import NullPool
|
||||
from openlp.core.common import delete_file
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder
|
||||
from openlp.core.common.json import OpenLPJSONDecoder, OpenLPJSONEncoder
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
|
||||
@ -132,7 +132,7 @@ def get_db_path(plugin_name, db_file_name=None):
|
||||
Create a path to a database from the plugin name and database name
|
||||
|
||||
:param plugin_name: Name of plugin
|
||||
:param openlp.core.common.path.Path | str | None db_file_name: File name of database
|
||||
:param pathlib.Path | str | None db_file_name: File name of database
|
||||
:return: The path to the database
|
||||
:rtype: str
|
||||
"""
|
||||
@ -150,7 +150,7 @@ def handle_db_error(plugin_name, db_file_path):
|
||||
Log and report to the user that a database cannot be loaded
|
||||
|
||||
:param plugin_name: Name of plugin
|
||||
:param openlp.core.common.path.Path db_file_path: File name of database
|
||||
:param pathlib.Path db_file_path: File name of database
|
||||
:return: None
|
||||
"""
|
||||
db_path = get_db_path(plugin_name, db_file_path)
|
||||
@ -165,8 +165,8 @@ def init_url(plugin_name, db_file_name=None):
|
||||
Construct the connection string for a database.
|
||||
|
||||
:param plugin_name: The name of the plugin for the database creation.
|
||||
:param openlp.core.common.path.Path | str | None db_file_name: The database file name. Defaults to None resulting
|
||||
in the plugin_name being used.
|
||||
:param pathlib.Path | str | None db_file_name: The database file name. Defaults to None resulting in the plugin_name
|
||||
being used.
|
||||
:return: The database URL
|
||||
:rtype: str
|
||||
"""
|
||||
@ -215,7 +215,7 @@ class PathType(types.TypeDecorator):
|
||||
Create a PathType for storing Path objects with SQLAlchemy. Behind the scenes we convert the Path object to a JSON
|
||||
representation and store it as a Unicode type
|
||||
"""
|
||||
impl = types.UnicodeText
|
||||
impl = types.Unicode
|
||||
|
||||
def coerce_compared_value(self, op, value):
|
||||
"""
|
||||
@ -224,10 +224,8 @@ class PathType(types.TypeDecorator):
|
||||
|
||||
:param op: The operation being carried out. Not used, as we only care about the type that is being used with the
|
||||
operation.
|
||||
:param openlp.core.common.path.Path | str value: The value being used for the comparison. Most likely a Path
|
||||
Object or str.
|
||||
:return: The coerced value stored in the db
|
||||
:rtype: PathType or UnicodeText
|
||||
:param pathlib.Path | str value: The value being used for the comparison. Most likely a Path Object or str.
|
||||
:return PathType | UnicodeText: The coerced value stored in the db
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
return UnicodeText()
|
||||
@ -238,13 +236,12 @@ class PathType(types.TypeDecorator):
|
||||
"""
|
||||
Convert the Path object to a JSON representation
|
||||
|
||||
:param openlp.core.common.path.Path value: The value to convert
|
||||
:param pathlib.Path value: The value to convert
|
||||
:param dialect: Not used
|
||||
:return: The Path object as a JSON string
|
||||
:rtype: str
|
||||
:return str: The Path object as a JSON string
|
||||
"""
|
||||
data_path = AppLocation.get_data_path()
|
||||
return json.dumps(value, cls=OpenLPJsonEncoder, base_path=data_path)
|
||||
return json.dumps(value, cls=OpenLPJSONEncoder, base_path=data_path)
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
"""
|
||||
@ -253,10 +250,10 @@ class PathType(types.TypeDecorator):
|
||||
:param types.UnicodeText value: The value to convert
|
||||
:param dialect: Not used
|
||||
:return: The JSON object converted Python object (in this case it should be a Path object)
|
||||
:rtype: openlp.core.common.path.Path
|
||||
:rtype: pathlib.Path
|
||||
"""
|
||||
data_path = AppLocation.get_data_path()
|
||||
return json.loads(value, cls=OpenLPJsonDecoder, base_path=data_path)
|
||||
return json.loads(value, cls=OpenLPJSONDecoder, base_path=data_path)
|
||||
|
||||
|
||||
def upgrade_db(url, upgrade):
|
||||
@ -351,8 +348,8 @@ class Manager(object):
|
||||
|
||||
:param plugin_name: The name to setup paths and settings section names
|
||||
:param init_schema: The init_schema function for this database
|
||||
:param openlp.core.common.path.Path db_file_path: The file name to use for this database. Defaults to None
|
||||
resulting in the plugin_name being used.
|
||||
:param pathlib.Path | None db_file_path: The file name to use for this database. Defaults to None resulting in
|
||||
the plugin_name being used.
|
||||
:param upgrade_mod: The upgrade_schema function for this database
|
||||
"""
|
||||
super().__init__()
|
||||
|
@ -369,7 +369,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
||||
Process a list for files either from the File Dialog or from Drag and
|
||||
Drop
|
||||
|
||||
:param list[openlp.core.common.path.Path] file_paths: The files to be loaded.
|
||||
:param list[pathlib.Path] file_paths: The files to be loaded.
|
||||
:param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
|
||||
"""
|
||||
full_list = []
|
||||
@ -410,7 +410,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
||||
"""
|
||||
Return the current list of files
|
||||
|
||||
:rtype: list[openlp.core.common.path.Path]
|
||||
:rtype: list[pathlib.Path]
|
||||
"""
|
||||
file_paths = []
|
||||
for index in range(self.list_view.count()):
|
||||
@ -462,7 +462,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
||||
:param item: The database item to be used to build the service item
|
||||
:param remote: Was this remote triggered (False)
|
||||
:param context: The service context
|
||||
:param openlp.core.common.path.Path file_path:
|
||||
:param pathlib.Path file_path:
|
||||
"""
|
||||
raise NotImplementedError('MediaManagerItem.generate_slide_data needs to be defined by the plugin')
|
||||
|
||||
@ -589,7 +589,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
||||
"""
|
||||
Add this item to the current service.
|
||||
|
||||
:param item: Item to be processed
|
||||
:param QtWidgets.QListWidgetItem | QtWidgets.QTreeWidgetItem | None item: Item to be processed
|
||||
:param replace: Replace the existing item
|
||||
:param remote: Triggered from remote
|
||||
:param position: Position to place item
|
||||
@ -627,7 +627,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
||||
def build_service_item(self, item=None, remote=False, context=ServiceItemContext.Live):
|
||||
"""
|
||||
Common method for generating a service item
|
||||
:param item: Service Item to be built.
|
||||
:param QtWidgets.QListWidgetItem | QtWidgets.QTreeWidgetItem | None item: Service Item to be built.
|
||||
:param remote: Remote triggered (False)
|
||||
:param context: The context on which this is called
|
||||
"""
|
||||
|
@ -29,6 +29,7 @@ import ntpath
|
||||
import os
|
||||
import uuid
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtGui
|
||||
|
||||
@ -37,7 +38,6 @@ from openlp.core.common import md5_hash
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.mixins import RegistryProperties
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.display.render import remove_tags, render_tags
|
||||
from openlp.core.lib import ItemCapabilities
|
||||
@ -264,8 +264,8 @@ 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(str(AppLocation.get_section_data_path(self.name)), 'thumbnails',
|
||||
file_location_hash, ntpath.basename(image)) # TODO: Pathlib
|
||||
image = os.path.join(AppLocation.get_section_data_path(self.name), 'thumbnails', file_location_hash,
|
||||
ntpath.basename(image)) # TODO: Pathlib
|
||||
self.slides.append({'title': file_name, 'image': image, 'path': path, 'display_title': display_title,
|
||||
'notes': notes, 'thumbnail': image})
|
||||
# if self.is_capable(ItemCapabilities.HasThumbnails):
|
||||
@ -593,9 +593,11 @@ class ServiceItem(RegistryProperties):
|
||||
"""
|
||||
return not bool(self.slides)
|
||||
|
||||
def validate_item(self, suffix_list=None):
|
||||
def validate_item(self, suffixes=None):
|
||||
"""
|
||||
Validates a service item to make sure it is valid
|
||||
|
||||
:param set[str] suffixes: A set of vaild suffixes
|
||||
"""
|
||||
self.is_valid = True
|
||||
for slide in self.slides:
|
||||
@ -612,8 +614,8 @@ class ServiceItem(RegistryProperties):
|
||||
if not os.path.exists(file_name):
|
||||
self.is_valid = False
|
||||
break
|
||||
if suffix_list and not self.is_text():
|
||||
if suffixes and not self.is_text():
|
||||
file_suffix = slide['title'].split('.')[-1]
|
||||
if file_suffix.lower() not in suffix_list:
|
||||
if file_suffix.lower() not in suffixes:
|
||||
self.is_valid = False
|
||||
break
|
||||
|
@ -29,7 +29,7 @@ from lxml import etree, objectify
|
||||
|
||||
from openlp.core.common import de_hump
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder
|
||||
from openlp.core.common.json import OpenLPJSONDecoder, OpenLPJSONEncoder
|
||||
from openlp.core.display.screens import ScreenList
|
||||
from openlp.core.lib import get_text_file_string, str_to_bool
|
||||
|
||||
@ -190,7 +190,7 @@ class Theme(object):
|
||||
"""
|
||||
Add the path name to the image name so the background can be rendered.
|
||||
|
||||
:param openlp.core.common.path.Path path: The path name to be added.
|
||||
:param pathlib.Path path: The path name to be added.
|
||||
:rtype: None
|
||||
"""
|
||||
if self.background_type == 'image' or self.background_type == 'video':
|
||||
@ -216,26 +216,27 @@ class Theme(object):
|
||||
Convert the JSON file and expand it.
|
||||
|
||||
:param theme: the theme string
|
||||
:param openlp.core.common.path.Path theme_path: The path to the theme
|
||||
:param pathlib.Path theme_path: The path to the theme
|
||||
:rtype: None
|
||||
"""
|
||||
if theme_path:
|
||||
jsn = json.loads(theme, cls=OpenLPJsonDecoder, base_path=theme_path)
|
||||
jsn = json.loads(theme, cls=OpenLPJSONDecoder, base_path=theme_path)
|
||||
else:
|
||||
jsn = json.loads(theme, cls=OpenLPJsonDecoder)
|
||||
jsn = json.loads(theme, cls=OpenLPJSONDecoder)
|
||||
self.expand_json(jsn)
|
||||
|
||||
def export_theme(self, theme_path=None):
|
||||
def export_theme(self, theme_path=None, is_js=False):
|
||||
"""
|
||||
Loop through the fields and build a dictionary of them
|
||||
|
||||
:param pathlib.Path | None theme_path:
|
||||
:param bool is_js: For internal use, for example with the theme js code.
|
||||
:return str: The json encoded theme object
|
||||
"""
|
||||
theme_data = {}
|
||||
for attr, value in self.__dict__.items():
|
||||
theme_data["{attr}".format(attr=attr)] = value
|
||||
if theme_path:
|
||||
return json.dumps(theme_data, cls=OpenLPJsonEncoder, base_path=theme_path)
|
||||
return json.dumps(theme_data, cls=OpenLPJsonEncoder)
|
||||
return json.dumps(theme_data, cls=OpenLPJSONEncoder, base_path=theme_path, is_js=is_js)
|
||||
|
||||
def parse(self, xml):
|
||||
"""
|
||||
|
@ -19,10 +19,11 @@
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtCore, QtNetwork
|
||||
|
||||
from openlp.core.common.mixins import LogMixin
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.registry import Registry
|
||||
|
||||
|
||||
|
@ -518,7 +518,7 @@ class AdvancedTab(SettingsTab):
|
||||
"""
|
||||
Handle the `editPathChanged` signal of the data_directory_path_edit
|
||||
|
||||
:param openlp.core.common.path.Path new_path: The new path
|
||||
:param pathlib.Path new_path: The new path
|
||||
:rtype: None
|
||||
"""
|
||||
# Make sure they want to change the data.
|
||||
@ -552,7 +552,7 @@ class AdvancedTab(SettingsTab):
|
||||
"""
|
||||
Check if there's already data in the target directory.
|
||||
|
||||
:param openlp.core.common.path.Path data_path: The target directory to check
|
||||
:param pathlib.Path data_path: The target directory to check
|
||||
"""
|
||||
if (data_path / 'songs').exists():
|
||||
self.data_exists = True
|
||||
|
@ -28,6 +28,7 @@ import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
@ -37,7 +38,7 @@ from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.httputils import DownloadWorker, download_file, get_url_file_size, get_web_page
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.mixins import RegistryProperties
|
||||
from openlp.core.common.path import Path, create_paths
|
||||
from openlp.core.common.path import create_paths
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib import build_icon
|
||||
|
@ -84,7 +84,7 @@ class FormattingTagController(object):
|
||||
'desc': desc,
|
||||
'start tag': '{{{tag}}}'.format(tag=tag),
|
||||
'start html': start_html,
|
||||
'end tag': '{{{tag}}}'.format(tag=tag),
|
||||
'end tag': '{{/{tag}}}'.format(tag=tag),
|
||||
'end html': end_html,
|
||||
'protected': False,
|
||||
'temporary': False
|
||||
|
@ -23,12 +23,12 @@
|
||||
The general tab of the configuration dialog.
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import get_images_filter
|
||||
from openlp.core.common.i18n import UiStrings, translate
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.display.screens import ScreenList
|
||||
from openlp.core.lib.settingstab import SettingsTab
|
||||
|
@ -23,9 +23,11 @@
|
||||
This is the main window, where all the action happens.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from distutils import dir_util
|
||||
from distutils.errors import DistutilsFileError
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
@ -38,7 +40,7 @@ from openlp.core.common.actions import ActionList, CategoryOrder
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.i18n import LanguageManager, UiStrings, translate
|
||||
from openlp.core.common.mixins import LogMixin, RegistryProperties
|
||||
from openlp.core.common.path import Path, copyfile, create_paths
|
||||
from openlp.core.common.path import create_paths
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.display.screens import ScreenList
|
||||
@ -658,7 +660,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
|
||||
plugin.first_time()
|
||||
self.application.process_events()
|
||||
temp_path = Path(gettempdir(), 'openlp')
|
||||
temp_path.rmtree(True)
|
||||
shutil.rmtree(temp_path, True)
|
||||
|
||||
def on_first_time_wizard_clicked(self):
|
||||
"""
|
||||
@ -861,7 +863,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
|
||||
temp_dir_path = Path(gettempdir(), 'openlp')
|
||||
create_paths(temp_dir_path)
|
||||
temp_config_path = temp_dir_path / import_file_path.name
|
||||
copyfile(import_file_path, temp_config_path)
|
||||
shutil.copyfile(import_file_path, temp_config_path)
|
||||
settings = Settings()
|
||||
import_settings = Settings(str(temp_config_path), Settings.IniFormat)
|
||||
|
||||
@ -1332,7 +1334,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
|
||||
self.show_status_message(
|
||||
translate('OpenLP.MainWindow', 'Copying OpenLP data to new data directory location - {path} '
|
||||
'- Please wait for copy to finish').format(path=self.new_data_path))
|
||||
dir_util.copy_tree(old_data_path, self.new_data_path)
|
||||
dir_util.copy_tree(str(old_data_path), str(self.new_data_path))
|
||||
self.log_info('Copy successful')
|
||||
except (OSError, DistutilsFileError) as why:
|
||||
self.application.set_normal_cursor()
|
||||
|
@ -26,6 +26,19 @@ import logging
|
||||
|
||||
log = logging.getLogger(__name__ + '.__init__')
|
||||
|
||||
# Audio and video extensions copied from 'include/vlc_interface.h' from vlc 2.2.0 source
|
||||
AUDIO_EXT = ['*.3ga', '*.669', '*.a52', '*.aac', '*.ac3', '*.adt', '*.adts', '*.aif', '*.aifc', '*.aiff', '*.amr',
|
||||
'*.aob', '*.ape', '*.awb', '*.caf', '*.dts', '*.flac', '*.it', '*.kar', '*.m4a', '*.m4b', '*.m4p', '*.m5p',
|
||||
'*.mid', '*.mka', '*.mlp', '*.mod', '*.mpa', '*.mp1', '*.mp2', '*.mp3', '*.mpc', '*.mpga', '*.mus',
|
||||
'*.oga', '*.ogg', '*.oma', '*.opus', '*.qcp', '*.ra', '*.rmi', '*.s3m', '*.sid', '*.spx', '*.thd', '*.tta',
|
||||
'*.voc', '*.vqf', '*.w64', '*.wav', '*.wma', '*.wv', '*.xa', '*.xm']
|
||||
VIDEO_EXT = ['*.3g2', '*.3gp', '*.3gp2', '*.3gpp', '*.amv', '*.asf', '*.avi', '*.bik', '*.divx', '*.drc', '*.dv',
|
||||
'*.f4v', '*.flv', '*.gvi', '*.gxf', '*.iso', '*.m1v', '*.m2v', '*.m2t', '*.m2ts', '*.m4v', '*.mkv',
|
||||
'*.mov', '*.mp2', '*.mp2v', '*.mp4', '*.mp4v', '*.mpe', '*.mpeg', '*.mpeg1', '*.mpeg2', '*.mpeg4', '*.mpg',
|
||||
'*.mpv2', '*.mts', '*.mtv', '*.mxf', '*.mxg', '*.nsv', '*.nuv', '*.ogg', '*.ogm', '*.ogv', '*.ogx', '*.ps',
|
||||
'*.rec', '*.rm', '*.rmvb', '*.rpl', '*.thp', '*.tod', '*.ts', '*.tts', '*.txd', '*.vob', '*.vro', '*.webm',
|
||||
'*.wm', '*.wmv', '*.wtv', '*.xesc', '*.nut', '*.rv', '*.xvid']
|
||||
|
||||
|
||||
class MediaState(object):
|
||||
"""
|
||||
|
@ -42,7 +42,7 @@ from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib.serviceitem import ItemCapabilities
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.ui import DisplayControllerType
|
||||
from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path
|
||||
from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path, VIDEO_EXT, AUDIO_EXT
|
||||
from openlp.core.ui.media.endpoint import media_endpoint
|
||||
from openlp.core.ui.media.vlcplayer import VlcPlayer, get_vlc
|
||||
|
||||
@ -65,11 +65,6 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
|
||||
current_media_players is an array of player instances keyed on ControllerType.
|
||||
|
||||
"""
|
||||
def __init__(self, parent=None):
|
||||
"""
|
||||
Constructor
|
||||
"""
|
||||
super(MediaController, self).__init__(parent)
|
||||
|
||||
def setup(self):
|
||||
self.vlc_player = None
|
||||
@ -95,28 +90,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
|
||||
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_suffixes', self._generate_extensions_lists)
|
||||
register_endpoint(media_endpoint)
|
||||
|
||||
def _generate_extensions_lists(self):
|
||||
"""
|
||||
Set the active players and available media files
|
||||
"""
|
||||
suffix_list = []
|
||||
self.audio_extensions_list = []
|
||||
if self.vlc_player.is_active:
|
||||
for item in self.vlc_player.audio_extensions_list:
|
||||
if item not in self.audio_extensions_list:
|
||||
self.audio_extensions_list.append(item)
|
||||
suffix_list.append(item[2:])
|
||||
self.video_extensions_list = []
|
||||
if self.vlc_player.is_active:
|
||||
for item in self.vlc_player.video_extensions_list:
|
||||
if item not in self.video_extensions_list:
|
||||
self.video_extensions_list.append(item)
|
||||
suffix_list.append(item[2:])
|
||||
self.service_manager.supported_suffixes(suffix_list)
|
||||
|
||||
def bootstrap_initialise(self):
|
||||
"""
|
||||
Check to see if we have any media Player's available.
|
||||
@ -129,9 +104,10 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
|
||||
State().update_pre_conditions('mediacontroller', True)
|
||||
State().update_pre_conditions('media_live', True)
|
||||
else:
|
||||
if hasattr(self.main_window, 'splash') and self.main_window.splash.isVisible():
|
||||
self.main_window.splash.hide()
|
||||
State().missing_text('media_live', translate('OpenLP.SlideController',
|
||||
'VLC or pymediainfo are missing, so you are unable to play any media'))
|
||||
self._generate_extensions_lists()
|
||||
return True
|
||||
|
||||
def bootstrap_post_set_up(self):
|
||||
@ -208,7 +184,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
|
||||
display.has_audio = False
|
||||
self.vlc_player.setup(display, preview)
|
||||
|
||||
def set_controls_visible(self, controller, value):
|
||||
@staticmethod
|
||||
def set_controls_visible(controller, value):
|
||||
"""
|
||||
After a new display is configured, all media related widget will be created too
|
||||
|
||||
@ -253,7 +230,10 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
|
||||
display = self._define_display(controller)
|
||||
if controller.is_live:
|
||||
# if this is an optical device use special handling
|
||||
if service_item.is_capable(ItemCapabilities.IsOptical):
|
||||
if service_item.is_capable(ItemCapabilities.CanStream):
|
||||
is_valid = self._check_file_type(controller, display, True)
|
||||
controller.media_info.media_type = MediaType.Stream
|
||||
elif service_item.is_capable(ItemCapabilities.IsOptical):
|
||||
log.debug('video is optical and live')
|
||||
path = service_item.get_frame_path()
|
||||
(name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path)
|
||||
@ -273,7 +253,10 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
|
||||
controller.media_info.start_time = service_item.start_time
|
||||
controller.media_info.end_time = service_item.end_time
|
||||
elif controller.preview_display:
|
||||
if service_item.is_capable(ItemCapabilities.IsOptical):
|
||||
if service_item.is_capable(ItemCapabilities.CanStream):
|
||||
controller.media_info.media_type = MediaType.Stream
|
||||
is_valid = self._check_file_type(controller, display, True)
|
||||
elif service_item.is_capable(ItemCapabilities.IsOptical):
|
||||
log.debug('video is optical and preview')
|
||||
path = service_item.get_frame_path()
|
||||
(name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path)
|
||||
@ -294,6 +277,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
|
||||
# display.frame.runJavaScript('show_video("setBackBoard", null, null,"visible");')
|
||||
# now start playing - Preview is autoplay!
|
||||
autoplay = False
|
||||
if service_item.is_capable(ItemCapabilities.CanStream):
|
||||
autoplay = True
|
||||
# Preview requested
|
||||
if not controller.is_live:
|
||||
autoplay = True
|
||||
@ -370,18 +355,26 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
|
||||
controller.media_info.media_type = MediaType.DVD
|
||||
return True
|
||||
|
||||
def _check_file_type(self, controller, display):
|
||||
def _check_file_type(self, controller, display, stream=False):
|
||||
"""
|
||||
Select the correct media Player type from the prioritized Player list
|
||||
|
||||
:param controller: First element is the controller which should be used
|
||||
:param display: Which display to use
|
||||
:param stream: Are we streaming or not
|
||||
"""
|
||||
if stream:
|
||||
self.resize(display, self.vlc_player)
|
||||
display.media_info.media_type = MediaType.Stream
|
||||
if self.vlc_player.load(display, None):
|
||||
self.current_media_players[controller.controller_type] = self.vlc_player
|
||||
return True
|
||||
return True
|
||||
for file in controller.media_info.file_info:
|
||||
if file.is_file:
|
||||
suffix = '*%s' % file.suffix.lower()
|
||||
file = str(file)
|
||||
if suffix in self.vlc_player.video_extensions_list:
|
||||
if suffix in VIDEO_EXT:
|
||||
if not controller.media_info.is_background or controller.media_info.is_background and \
|
||||
self.vlc_player.can_background:
|
||||
self.resize(display, self.vlc_player)
|
||||
@ -389,7 +382,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
|
||||
self.current_media_players[controller.controller_type] = self.vlc_player
|
||||
controller.media_info.media_type = MediaType.Video
|
||||
return True
|
||||
if suffix in self.vlc_player.audio_extensions_list:
|
||||
if suffix in AUDIO_EXT:
|
||||
if self.vlc_player.load(display, file):
|
||||
self.current_media_players[controller.controller_type] = self.vlc_player
|
||||
controller.media_info.media_type = MediaType.Audio
|
||||
|
@ -38,13 +38,10 @@ class MediaPlayer(RegistryProperties):
|
||||
self.parent = parent
|
||||
self.name = name
|
||||
self.available = self.check_available()
|
||||
self.is_active = False
|
||||
self.can_background = False
|
||||
self.can_folder = False
|
||||
self.state = {0: MediaState.Off, 1: MediaState.Off}
|
||||
self.has_own_widget = False
|
||||
self.audio_extensions_list = []
|
||||
self.video_extensions_list = []
|
||||
|
||||
def check_available(self):
|
||||
"""
|
||||
@ -166,12 +163,6 @@ class MediaPlayer(RegistryProperties):
|
||||
"""
|
||||
return ''
|
||||
|
||||
def get_info(self):
|
||||
"""
|
||||
Returns Information about the player
|
||||
"""
|
||||
return ''
|
||||
|
||||
def get_live_state(self):
|
||||
"""
|
||||
Get the state of the live player
|
||||
|
@ -33,7 +33,7 @@ from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib.settingstab import SettingsTab
|
||||
from openlp.core.ui.icons import UiIcons
|
||||
|
||||
LINUX_STREAM = 'v4l2://{video} :v4l2-standard= :input-slave={audio} :live-caching=300'
|
||||
LINUX_STREAM = 'v4l2://{video}:v4l2-standard= :input-slave=alsa://{audio} :live-caching=300'
|
||||
WIN_STREAM = 'dshow://:dshow-vdev={video} :dshow-adev={audio} :live-caching=300'
|
||||
OSX_STREAM = 'avcapture://{video}:qtsound://{audio} :live-caching=300'
|
||||
|
||||
@ -68,11 +68,15 @@ class MediaTab(SettingsTab):
|
||||
self.left_layout.addWidget(self.live_media_group_box)
|
||||
self.stream_media_group_box = QtWidgets.QGroupBox(self.left_column)
|
||||
self.stream_media_group_box.setObjectName('stream_media_group_box')
|
||||
self.stream_media_layout = QtWidgets.QHBoxLayout(self.stream_media_group_box)
|
||||
self.stream_media_layout = QtWidgets.QFormLayout(self.stream_media_group_box)
|
||||
self.stream_media_layout.setObjectName('stream_media_layout')
|
||||
self.stream_media_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.stream_edit = QtWidgets.QLabel(self)
|
||||
self.stream_media_layout.addWidget(self.stream_edit)
|
||||
self.video_edit = QtWidgets.QLineEdit(self)
|
||||
self.stream_media_layout.addRow(translate('MediaPlugin.MediaTab', 'Video:'), self.video_edit)
|
||||
self.audio_edit = QtWidgets.QLineEdit(self)
|
||||
self.stream_media_layout.addRow(translate('MediaPlugin.MediaTab', 'Audio:'), self.audio_edit)
|
||||
self.stream_cmd = QtWidgets.QLabel(self)
|
||||
self.stream_media_layout.addWidget(self.stream_cmd)
|
||||
self.left_layout.addWidget(self.stream_media_group_box)
|
||||
self.vlc_arguments_group_box = QtWidgets.QGroupBox(self.left_column)
|
||||
self.vlc_arguments_group_box.setObjectName('vlc_arguments_group_box')
|
||||
@ -84,7 +88,6 @@ class MediaTab(SettingsTab):
|
||||
self.left_layout.addWidget(self.vlc_arguments_group_box)
|
||||
self.left_layout.addStretch()
|
||||
self.right_layout.addStretch()
|
||||
# # Signals and slots
|
||||
|
||||
def retranslate_ui(self):
|
||||
"""
|
||||
@ -100,22 +103,28 @@ class MediaTab(SettingsTab):
|
||||
Load the settings
|
||||
"""
|
||||
self.auto_start_check_box.setChecked(Settings().value(self.settings_section + '/media auto start'))
|
||||
self.stream_edit.setText(Settings().value(self.settings_section + '/stream command'))
|
||||
if not self.stream_edit.text():
|
||||
if is_linux:
|
||||
self.stream_edit.setText(LINUX_STREAM)
|
||||
elif is_win:
|
||||
self.stream_edit.setText(WIN_STREAM)
|
||||
else:
|
||||
self.stream_edit.setText(OSX_STREAM)
|
||||
self.stream_cmd.setText(Settings().value(self.settings_section + '/stream command'))
|
||||
self.audio_edit.setText(Settings().value(self.settings_section + '/audio'))
|
||||
self.video_edit.setText(Settings().value(self.settings_section + '/video'))
|
||||
if not self.stream_cmd.text():
|
||||
self.set_base_stream()
|
||||
self.vlc_arguments_edit.setPlainText(Settings().value(self.settings_section + '/vlc arguments'))
|
||||
if Settings().value('advanced/experimental'):
|
||||
# vlc.MediaPlayer().audio_output_device_enum()
|
||||
for cam in QCameraInfo.availableCameras():
|
||||
log.debug(cam.deviceName())
|
||||
log.debug(cam.description())
|
||||
for au in QAudioDeviceInfo.availableDevices(QAudio.AudioInput):
|
||||
log.debug(au.deviceName())
|
||||
|
||||
def set_base_stream(self):
|
||||
if is_linux:
|
||||
self.stream_cmd.setText(LINUX_STREAM)
|
||||
elif is_win:
|
||||
self.stream_cmd.setText(WIN_STREAM)
|
||||
else:
|
||||
self.stream_cmd.setText(OSX_STREAM)
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Save the settings
|
||||
@ -123,8 +132,12 @@ class MediaTab(SettingsTab):
|
||||
setting_key = self.settings_section + '/media auto start'
|
||||
if Settings().value(setting_key) != self.auto_start_check_box.checkState():
|
||||
Settings().setValue(setting_key, self.auto_start_check_box.checkState())
|
||||
Settings().setValue(self.settings_section + '/stream command', self.stream_edit.text())
|
||||
Settings().setValue(self.settings_section + '/stream command', self.stream_cmd.text())
|
||||
Settings().setValue(self.settings_section + '/vlc arguments', self.vlc_arguments_edit.toPlainText())
|
||||
Settings().setValue(self.settings_section + '/video', self.video_edit.text())
|
||||
Settings().setValue(self.settings_section + '/audio', self.audio_edit.text())
|
||||
self.stream_cmd.setText(self.stream_cmd.text().format(video=self.video_edit.text(),
|
||||
audio=self.audio_edit.text()))
|
||||
|
||||
def post_set_up(self, post_update=False):
|
||||
"""
|
||||
|
@ -28,12 +28,10 @@ import os
|
||||
import sys
|
||||
import threading
|
||||
from datetime import datetime
|
||||
import vlc
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from openlp.core.common import is_linux, is_macosx, is_win
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.ui.media import MediaState, MediaType
|
||||
from openlp.core.ui.media.mediaplayer import MediaPlayer
|
||||
@ -42,20 +40,6 @@ from openlp.core.ui.media.mediaplayer import MediaPlayer
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Audio and video extensions copied from 'include/vlc_interface.h' from vlc 2.2.0 source
|
||||
AUDIO_EXT = ['*.3ga', '*.669', '*.a52', '*.aac', '*.ac3', '*.adt', '*.adts', '*.aif', '*.aifc', '*.aiff', '*.amr',
|
||||
'*.aob', '*.ape', '*.awb', '*.caf', '*.dts', '*.flac', '*.it', '*.kar', '*.m4a', '*.m4b', '*.m4p', '*.m5p',
|
||||
'*.mid', '*.mka', '*.mlp', '*.mod', '*.mpa', '*.mp1', '*.mp2', '*.mp3', '*.mpc', '*.mpga', '*.mus',
|
||||
'*.oga', '*.ogg', '*.oma', '*.opus', '*.qcp', '*.ra', '*.rmi', '*.s3m', '*.sid', '*.spx', '*.thd', '*.tta',
|
||||
'*.voc', '*.vqf', '*.w64', '*.wav', '*.wma', '*.wv', '*.xa', '*.xm']
|
||||
|
||||
VIDEO_EXT = ['*.3g2', '*.3gp', '*.3gp2', '*.3gpp', '*.amv', '*.asf', '*.avi', '*.bik', '*.divx', '*.drc', '*.dv',
|
||||
'*.f4v', '*.flv', '*.gvi', '*.gxf', '*.iso', '*.m1v', '*.m2v', '*.m2t', '*.m2ts', '*.m4v', '*.mkv',
|
||||
'*.mov', '*.mp2', '*.mp2v', '*.mp4', '*.mp4v', '*.mpe', '*.mpeg', '*.mpeg1', '*.mpeg2', '*.mpeg4', '*.mpg',
|
||||
'*.mpv2', '*.mts', '*.mtv', '*.mxf', '*.mxg', '*.nsv', '*.nuv', '*.ogg', '*.ogm', '*.ogv', '*.ogx', '*.ps',
|
||||
'*.rec', '*.rm', '*.rmvb', '*.rpl', '*.thp', '*.tod', '*.ts', '*.tts', '*.txd', '*.vob', '*.vro', '*.webm',
|
||||
'*.wm', '*.wmv', '*.wtv', '*.xesc',
|
||||
# These extensions was not in the official list, added manually.
|
||||
'*.nut', '*.rv', '*.xvid']
|
||||
|
||||
|
||||
def get_vlc():
|
||||
@ -65,8 +49,13 @@ def get_vlc():
|
||||
|
||||
:return: The "vlc" module, or None
|
||||
"""
|
||||
if 'vlc' in sys.modules:
|
||||
# If VLC has already been imported, no need to do all the stuff below again
|
||||
# Import the VLC module if not already done
|
||||
if 'vlc' not in sys.modules:
|
||||
try:
|
||||
import vlc # noqa module is not used directly, but is used via sys.modules['vlc']
|
||||
except (ImportError, OSError):
|
||||
return None
|
||||
# Verify that VLC is also loadable
|
||||
is_vlc_available = False
|
||||
try:
|
||||
is_vlc_available = bool(sys.modules['vlc'].get_default_instance())
|
||||
@ -74,16 +63,13 @@ def get_vlc():
|
||||
pass
|
||||
if is_vlc_available:
|
||||
return sys.modules['vlc']
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return vlc
|
||||
|
||||
|
||||
# On linux we need to initialise X threads, but not when running tests.
|
||||
# This needs to happen on module load and not in get_vlc(), otherwise it can cause crashes on some DE on some setups
|
||||
# (reported on Gnome3, Unity, Cinnamon, all GTK+ based) when using native filedialogs...
|
||||
if is_linux() and 'nose' not in sys.argv[0] and get_vlc():
|
||||
if is_linux() and 'pytest' not in sys.argv[0] and get_vlc():
|
||||
try:
|
||||
try:
|
||||
x11 = ctypes.cdll.LoadLibrary('libX11.so.6')
|
||||
@ -109,8 +95,6 @@ class VlcPlayer(MediaPlayer):
|
||||
self.display_name = '&VLC'
|
||||
self.parent = parent
|
||||
self.can_folder = True
|
||||
self.audio_extensions_list = AUDIO_EXT
|
||||
self.video_extensions_list = VIDEO_EXT
|
||||
|
||||
def setup(self, output_display, live_display):
|
||||
"""
|
||||
@ -163,16 +147,15 @@ class VlcPlayer(MediaPlayer):
|
||||
Load a video into VLC
|
||||
|
||||
:param output_display: The display where the media is
|
||||
:param file: file to be played
|
||||
:param file: file to be played or None for live streaming
|
||||
:return:
|
||||
"""
|
||||
vlc = get_vlc()
|
||||
log.debug('load vid in Vlc Controller')
|
||||
controller = output_display
|
||||
volume = controller.media_info.volume
|
||||
if file:
|
||||
path = os.path.normcase(file)
|
||||
# create the media
|
||||
if controller.media_info.media_type == MediaType.CD:
|
||||
if output_display.media_info.media_type == MediaType.CD:
|
||||
if is_win():
|
||||
path = '/' + path
|
||||
output_display.vlc_media = output_display.vlc_instance.media_new_location('cdda://' + path)
|
||||
@ -184,8 +167,8 @@ class VlcPlayer(MediaPlayer):
|
||||
audio_cd_tracks = output_display.vlc_media.subitems()
|
||||
if not audio_cd_tracks or audio_cd_tracks.count() < 1:
|
||||
return False
|
||||
output_display.vlc_media = audio_cd_tracks.item_at_index(controller.media_info.title_track)
|
||||
elif controller.media_info.media_type == MediaType.Stream:
|
||||
output_display.vlc_media = audio_cd_tracks.item_at_index(output_display.media_info.title_track)
|
||||
elif output_display.media_info.media_type == MediaType.Stream:
|
||||
stream_cmd = Settings().value('media/stream command')
|
||||
output_display.vlc_media = output_display.vlc_instance.media_new_location(stream_cmd)
|
||||
else:
|
||||
@ -194,7 +177,7 @@ class VlcPlayer(MediaPlayer):
|
||||
output_display.vlc_media_player.set_media(output_display.vlc_media)
|
||||
# parse the metadata of the file
|
||||
output_display.vlc_media.parse()
|
||||
self.volume(output_display, volume)
|
||||
self.volume(output_display, output_display.media_info.volume)
|
||||
return True
|
||||
|
||||
def media_state_wait(self, output_display, media_state):
|
||||
@ -374,14 +357,3 @@ class VlcPlayer(MediaPlayer):
|
||||
else:
|
||||
controller.seek_slider.setSliderPosition(output_display.vlc_media_player.get_time())
|
||||
controller.seek_slider.blockSignals(False)
|
||||
|
||||
def get_info(self):
|
||||
"""
|
||||
Return some information about this player
|
||||
"""
|
||||
return(translate('Media.player', 'VLC is an external player which '
|
||||
'supports a number of different formats.') +
|
||||
'<br/> <strong>' + translate('Media.player', 'Audio') +
|
||||
'</strong><br/>' + str(AUDIO_EXT) + '<br/><strong>' +
|
||||
translate('Media.player', 'Video') + '</strong><br/>' +
|
||||
str(VIDEO_EXT) + '<br/>')
|
||||
|
@ -24,10 +24,12 @@ The service manager sets up, loads, saves and manages services.
|
||||
"""
|
||||
import html
|
||||
import json
|
||||
import shutil
|
||||
import os
|
||||
import zipfile
|
||||
from contextlib import suppress
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
@ -36,9 +38,8 @@ from openlp.core.common import ThemeLevel, delete_file
|
||||
from openlp.core.common.actions import ActionList, CategoryOrder
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.i18n import UiStrings, format_time, translate
|
||||
from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder
|
||||
from openlp.core.common.json import OpenLPJSONDecoder, OpenLPJSONEncoder
|
||||
from openlp.core.common.mixins import LogMixin, RegistryProperties
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.registry import Registry, RegistryBase
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib import build_icon
|
||||
@ -47,6 +48,7 @@ from openlp.core.lib.plugin import PluginStatus
|
||||
from openlp.core.lib.serviceitem import ItemCapabilities, ServiceItem
|
||||
from openlp.core.lib.ui import create_widget_action, critical_error_message_box, find_and_set_in_combo_box
|
||||
from openlp.core.ui.icons import UiIcons
|
||||
from openlp.core.ui.media import AUDIO_EXT, VIDEO_EXT
|
||||
from openlp.core.ui.serviceitemeditform import ServiceItemEditForm
|
||||
from openlp.core.ui.servicenoteform import ServiceNoteForm
|
||||
from openlp.core.ui.starttimeform import StartTimeForm
|
||||
@ -319,7 +321,8 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.service_items = []
|
||||
self.suffixes = []
|
||||
self.suffixes = set()
|
||||
self.add_media_suffixes()
|
||||
self.drop_position = -1
|
||||
self.service_id = 0
|
||||
# is a new service and has not been saved
|
||||
@ -346,6 +349,13 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
self.service_item_edit_form = ServiceItemEditForm()
|
||||
self.start_time_form = StartTimeForm()
|
||||
|
||||
def add_media_suffixes(self):
|
||||
"""
|
||||
Add the suffixes supported by :mod:`openlp.core.ui.media.vlcplayer`
|
||||
"""
|
||||
self.suffixes.update(AUDIO_EXT)
|
||||
self.suffixes.update(VIDEO_EXT)
|
||||
|
||||
def set_modified(self, modified=True):
|
||||
"""
|
||||
Setter for property "modified". Sets whether or not the current service has been modified.
|
||||
@ -371,7 +381,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
"""
|
||||
Setter for service file.
|
||||
|
||||
:param openlp.core.common.path.Path file_path: The service file name
|
||||
:param Path file_path: The service file name
|
||||
:rtype: None
|
||||
"""
|
||||
self._service_path = file_path
|
||||
@ -386,7 +396,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
"""
|
||||
Return the current file name including path.
|
||||
|
||||
:rtype: openlp.core.common.path.Path
|
||||
:rtype: Path
|
||||
"""
|
||||
return self._service_path
|
||||
|
||||
@ -400,22 +410,19 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
def reset_supported_suffixes(self):
|
||||
"""
|
||||
Resets the Suffixes list.
|
||||
|
||||
"""
|
||||
self.suffixes = []
|
||||
self.suffixes.clear()
|
||||
|
||||
def supported_suffixes(self, suffix_list):
|
||||
"""
|
||||
Adds Suffixes supported to the master list. Called from Plugins.
|
||||
|
||||
:param suffix_list: New Suffix's to be supported
|
||||
:param list[str] | str suffix_list: New suffix(s) to be supported
|
||||
"""
|
||||
if isinstance(suffix_list, str):
|
||||
self.suffixes.append(suffix_list)
|
||||
self.suffixes.add(suffix_list)
|
||||
else:
|
||||
for suffix in suffix_list:
|
||||
if suffix not in self.suffixes:
|
||||
self.suffixes.append(suffix)
|
||||
self.suffixes.update(suffix_list)
|
||||
|
||||
def on_new_service_clicked(self):
|
||||
"""
|
||||
@ -443,7 +450,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
"""
|
||||
Loads the service file and saves the existing one it there is one unchanged.
|
||||
|
||||
:param openlp.core.common.path.Path | None file_path: The service file to the loaded.
|
||||
:param Path | None file_path: The service file to the loaded.
|
||||
"""
|
||||
if self.is_modified():
|
||||
result = self.save_modified_service()
|
||||
@ -474,9 +481,11 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard |
|
||||
QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Save)
|
||||
|
||||
def on_recent_service_clicked(self):
|
||||
def on_recent_service_clicked(self, checked):
|
||||
"""
|
||||
Load a recent file as the service triggered by mainwindow recent service list.
|
||||
|
||||
:param bool checked: Not used
|
||||
"""
|
||||
if self.is_modified():
|
||||
result = self.save_modified_service()
|
||||
@ -518,7 +527,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
Get a list of files used in the service and files that are missing.
|
||||
|
||||
:return: A list of files used in the service that exist, and a list of files that don't.
|
||||
:rtype: (list[openlp.core.common.path.Path], list[openlp.core.common.path.Path])
|
||||
:rtype: (list[Path], list[Path])
|
||||
"""
|
||||
write_list = []
|
||||
missing_list = []
|
||||
@ -581,7 +590,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
# Add the service item to the service.
|
||||
service.append({'serviceitem': service_item})
|
||||
self.repaint_service_list(-1, -1)
|
||||
service_content = json.dumps(service, cls=OpenLPJsonEncoder)
|
||||
service_content = json.dumps(service, cls=OpenLPJSONEncoder)
|
||||
service_content_size = len(bytes(service_content, encoding='utf-8'))
|
||||
total_size = service_content_size
|
||||
for file_item in write_list:
|
||||
@ -679,7 +688,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
"""
|
||||
Load an existing service file.
|
||||
|
||||
:param openlp.core.common.path.Path file_path: The service file to load.
|
||||
:param Path file_path: The service file to load.
|
||||
"""
|
||||
if not file_path.exists():
|
||||
return False
|
||||
@ -702,7 +711,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
zip_file.extract(zip_info, self.service_path)
|
||||
self.main_window.increment_progress_bar(zip_info.compress_size)
|
||||
if service_data:
|
||||
items = json.loads(service_data, cls=OpenLPJsonDecoder)
|
||||
items = json.loads(service_data, cls=OpenLPJSONDecoder)
|
||||
self.new_file()
|
||||
self.process_service_items(items)
|
||||
self.set_file_name(file_path)
|
||||
@ -975,8 +984,10 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
prev_item_last_slide = None
|
||||
service_iterator = QtWidgets.QTreeWidgetItemIterator(self.service_manager_list)
|
||||
while service_iterator.value():
|
||||
# Found the selected/current service item
|
||||
if service_iterator.value() == selected:
|
||||
if last_slide and prev_item_last_slide:
|
||||
# Go to the last slide of the previous service item
|
||||
pos = prev_item.data(0, QtCore.Qt.UserRole)
|
||||
check_expanded = self.service_items[pos - 1]['expanded']
|
||||
self.service_manager_list.setCurrentItem(prev_item_last_slide)
|
||||
@ -985,13 +996,17 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
self.make_live()
|
||||
self.service_manager_list.setCurrentItem(prev_item)
|
||||
elif prev_item:
|
||||
# Go to the first slide of the previous service item
|
||||
self.service_manager_list.setCurrentItem(prev_item)
|
||||
self.make_live()
|
||||
return
|
||||
# Found the previous service item root
|
||||
if service_iterator.value().parent() is None:
|
||||
prev_item = service_iterator.value()
|
||||
# Found the last slide of the previous item
|
||||
if service_iterator.value().parent() is prev_item:
|
||||
prev_item_last_slide = service_iterator.value()
|
||||
# Go to next item in the tree
|
||||
service_iterator += 1
|
||||
|
||||
def on_set_item(self, message):
|
||||
@ -1250,7 +1265,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
delete_file(file_path)
|
||||
audio_path = self.service_path / 'audio'
|
||||
if audio_path.exists():
|
||||
audio_path.rmtree(True)
|
||||
shutil.rmtree(audio_path, True)
|
||||
|
||||
def on_theme_combo_box_selected(self, current_index):
|
||||
"""
|
||||
|
@ -25,6 +25,7 @@ The :mod:`slidecontroller` module contains the most important part of OpenLP - t
|
||||
import copy
|
||||
import datetime
|
||||
from collections import deque
|
||||
from pathlib import Path
|
||||
from threading import Lock
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
@ -33,7 +34,6 @@ from openlp.core.common import SlideLimits
|
||||
from openlp.core.common.actions import ActionList, CategoryOrder
|
||||
from openlp.core.common.i18n import UiStrings, translate
|
||||
from openlp.core.common.mixins import LogMixin, RegistryProperties
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.registry import Registry, RegistryBase
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.display.screens import ScreenList
|
||||
@ -1261,9 +1261,18 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
|
||||
if not self.service_item:
|
||||
return
|
||||
if self.service_item.is_command():
|
||||
Registry().execute('{text}_next'.format(text=self.service_item.name.lower()),
|
||||
past_end = Registry().execute('{text}_next'.format(text=self.service_item.name.lower()),
|
||||
[self.service_item, self.is_live])
|
||||
if self.is_live:
|
||||
# Check if we have gone past the end of the last slide
|
||||
if self.is_live and past_end and past_end[0]:
|
||||
if wrap is None:
|
||||
if self.slide_limits == SlideLimits.Wrap:
|
||||
self.on_slide_selected_index([0])
|
||||
elif self.is_live and self.slide_limits == SlideLimits.Next:
|
||||
self.service_next()
|
||||
elif wrap:
|
||||
self.on_slide_selected_index([0])
|
||||
elif self.is_live:
|
||||
self.update_preview()
|
||||
else:
|
||||
row = self.preview_widget.current_slide_number() + 1
|
||||
@ -1290,9 +1299,16 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
|
||||
if not self.service_item:
|
||||
return
|
||||
if self.service_item.is_command():
|
||||
Registry().execute('{text}_previous'.format(text=self.service_item.name.lower()),
|
||||
before_start = Registry().execute('{text}_previous'.format(text=self.service_item.name.lower()),
|
||||
[self.service_item, self.is_live])
|
||||
if self.is_live:
|
||||
# Check id we have tried to go before that start slide
|
||||
if self.is_live and before_start and before_start[0]:
|
||||
if self.slide_limits == SlideLimits.Wrap:
|
||||
self.on_slide_selected_index([self.preview_widget.slide_count() - 1])
|
||||
elif self.is_live and self.slide_limits == SlideLimits.Next:
|
||||
self.keypress_queue.append(ServiceItemAction.PreviousLastSlide)
|
||||
self._process_queue()
|
||||
elif self.is_live:
|
||||
self.update_preview()
|
||||
else:
|
||||
row = self.preview_widget.current_slide_number() - 1
|
||||
|
@ -32,8 +32,7 @@ from openlp.core.common.mixins import RegistryProperties
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.lib.theme import BackgroundGradientType, BackgroundType
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
# TODO: Fix this. Use a "get_video_extensions" method which uses the current media player
|
||||
from openlp.core.ui.media.vlcplayer import VIDEO_EXT
|
||||
from openlp.core.ui.media import VIDEO_EXT
|
||||
from openlp.core.ui.themelayoutform import ThemeLayoutForm
|
||||
from openlp.core.ui.themewizard import Ui_ThemeWizard
|
||||
|
||||
@ -76,9 +75,8 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
||||
self.image_path_edit.filters = \
|
||||
'{name};;{text} (*)'.format(name=get_images_filter(), text=UiStrings().AllFiles)
|
||||
self.image_path_edit.pathChanged.connect(self.on_image_path_edit_path_changed)
|
||||
# TODO: Should work
|
||||
visible_formats = '({name})'.format(name='; '.join(VIDEO_EXT))
|
||||
actual_formats = '({name})'.format(name=' '.join(VIDEO_EXT))
|
||||
visible_formats = '(*.{name})'.format(name='; *.'.join(VIDEO_EXT))
|
||||
actual_formats = '(*.{name})'.format(name=' *.'.join(VIDEO_EXT))
|
||||
video_filter = '{trans} {visible} {actual}'.format(trans=translate('OpenLP', 'Video Files'),
|
||||
visible=visible_formats, actual=actual_formats)
|
||||
self.video_path_edit.filters = '{video};;{ui} (*)'.format(video=video_filter, ui=UiStrings().AllFiles)
|
||||
@ -174,16 +172,14 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
||||
if not event:
|
||||
event = QtGui.QResizeEvent(self.size(), self.size())
|
||||
QtWidgets.QWizard.resizeEvent(self, event)
|
||||
if hasattr(self, 'preview_page') and self.currentPage() == self.preview_page:
|
||||
frame_width = self.preview_box_label.lineWidth()
|
||||
pixmap_width = self.preview_area.width() - 2 * frame_width
|
||||
pixmap_height = self.preview_area.height() - 2 * frame_width
|
||||
aspect_ratio = float(pixmap_width) / pixmap_height
|
||||
if aspect_ratio < self.display_aspect_ratio:
|
||||
pixmap_height = int(pixmap_width / self.display_aspect_ratio + 0.5)
|
||||
else:
|
||||
pixmap_width = int(pixmap_height * self.display_aspect_ratio + 0.5)
|
||||
self.preview_box_label.setFixedSize(pixmap_width + 2 * frame_width, pixmap_height + 2 * frame_width)
|
||||
try:
|
||||
self.display_aspect_ratio = self.renderer.width() / self.renderer.height()
|
||||
except ZeroDivisionError:
|
||||
self.display_aspect_ratio = 1
|
||||
# Make sure we don't resize before the widgets are actually created
|
||||
if hasattr(self, 'preview_area_layout'):
|
||||
self.preview_area_layout.set_aspect_ratio(self.display_aspect_ratio)
|
||||
self.preview_box.set_scale(float(self.preview_box.width()) / self.renderer.width())
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
@ -208,11 +204,17 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
||||
self.setOption(QtWidgets.QWizard.HaveCustomButton1, enabled)
|
||||
if self.page(page_id) == self.preview_page:
|
||||
self.update_theme()
|
||||
frame = self.theme_manager.generate_image(self.theme)
|
||||
frame.setDevicePixelRatio(self.devicePixelRatio())
|
||||
self.preview_box_label.setPixmap(frame)
|
||||
self.display_aspect_ratio = float(frame.width()) / frame.height()
|
||||
self.preview_box.set_theme(self.theme)
|
||||
self.preview_box.clear_slides()
|
||||
self.preview_box.set_scale(float(self.preview_box.width()) / self.renderer.width())
|
||||
try:
|
||||
self.display_aspect_ratio = self.renderer.width() / self.renderer.height()
|
||||
except ZeroDivisionError:
|
||||
self.display_aspect_ratio = 1
|
||||
self.preview_area_layout.set_aspect_ratio(self.display_aspect_ratio)
|
||||
self.resizeEvent()
|
||||
self.preview_box.show()
|
||||
self.preview_box.generate_preview(self.theme, False, False)
|
||||
|
||||
def on_custom_1_button_clicked(self, number):
|
||||
"""
|
||||
@ -400,6 +402,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
||||
Handle the display and state of the Preview page.
|
||||
"""
|
||||
self.setField('name', self.theme.theme_name)
|
||||
self.preview_box.set_theme(self.theme)
|
||||
|
||||
def on_background_combo_box_current_index_changed(self, index):
|
||||
"""
|
||||
@ -462,7 +465,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
||||
"""
|
||||
Handle the `pathEditChanged` signal from image_path_edit
|
||||
|
||||
:param openlp.core.common.path.Path new_path: Path to the new image
|
||||
:param pathlib.Path new_path: Path to the new image
|
||||
:rtype: None
|
||||
"""
|
||||
self.theme.background_filename = new_path
|
||||
@ -472,7 +475,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
||||
"""
|
||||
Handle the `pathEditChanged` signal from video_path_edit
|
||||
|
||||
:param openlp.core.common.path.Path new_path: Path to the new video
|
||||
:param pathlib.Path new_path: Path to the new video
|
||||
:rtype: None
|
||||
"""
|
||||
self.theme.background_filename = new_path
|
||||
@ -560,5 +563,5 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
||||
source_path = self.theme.background_filename
|
||||
if not self.edit_mode and not self.theme_manager.check_if_theme_exists(self.theme.theme_name):
|
||||
return
|
||||
self.theme_manager.save_theme(self.theme, source_path, destination_path)
|
||||
self.theme_manager.save_theme(self.theme, source_path, destination_path, self.preview_box.save_screenshot())
|
||||
return QtWidgets.QDialog.accept(self)
|
||||
|
@ -23,7 +23,9 @@
|
||||
The Theme Manager manages adding, deleteing and modifying of themes.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from xml.etree.ElementTree import XML, ElementTree
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
@ -32,7 +34,7 @@ from openlp.core.common import delete_file
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.i18n import UiStrings, get_locale_key, translate
|
||||
from openlp.core.common.mixins import LogMixin, RegistryProperties
|
||||
from openlp.core.common.path import Path, copyfile, create_paths
|
||||
from openlp.core.common.path import create_paths
|
||||
from openlp.core.common.registry import Registry, RegistryBase
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib import build_icon, check_item_selected, create_thumb, get_text_file_string, validate_thumb
|
||||
@ -171,7 +173,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
for xml_file_path in xml_file_paths:
|
||||
theme_data = get_text_file_string(xml_file_path)
|
||||
theme = self._create_theme_from_xml(theme_data, self.theme_path)
|
||||
self._write_theme(theme)
|
||||
self.save_theme(theme)
|
||||
xml_file_path.unlink()
|
||||
|
||||
def build_theme_path(self):
|
||||
@ -378,7 +380,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
delete_file(self.theme_path / thumb)
|
||||
delete_file(self.thumb_path / thumb)
|
||||
try:
|
||||
(self.theme_path / theme).rmtree()
|
||||
shutil.rmtree(self.theme_path / theme)
|
||||
except OSError:
|
||||
self.log_exception('Error deleting theme {name}'.format(name=theme))
|
||||
|
||||
@ -415,7 +417,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
"""
|
||||
Create the zipfile with the theme contents.
|
||||
|
||||
:param openlp.core.common.path.Path theme_path: Location where the zip file will be placed
|
||||
:param Path theme_path: Location where the zip file will be placed
|
||||
:param str theme_name: The name of the theme to be exported
|
||||
:return: The success of creating the zip file
|
||||
:rtype: bool
|
||||
@ -433,7 +435,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
'The {theme_name} export failed because this error occurred: {err}')
|
||||
.format(theme_name=theme_name, err=ose.strerror))
|
||||
if theme_path.exists():
|
||||
theme_path.rmtree(ignore_errors=True)
|
||||
shutil.rmtree(theme_path, ignore_errors=True)
|
||||
return False
|
||||
|
||||
def on_import_theme(self, checked=None):
|
||||
@ -474,7 +476,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
if not theme_paths:
|
||||
theme = Theme()
|
||||
theme.theme_name = UiStrings().Default
|
||||
self._write_theme(theme)
|
||||
self.save_theme(theme)
|
||||
Settings().setValue(self.settings_section + '/global theme', theme.theme_name)
|
||||
self.application.set_normal_cursor()
|
||||
|
||||
@ -557,8 +559,8 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
"""
|
||||
Unzip the theme, remove the preview file if stored. Generate a new preview file. Check the XML theme version
|
||||
and upgrade if necessary.
|
||||
:param openlp.core.common.path.Path file_path:
|
||||
:param openlp.core.common.path.Path directory_path:
|
||||
:param Path file_path:
|
||||
:param Path directory_path:
|
||||
"""
|
||||
self.log_debug('Unzipping theme {name}'.format(name=file_path))
|
||||
file_xml = None
|
||||
@ -637,24 +639,14 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
return False
|
||||
return True
|
||||
|
||||
def save_theme(self, theme, image_source_path, image_destination_path):
|
||||
"""
|
||||
Called by theme maintenance Dialog to save the theme and to trigger the reload of the theme list
|
||||
|
||||
:param Theme theme: The theme data object.
|
||||
:param openlp.core.common.path.Path image_source_path: Where the theme image is currently located.
|
||||
:param openlp.core.common.path.Path image_destination_path: Where the Theme Image is to be saved to
|
||||
:rtype: None
|
||||
"""
|
||||
self._write_theme(theme, image_source_path, image_destination_path)
|
||||
|
||||
def _write_theme(self, theme, image_source_path=None, image_destination_path=None):
|
||||
def save_theme(self, theme, image_source_path=None, image_destination_path=None, image=None):
|
||||
"""
|
||||
Writes the theme to the disk and handles the background image if necessary
|
||||
|
||||
:param Theme theme: The theme data object.
|
||||
:param openlp.core.common.path.Path image_source_path: Where the theme image is currently located.
|
||||
:param openlp.core.common.path.Path image_destination_path: Where the Theme Image is to be saved to
|
||||
:param Path image_source_path: Where the theme image is currently located.
|
||||
:param Path image_destination_path: Where the Theme Image is to be saved to
|
||||
:param image: The example image of the theme. Optionally.
|
||||
:rtype: None
|
||||
"""
|
||||
name = theme.theme_name
|
||||
@ -671,9 +663,17 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
delete_file(self.old_background_image_path)
|
||||
if image_source_path != image_destination_path:
|
||||
try:
|
||||
copyfile(image_source_path, image_destination_path)
|
||||
shutil.copyfile(image_source_path, image_destination_path)
|
||||
except OSError:
|
||||
self.log_exception('Failed to save theme image')
|
||||
if image:
|
||||
sample_path_name = self.theme_path / '{file_name}.png'.format(file_name=name)
|
||||
if sample_path_name.exists():
|
||||
sample_path_name.unlink()
|
||||
image.save(str(sample_path_name), 'png')
|
||||
thumb_path = self.thumb_path / '{name}.png'.format(name=name)
|
||||
create_thumb(sample_path_name, thumb_path, False)
|
||||
else:
|
||||
self.generate_and_save_image(name, theme)
|
||||
|
||||
def generate_and_save_image(self, theme_name, theme):
|
||||
@ -718,7 +718,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
Return a theme object using information parsed from XML
|
||||
|
||||
:param theme_xml: The Theme data object.
|
||||
:param openlp.core.common.path.Path image_path: Where the theme image is stored
|
||||
:param Path image_path: Where the theme image is stored
|
||||
:return: Theme data.
|
||||
:rtype: Theme
|
||||
"""
|
||||
@ -732,7 +732,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
Return a theme object using information parsed from JSON
|
||||
|
||||
:param theme_json: The Theme data object.
|
||||
:param openlp.core.common.path.Path image_path: Where the theme image is stored
|
||||
:param Path image_path: Where the theme image is stored
|
||||
:return: Theme data.
|
||||
:rtype: Theme
|
||||
"""
|
||||
|
@ -31,6 +31,8 @@ from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets
|
||||
from openlp.core.ui.icons import UiIcons
|
||||
from openlp.core.widgets.buttons import ColorButton
|
||||
from openlp.core.widgets.edits import PathEdit
|
||||
from openlp.core.widgets.layouts import AspectRatioLayout
|
||||
from openlp.core.display.render import ThemePreviewRenderer
|
||||
|
||||
|
||||
class Ui_ThemeWizard(object):
|
||||
@ -363,16 +365,13 @@ class Ui_ThemeWizard(object):
|
||||
self.preview_layout.addLayout(self.theme_name_layout)
|
||||
self.preview_area = QtWidgets.QWidget(self.preview_page)
|
||||
self.preview_area.setObjectName('PreviewArea')
|
||||
self.preview_area_layout = QtWidgets.QGridLayout(self.preview_area)
|
||||
self.preview_area_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.preview_area_layout.setColumnStretch(0, 1)
|
||||
self.preview_area_layout.setRowStretch(0, 1)
|
||||
self.preview_area_layout.setObjectName('preview_area_layout')
|
||||
self.preview_box_label = QtWidgets.QLabel(self.preview_area)
|
||||
self.preview_box_label.setFrameShape(QtWidgets.QFrame.Box)
|
||||
self.preview_box_label.setScaledContents(True)
|
||||
self.preview_box_label.setObjectName('preview_box_label')
|
||||
self.preview_area_layout.addWidget(self.preview_box_label)
|
||||
self.preview_area_layout = AspectRatioLayout(self.preview_area, 0.75) # Dummy ratio, will be update
|
||||
self.preview_area_layout.margin = 8
|
||||
self.preview_area_layout.setSpacing(0)
|
||||
self.preview_area_layout.setObjectName('preview_web_layout')
|
||||
self.preview_box = ThemePreviewRenderer(self)
|
||||
self.preview_box.setObjectName('preview_box')
|
||||
self.preview_area_layout.addWidget(self.preview_box)
|
||||
self.preview_layout.addWidget(self.preview_area)
|
||||
theme_wizard.addPage(self.preview_page)
|
||||
self.retranslate_ui(theme_wizard)
|
||||
|
@ -33,9 +33,9 @@ class FileDialog(QtWidgets.QFileDialog):
|
||||
|
||||
:type parent: QtWidgets.QWidget | None
|
||||
:type caption: str
|
||||
:type directory: openlp.core.common.path.Path
|
||||
:type directory: pathlib.Path
|
||||
:type options: QtWidgets.QFileDialog.Options
|
||||
:rtype: openlp.core.common.path.Path
|
||||
:rtype: pathlib.Path
|
||||
"""
|
||||
args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
|
||||
|
||||
@ -52,11 +52,11 @@ class FileDialog(QtWidgets.QFileDialog):
|
||||
|
||||
:type parent: QtWidgets.QWidget | None
|
||||
:type caption: str
|
||||
:type directory: openlp.core.common.path.Path
|
||||
:type directory: pathlib.Path
|
||||
:type filter: str
|
||||
:type initialFilter: str
|
||||
:type options: QtWidgets.QFileDialog.Options
|
||||
:rtype: tuple[openlp.core.common.path.Path, str]
|
||||
:rtype: tuple[pathlib.Path, str]
|
||||
"""
|
||||
args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
|
||||
|
||||
@ -73,11 +73,11 @@ class FileDialog(QtWidgets.QFileDialog):
|
||||
|
||||
:type parent: QtWidgets.QWidget | None
|
||||
:type caption: str
|
||||
:type directory: openlp.core.common.path.Path
|
||||
:type directory: pathlib.Path
|
||||
:type filter: str
|
||||
:type initialFilter: str
|
||||
:type options: QtWidgets.QFileDialog.Options
|
||||
:rtype: tuple[list[openlp.core.common.path.Path], str]
|
||||
:rtype: tuple[list[pathlib.Path], str]
|
||||
"""
|
||||
args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
|
||||
|
||||
@ -95,11 +95,11 @@ class FileDialog(QtWidgets.QFileDialog):
|
||||
|
||||
:type parent: QtWidgets.QWidget | None
|
||||
:type caption: str
|
||||
:type directory: openlp.core.common.path.Path
|
||||
:type directory: pathlib.Path
|
||||
:type filter: str
|
||||
:type initialFilter: str
|
||||
:type options: QtWidgets.QFileDialog.Options
|
||||
:rtype: tuple[openlp.core.common.path.Path | None, str]
|
||||
:rtype: tuple[pathlib.Path | None, str]
|
||||
"""
|
||||
args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
|
||||
|
||||
|
@ -24,12 +24,13 @@ The :mod:`~openlp.core.widgets.edits` module contains all the customised edit wi
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import CONTROL_CHARS
|
||||
from openlp.core.common.i18n import UiStrings, translate
|
||||
from openlp.core.common.path import Path, path_to_str, str_to_path
|
||||
from openlp.core.common.path import path_to_str, str_to_path
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib.formattingtags import FormattingTags
|
||||
from openlp.core.lib.ui import create_action, create_widget_action
|
||||
@ -207,7 +208,7 @@ class PathEdit(QtWidgets.QWidget):
|
||||
|
||||
: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
|
||||
:param 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
|
||||
@ -250,7 +251,7 @@ class PathEdit(QtWidgets.QWidget):
|
||||
A property getter method to return the selected path.
|
||||
|
||||
:return: The selected path
|
||||
:rtype: openlp.core.common.path.Path
|
||||
:rtype: Path
|
||||
"""
|
||||
return self._path
|
||||
|
||||
@ -259,7 +260,7 @@ class PathEdit(QtWidgets.QWidget):
|
||||
"""
|
||||
A Property setter method to set the selected path
|
||||
|
||||
:param openlp.core.common.path.Path path: The path to set the widget to
|
||||
:param Path path: The path to set the widget to
|
||||
:rtype: None
|
||||
"""
|
||||
self._path = path
|
||||
@ -348,11 +349,11 @@ class PathEdit(QtWidgets.QWidget):
|
||||
|
||||
Emits the pathChanged Signal
|
||||
|
||||
:param openlp.core.common.path.Path path: The new path
|
||||
:param Path path: The new path
|
||||
:rtype: None
|
||||
"""
|
||||
if self._path != path:
|
||||
self._path = path
|
||||
self.path = path
|
||||
self.pathChanged.emit(path)
|
||||
|
||||
|
||||
|
@ -23,12 +23,13 @@
|
||||
The :mod:`listpreviewwidget` is a widget that lists the slides in the slide controller.
|
||||
It is based on a QTableWidget but represents its contents in list form.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import is_win
|
||||
from openlp.core.common.i18n import UiStrings
|
||||
from openlp.core.common.mixins import RegistryProperties
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib.serviceitem import ItemCapabilities, ServiceItem
|
||||
@ -41,7 +42,7 @@ def handle_mime_data_urls(mime_data):
|
||||
|
||||
:param QtCore.QMimeData mime_data: The mime data from the drag and drop opperation.
|
||||
:return: A list of file paths that were dropped
|
||||
:rtype: list[openlp.core.common.path.Path]
|
||||
:rtype: list[Path]
|
||||
"""
|
||||
file_paths = []
|
||||
for url in mime_data.urls():
|
||||
@ -201,14 +202,14 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties):
|
||||
label.setScaledContents(True)
|
||||
if self.service_item.is_command():
|
||||
if self.service_item.is_capable(ItemCapabilities.HasThumbnails):
|
||||
pixmap = QtGui.QPixmap(remove_url_prefix(slide['thumbnail']))
|
||||
pixmap = QtGui.QPixmap(str(slide['thumbnail']))
|
||||
else:
|
||||
if isinstance(slide['image'], QtGui.QIcon):
|
||||
pixmap = slide['image'].pixmap(QtCore.QSize(32, 32))
|
||||
else:
|
||||
pixmap = QtGui.QPixmap(remove_url_prefix(slide['image']))
|
||||
pixmap = QtGui.QPixmap(str(slide['image']))
|
||||
else:
|
||||
pixmap = QtGui.QPixmap(remove_url_prefix(slide['path']))
|
||||
pixmap = QtGui.QPixmap(str(slide['path']))
|
||||
label.setPixmap(pixmap)
|
||||
container = QtWidgets.QWidget()
|
||||
layout = AspectRatioLayout(container, self.screen_ratio)
|
||||
|
@ -103,8 +103,8 @@ class ProxyWidget(QtWidgets.QGroupBox):
|
||||
:param QtWidgets.QRadioButton button: The button that has toggled
|
||||
:param bool checked: The buttons new state
|
||||
"""
|
||||
id = self.radio_group.id(button) # The work around (see above comment)
|
||||
enable_manual_edits = id == ProxyMode.MANUAL_PROXY and checked
|
||||
group_id = self.radio_group.id(button) # The work around (see above comment)
|
||||
enable_manual_edits = group_id == ProxyMode.MANUAL_PROXY and checked
|
||||
self.http_edit.setEnabled(enable_manual_edits)
|
||||
self.https_edit.setEnabled(enable_manual_edits)
|
||||
self.username_edit.setEnabled(enable_manual_edits)
|
||||
|
@ -48,7 +48,7 @@ class BibleImport(BibleDB, LogMixin, RegistryProperties):
|
||||
"""
|
||||
Check if the supplied file is compressed
|
||||
|
||||
:param openlp.core.common.path.Path file_path: A path to the file to check
|
||||
:param pathlib.Path file_path: A path to the file to check
|
||||
"""
|
||||
if is_zipfile(file_path):
|
||||
critical_error_message_box(
|
||||
|
@ -24,6 +24,7 @@ import logging
|
||||
import re
|
||||
import sqlite3
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import chardet
|
||||
from PyQt5 import QtCore
|
||||
@ -35,7 +36,6 @@ from sqlalchemy.orm.exc import UnmappedClassError
|
||||
from openlp.core.common import clean_filename
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.lib.db import BaseModel, Manager, init_db
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.plugins.bibles.lib import BibleStrings, LanguageSelection, upgrade
|
||||
@ -130,13 +130,13 @@ class BibleDB(Manager):
|
||||
:param parent:
|
||||
:param kwargs:
|
||||
``path``
|
||||
The path to the bible database file. Type: openlp.core.common.path.Path
|
||||
The path to the bible database file. Type: Path
|
||||
|
||||
``name``
|
||||
The name of the database. This is also used as the file name for SQLite databases.
|
||||
|
||||
``file``
|
||||
Type: openlp.core.common.path.Path
|
||||
Type: Path
|
||||
|
||||
:rtype: None
|
||||
"""
|
||||
|
@ -96,7 +96,7 @@ class CSVBible(BibleImport):
|
||||
"""
|
||||
Parse the supplied CSV file.
|
||||
|
||||
:param openlp.core.common.path.Path file_path: The name of the file to parse.
|
||||
:param pathlib.Path file_path: The name of the file to parse.
|
||||
:param namedtuple results_tuple: The namedtuple to use to store the results.
|
||||
:return: An list of namedtuples of type results_tuple
|
||||
:rtype: list[namedtuple]
|
||||
|
@ -21,12 +21,12 @@
|
||||
##########################################################################
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from zipfile import ZipFile
|
||||
|
||||
from bs4 import BeautifulSoup, NavigableString, Tag
|
||||
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.plugins.bibles.lib.bibleimport import BibleImport
|
||||
|
||||
|
||||
|
@ -20,12 +20,12 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from openlp.core.common import delete_file
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.i18n import UiStrings, translate
|
||||
from openlp.core.common.mixins import LogMixin, RegistryProperties
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.plugins.bibles.lib import LanguageSelection, parse_reference
|
||||
from openlp.plugins.bibles.lib.db import BibleDB, BibleMeta
|
||||
|
@ -21,13 +21,14 @@
|
||||
##########################################################################
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import delete_file, get_images_filter
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.i18n import UiStrings, get_natural_key, translate
|
||||
from openlp.core.common.path import Path, create_paths
|
||||
from openlp.core.common.path import create_paths
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib import ServiceItemContext, build_icon, check_item_selected, create_thumb, validate_thumb
|
||||
@ -343,7 +344,7 @@ class ImageMediaItem(MediaManagerItem):
|
||||
|
||||
:param openlp.plugins.images.lib.db.ImageFilenames image: The image to generate the thumbnail path for.
|
||||
:return: A path to the thumbnail
|
||||
:rtype: openlp.core.common.path.Path
|
||||
:rtype: Path
|
||||
"""
|
||||
ext = image.file_path.suffix.lower()
|
||||
return self.service_path / '{name:d}{ext}'.format(name=image.id, ext=ext)
|
||||
@ -401,7 +402,7 @@ class ImageMediaItem(MediaManagerItem):
|
||||
Process a list for files either from the File Dialog or from Drag and Drop.
|
||||
This method is overloaded from MediaManagerItem.
|
||||
|
||||
:param list[openlp.core.common.path.Path] file_paths: A List of paths to be loaded
|
||||
:param list[Path] file_paths: A List of paths to be loaded
|
||||
:param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
|
||||
"""
|
||||
self.application.set_normal_cursor()
|
||||
@ -413,7 +414,7 @@ class ImageMediaItem(MediaManagerItem):
|
||||
"""
|
||||
Add new images to the database. This method is called when adding images using the Add button or DnD.
|
||||
|
||||
:param list[openlp.core.common.Path] image_paths: A list of file paths to the images to be loaded
|
||||
:param list[Path] image_paths: A list of file paths to the images to be loaded
|
||||
:param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
|
||||
:param initial_load: When set to False, the busy cursor and progressbar will be shown while loading images
|
||||
"""
|
||||
@ -610,7 +611,7 @@ class ImageMediaItem(MediaManagerItem):
|
||||
for image in images:
|
||||
name = image.file_path.name
|
||||
thumbnail_path = self.generate_thumbnail_path(image)
|
||||
service_item.add_from_image(str(image.file_path), name, background, str(thumbnail_path))
|
||||
service_item.add_from_image(image.file_path, name, background, str(thumbnail_path))
|
||||
return True
|
||||
|
||||
def check_group_exists(self, new_group):
|
||||
|
@ -24,13 +24,13 @@ The :mod:`upgrade` module provides the migration path for the OLP Paths database
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import Column, Table
|
||||
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.db import drop_columns
|
||||
from openlp.core.common.json import OpenLPJsonEncoder
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.json import OpenLPJSONEncoder
|
||||
from openlp.core.lib.db import PathType, get_upgrade_op
|
||||
|
||||
|
||||
@ -58,7 +58,7 @@ def upgrade_2(session, metadata):
|
||||
results = conn.execute('SELECT * FROM image_filenames')
|
||||
data_path = AppLocation.get_data_path()
|
||||
for row in results.fetchall():
|
||||
file_path_json = json.dumps(Path(row.filename), cls=OpenLPJsonEncoder, base_path=data_path)
|
||||
file_path_json = json.dumps(Path(row.filename), cls=OpenLPJSONEncoder, base_path=data_path)
|
||||
sql = 'UPDATE image_filenames SET file_path = \'{file_path_json}\' WHERE id = {id}'.format(
|
||||
file_path_json=file_path_json, id=row.id)
|
||||
conn.execute(sql)
|
||||
|
@ -23,6 +23,7 @@ import logging
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from time import sleep
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
@ -30,7 +31,6 @@ from PyQt5 import QtCore, QtWidgets
|
||||
from openlp.core.common import is_linux, is_macosx, is_win
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.mixins import RegistryProperties
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.ui.icons import UiIcons
|
||||
from openlp.core.ui.media.vlcplayer import get_vlc
|
||||
|
@ -37,7 +37,7 @@ from openlp.core.lib.mediamanageritem import MediaManagerItem
|
||||
from openlp.core.lib.serviceitem import ItemCapabilities
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.ui.icons import UiIcons
|
||||
from openlp.core.ui.media import parse_optical_path, format_milliseconds
|
||||
from openlp.core.ui.media import parse_optical_path, format_milliseconds, AUDIO_EXT, VIDEO_EXT
|
||||
from openlp.core.ui.media.vlcplayer import get_vlc
|
||||
|
||||
|
||||
@ -173,9 +173,13 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
item = self.list_view.currentItem()
|
||||
if item is None:
|
||||
return False
|
||||
filename = item.data(QtCore.Qt.UserRole)
|
||||
filename = str(item.data(QtCore.Qt.UserRole))
|
||||
# Special handling if the filename is a optical clip
|
||||
if filename.startswith('optical:'):
|
||||
if filename == 'live':
|
||||
service_item.processor = 'vlc'
|
||||
service_item.title = filename
|
||||
service_item.add_capability(ItemCapabilities.CanStream)
|
||||
elif filename.startswith('optical:'):
|
||||
(name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(filename)
|
||||
if not os.path.exists(name):
|
||||
if not remote:
|
||||
@ -233,8 +237,8 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
# self.populate_display_types()
|
||||
self.on_new_file_masks = translate('MediaPlugin.MediaItem',
|
||||
'Videos ({video});;Audio ({audio});;{files} '
|
||||
'(*)').format(video=' '.join(self.media_controller.video_extensions_list),
|
||||
audio=' '.join(self.media_controller.audio_extensions_list),
|
||||
'(*)').format(video=' '.join(VIDEO_EXT),
|
||||
audio=' '.join(AUDIO_EXT),
|
||||
files=UiStrings().AllFiles)
|
||||
|
||||
def on_delete_click(self):
|
||||
@ -258,12 +262,19 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
"""
|
||||
# TODO needs to be fixed as no idea why this fails
|
||||
# media.sort(key=lambda file_path: get_natural_key(file_path.name))
|
||||
file_name = translate('MediaPlugin.MediaItem', 'Live Stream')
|
||||
item_name = QtWidgets.QListWidgetItem(file_name)
|
||||
item_name.setIcon(UiIcons().video)
|
||||
item_name.setData(QtCore.Qt.UserRole, 'live')
|
||||
item_name.setToolTip(translate('MediaPlugin.MediaItem', 'Show Live Stream'))
|
||||
self.list_view.addItem(item_name)
|
||||
for track in media:
|
||||
track_info = QtCore.QFileInfo(track)
|
||||
track_str = str(track)
|
||||
track_info = QtCore.QFileInfo(track_str)
|
||||
item_name = None
|
||||
if track.startswith('optical:'):
|
||||
if track_str.startswith('optical:'):
|
||||
# Handle optical based item
|
||||
(file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track)
|
||||
(file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track_str)
|
||||
item_name = QtWidgets.QListWidgetItem(clip_name)
|
||||
item_name.setIcon(UiIcons().optical)
|
||||
item_name.setData(QtCore.Qt.UserRole, track)
|
||||
@ -272,22 +283,22 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
end=format_milliseconds(end)))
|
||||
elif not os.path.exists(track):
|
||||
# File doesn't exist, mark as error.
|
||||
file_name = os.path.split(str(track))[1]
|
||||
file_name = os.path.split(track_str)[1]
|
||||
item_name = QtWidgets.QListWidgetItem(file_name)
|
||||
item_name.setIcon(UiIcons().error)
|
||||
item_name.setData(QtCore.Qt.UserRole, track)
|
||||
item_name.setToolTip(track)
|
||||
item_name.setToolTip(track_str)
|
||||
elif track_info.isFile():
|
||||
# Normal media file handling.
|
||||
file_name = os.path.split(str(track))[1]
|
||||
file_name = os.path.split(track_str)[1]
|
||||
item_name = QtWidgets.QListWidgetItem(file_name)
|
||||
search = file_name.split('.')[-1].lower()
|
||||
if '*.{text}'.format(text=search) in self.media_controller.audio_extensions_list:
|
||||
if search in AUDIO_EXT:
|
||||
item_name.setIcon(UiIcons().audio)
|
||||
else:
|
||||
item_name.setIcon(UiIcons().video)
|
||||
item_name.setData(QtCore.Qt.UserRole, track)
|
||||
item_name.setToolTip(track)
|
||||
item_name.setToolTip(track_str)
|
||||
if item_name:
|
||||
self.list_view.addItem(item_name)
|
||||
|
||||
@ -301,9 +312,9 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
media_file_paths = Settings().value(self.settings_section + '/media files')
|
||||
media_file_paths.sort(key=lambda file_path: get_natural_key(file_path.name))
|
||||
if media_type == MediaType.Audio:
|
||||
extension = self.media_controller.audio_extensions_list
|
||||
extension = AUDIO_EXT
|
||||
else:
|
||||
extension = self.media_controller.video_extensions_list
|
||||
extension = VIDEO_EXT
|
||||
extension = [x[1:] for x in extension]
|
||||
media = [x for x in media_file_paths if x.suffix in extension]
|
||||
return media
|
||||
|
@ -36,31 +36,49 @@ import time
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common import delete_file, get_uno_command, get_uno_instance, is_win
|
||||
from openlp.core.common import delete_file, get_uno_command, get_uno_instance, is_win, trace_error_handler
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.display.screens import ScreenList
|
||||
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \
|
||||
TextType
|
||||
|
||||
|
||||
# Load the XSlideShowListener class so we can inherit from it
|
||||
if is_win():
|
||||
from win32com.client import Dispatch
|
||||
import pywintypes
|
||||
uno_available = False
|
||||
# Declare an empty exception to match the exception imported from UNO
|
||||
try:
|
||||
service_manager = Dispatch('com.sun.star.ServiceManager')
|
||||
service_manager._FlagAsMethod('Bridge_GetStruct')
|
||||
XSlideShowListenerObj = service_manager.Bridge_GetStruct('com.sun.star.presentation.XSlideShowListener')
|
||||
|
||||
class SlideShowListenerImport(XSlideShowListenerObj.__class__):
|
||||
pass
|
||||
except (AttributeError, pywintypes.com_error):
|
||||
class SlideShowListenerImport():
|
||||
pass
|
||||
|
||||
# Declare an empty exception to match the exception imported from UNO
|
||||
class ErrorCodeIOException(Exception):
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
import uno
|
||||
import unohelper
|
||||
from com.sun.star.beans import PropertyValue
|
||||
from com.sun.star.task import ErrorCodeIOException
|
||||
from com.sun.star.presentation import XSlideShowListener
|
||||
|
||||
class SlideShowListenerImport(unohelper.Base, XSlideShowListener):
|
||||
pass
|
||||
|
||||
uno_available = True
|
||||
except ImportError:
|
||||
uno_available = False
|
||||
|
||||
class SlideShowListenerImport():
|
||||
pass
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -82,6 +100,8 @@ class ImpressController(PresentationController):
|
||||
self.process = None
|
||||
self.desktop = None
|
||||
self.manager = None
|
||||
self.conf_provider = None
|
||||
self.presenter_screen_disabled_by_openlp = False
|
||||
|
||||
def check_available(self):
|
||||
"""
|
||||
@ -90,7 +110,6 @@ class ImpressController(PresentationController):
|
||||
log.debug('check_available')
|
||||
if is_win():
|
||||
return self.get_com_servicemanager() is not None
|
||||
else:
|
||||
return uno_available
|
||||
|
||||
def start_process(self):
|
||||
@ -131,6 +150,7 @@ class ImpressController(PresentationController):
|
||||
self.manager = uno_instance.ServiceManager
|
||||
log.debug('get UNO Desktop Openoffice - createInstanceWithContext - Desktop')
|
||||
desktop = self.manager.createInstanceWithContext("com.sun.star.frame.Desktop", uno_instance)
|
||||
self.toggle_presentation_screen(False)
|
||||
return desktop
|
||||
except Exception:
|
||||
log.warning('Failed to get UNO desktop')
|
||||
@ -148,6 +168,7 @@ class ImpressController(PresentationController):
|
||||
desktop = self.manager.createInstance('com.sun.star.frame.Desktop')
|
||||
except (AttributeError, pywintypes.com_error):
|
||||
log.warning('Failure to find desktop - Impress may have closed')
|
||||
self.toggle_presentation_screen(False)
|
||||
return desktop if desktop else None
|
||||
|
||||
def get_com_servicemanager(self):
|
||||
@ -166,6 +187,8 @@ class ImpressController(PresentationController):
|
||||
Called at system exit to clean up any running presentations.
|
||||
"""
|
||||
log.debug('Kill OpenOffice')
|
||||
if self.presenter_screen_disabled_by_openlp:
|
||||
self.toggle_presentation_screen(True)
|
||||
while self.docs:
|
||||
self.docs[0].close_presentation()
|
||||
desktop = None
|
||||
@ -195,6 +218,60 @@ class ImpressController(PresentationController):
|
||||
except Exception:
|
||||
log.warning('Failed to terminate OpenOffice')
|
||||
|
||||
def toggle_presentation_screen(self, set_visible):
|
||||
"""
|
||||
Enable or disable the Presentation Screen/Console
|
||||
|
||||
:param bool set_visible: Should the presentation screen/console be set to be visible.
|
||||
:rtype: None
|
||||
"""
|
||||
# Create Instance of ConfigurationProvider
|
||||
if not self.conf_provider:
|
||||
if is_win():
|
||||
self.conf_provider = self.manager.createInstance('com.sun.star.configuration.ConfigurationProvider')
|
||||
else:
|
||||
self.conf_provider = self.manager.createInstanceWithContext(
|
||||
'com.sun.star.configuration.ConfigurationProvider', uno.getComponentContext())
|
||||
# Setup lookup properties to get Impress settings
|
||||
properties = []
|
||||
properties.append(self.create_property('nodepath', 'org.openoffice.Office.Impress'))
|
||||
properties = tuple(properties)
|
||||
try:
|
||||
# Get an updateable configuration view
|
||||
impress_conf_props = self.conf_provider.createInstanceWithArguments(
|
||||
'com.sun.star.configuration.ConfigurationUpdateAccess', properties)
|
||||
# Get the specific setting for presentation screen
|
||||
presenter_screen_enabled = impress_conf_props.getHierarchicalPropertyValue(
|
||||
'Misc/Start/EnablePresenterScreen')
|
||||
# If the presentation screen is enabled we disable it
|
||||
if presenter_screen_enabled != set_visible:
|
||||
impress_conf_props.setHierarchicalPropertyValue('Misc/Start/EnablePresenterScreen', set_visible)
|
||||
impress_conf_props.commitChanges()
|
||||
# if set_visible is False this is an attempt to disable the Presenter Screen
|
||||
# so we make a note that it has been disabled, so it can be enabled again on close.
|
||||
if set_visible is False:
|
||||
self.presenter_screen_disabled_by_openlp = True
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
trace_error_handler(log)
|
||||
|
||||
def create_property(self, name, value):
|
||||
"""
|
||||
Create an OOo style property object which are passed into some Uno methods.
|
||||
|
||||
:param str name: The name of the property
|
||||
:param str value: The value of the property
|
||||
:rtype: com.sun.star.beans.PropertyValue
|
||||
"""
|
||||
log.debug('create property OpenOffice')
|
||||
if is_win():
|
||||
property_object = self.manager.Bridge_GetStruct('com.sun.star.beans.PropertyValue')
|
||||
else:
|
||||
property_object = PropertyValue()
|
||||
property_object.Name = name
|
||||
property_object.Value = value
|
||||
return property_object
|
||||
|
||||
|
||||
class ImpressDocument(PresentationDocument):
|
||||
"""
|
||||
@ -205,7 +282,7 @@ class ImpressDocument(PresentationDocument):
|
||||
"""
|
||||
Constructor, store information about the file and initialise.
|
||||
|
||||
:param openlp.core.common.path.Path document_path: File path for the document to load
|
||||
:param pathlib.Path document_path: File path for the document to load
|
||||
:rtype: None
|
||||
"""
|
||||
log.debug('Init Presentation OpenOffice')
|
||||
@ -213,6 +290,8 @@ class ImpressDocument(PresentationDocument):
|
||||
self.document = None
|
||||
self.presentation = None
|
||||
self.control = None
|
||||
self.slide_ended = False
|
||||
self.slide_ended_reverse = False
|
||||
|
||||
def load_presentation(self):
|
||||
"""
|
||||
@ -233,13 +312,16 @@ class ImpressDocument(PresentationDocument):
|
||||
return False
|
||||
self.desktop = desktop
|
||||
properties = []
|
||||
properties.append(self.create_property('Hidden', True))
|
||||
properties.append(self.controller.create_property('Hidden', True))
|
||||
properties = tuple(properties)
|
||||
try:
|
||||
self.document = desktop.loadComponentFromURL(url, '_blank', 0, properties)
|
||||
except Exception:
|
||||
log.warning('Failed to load presentation {url}'.format(url=url))
|
||||
return False
|
||||
if self.document is None:
|
||||
log.warning('Presentation {url} could not be loaded'.format(url=url))
|
||||
return False
|
||||
self.presentation = self.document.getPresentation()
|
||||
self.presentation.Display = ScreenList().current.number + 1
|
||||
self.control = None
|
||||
@ -257,7 +339,7 @@ class ImpressDocument(PresentationDocument):
|
||||
temp_folder_path = self.get_temp_folder()
|
||||
thumb_dir_url = temp_folder_path.as_uri()
|
||||
properties = []
|
||||
properties.append(self.create_property('FilterName', 'impress_png_Export'))
|
||||
properties.append(self.controller.create_property('FilterName', 'impress_png_Export'))
|
||||
properties = tuple(properties)
|
||||
doc = self.document
|
||||
pages = doc.getDrawPages()
|
||||
@ -279,19 +361,6 @@ class ImpressDocument(PresentationDocument):
|
||||
except Exception:
|
||||
log.exception('{path} - Unable to store openoffice preview'.format(path=path))
|
||||
|
||||
def create_property(self, name, value):
|
||||
"""
|
||||
Create an OOo style property object which are passed into some Uno methods.
|
||||
"""
|
||||
log.debug('create property OpenOffice')
|
||||
if is_win():
|
||||
property_object = self.controller.manager.Bridge_GetStruct('com.sun.star.beans.PropertyValue')
|
||||
else:
|
||||
property_object = PropertyValue()
|
||||
property_object.Name = name
|
||||
property_object.Value = value
|
||||
return property_object
|
||||
|
||||
def close_presentation(self):
|
||||
"""
|
||||
Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being
|
||||
@ -356,7 +425,6 @@ class ImpressDocument(PresentationDocument):
|
||||
log.debug('is blank OpenOffice')
|
||||
if self.control and self.control.isRunning():
|
||||
return self.control.isPaused()
|
||||
else:
|
||||
return False
|
||||
|
||||
def stop_presentation(self):
|
||||
@ -384,6 +452,8 @@ class ImpressDocument(PresentationDocument):
|
||||
sleep_count += 1
|
||||
self.control = self.presentation.getController()
|
||||
window.setVisible(False)
|
||||
listener = SlideShowListener(self)
|
||||
self.control.getSlideShow().addSlideShowListener(listener)
|
||||
else:
|
||||
self.control.activate()
|
||||
self.goto_slide(1)
|
||||
@ -415,17 +485,33 @@ class ImpressDocument(PresentationDocument):
|
||||
"""
|
||||
Triggers the next effect of slide on the running presentation.
|
||||
"""
|
||||
# if we are at the presentations end don't go further, just return True
|
||||
if self.slide_ended and self.get_slide_count() == self.get_slide_number():
|
||||
return True
|
||||
self.slide_ended = False
|
||||
self.slide_ended_reverse = False
|
||||
past_end = False
|
||||
is_paused = self.control.isPaused()
|
||||
self.control.gotoNextEffect()
|
||||
time.sleep(0.1)
|
||||
# If for some reason the presentation end was not detected above, this will catch it.
|
||||
# The presentation is set to paused when going past the end.
|
||||
if not is_paused and self.control.isPaused():
|
||||
self.control.gotoPreviousEffect()
|
||||
past_end = True
|
||||
return past_end
|
||||
|
||||
def previous_step(self):
|
||||
"""
|
||||
Triggers the previous slide on the running presentation.
|
||||
"""
|
||||
# if we are at the presentations start don't go further back, just return True
|
||||
if self.slide_ended_reverse and self.get_slide_number() == 1:
|
||||
return True
|
||||
self.slide_ended = False
|
||||
self.slide_ended_reverse = False
|
||||
self.control.gotoPreviousEffect()
|
||||
return False
|
||||
|
||||
def get_slide_text(self, slide_no):
|
||||
"""
|
||||
@ -483,3 +569,100 @@ class ImpressDocument(PresentationDocument):
|
||||
note = ' '
|
||||
notes.append(note)
|
||||
self.save_titles_and_notes(titles, notes)
|
||||
|
||||
|
||||
class SlideShowListener(SlideShowListenerImport):
|
||||
"""
|
||||
Listener interface to receive global slide show events.
|
||||
"""
|
||||
|
||||
def __init__(self, document):
|
||||
"""
|
||||
Constructor
|
||||
|
||||
:param document: The ImpressDocument being presented
|
||||
"""
|
||||
self.document = document
|
||||
|
||||
def paused(self):
|
||||
"""
|
||||
Notify that the slide show is paused
|
||||
"""
|
||||
log.debug('LibreOffice SlideShowListener event: paused')
|
||||
|
||||
def resumed(self):
|
||||
"""
|
||||
Notify that the slide show is resumed from a paused state
|
||||
"""
|
||||
log.debug('LibreOffice SlideShowListener event: resumed')
|
||||
|
||||
def slideTransitionStarted(self):
|
||||
"""
|
||||
Notify that a new slide starts to become visible.
|
||||
"""
|
||||
log.debug('LibreOffice SlideShowListener event: slideTransitionStarted')
|
||||
|
||||
def slideTransitionEnded(self):
|
||||
"""
|
||||
Notify that the slide transtion of the current slide ended.
|
||||
"""
|
||||
log.debug('LibreOffice SlideShowListener event: slideTransitionEnded')
|
||||
|
||||
def slideAnimationsEnded(self):
|
||||
"""
|
||||
Notify that the last animation from the main sequence of the current slide has ended.
|
||||
"""
|
||||
log.debug('LibreOffice SlideShowListener event: slideAnimationsEnded')
|
||||
if not Registry().get('main_window').isActiveWindow():
|
||||
log.debug('main window is not in focus - should update slidecontroller')
|
||||
Registry().execute('slidecontroller_live_change', self.document.control.getCurrentSlideIndex() + 1)
|
||||
|
||||
def slideEnded(self, reverse):
|
||||
"""
|
||||
Notify that the current slide has ended, e.g. the user has clicked on the slide. Calling displaySlide()
|
||||
twice will not issue this event.
|
||||
|
||||
:param bool reverse: Whether or not the direction of the "slide movement" is reversed/backwards.
|
||||
:rtype: None
|
||||
"""
|
||||
log.debug('LibreOffice SlideShowListener event: slideEnded %d' % reverse)
|
||||
if reverse:
|
||||
self.document.slide_ended = False
|
||||
self.document.slide_ended_reverse = True
|
||||
else:
|
||||
self.document.slide_ended = True
|
||||
self.document.slide_ended_reverse = False
|
||||
|
||||
def hyperLinkClicked(self, hyperLink):
|
||||
"""
|
||||
Notifies that a hyperlink has been clicked.
|
||||
"""
|
||||
log.debug('LibreOffice SlideShowListener event: hyperLinkClicked %s' % hyperLink)
|
||||
|
||||
def disposing(self, source):
|
||||
"""
|
||||
gets called when the broadcaster is about to be disposed.
|
||||
:param source:
|
||||
"""
|
||||
log.debug('LibreOffice SlideShowListener event: disposing')
|
||||
|
||||
def beginEvent(self, node):
|
||||
"""
|
||||
This event is raised when the element local timeline begins to play.
|
||||
:param node:
|
||||
"""
|
||||
log.debug('LibreOffice SlideShowListener event: beginEvent')
|
||||
|
||||
def endEvent(self, node):
|
||||
"""
|
||||
This event is raised at the active end of the element.
|
||||
:param node:
|
||||
"""
|
||||
log.debug('LibreOffice SlideShowListener event: endEvent')
|
||||
|
||||
def repeat(self, node):
|
||||
"""
|
||||
This event is raised when the element local timeline repeats.
|
||||
:param node:
|
||||
"""
|
||||
log.debug('LibreOffice SlideShowListener event: repeat')
|
||||
|
431
openlp/plugins/presentations/lib/libreofficeserver.py
Normal file
431
openlp/plugins/presentations/lib/libreofficeserver.py
Normal file
@ -0,0 +1,431 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2019 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, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# 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, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
"""
|
||||
This module runs a Pyro4 server using LibreOffice's version of Python
|
||||
|
||||
Please Note: This intentionally uses os.path over pathlib because we don't know which version of Python is shipped with
|
||||
the version of LibreOffice on the user's computer.
|
||||
"""
|
||||
from subprocess import Popen
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
import time
|
||||
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
# Only make the log file on OS X when running as a server
|
||||
logfile = os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp', 'libreofficeserver.log')
|
||||
print('Setting up log file: {logfile}'.format(logfile=logfile))
|
||||
logging.basicConfig(filename=logfile, level=logging.INFO)
|
||||
|
||||
|
||||
# Add the current directory to sys.path so that we can load the serializers
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__)))
|
||||
# Add the vendor directory to sys.path so that we can load Pyro4
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), 'vendor'))
|
||||
|
||||
from serializers import register_classes
|
||||
from Pyro4 import Daemon, expose
|
||||
|
||||
try:
|
||||
# Wrap these imports in a try so that we can run the tests on macOS
|
||||
import uno
|
||||
from com.sun.star.beans import PropertyValue
|
||||
from com.sun.star.task import ErrorCodeIOException
|
||||
except ImportError:
|
||||
# But they need to be defined for mocking
|
||||
uno = None
|
||||
PropertyValue = None
|
||||
ErrorCodeIOException = Exception
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
register_classes()
|
||||
|
||||
|
||||
class TextType(object):
|
||||
"""
|
||||
Type Enumeration for Types of Text to request
|
||||
"""
|
||||
Title = 0
|
||||
SlideText = 1
|
||||
Notes = 2
|
||||
|
||||
|
||||
class LibreOfficeException(Exception):
|
||||
"""
|
||||
A specific exception for LO
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@expose
|
||||
class LibreOfficeServer(object):
|
||||
"""
|
||||
A Pyro4 server which controls LibreOffice
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
Set up the server
|
||||
"""
|
||||
self._desktop = None
|
||||
self._control = None
|
||||
self._document = None
|
||||
self._presentation = None
|
||||
self._process = None
|
||||
self._manager = None
|
||||
|
||||
def _create_property(self, name, value):
|
||||
"""
|
||||
Create an OOo style property object which are passed into some Uno methods.
|
||||
"""
|
||||
log.debug('create property')
|
||||
property_object = PropertyValue()
|
||||
property_object.Name = name
|
||||
property_object.Value = value
|
||||
return property_object
|
||||
|
||||
def _get_text_from_page(self, slide_no, text_type=TextType.SlideText):
|
||||
"""
|
||||
Return any text extracted from the presentation page.
|
||||
|
||||
:param slide_no: The slide the notes are required for, starting at 1
|
||||
:param notes: A boolean. If set the method searches the notes of the slide.
|
||||
:param text_type: A TextType. Enumeration of the types of supported text.
|
||||
"""
|
||||
text = ''
|
||||
if TextType.Title <= text_type <= TextType.Notes:
|
||||
pages = self._document.getDrawPages()
|
||||
if 0 < slide_no <= pages.getCount():
|
||||
page = pages.getByIndex(slide_no - 1)
|
||||
if text_type == TextType.Notes:
|
||||
page = page.getNotesPage()
|
||||
for index in range(page.getCount()):
|
||||
shape = page.getByIndex(index)
|
||||
shape_type = shape.getShapeType()
|
||||
if shape.supportsService('com.sun.star.drawing.Text'):
|
||||
# if they requested title, make sure it is the title
|
||||
if text_type != TextType.Title or shape_type == 'com.sun.star.presentation.TitleTextShape':
|
||||
text += shape.getString() + '\n'
|
||||
return text
|
||||
|
||||
def start_process(self):
|
||||
"""
|
||||
Initialise Impress
|
||||
"""
|
||||
uno_command = [
|
||||
'/Applications/LibreOffice.app/Contents/MacOS/soffice',
|
||||
'--nologo',
|
||||
'--norestore',
|
||||
'--minimized',
|
||||
'--nodefault',
|
||||
'--nofirststartwizard',
|
||||
'--accept=pipe,name=openlp_maclo;urp;StarOffice.ServiceManager'
|
||||
]
|
||||
self._process = Popen(uno_command)
|
||||
|
||||
@property
|
||||
def desktop(self):
|
||||
"""
|
||||
Set up an UNO desktop instance
|
||||
"""
|
||||
if self._desktop is not None:
|
||||
return self._desktop
|
||||
uno_instance = None
|
||||
context = uno.getComponentContext()
|
||||
resolver = context.ServiceManager.createInstanceWithContext('com.sun.star.bridge.UnoUrlResolver', context)
|
||||
loop = 0
|
||||
while uno_instance is None and loop < 3:
|
||||
try:
|
||||
uno_instance = resolver.resolve('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext')
|
||||
except Exception:
|
||||
log.exception('Unable to find running instance, retrying...')
|
||||
loop += 1
|
||||
try:
|
||||
self._manager = uno_instance.ServiceManager
|
||||
log.debug('get UNO Desktop Openoffice - createInstanceWithContext - Desktop')
|
||||
desktop = self._manager.createInstanceWithContext('com.sun.star.frame.Desktop', uno_instance)
|
||||
if not desktop:
|
||||
raise Exception('Failed to get UNO desktop')
|
||||
self._desktop = desktop
|
||||
return desktop
|
||||
except Exception:
|
||||
log.exception('Failed to get UNO desktop')
|
||||
return None
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
Shut down the server
|
||||
"""
|
||||
can_kill = True
|
||||
if hasattr(self, '_docs'):
|
||||
while self._docs:
|
||||
self._docs[0].close_presentation()
|
||||
docs = self.desktop.getComponents()
|
||||
count = 0
|
||||
if docs.hasElements():
|
||||
list_elements = docs.createEnumeration()
|
||||
while list_elements.hasMoreElements():
|
||||
doc = list_elements.nextElement()
|
||||
if doc.getImplementationName() != 'com.sun.star.comp.framework.BackingComp':
|
||||
count += 1
|
||||
if count > 0:
|
||||
log.debug('LibreOffice not terminated as docs are still open')
|
||||
can_kill = False
|
||||
else:
|
||||
try:
|
||||
self.desktop.terminate()
|
||||
log.debug('LibreOffice killed')
|
||||
except Exception:
|
||||
log.exception('Failed to terminate LibreOffice')
|
||||
if getattr(self, '_process') and can_kill:
|
||||
self._process.kill()
|
||||
|
||||
def load_presentation(self, file_path, screen_number):
|
||||
"""
|
||||
Load a presentation
|
||||
"""
|
||||
self._file_path = file_path
|
||||
url = uno.systemPathToFileUrl(file_path)
|
||||
properties = (self._create_property('Hidden', True),)
|
||||
self._document = None
|
||||
loop_count = 0
|
||||
while loop_count < 3:
|
||||
try:
|
||||
self._document = self.desktop.loadComponentFromURL(url, '_blank', 0, properties)
|
||||
except Exception:
|
||||
log.exception('Failed to load presentation {url}'.format(url=url))
|
||||
if self._document:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
loop_count += 1
|
||||
if loop_count == 3:
|
||||
log.error('Looped too many times')
|
||||
return False
|
||||
self._presentation = self._document.getPresentation()
|
||||
self._presentation.Display = screen_number
|
||||
self._control = None
|
||||
return True
|
||||
|
||||
def extract_thumbnails(self, temp_folder):
|
||||
"""
|
||||
Create thumbnails for the presentation
|
||||
"""
|
||||
thumbnails = []
|
||||
thumb_dir_url = uno.systemPathToFileUrl(temp_folder)
|
||||
properties = (self._create_property('FilterName', 'impress_png_Export'),)
|
||||
pages = self._document.getDrawPages()
|
||||
if not pages:
|
||||
return []
|
||||
if not os.path.isdir(temp_folder):
|
||||
os.makedirs(temp_folder)
|
||||
for index in range(pages.getCount()):
|
||||
page = pages.getByIndex(index)
|
||||
self._document.getCurrentController().setCurrentPage(page)
|
||||
url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1))
|
||||
path = os.path.join(temp_folder, str(index + 1) + '.png')
|
||||
try:
|
||||
self._document.storeToURL(url_path, properties)
|
||||
thumbnails.append(path)
|
||||
except ErrorCodeIOException as exception:
|
||||
log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode))
|
||||
except Exception:
|
||||
log.exception('{path} - Unable to store openoffice preview'.format(path=path))
|
||||
return thumbnails
|
||||
|
||||
def get_titles_and_notes(self):
|
||||
"""
|
||||
Extract the titles and the notes from the slides.
|
||||
"""
|
||||
titles = []
|
||||
notes = []
|
||||
pages = self._document.getDrawPages()
|
||||
for slide_no in range(1, pages.getCount() + 1):
|
||||
titles.append(self._get_text_from_page(slide_no, TextType.Title).replace('\n', ' ') + '\n')
|
||||
note = self._get_text_from_page(slide_no, TextType.Notes)
|
||||
if len(note) == 0:
|
||||
note = ' '
|
||||
notes.append(note)
|
||||
return titles, notes
|
||||
|
||||
def close_presentation(self):
|
||||
"""
|
||||
Close presentation and clean up objects.
|
||||
"""
|
||||
log.debug('close Presentation LibreOffice')
|
||||
if self._document:
|
||||
if self._presentation:
|
||||
try:
|
||||
self._presentation.end()
|
||||
self._presentation = None
|
||||
self._document.dispose()
|
||||
except Exception:
|
||||
log.exception("Closing presentation failed")
|
||||
self._document = None
|
||||
|
||||
def is_loaded(self):
|
||||
"""
|
||||
Returns true if a presentation is loaded.
|
||||
"""
|
||||
log.debug('is loaded LibreOffice')
|
||||
if self._presentation is None or self._document is None:
|
||||
log.debug("is_loaded: no presentation or document")
|
||||
return False
|
||||
try:
|
||||
if self._document.getPresentation() is None:
|
||||
log.debug("getPresentation failed to find a presentation")
|
||||
return False
|
||||
except Exception:
|
||||
log.exception("getPresentation failed to find a presentation")
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_active(self):
|
||||
"""
|
||||
Returns true if a presentation is active and running.
|
||||
"""
|
||||
log.debug('is active LibreOffice')
|
||||
if not self.is_loaded():
|
||||
return False
|
||||
return self._control.isRunning() if self._control else False
|
||||
|
||||
def unblank_screen(self):
|
||||
"""
|
||||
Unblanks the screen.
|
||||
"""
|
||||
log.debug('unblank screen LibreOffice')
|
||||
return self._control.resume()
|
||||
|
||||
def blank_screen(self):
|
||||
"""
|
||||
Blanks the screen.
|
||||
"""
|
||||
log.debug('blank screen LibreOffice')
|
||||
self._control.blankScreen(0)
|
||||
|
||||
def is_blank(self):
|
||||
"""
|
||||
Returns true if screen is blank.
|
||||
"""
|
||||
log.debug('is blank LibreOffice')
|
||||
if self._control and self._control.isRunning():
|
||||
return self._control.isPaused()
|
||||
else:
|
||||
return False
|
||||
|
||||
def stop_presentation(self):
|
||||
"""
|
||||
Stop the presentation, remove from screen.
|
||||
"""
|
||||
log.debug('stop presentation LibreOffice')
|
||||
self._presentation.end()
|
||||
self._control = None
|
||||
|
||||
def start_presentation(self):
|
||||
"""
|
||||
Start the presentation from the beginning.
|
||||
"""
|
||||
log.debug('start presentation LibreOffice')
|
||||
if self._control is None or not self._control.isRunning():
|
||||
window = self._document.getCurrentController().getFrame().getContainerWindow()
|
||||
window.setVisible(True)
|
||||
self._presentation.start()
|
||||
self._control = self._presentation.getController()
|
||||
# start() returns before the Component is ready. Try for 15 seconds.
|
||||
sleep_count = 1
|
||||
while not self._control and sleep_count < 150:
|
||||
time.sleep(0.1)
|
||||
sleep_count += 1
|
||||
self._control = self._presentation.getController()
|
||||
window.setVisible(False)
|
||||
else:
|
||||
self._control.activate()
|
||||
self.goto_slide(1)
|
||||
|
||||
def get_slide_number(self):
|
||||
"""
|
||||
Return the current slide number on the screen, from 1.
|
||||
"""
|
||||
return self._control.getCurrentSlideIndex() + 1
|
||||
|
||||
def get_slide_count(self):
|
||||
"""
|
||||
Return the total number of slides.
|
||||
"""
|
||||
return self._document.getDrawPages().getCount()
|
||||
|
||||
def goto_slide(self, slide_no):
|
||||
"""
|
||||
Go to a specific slide (from 1).
|
||||
|
||||
:param slide_no: The slide the text is required for, starting at 1
|
||||
"""
|
||||
self._control.gotoSlideIndex(slide_no - 1)
|
||||
|
||||
def next_step(self):
|
||||
"""
|
||||
Triggers the next effect of slide on the running presentation.
|
||||
"""
|
||||
is_paused = self._control.isPaused()
|
||||
self._control.gotoNextEffect()
|
||||
time.sleep(0.1)
|
||||
if not is_paused and self._control.isPaused():
|
||||
self._control.gotoPreviousEffect()
|
||||
|
||||
def previous_step(self):
|
||||
"""
|
||||
Triggers the previous slide on the running presentation.
|
||||
"""
|
||||
self._control.gotoPreviousEffect()
|
||||
|
||||
def get_slide_text(self, slide_no):
|
||||
"""
|
||||
Returns the text on the slide.
|
||||
|
||||
:param slide_no: The slide the text is required for, starting at 1
|
||||
"""
|
||||
return self._get_text_from_page(slide_no)
|
||||
|
||||
def get_slide_notes(self, slide_no):
|
||||
"""
|
||||
Returns the text in the slide notes.
|
||||
|
||||
:param slide_no: The slide the notes are required for, starting at 1
|
||||
"""
|
||||
return self._get_text_from_page(slide_no, TextType.Notes)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
The main function which runs the server
|
||||
"""
|
||||
daemon = Daemon(host='localhost', port=4310)
|
||||
daemon.register(LibreOfficeServer, 'openlp.libreofficeserver')
|
||||
try:
|
||||
daemon.requestLoop()
|
||||
finally:
|
||||
daemon.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
266
openlp/plugins/presentations/lib/maclocontroller.py
Normal file
266
openlp/plugins/presentations/lib/maclocontroller.py
Normal file
@ -0,0 +1,266 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2019 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, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# 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, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
|
||||
import logging
|
||||
from subprocess import Popen
|
||||
|
||||
from Pyro4 import Proxy
|
||||
|
||||
from openlp.core.common import delete_file, is_macosx
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.display.screens import ScreenList
|
||||
from openlp.plugins.presentations.lib.serializers import register_classes
|
||||
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
|
||||
|
||||
|
||||
LIBREOFFICE_PATH = Path('/Applications/LibreOffice.app')
|
||||
LIBREOFFICE_PYTHON = LIBREOFFICE_PATH / 'Contents' / 'Resources' / 'python'
|
||||
|
||||
if is_macosx() and LIBREOFFICE_PATH.exists():
|
||||
macuno_available = True
|
||||
else:
|
||||
macuno_available = False
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
register_classes()
|
||||
|
||||
|
||||
class MacLOController(PresentationController):
|
||||
"""
|
||||
Class to control interactions with MacLO presentations on Mac OS X via Pyro4. It starts the Pyro4 nameserver,
|
||||
starts the LibreOfficeServer, and then controls MacLO via Pyro4.
|
||||
"""
|
||||
log.info('MacLOController loaded')
|
||||
|
||||
def __init__(self, plugin):
|
||||
"""
|
||||
Initialise the class
|
||||
"""
|
||||
log.debug('Initialising')
|
||||
super(MacLOController, self).__init__(plugin, 'maclo', MacLODocument, 'Impress on macOS')
|
||||
self.supports = ['odp']
|
||||
self.also_supports = ['ppt', 'pps', 'pptx', 'ppsx', 'pptm']
|
||||
self.server_process = None
|
||||
self._client = None
|
||||
self._start_server()
|
||||
|
||||
def _start_server(self):
|
||||
"""
|
||||
Start a LibreOfficeServer
|
||||
"""
|
||||
libreoffice_python = Path('/Applications/LibreOffice.app/Contents/Resources/python')
|
||||
libreoffice_server = AppLocation.get_directory(AppLocation.PluginsDir).joinpath('presentations', 'lib',
|
||||
'libreofficeserver.py')
|
||||
if libreoffice_python.exists():
|
||||
self.server_process = Popen([str(libreoffice_python), str(libreoffice_server)])
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
"""
|
||||
Set up a Pyro4 client so that we can talk to the LibreOfficeServer
|
||||
"""
|
||||
if not self._client:
|
||||
self._client = Proxy('PYRO:openlp.libreofficeserver@localhost:4310')
|
||||
if not self._client._pyroConnection:
|
||||
self._client._pyroReconnect()
|
||||
return self._client
|
||||
|
||||
def check_available(self):
|
||||
"""
|
||||
MacLO is able to run on this machine.
|
||||
"""
|
||||
log.debug('check_available')
|
||||
return macuno_available
|
||||
|
||||
def start_process(self):
|
||||
"""
|
||||
Loads a running version of LibreOffice in the background. It is not displayed to the user but is available to
|
||||
the UNO interface when required.
|
||||
"""
|
||||
log.debug('Started automatically by the Pyro server')
|
||||
self.client.start_process()
|
||||
|
||||
def kill(self):
|
||||
"""
|
||||
Called at system exit to clean up any running presentations.
|
||||
"""
|
||||
log.debug('Kill LibreOffice')
|
||||
self.client.shutdown()
|
||||
self.server_process.kill()
|
||||
|
||||
|
||||
class MacLODocument(PresentationDocument):
|
||||
"""
|
||||
Class which holds information and controls a single presentation.
|
||||
"""
|
||||
|
||||
def __init__(self, controller, presentation):
|
||||
"""
|
||||
Constructor, store information about the file and initialise.
|
||||
"""
|
||||
log.debug('Init Presentation LibreOffice')
|
||||
super(MacLODocument, self).__init__(controller, presentation)
|
||||
self.client = controller.client
|
||||
|
||||
def load_presentation(self):
|
||||
"""
|
||||
Tell the LibreOfficeServer to start the presentation.
|
||||
"""
|
||||
log.debug('Load Presentation LibreOffice')
|
||||
if not self.client.load_presentation(str(self.file_path), ScreenList().current.number + 1):
|
||||
return False
|
||||
self.create_thumbnails()
|
||||
self.create_titles_and_notes()
|
||||
return True
|
||||
|
||||
def create_thumbnails(self):
|
||||
"""
|
||||
Create thumbnail images for presentation.
|
||||
"""
|
||||
log.debug('create thumbnails LibreOffice')
|
||||
if self.check_thumbnails():
|
||||
return
|
||||
temp_thumbnails = self.client.extract_thumbnails(str(self.get_temp_folder()))
|
||||
for index, temp_thumb in enumerate(temp_thumbnails):
|
||||
temp_thumb = Path(temp_thumb)
|
||||
self.convert_thumbnail(temp_thumb, index + 1)
|
||||
delete_file(temp_thumb)
|
||||
|
||||
def create_titles_and_notes(self):
|
||||
"""
|
||||
Writes the list of titles (one per slide) to 'titles.txt' and the notes to 'slideNotes[x].txt'
|
||||
in the thumbnails directory
|
||||
"""
|
||||
titles, notes = self.client.get_titles_and_notes()
|
||||
self.save_titles_and_notes(titles, notes)
|
||||
|
||||
def close_presentation(self):
|
||||
"""
|
||||
Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being
|
||||
shutdown.
|
||||
"""
|
||||
log.debug('close Presentation LibreOffice')
|
||||
self.client.close_presentation()
|
||||
self.controller.remove_doc(self)
|
||||
|
||||
def is_loaded(self):
|
||||
"""
|
||||
Returns true if a presentation is loaded.
|
||||
"""
|
||||
log.debug('is loaded LibreOffice')
|
||||
return self.client.is_loaded()
|
||||
|
||||
def is_active(self):
|
||||
"""
|
||||
Returns true if a presentation is active and running.
|
||||
"""
|
||||
log.debug('is active LibreOffice')
|
||||
return self.client.is_active()
|
||||
|
||||
def unblank_screen(self):
|
||||
"""
|
||||
Unblanks the screen.
|
||||
"""
|
||||
log.debug('unblank screen LibreOffice')
|
||||
return self.client.unblank_screen()
|
||||
|
||||
def blank_screen(self):
|
||||
"""
|
||||
Blanks the screen.
|
||||
"""
|
||||
log.debug('blank screen LibreOffice')
|
||||
self.client.blank_screen()
|
||||
|
||||
def is_blank(self):
|
||||
"""
|
||||
Returns true if screen is blank.
|
||||
"""
|
||||
log.debug('is blank LibreOffice')
|
||||
return self.client.is_blank()
|
||||
|
||||
def stop_presentation(self):
|
||||
"""
|
||||
Stop the presentation, remove from screen.
|
||||
"""
|
||||
log.debug('stop presentation LibreOffice')
|
||||
self.client.stop_presentation()
|
||||
|
||||
def start_presentation(self):
|
||||
"""
|
||||
Start the presentation from the beginning.
|
||||
"""
|
||||
log.debug('start presentation LibreOffice')
|
||||
self.client.start_presentation()
|
||||
# Make sure impress doesn't steal focus, unless we're on a single screen setup
|
||||
if len(ScreenList()) > 1:
|
||||
Registry().get('main_window').activateWindow()
|
||||
|
||||
def get_slide_number(self):
|
||||
"""
|
||||
Return the current slide number on the screen, from 1.
|
||||
"""
|
||||
return self.client.get_slide_number()
|
||||
|
||||
def get_slide_count(self):
|
||||
"""
|
||||
Return the total number of slides.
|
||||
"""
|
||||
return self.client.get_slide_count()
|
||||
|
||||
def goto_slide(self, slide_no):
|
||||
"""
|
||||
Go to a specific slide (from 1).
|
||||
|
||||
:param slide_no: The slide the text is required for, starting at 1
|
||||
"""
|
||||
self.client.goto_slide(slide_no)
|
||||
|
||||
def next_step(self):
|
||||
"""
|
||||
Triggers the next effect of slide on the running presentation.
|
||||
"""
|
||||
self.client.next_step()
|
||||
|
||||
def previous_step(self):
|
||||
"""
|
||||
Triggers the previous slide on the running presentation.
|
||||
"""
|
||||
self.client.previous_step()
|
||||
|
||||
def get_slide_text(self, slide_no):
|
||||
"""
|
||||
Returns the text on the slide.
|
||||
|
||||
:param slide_no: The slide the text is required for, starting at 1
|
||||
"""
|
||||
return self.client.get_slide_text(slide_no)
|
||||
|
||||
def get_slide_notes(self, slide_no):
|
||||
"""
|
||||
Returns the text in the slide notes.
|
||||
|
||||
:param slide_no: The slide the notes are required for, starting at 1
|
||||
"""
|
||||
return self.client.get_slide_notes(slide_no)
|
@ -156,7 +156,7 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
Add presentations into the media manager. This is called both on initial load of the plugin to populate with
|
||||
existing files, and when the user adds new files via the media manager.
|
||||
|
||||
:param list[openlp.core.common.path.Path] file_paths: List of file paths to add to the media manager.
|
||||
:param list[pathlib.Path] file_paths: List of file paths to add to the media manager.
|
||||
"""
|
||||
current_paths = self.get_file_list()
|
||||
titles = [file_path.name for file_path in current_paths]
|
||||
@ -241,7 +241,7 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
"""
|
||||
Clean up the files created such as thumbnails
|
||||
|
||||
:param openlp.core.common.path.Path file_path: File path of the presentation to clean up after
|
||||
:param pathlib.Path file_path: File path of the presentation to clean up after
|
||||
:param bool clean_for_update: Only clean thumbnails if update is needed
|
||||
:rtype: None
|
||||
"""
|
||||
@ -385,7 +385,7 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
set as the preferred controller. Find the first (alphabetic) enabled controller which "supports" the extension.
|
||||
If none found, then look for a controller which "also supports" it instead.
|
||||
|
||||
:param openlp.core.common.path.Path file_path: The file path
|
||||
:param pathlib.Path file_path: The file path
|
||||
:return: The default application controller for this file type, or None if not supported
|
||||
:rtype: PresentationController
|
||||
"""
|
||||
|
@ -21,10 +21,10 @@
|
||||
##########################################################################
|
||||
import copy
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib import ServiceItemContext
|
||||
@ -169,24 +169,21 @@ class Controller(object):
|
||||
"""
|
||||
log.debug('Live = {live}, next'.format(live=self.is_live))
|
||||
if not self.doc:
|
||||
return
|
||||
return False
|
||||
if not self.is_live:
|
||||
return
|
||||
return False
|
||||
if self.hide_mode:
|
||||
if not self.doc.is_active():
|
||||
return
|
||||
return False
|
||||
if self.doc.slidenumber < self.doc.get_slide_count():
|
||||
self.doc.slidenumber += 1
|
||||
self.poll()
|
||||
return
|
||||
return False
|
||||
if not self.activate():
|
||||
return
|
||||
# The "End of slideshow" screen is after the last slide. Note, we can't just stop on the last slide, since it
|
||||
# may contain animations that need to be stepped through.
|
||||
if self.doc.slidenumber > self.doc.get_slide_count():
|
||||
return
|
||||
self.doc.next_step()
|
||||
return False
|
||||
ret = self.doc.next_step()
|
||||
self.poll()
|
||||
return ret
|
||||
|
||||
def previous(self):
|
||||
"""
|
||||
@ -194,20 +191,21 @@ class Controller(object):
|
||||
"""
|
||||
log.debug('Live = {live}, previous'.format(live=self.is_live))
|
||||
if not self.doc:
|
||||
return
|
||||
return False
|
||||
if not self.is_live:
|
||||
return
|
||||
return False
|
||||
if self.hide_mode:
|
||||
if not self.doc.is_active():
|
||||
return
|
||||
return False
|
||||
if self.doc.slidenumber > 1:
|
||||
self.doc.slidenumber -= 1
|
||||
self.poll()
|
||||
return
|
||||
return False
|
||||
if not self.activate():
|
||||
return
|
||||
self.doc.previous_step()
|
||||
return False
|
||||
ret = self.doc.previous_step()
|
||||
self.poll()
|
||||
return ret
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
@ -418,11 +416,12 @@ class MessageListener(object):
|
||||
"""
|
||||
is_live = message[1]
|
||||
if is_live:
|
||||
self.live_handler.next()
|
||||
ret = self.live_handler.next()
|
||||
if Settings().value('core/click live slide to unblank'):
|
||||
Registry().execute('slidecontroller_live_unblank')
|
||||
return ret
|
||||
else:
|
||||
self.preview_handler.next()
|
||||
return self.preview_handler.next()
|
||||
|
||||
def previous(self, message):
|
||||
"""
|
||||
@ -432,11 +431,12 @@ class MessageListener(object):
|
||||
"""
|
||||
is_live = message[1]
|
||||
if is_live:
|
||||
self.live_handler.previous()
|
||||
ret = self.live_handler.previous()
|
||||
if Settings().value('core/click live slide to unblank'):
|
||||
Registry().execute('slidecontroller_live_unblank')
|
||||
return ret
|
||||
else:
|
||||
self.preview_handler.previous()
|
||||
return self.preview_handler.previous()
|
||||
|
||||
def shutdown(self, message):
|
||||
"""
|
||||
|
@ -21,11 +21,11 @@
|
||||
##########################################################################
|
||||
import logging
|
||||
import re
|
||||
from shutil import which
|
||||
from subprocess import CalledProcessError, check_output
|
||||
|
||||
from openlp.core.common import check_binary_exists, is_win
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.path import which
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.display.screens import ScreenList
|
||||
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
|
||||
@ -71,7 +71,7 @@ class PdfController(PresentationController):
|
||||
Function that checks whether a binary is either ghostscript or mudraw or neither.
|
||||
Is also used from presentationtab.py
|
||||
|
||||
:param openlp.core.common.path.Path program_path: The full path to the binary to check.
|
||||
:param pathlib.Path program_path: The full path to the binary to check.
|
||||
:return: Type of the binary, 'gs' if ghostscript, 'mudraw' if mudraw, None if invalid.
|
||||
:rtype: str | None
|
||||
"""
|
||||
@ -182,7 +182,7 @@ class PdfDocument(PresentationDocument):
|
||||
"""
|
||||
Constructor, store information about the file and initialise.
|
||||
|
||||
:param openlp.core.common.path.Path document_path: Path to the document to load
|
||||
:param pathlib.Path document_path: Path to the document to load
|
||||
:rtype: None
|
||||
"""
|
||||
log.debug('Init Presentation Pdf')
|
||||
|
@ -124,7 +124,7 @@ class PowerpointDocument(PresentationDocument):
|
||||
Constructor, store information about the file and initialise.
|
||||
|
||||
:param controller:
|
||||
:param openlp.core.common.path.Path document_path: Path to the document to load
|
||||
:param pathlib.Path document_path: Path to the document to load
|
||||
:rtype: None
|
||||
"""
|
||||
log.debug('Init Presentation Powerpoint')
|
||||
@ -145,8 +145,8 @@ class PowerpointDocument(PresentationDocument):
|
||||
try:
|
||||
if not self.controller.process:
|
||||
self.controller.start_process()
|
||||
self.controller.process.Presentations.Open(str(self.file_path), False, False, False)
|
||||
self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count)
|
||||
self.presentation = self.controller.process.Presentations.Open(str(self.file_path), False, False, False)
|
||||
log.debug('Loaded presentation %s' % self.presentation.FullName)
|
||||
self.create_thumbnails()
|
||||
self.create_titles_and_notes()
|
||||
# Make sure powerpoint doesn't steal focus, unless we're on a single screen setup
|
||||
@ -170,12 +170,15 @@ class PowerpointDocument(PresentationDocument):
|
||||
However, for the moment, we want a physical file since it makes life easier elsewhere.
|
||||
"""
|
||||
log.debug('create_thumbnails')
|
||||
generate_thumbs = True
|
||||
if self.check_thumbnails():
|
||||
return
|
||||
# No need for thumbnails but we still need the index
|
||||
generate_thumbs = False
|
||||
key = 1
|
||||
for num in range(self.presentation.Slides.Count):
|
||||
if not self.presentation.Slides(num + 1).SlideShowTransition.Hidden:
|
||||
self.index_map[key] = num + 1
|
||||
if generate_thumbs:
|
||||
self.presentation.Slides(num + 1).Export(
|
||||
str(self.get_thumbnail_folder() / 'slide{key:d}.png'.format(key=key)), 'png', 320, 240)
|
||||
key += 1
|
||||
@ -318,6 +321,9 @@ class PowerpointDocument(PresentationDocument):
|
||||
size = ScreenList().current.display_geometry
|
||||
ppt_window = None
|
||||
try:
|
||||
# Disable the presentation console
|
||||
self.presentation.SlideShowSettings.ShowPresenterView = 0
|
||||
# Start the presentation
|
||||
ppt_window = self.presentation.SlideShowSettings.Run()
|
||||
except (AttributeError, pywintypes.com_error):
|
||||
log.exception('Caught exception while in start_presentation')
|
||||
@ -437,6 +443,12 @@ class PowerpointDocument(PresentationDocument):
|
||||
Triggers the next effect of slide on the running presentation.
|
||||
"""
|
||||
log.debug('next_step')
|
||||
# if we are at the presentations end don't go further, just return True
|
||||
if self.presentation.SlideShowWindow.View.GetClickCount() == \
|
||||
self.presentation.SlideShowWindow.View.GetClickIndex() \
|
||||
and self.get_slide_number() == self.get_slide_count():
|
||||
return True
|
||||
past_end = False
|
||||
try:
|
||||
self.presentation.SlideShowWindow.Activate()
|
||||
self.presentation.SlideShowWindow.View.Next()
|
||||
@ -444,28 +456,35 @@ class PowerpointDocument(PresentationDocument):
|
||||
log.exception('Caught exception while in next_step')
|
||||
trace_error_handler(log)
|
||||
self.show_error_msg()
|
||||
return
|
||||
return past_end
|
||||
# If for some reason the presentation end was not detected above, this will catch it.
|
||||
if self.get_slide_number() > self.get_slide_count():
|
||||
log.debug('past end, stepping back to previous')
|
||||
self.previous_step()
|
||||
past_end = True
|
||||
# Stop powerpoint from flashing in the taskbar
|
||||
if self.presentation_hwnd:
|
||||
win32gui.FlashWindowEx(self.presentation_hwnd, win32con.FLASHW_STOP, 0, 0)
|
||||
# Make sure powerpoint doesn't steal focus, unless we're on a single screen setup
|
||||
if len(ScreenList()) > 1:
|
||||
Registry().get('main_window').activateWindow()
|
||||
return past_end
|
||||
|
||||
def previous_step(self):
|
||||
"""
|
||||
Triggers the previous slide on the running presentation.
|
||||
"""
|
||||
log.debug('previous_step')
|
||||
# if we are at the presentations start we can't go further back, just return True
|
||||
if self.presentation.SlideShowWindow.View.GetClickIndex() == 0 and self.get_slide_number() == 1:
|
||||
return True
|
||||
try:
|
||||
self.presentation.SlideShowWindow.View.Previous()
|
||||
except (AttributeError, pywintypes.com_error):
|
||||
log.exception('Caught exception while in previous_step')
|
||||
trace_error_handler(log)
|
||||
self.show_error_msg()
|
||||
return False
|
||||
|
||||
def get_slide_text(self, slide_no):
|
||||
"""
|
||||
|
@ -20,12 +20,14 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
import logging
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common import md5_hash
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.path import Path, create_paths
|
||||
from openlp.core.common.path import create_paths
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib import create_thumb, validate_thumb
|
||||
@ -92,7 +94,7 @@ class PresentationDocument(object):
|
||||
Constructor for the PresentationController class
|
||||
|
||||
:param controller:
|
||||
:param openlp.core.common.path.Path document_path: Path to the document to load.
|
||||
:param Path document_path: Path to the document to load.
|
||||
:rtype: None
|
||||
"""
|
||||
self.controller = controller
|
||||
@ -102,7 +104,7 @@ class PresentationDocument(object):
|
||||
"""
|
||||
Run some initial setup. This method is separate from __init__ in order to mock it out in tests.
|
||||
|
||||
:param openlp.core.common.path.Path document_path: Path to the document to load.
|
||||
:param Path document_path: Path to the document to load.
|
||||
:rtype: None
|
||||
"""
|
||||
self.slide_number = 0
|
||||
@ -127,9 +129,9 @@ class PresentationDocument(object):
|
||||
thumbnail_folder_path = self.get_thumbnail_folder()
|
||||
temp_folder_path = self.get_temp_folder()
|
||||
if thumbnail_folder_path.exists():
|
||||
thumbnail_folder_path.rmtree()
|
||||
shutil.rmtree(thumbnail_folder_path)
|
||||
if temp_folder_path.exists():
|
||||
temp_folder_path.rmtree()
|
||||
shutil.rmtree(temp_folder_path)
|
||||
except OSError:
|
||||
log.exception('Failed to delete presentation controller files')
|
||||
|
||||
@ -138,7 +140,7 @@ class PresentationDocument(object):
|
||||
The location where thumbnail images will be stored
|
||||
|
||||
:return: The path to the thumbnail
|
||||
:rtype: openlp.core.common.path.Path
|
||||
:rtype: Path
|
||||
"""
|
||||
# TODO: Can be removed when the upgrade path to OpenLP 3.0 is no longer needed, also ensure code in
|
||||
# get_temp_folder and PresentationPluginapp_startup is removed
|
||||
@ -153,7 +155,7 @@ class PresentationDocument(object):
|
||||
The location where thumbnail images will be stored
|
||||
|
||||
:return: The path to the temporary file folder
|
||||
:rtype: openlp.core.common.path.Path
|
||||
:rtype: Path
|
||||
"""
|
||||
# TODO: Can be removed when the upgrade path to OpenLP 3.0 is no longer needed, also ensure code in
|
||||
# get_thumbnail_folder and PresentationPluginapp_startup is removed
|
||||
@ -246,21 +248,23 @@ class PresentationDocument(object):
|
||||
def next_step(self):
|
||||
"""
|
||||
Triggers the next effect of slide on the running presentation. This might be the next animation on the current
|
||||
slide, or the next slide
|
||||
slide, or the next slide.
|
||||
:rtype bool: True if we stepped beyond the slides of the presentation
|
||||
"""
|
||||
pass
|
||||
return False
|
||||
|
||||
def previous_step(self):
|
||||
"""
|
||||
Triggers the previous slide on the running presentation
|
||||
:rtype bool: True if we stepped beyond the slides of the presentation
|
||||
"""
|
||||
pass
|
||||
return False
|
||||
|
||||
def convert_thumbnail(self, image_path, index):
|
||||
"""
|
||||
Convert the slide image the application made to a scaled 360px height .png image.
|
||||
|
||||
:param openlp.core.common.path.Path image_path: Path to the image to create a thumb nail of
|
||||
:param Path image_path: Path to the image to create a thumbnail of
|
||||
:param int index: The index of the slide to create the thumbnail for.
|
||||
:rtype: None
|
||||
"""
|
||||
@ -277,7 +281,7 @@ class PresentationDocument(object):
|
||||
:param int slide_no: The slide an image is required for, starting at 1
|
||||
:param bool check_exists: Check if the generated path exists
|
||||
:return: The path, or None if the :param:`check_exists` is True and the file does not exist
|
||||
:rtype: openlp.core.common.path.Path | None
|
||||
:rtype: Path | None
|
||||
"""
|
||||
path = self.get_thumbnail_folder() / (self.controller.thumbnail_prefix + str(slide_no) + '.png')
|
||||
if path.is_file() or not check_exists:
|
||||
@ -408,7 +412,8 @@ class PresentationController(object):
|
||||
"""
|
||||
log.info('PresentationController loaded')
|
||||
|
||||
def __init__(self, plugin=None, name='PresentationController', document_class=PresentationDocument):
|
||||
def __init__(self, plugin=None, name='PresentationController', document_class=PresentationDocument,
|
||||
display_name=None):
|
||||
"""
|
||||
This is the constructor for the presentationcontroller object. This provides an easy way for descendent plugins
|
||||
|
||||
@ -428,6 +433,7 @@ class PresentationController(object):
|
||||
self.docs = []
|
||||
self.plugin = plugin
|
||||
self.name = name
|
||||
self.display_name = display_name if display_name is not None else name
|
||||
self.document_class = document_class
|
||||
self.settings_section = self.plugin.settings_section
|
||||
self.available = None
|
||||
@ -473,7 +479,7 @@ class PresentationController(object):
|
||||
"""
|
||||
Called when a new presentation document is opened.
|
||||
|
||||
:param openlp.core.common.path.Path document_path: Path to the document to load
|
||||
:param Path document_path: Path to the document to load
|
||||
:return: The document
|
||||
:rtype: PresentationDocument
|
||||
"""
|
||||
|
@ -127,10 +127,10 @@ class PresentationTab(SettingsTab):
|
||||
|
||||
def set_controller_text(self, checkbox, controller):
|
||||
if checkbox.isEnabled():
|
||||
checkbox.setText(controller.name)
|
||||
checkbox.setText(controller.display_name)
|
||||
else:
|
||||
checkbox.setText(translate('PresentationPlugin.PresentationTab',
|
||||
'{name} (unavailable)').format(name=controller.name))
|
||||
'{name} (unavailable)').format(name=controller.display_name))
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
@ -221,7 +221,7 @@ class PresentationTab(SettingsTab):
|
||||
"""
|
||||
Handle the `pathEditChanged` signal from program_path_edit
|
||||
|
||||
:param openlp.core.common.path.Path new_path: File path to the new program
|
||||
:param pathlib.Path new_path: File path to the new program
|
||||
:rtype: None
|
||||
"""
|
||||
if new_path:
|
||||
|
52
openlp/plugins/presentations/lib/serializers.py
Normal file
52
openlp/plugins/presentations/lib/serializers.py
Normal file
@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2019 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, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# 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, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
"""
|
||||
This module contains some helpers for serializing Path objects in Pyro4
|
||||
"""
|
||||
try:
|
||||
from openlp.core.common.path import Path
|
||||
except ImportError:
|
||||
from pathlib import Path
|
||||
|
||||
from Pyro4.util import SerializerBase
|
||||
|
||||
|
||||
def path_class_to_dict(obj):
|
||||
"""
|
||||
Serialize a Path object for Pyro4
|
||||
"""
|
||||
return {
|
||||
'__class__': 'Path',
|
||||
'parts': obj.parts
|
||||
}
|
||||
|
||||
|
||||
def path_dict_to_class(classname, d):
|
||||
return Path(d['parts'])
|
||||
|
||||
|
||||
def register_classes():
|
||||
"""
|
||||
Register the serializers
|
||||
"""
|
||||
SerializerBase.register_class_to_dict(Path, path_class_to_dict)
|
||||
SerializerBase.register_dict_to_class('Path', path_dict_to_class)
|
5
openlp/plugins/presentations/lib/vendor/do_not_delete.txt
vendored
Normal file
5
openlp/plugins/presentations/lib/vendor/do_not_delete.txt
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
Vendor Directory
|
||||
================
|
||||
|
||||
Do not delete this directory, it is used on Mac OS to place Pyro4 and serpent for use with Impress.
|
||||
|
@ -28,13 +28,13 @@ import os
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.state import State
|
||||
from openlp.core.api.http import register_endpoint
|
||||
from openlp.core.common import extension_loader
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib import build_icon
|
||||
from openlp.core.lib.plugin import Plugin, StringContent
|
||||
from openlp.core.state import State
|
||||
from openlp.core.ui.icons import UiIcons
|
||||
from openlp.plugins.presentations.endpoint import api_presentations_endpoint, presentations_endpoint
|
||||
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController
|
||||
@ -45,9 +45,11 @@ from openlp.plugins.presentations.lib.presentationtab import PresentationTab
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
__default_settings__ = {'presentations/override app': QtCore.Qt.Unchecked,
|
||||
__default_settings__ = {
|
||||
'presentations/override app': QtCore.Qt.Unchecked,
|
||||
'presentations/enable_pdf_program': QtCore.Qt.Unchecked,
|
||||
'presentations/pdf_program': None,
|
||||
'presentations/maclo': QtCore.Qt.Checked,
|
||||
'presentations/Impress': QtCore.Qt.Checked,
|
||||
'presentations/Powerpoint': QtCore.Qt.Checked,
|
||||
'presentations/Pdf': QtCore.Qt.Checked,
|
||||
@ -100,7 +102,7 @@ class PresentationPlugin(Plugin):
|
||||
try:
|
||||
self.controllers[controller].start_process()
|
||||
except Exception:
|
||||
log.warning('Failed to start controller process')
|
||||
log.exception('Failed to start controller process')
|
||||
self.controllers[controller].available = False
|
||||
self.media_item.build_file_mask_string()
|
||||
|
||||
|
@ -25,13 +25,14 @@ used to edit songs.
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
from shutil import copyfile
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.i18n import UiStrings, get_natural_key, translate
|
||||
from openlp.core.common.mixins import RegistryProperties
|
||||
from openlp.core.common.path import copyfile, create_paths
|
||||
from openlp.core.common.path import create_paths
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.lib import MediaType, create_separated_list
|
||||
from openlp.core.lib.plugin import PluginStatus
|
||||
|
@ -43,7 +43,7 @@ class MediaFilesForm(QtWidgets.QDialog, Ui_MediaFilesDialog):
|
||||
|
||||
def populate_files(self, file_paths):
|
||||
"""
|
||||
:param list[openlp.core.common.path.Path] file_paths:
|
||||
:param list[pathlib.Path] file_paths:
|
||||
:return:
|
||||
"""
|
||||
self.file_list_widget.clear()
|
||||
@ -54,6 +54,6 @@ class MediaFilesForm(QtWidgets.QDialog, Ui_MediaFilesDialog):
|
||||
|
||||
def get_selected_files(self):
|
||||
"""
|
||||
:rtype: list[openlp.core.common.path.Path]
|
||||
:rtype: list[pathlib.Path]
|
||||
"""
|
||||
return [item.data(QtCore.Qt.UserRole) for item in self.file_list_widget.selectedItems()]
|
||||
|
@ -88,52 +88,72 @@ class SongReviewWidget(QtWidgets.QWidget):
|
||||
self.song_alternate_title_content.setText(self.song.alternate_title)
|
||||
self.song_alternate_title_content.setWordWrap(True)
|
||||
self.song_info_form_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.song_alternate_title_content)
|
||||
# Add last modified date.
|
||||
self.song_last_modified_label = QtWidgets.QLabel(self)
|
||||
self.song_last_modified_label.setObjectName('last_modified_label')
|
||||
self.song_last_modified_label.setText('Last Modified:')
|
||||
self.song_info_form_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.song_last_modified_label)
|
||||
self.song_last_modified_content = QtWidgets.QLabel(self)
|
||||
self.song_last_modified_content.setObjectName('last_modified_content')
|
||||
self.song_last_modified_content.setText(self.song.last_modified.strftime("%Y-%m-%d %H:%M:%S"))
|
||||
self.song_last_modified_content.setWordWrap(True)
|
||||
self.song_info_form_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.song_last_modified_content)
|
||||
# Add Theme widget.
|
||||
self.song_theme_label = QtWidgets.QLabel(self)
|
||||
self.song_theme_label.setObjectName('song_theme_label')
|
||||
self.song_theme_label.setText('Theme:')
|
||||
self.song_info_form_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.song_theme_label)
|
||||
self.song_theme_content = QtWidgets.QLabel(self)
|
||||
self.song_theme_content.setObjectName('song_theme_content')
|
||||
self.song_theme_content.setText(self.song.theme_name)
|
||||
self.song_theme_content.setWordWrap(True)
|
||||
self.song_info_form_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.song_theme_content)
|
||||
# Add CCLI number widget.
|
||||
self.song_ccli_number_label = QtWidgets.QLabel(self)
|
||||
self.song_ccli_number_label.setObjectName('song_ccli_number_label')
|
||||
self.song_info_form_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.song_ccli_number_label)
|
||||
self.song_info_form_layout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.song_ccli_number_label)
|
||||
self.song_ccli_number_content = QtWidgets.QLabel(self)
|
||||
self.song_ccli_number_content.setObjectName('song_ccli_number_content')
|
||||
self.song_ccli_number_content.setText(self.song.ccli_number)
|
||||
self.song_ccli_number_content.setWordWrap(True)
|
||||
self.song_info_form_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.song_ccli_number_content)
|
||||
self.song_info_form_layout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.song_ccli_number_content)
|
||||
# Add copyright widget.
|
||||
self.song_copyright_label = QtWidgets.QLabel(self)
|
||||
self.song_copyright_label.setObjectName('song_copyright_label')
|
||||
self.song_info_form_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.song_copyright_label)
|
||||
self.song_info_form_layout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.song_copyright_label)
|
||||
self.song_copyright_content = QtWidgets.QLabel(self)
|
||||
self.song_copyright_content.setObjectName('song_copyright_content')
|
||||
self.song_copyright_content.setWordWrap(True)
|
||||
self.song_copyright_content.setText(self.song.copyright)
|
||||
self.song_info_form_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.song_copyright_content)
|
||||
self.song_info_form_layout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.song_copyright_content)
|
||||
# Add comments widget.
|
||||
self.song_comments_label = QtWidgets.QLabel(self)
|
||||
self.song_comments_label.setObjectName('song_comments_label')
|
||||
self.song_info_form_layout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.song_comments_label)
|
||||
self.song_info_form_layout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.song_comments_label)
|
||||
self.song_comments_content = QtWidgets.QLabel(self)
|
||||
self.song_comments_content.setObjectName('song_comments_content')
|
||||
self.song_comments_content.setText(self.song.comments)
|
||||
self.song_comments_content.setWordWrap(True)
|
||||
self.song_info_form_layout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.song_comments_content)
|
||||
self.song_info_form_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.song_comments_content)
|
||||
# Add authors widget.
|
||||
self.song_authors_label = QtWidgets.QLabel(self)
|
||||
self.song_authors_label.setObjectName('song_authors_label')
|
||||
self.song_info_form_layout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.song_authors_label)
|
||||
self.song_info_form_layout.setWidget(7, QtWidgets.QFormLayout.LabelRole, self.song_authors_label)
|
||||
self.song_authors_content = QtWidgets.QLabel(self)
|
||||
self.song_authors_content.setObjectName('song_authors_content')
|
||||
self.song_authors_content.setWordWrap(True)
|
||||
authors_text = ', '.join([author.display_name for author in self.song.authors])
|
||||
self.song_authors_content.setText(authors_text)
|
||||
self.song_info_form_layout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.song_authors_content)
|
||||
self.song_info_form_layout.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.song_authors_content)
|
||||
# Add verse order widget.
|
||||
self.song_verse_order_label = QtWidgets.QLabel(self)
|
||||
self.song_verse_order_label.setObjectName('song_verse_order_label')
|
||||
self.song_info_form_layout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.song_verse_order_label)
|
||||
self.song_info_form_layout.setWidget(8, QtWidgets.QFormLayout.LabelRole, self.song_verse_order_label)
|
||||
self.song_verse_order_content = QtWidgets.QLabel(self)
|
||||
self.song_verse_order_content.setObjectName('song_verse_order_content')
|
||||
self.song_verse_order_content.setText(self.song.verse_order)
|
||||
self.song_verse_order_content.setWordWrap(True)
|
||||
self.song_info_form_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.song_verse_order_content)
|
||||
self.song_info_form_layout.setWidget(8, QtWidgets.QFormLayout.FieldRole, self.song_verse_order_content)
|
||||
self.song_group_box_layout.addLayout(self.song_info_form_layout)
|
||||
# Add verses widget.
|
||||
self.song_info_verse_list_widget = QtWidgets.QTableWidget(self.song_group_box)
|
||||
|
@ -28,9 +28,9 @@ import re
|
||||
import sqlite3
|
||||
import struct
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.plugins.songs.lib import VerseType, retrieve_windows_encoding, strip_rtf
|
||||
|
||||
from .songimport import SongImport
|
||||
|
@ -24,9 +24,9 @@ The :mod:`powersong` module provides the functionality for importing
|
||||
PowerSong songs into the OpenLP database.
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||
|
||||
|
||||
@ -73,7 +73,7 @@ class PowerSongImport(SongImport):
|
||||
* is a directory
|
||||
* contains at least one * .song file
|
||||
|
||||
:param openlp.core.common.path.Path import_source: Should be a Path object that fulfills the above criteria
|
||||
:param Path import_source: Should be a Path object that fulfills the above criteria
|
||||
:return: If the source is valid
|
||||
:rtype: bool
|
||||
"""
|
||||
|
@ -78,7 +78,7 @@ class PresentationManagerImport(SongImport):
|
||||
def process_song(self, root, file_path):
|
||||
"""
|
||||
:param root:
|
||||
:param openlp.core.common.path.Path file_path: Path to the file to process
|
||||
:param pathlib.Path file_path: Path to the file to process
|
||||
:rtype: None
|
||||
"""
|
||||
self.set_defaults()
|
||||
|
@ -55,7 +55,7 @@ class ProPresenterImport(SongImport):
|
||||
def process_song(self, root, file_path):
|
||||
"""
|
||||
:param root:
|
||||
:param openlp.core.common.path.Path file_path: Path to the file thats being imported
|
||||
:param pathlib.Path file_path: Path to the file thats being imported
|
||||
:rtype: None
|
||||
"""
|
||||
self.set_defaults()
|
||||
|
@ -27,9 +27,9 @@ import logging
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from openlp.core.common import get_file_encoding, is_macosx, is_win
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.plugins.songs.lib import VerseType
|
||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||
|
@ -22,13 +22,14 @@
|
||||
|
||||
import logging
|
||||
import re
|
||||
from shutil import copyfile
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common import normalize_str
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.path import copyfile, create_paths
|
||||
from openlp.core.common.path import create_paths
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.widgets.wizard import WizardStrings
|
||||
from openlp.plugins.songs.lib import VerseType, clean_song
|
||||
@ -401,9 +402,9 @@ class SongImport(QtCore.QObject):
|
||||
the new file location.
|
||||
|
||||
:param song_id:
|
||||
:param openlp.core.common.path.Path file_path: The file to copy.
|
||||
:param pathlib.Path file_path: The file to copy.
|
||||
:return: The new location of the file
|
||||
:rtype: openlp.core.common.path.Path
|
||||
:rtype: pathlib.Path
|
||||
"""
|
||||
if not hasattr(self, 'save_path'):
|
||||
self.save_path = AppLocation.get_section_data_path(self.import_wizard.plugin.name) / 'audio' / str(song_id)
|
||||
|
@ -24,8 +24,8 @@ The :mod:`songpro` module provides the functionality for importing SongPro
|
||||
songs into the OpenLP database.
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.plugins.songs.lib import strip_rtf
|
||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||
|
||||
|
@ -65,7 +65,7 @@ class SundayPlusImport(SongImport):
|
||||
"""
|
||||
Process the Sunday Plus song file
|
||||
|
||||
:param openlp.core.common.path.Path file_path: The song file to import
|
||||
:param pathlib.Path file_path: The song file to import
|
||||
:rtype: None
|
||||
"""
|
||||
with file_path.open('rb') as song_file:
|
||||
@ -180,7 +180,7 @@ class SundayPlusImport(SongImport):
|
||||
"""
|
||||
Extract the title from the filename
|
||||
|
||||
:param openlp.core.common.path.Path file_path: File being imported
|
||||
:param pathlib.Path file_path: File being imported
|
||||
:return: The song title
|
||||
:rtype: str
|
||||
"""
|
||||
|
@ -25,9 +25,9 @@ exported from Lyrix."""
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.plugins.songs.lib.db import AuthorType
|
||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||
|
@ -26,7 +26,8 @@ Worship songs into the OpenLP database.
|
||||
import logging
|
||||
import os
|
||||
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.i18n import UiStrings, translate
|
||||
from openlp.core.lib import DataType, read_int, read_or_fail, seek_or_fail
|
||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||
|
||||
|
||||
@ -48,52 +49,138 @@ class WordsOfWorshipImport(SongImport):
|
||||
the author and the copyright.
|
||||
* A block can be a verse, chorus or bridge.
|
||||
|
||||
Little endian is used.
|
||||
|
||||
File Header:
|
||||
Bytes are counted from one, i.e. the first byte is byte 1. The first 19
|
||||
bytes should be "WoW File \\nSong Words" The bytes after this and up to
|
||||
the 56th byte, can change but no real meaning has been found. The
|
||||
56th byte specifies how many blocks there are. The first block starts
|
||||
with byte 83 after the "CSongDoc::CBlock" declaration.
|
||||
Bytes are counted from one, i.e. the first byte is byte 1.
|
||||
|
||||
0x00 - 0x13 Should be "WoW File \nSong Words\n"
|
||||
0x14 - 0x1F Minimum version of Words Of Worship required to open this file
|
||||
0x20 - 0x2B Minimum version of Words Of Worship required to save this file without data loss
|
||||
0x2C - 0x37 The version of Words of Worship that this file is from. From test data, it looks like this might be
|
||||
the version that originally created this file, not the last version to save it.
|
||||
|
||||
The Words Of Worship versioning system seems to be in the format:
|
||||
``Major.Minor.Patch``
|
||||
|
||||
Where each part of the version number is stored by a 32-bit int
|
||||
|
||||
0x38 - 0x3B Specifies how many blocks there are.
|
||||
|
||||
0x42 - 0x51 Should be "CSongDoc::CBlock"
|
||||
|
||||
0x52 The first song blocks start from here.
|
||||
|
||||
Blocks:
|
||||
Each block has a starting header, some lines of text, and an ending
|
||||
footer. Each block starts with a 32 bit number, which specifies how
|
||||
many lines are in that block.
|
||||
Each block starts with a 32-bit int which specifies how many lines are in that block.
|
||||
|
||||
Then there are a number of lines corresponding to the value above.
|
||||
|
||||
Each block ends with a 32 bit number, which defines what type of
|
||||
block it is:
|
||||
|
||||
* ``NUL`` (0x00) - Verse
|
||||
* ``SOH`` (0x01) - Chorus
|
||||
* ``STX`` (0x02) - Bridge
|
||||
* 0x00000000 = Verse
|
||||
* 0x01000000 = Chorus
|
||||
* 0x02000000 = Bridge
|
||||
|
||||
Blocks are separated by two bytes. The first byte is 0x01, and the
|
||||
second byte is 0x80.
|
||||
|
||||
Lines:
|
||||
Each line starts with a byte which specifies how long that line is,
|
||||
the line text, and ends with a null byte.
|
||||
Each line consists of a "Pascal" string.
|
||||
In later versions, a byte follows which denotes the formatting of the line:
|
||||
|
||||
* 0x00 = Normal
|
||||
* 0x01 = Minor
|
||||
|
||||
It looks like this may have been introduced in Words of Worship song version 2.1.0, though this is an educated
|
||||
guess.
|
||||
|
||||
Footer:
|
||||
The footer follows on after the last block, the first byte specifies
|
||||
the length of the author text, followed by the author text, if
|
||||
this byte is null, then there is no author text. The byte after the
|
||||
author text specifies the length of the copyright text, followed
|
||||
by the copyright text.
|
||||
The footer follows on after the last block. Its format is as follows:
|
||||
|
||||
The file is ended with four null bytes.
|
||||
Author String (as a 'Pascal' string)
|
||||
Copyright String (as a 'Pascal' string)
|
||||
|
||||
Finally in newer versions of Word Of Worship song files there is a 32 bit int describing the copyright.
|
||||
|
||||
0x00000000 = Covered by CCL
|
||||
0x01000000 = Authors explicit permission
|
||||
0x02000000 = Public Domain
|
||||
0x03000000 = Copyright expired
|
||||
0x04000000 = Other
|
||||
|
||||
Pascal Strings:
|
||||
Strings are preceded by a variable length integer which specifies how many bytes are in the string. An example
|
||||
of the variable length integer is below.
|
||||
|
||||
Lentgh bytes 'Little'| Str len
|
||||
-------------------------------
|
||||
01 | 01
|
||||
02 | 02
|
||||
.... |
|
||||
FD | FD
|
||||
FE | FE
|
||||
FF FF 00 | FF
|
||||
FF 00 01 | 01 00
|
||||
FF 01 01 | 01 01
|
||||
FF 02 01 | 01 02
|
||||
.... |
|
||||
FF FC FF | FF FC
|
||||
FF FD FF | FF FD
|
||||
FF FF FF FE FF | FF FE
|
||||
FF FF FF FF FF 00 00 | FF FF
|
||||
FF FF FF 00 00 01 00 | 01 00 00
|
||||
FF FF FF 01 00 01 00 | 01 00 01
|
||||
FF FF FF 02 00 02 00 | 01 00 02
|
||||
|
||||
Valid extensions for a Words of Worship song file are:
|
||||
|
||||
* .wsg
|
||||
* .wow-song
|
||||
"""
|
||||
@staticmethod
|
||||
def parse_string(song_data):
|
||||
length_bytes = song_data.read(DataType.U8)
|
||||
if length_bytes == b'\xff':
|
||||
length_bytes = song_data.read(DataType.U16)
|
||||
length = int.from_bytes(length_bytes, 'little')
|
||||
return read_or_fail(song_data, length).decode('cp1252')
|
||||
|
||||
def __init__(self, manager, **kwargs):
|
||||
"""
|
||||
Initialise the Words of Worship importer.
|
||||
"""
|
||||
super(WordsOfWorshipImport, self).__init__(manager, **kwargs)
|
||||
def parse_lines(self, song_data):
|
||||
lines = []
|
||||
lines_to_read = read_int(song_data, DataType.U32, 'little')
|
||||
for line_no in range(0, lines_to_read):
|
||||
line_text = self.parse_string(song_data)
|
||||
if self.read_version >= (2, 1, 0):
|
||||
if read_or_fail(song_data, DataType.U8) == b'\x01':
|
||||
line_text = '{{minor}}{text}{{/minor}}'.format(text=line_text)
|
||||
lines.append(line_text)
|
||||
return '\n'.join(lines)
|
||||
|
||||
@staticmethod
|
||||
def parse_version(song_data):
|
||||
return (read_int(song_data, DataType.U32, 'little'),
|
||||
read_int(song_data, DataType.U32, 'little'),
|
||||
read_int(song_data, DataType.U32, 'little'))
|
||||
|
||||
def vaildate(self, file_path, song_data):
|
||||
seek_or_fail(song_data, 0x00)
|
||||
err_text = b''
|
||||
data = read_or_fail(song_data, 20)
|
||||
if data != b'WoW File\nSong Words\n':
|
||||
err_text = data
|
||||
seek_or_fail(song_data, 0x42)
|
||||
data = read_or_fail(song_data, 16)
|
||||
if data != b'CSongDoc::CBlock':
|
||||
err_text = data
|
||||
if err_text:
|
||||
self.log_error(file_path,
|
||||
translate('SongsPlugin.WordsofWorshipSongImport',
|
||||
'Invalid Words of Worship song file. Missing {text!r} header.'
|
||||
).format(text=err_text))
|
||||
return False
|
||||
return True
|
||||
|
||||
def do_import(self):
|
||||
"""
|
||||
@ -104,57 +191,37 @@ class WordsOfWorshipImport(SongImport):
|
||||
for file_path in self.import_source:
|
||||
if self.stop_import_flag:
|
||||
return
|
||||
log.debug('Importing %s', file_path)
|
||||
try:
|
||||
self.set_defaults()
|
||||
with file_path.open('rb') as song_data:
|
||||
if song_data.read(19).decode() != 'WoW File\nSong Words':
|
||||
self.log_error(file_path,
|
||||
translate('SongsPlugin.WordsofWorshipSongImport',
|
||||
'Invalid Words of Worship song file. Missing "{text}" '
|
||||
'header.').format(text='WoW File\\nSong Words'))
|
||||
continue
|
||||
# Seek to byte which stores number of blocks in the song
|
||||
song_data.seek(56)
|
||||
no_of_blocks = ord(song_data.read(1))
|
||||
song_data.seek(66)
|
||||
if song_data.read(16).decode() != 'CSongDoc::CBlock':
|
||||
self.log_error(file_path,
|
||||
translate('SongsPlugin.WordsofWorshipSongImport',
|
||||
'Invalid Words of Worship song file. Missing "{text}" '
|
||||
'string.').format(text='CSongDoc::CBlock'))
|
||||
continue
|
||||
# Seek to the beginning of the first block
|
||||
song_data.seek(82)
|
||||
for block in range(no_of_blocks):
|
||||
skip_char_at_end = True
|
||||
self.lines_to_read = ord(song_data.read(4)[:1])
|
||||
block_text = ''
|
||||
while self.lines_to_read:
|
||||
self.line_text = str(song_data.read(ord(song_data.read(1))), 'cp1252')
|
||||
if skip_char_at_end:
|
||||
skip_char = ord(song_data.read(1))
|
||||
# Check if we really should skip a char. In some wsg files we shouldn't
|
||||
if skip_char != 0:
|
||||
song_data.seek(-1, os.SEEK_CUR)
|
||||
skip_char_at_end = False
|
||||
if block_text:
|
||||
block_text += '\n'
|
||||
block_text += self.line_text
|
||||
self.lines_to_read -= 1
|
||||
block_type = BLOCK_TYPES[ord(song_data.read(4)[:1])]
|
||||
# Blocks are separated by 2 bytes, skip them, but not if
|
||||
# this is the last block!
|
||||
if block + 1 < no_of_blocks:
|
||||
song_data.seek(2, os.SEEK_CUR)
|
||||
self.add_verse(block_text, block_type)
|
||||
# Now to extract the author
|
||||
author_length = ord(song_data.read(1))
|
||||
if author_length:
|
||||
self.parse_author(str(song_data.read(author_length), 'cp1252'))
|
||||
# Finally the copyright
|
||||
copyright_length = ord(song_data.read(1))
|
||||
if copyright_length:
|
||||
self.add_copyright(str(song_data.read(copyright_length), 'cp1252'))
|
||||
# Get the song title
|
||||
self.title = file_path.stem
|
||||
with file_path.open('rb') as song_data:
|
||||
if not self.vaildate(file_path, song_data):
|
||||
continue
|
||||
seek_or_fail(song_data, 20)
|
||||
self.read_version = self.parse_version(song_data)
|
||||
# Seek to byte which stores number of blocks in the song
|
||||
seek_or_fail(song_data, 56)
|
||||
no_of_blocks = read_int(song_data, DataType.U8)
|
||||
|
||||
# Seek to the beginning of the first block
|
||||
seek_or_fail(song_data, 82)
|
||||
for block_no in range(no_of_blocks):
|
||||
# Blocks are separated by 2 bytes, skip them, but not if this is the last block!
|
||||
if block_no != 0:
|
||||
seek_or_fail(song_data, 2, os.SEEK_CUR)
|
||||
text = self.parse_lines(song_data)
|
||||
block_type = BLOCK_TYPES[read_int(song_data, DataType.U32, 'little')]
|
||||
self.add_verse(text, block_type)
|
||||
|
||||
# Now to extract the author
|
||||
self.parse_author(self.parse_string(song_data))
|
||||
# Finally the copyright
|
||||
self.add_copyright(self.parse_string(song_data))
|
||||
if not self.finish():
|
||||
self.log_error(file_path)
|
||||
except IndexError:
|
||||
self.log_error(file_path, UiStrings().FileCorrupt)
|
||||
except Exception as e:
|
||||
self.log_error(file_path, e)
|
||||
|
@ -20,8 +20,9 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
import logging
|
||||
import os
|
||||
import mako
|
||||
import os
|
||||
from shutil import copyfile
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
from sqlalchemy.sql import and_, or_
|
||||
@ -29,7 +30,7 @@ from sqlalchemy.sql import and_, or_
|
||||
from openlp.core.state import State
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.i18n import UiStrings, get_natural_key, translate
|
||||
from openlp.core.common.path import copyfile, create_paths
|
||||
from openlp.core.common.path import create_paths
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib import ServiceItemContext, check_item_selected, create_separated_list
|
||||
|
@ -45,7 +45,7 @@ class OpenLyricsExport(RegistryProperties):
|
||||
"""
|
||||
Initialise the export.
|
||||
|
||||
:param openlp.core.common.path.Path save_path: The directory to save the exported songs in
|
||||
:param pathlib.Path save_path: The directory to save the exported songs in
|
||||
:rtype: None
|
||||
"""
|
||||
log.debug('initialise OpenLyricsExport')
|
||||
|
@ -88,7 +88,7 @@ class SongsTab(SettingsTab):
|
||||
self.footer_group_box = QtWidgets.QGroupBox(self.left_column)
|
||||
self.footer_group_box.setObjectName('footer_group_box')
|
||||
self.footer_layout = QtWidgets.QVBoxLayout(self.footer_group_box)
|
||||
self.footer_layout.setObjectName('chords_layout')
|
||||
self.footer_layout.setObjectName('footer_layout')
|
||||
self.footer_info_label = QtWidgets.QLabel(self.footer_group_box)
|
||||
self.footer_layout.addWidget(self.footer_info_label)
|
||||
self.footer_placeholder_info = QtWidgets.QTextEdit(self.footer_group_box)
|
||||
|
@ -25,14 +25,14 @@ backend for the Songs plugin
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Table, types
|
||||
from sqlalchemy.sql.expression import false, func, null, text
|
||||
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.db import drop_columns
|
||||
from openlp.core.common.json import OpenLPJsonEncoder
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.json import OpenLPJSONEncoder
|
||||
from openlp.core.lib.db import PathType, get_upgrade_op
|
||||
|
||||
|
||||
@ -182,7 +182,7 @@ def upgrade_7(session, metadata):
|
||||
results = conn.execute('SELECT * FROM media_files')
|
||||
data_path = AppLocation.get_data_path()
|
||||
for row in results.fetchall():
|
||||
file_path_json = json.dumps(Path(row.file_name), cls=OpenLPJsonEncoder, base_path=data_path)
|
||||
file_path_json = json.dumps(Path(row.file_name), cls=OpenLPJSONEncoder, base_path=data_path)
|
||||
sql = 'UPDATE media_files SET file_path = \'{file_path_json}\' WHERE id = {id}'.format(
|
||||
file_path_json=file_path_json, id=row.id)
|
||||
conn.execute(sql)
|
||||
|
@ -24,9 +24,9 @@ The :mod:`db` module provides the ability to provide a csv file of all songs
|
||||
"""
|
||||
import csv
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.widgets.dialogs import FileDialog
|
||||
|
@ -66,8 +66,8 @@ __default_settings__ = {
|
||||
'songs/add song from service': True,
|
||||
'songs/add songbook slide': False,
|
||||
'songs/display songbar': True,
|
||||
'songs/last directory import': '',
|
||||
'songs/last directory export': '',
|
||||
'songs/last directory import': None,
|
||||
'songs/last directory export': None,
|
||||
'songs/songselect username': '',
|
||||
'songs/songselect password': '',
|
||||
'songs/songselect searches': '',
|
||||
@ -423,7 +423,7 @@ class SongsPlugin(Plugin):
|
||||
"""
|
||||
Remove temporary songs from the database
|
||||
"""
|
||||
songs = self.manager.get_all_objects(Song, Song.temporary is True)
|
||||
songs = self.manager.get_all_objects(Song, Song.temporary == True) # noqa: E712
|
||||
for song in songs:
|
||||
self.manager.delete_object(Song, song.id)
|
||||
|
||||
@ -432,7 +432,7 @@ class SongsPlugin(Plugin):
|
||||
"""
|
||||
Provide a count of the songs in the database
|
||||
|
||||
:param openlp.core.common.path.Path db_path: The database to use
|
||||
:param Path db_path: The database to use
|
||||
:return: The number of songs in the db.
|
||||
:rtype: int
|
||||
"""
|
||||
|
@ -70,7 +70,7 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP
|
||||
"""
|
||||
Handle the `pathEditChanged` signal from report_path_edit
|
||||
|
||||
:param openlp.core.common.path.Path file_path: The new path.
|
||||
:param pathlib.Path file_path: The new path.
|
||||
:rtype: None
|
||||
"""
|
||||
Settings().setValue(self.plugin.settings_section + '/last directory export', file_path)
|
||||
|
@ -12,11 +12,13 @@ clone_script:
|
||||
environment:
|
||||
matrix:
|
||||
- PYTHON: C:\\Python37-x64
|
||||
CHOCO_VLC: vlc
|
||||
- PYTHON: C:\\Python37
|
||||
CHOCO_VLC: vlc --forcex86
|
||||
|
||||
install:
|
||||
# Install dependencies from pypi
|
||||
- "%PYTHON%\\python.exe -m pip install sqlalchemy alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock pyodbc psycopg2 pypiwin32 websockets asyncio waitress six webob requests QtAwesome PyQt5 PyQtWebEngine pymediainfo PyMuPDF"
|
||||
- "%PYTHON%\\python.exe -m pip install sqlalchemy alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock pyodbc psycopg2 pypiwin32 websockets asyncio waitress six webob requests QtAwesome PyQt5 PyQtWebEngine pymediainfo PyMuPDF QDarkStyle python-vlc Pyro4"
|
||||
|
||||
build: off
|
||||
|
||||
@ -31,12 +33,6 @@ after_test:
|
||||
# This is where we create a package using PyInstaller
|
||||
# Install PyInstaller
|
||||
- "%PYTHON%\\python.exe -m pip install pyinstaller"
|
||||
# Download and install Inno Setup - used for packaging
|
||||
- appveyor DownloadFile http://www.jrsoftware.org/download.php/is-unicode.exe
|
||||
- is-unicode.exe /VERYSILENT /SUPPRESSMSGBOXES /SP-
|
||||
# Download and unpack portable-bundle
|
||||
- appveyor DownloadFile https://get.openlp.org/win-sdk/portable-setup.7z
|
||||
- 7z x portable-setup.7z
|
||||
# Disabled portable installers - can't figure out how to make them silent
|
||||
# - curl -L -O http://downloads.sourceforge.net/project/portableapps/PortableApps.com%20Installer/PortableApps.comInstaller_3.4.4.paf.exe
|
||||
# - PortableApps.comInstaller_3.4.4.paf.exe /S
|
||||
@ -44,6 +40,11 @@ after_test:
|
||||
# - PortableApps.comLauncher_2.2.1.paf.exe /S
|
||||
# - curl -L -O http://downloads.sourceforge.net/project/portableapps/NSIS%20Portable/NSISPortable_3.0_English.paf.exe
|
||||
# - NSISPortable_3.0_English.paf.exe /S
|
||||
# Download and unpack portable-bundle
|
||||
- appveyor DownloadFile https://get.openlp.org/win-sdk/portable-setup.7z
|
||||
- 7z x portable-setup.7z
|
||||
# Install VLC
|
||||
- choco install %CHOCO_VLC%
|
||||
# Get the packaging code
|
||||
- appveyor DownloadFile http://bazaar.launchpad.net/~openlp-core/openlp/packaging/tarball -FileName packaging.tar.gz
|
||||
- 7z e packaging.tar.gz
|
||||
@ -66,3 +67,6 @@ after_test:
|
||||
|
||||
artifacts:
|
||||
- path: openlp-branch\dist\*.exe
|
||||
name: Portable-installer
|
||||
- path: openlp-branch\dist\*.msi
|
||||
name: Installer
|
||||
|
@ -100,12 +100,13 @@ OPTIONAL_MODULES = [
|
||||
('pyodbc', '(ODBC support)'),
|
||||
('psycopg2', '(PostgreSQL support)'),
|
||||
('enchant', '(spell checker)'),
|
||||
('fitz', '(executable-independent PDF support)'),
|
||||
('pysword', '(import SWORD bibles)'),
|
||||
('uno', '(LibreOffice/OpenOffice support)'),
|
||||
# development/testing modules
|
||||
('jenkins', '(access jenkins api)'),
|
||||
('launchpadlib', '(launchpad script support)'),
|
||||
('nose2', '(testing framework)'),
|
||||
('pytest', '(testing framework)'),
|
||||
('pylint', '(linter)')
|
||||
]
|
||||
|
||||
@ -159,6 +160,8 @@ def check_module(mod, text='', indent=' '):
|
||||
w('OK')
|
||||
except ImportError:
|
||||
w('FAIL')
|
||||
except Exception:
|
||||
w('ERROR')
|
||||
w(os.linesep)
|
||||
|
||||
|
||||
|
@ -2,6 +2,9 @@
|
||||
# E722 do not use bare except, specify exception instead
|
||||
# F841 local variable '<variable>' is assigned to but never used
|
||||
|
||||
[aliases]
|
||||
test=pytest
|
||||
|
||||
[pep8]
|
||||
exclude=resources.py,vlc.py
|
||||
max-line-length = 120
|
||||
|
5
setup.py
5
setup.py
@ -178,6 +178,7 @@ using a computer and a data projector.""",
|
||||
'pyobjc-framework-Cocoa; platform_system=="Darwin"',
|
||||
'PyQt5 >= 5.12',
|
||||
'PyQtWebEngine',
|
||||
'python-vlc',
|
||||
'pywin32; platform_system=="Windows"',
|
||||
'QtAwesome',
|
||||
'requests',
|
||||
@ -199,13 +200,13 @@ using a computer and a data projector.""",
|
||||
'launchpad': ['launchpadlib']
|
||||
},
|
||||
tests_require=[
|
||||
'nose2',
|
||||
'pylint',
|
||||
'PyMuPDF',
|
||||
'pyodbc',
|
||||
'pysword',
|
||||
'pytest',
|
||||
'python-xlib; platform_system=="Linux"'
|
||||
],
|
||||
test_suite='nose2.collector.collector',
|
||||
setup_requires=['pytest-runner'],
|
||||
entry_points={'gui_scripts': ['openlp = run_openlp:start']}
|
||||
)
|
||||
|
@ -20,12 +20,13 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from tempfile import mkdtemp
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.api.deploy import deploy_zipfile, download_and_check, download_sha256
|
||||
from openlp.core.common.path import Path
|
||||
|
||||
|
||||
CONFIG_FILE = '2c266badff1e3d140664c50fd1460a2b332b24d5ad8c267fa62e506b5eb6d894 deploy/site.zip\n2017_06_27'
|
||||
@ -46,7 +47,7 @@ class TestRemoteDeploy(TestCase):
|
||||
"""
|
||||
Clean up after tests
|
||||
"""
|
||||
self.app_root_path.rmtree()
|
||||
shutil.rmtree(self.app_root_path)
|
||||
|
||||
@patch('openlp.core.api.deploy.ZipFile')
|
||||
def test_deploy_zipfile(self, MockZipFile):
|
||||
|
@ -23,11 +23,11 @@
|
||||
Functional tests to test the AppLocation class and related methods.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from openlp.core.common import get_frozen_path
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.path import Path
|
||||
|
||||
|
||||
FILE_LIST = ['file1', 'file2', 'file3.txt', 'file4.txt', 'file5.mp3', 'file6.mp3']
|
||||
|
@ -22,12 +22,12 @@
|
||||
"""
|
||||
Functional tests to test the AppLocation class and related methods.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from openlp.core.common import clean_button_text, de_hump, extension_loader, is_linux, is_macosx, is_win, \
|
||||
normalize_str, path_to_module, trace_error_handler
|
||||
from openlp.core.common.path import Path
|
||||
|
||||
|
||||
class TestCommonFunctions(TestCase):
|
||||
|
@ -24,12 +24,12 @@ Functional tests to test the AppLocation class and related methods.
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.common.httputils import ProxyMode, download_file, get_proxy_settings, get_url_file_size, \
|
||||
get_user_agent, get_web_page
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.settings import Settings
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
@ -23,12 +23,12 @@
|
||||
Functional tests to test the AppLocation class and related methods.
|
||||
"""
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, PropertyMock, call, patch
|
||||
|
||||
from openlp.core.common import add_actions, clean_filename, delete_file, get_file_encoding, get_filesystem_encoding, \
|
||||
get_uno_command, get_uno_instance
|
||||
from openlp.core.common.path import Path
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
|
@ -23,14 +23,134 @@
|
||||
Package to test the openlp.core.common.json package.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.json import JSONMixin, OpenLPJSONDecoder, OpenLPJSONEncoder, PathSerializer, _registered_classes
|
||||
|
||||
|
||||
class TestOpenLPJsonDecoder(TestCase):
|
||||
class TestClassBase(object):
|
||||
"""
|
||||
Simple class to avoid repetition
|
||||
"""
|
||||
def __init__(self, a=None, b=None, c=None):
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
|
||||
|
||||
class TestJSONMixin(TestCase):
|
||||
"""
|
||||
Test the JSONMixin class
|
||||
"""
|
||||
def setUp(self):
|
||||
self._registered_classes_patcher = patch.dict(_registered_classes, clear=True)
|
||||
self.addCleanup(self._registered_classes_patcher.stop)
|
||||
self._registered_classes_patcher.start()
|
||||
|
||||
def test_subclass_json_mixin(self):
|
||||
"""
|
||||
Test that a class is `registered` when subclassing JSONMixin
|
||||
"""
|
||||
# GIVEN: The JSONMixin class
|
||||
# WHEN: Subclassing it
|
||||
class TestClass(JSONMixin):
|
||||
pass
|
||||
|
||||
# THEN: The TestClass should have been `registered`
|
||||
assert _registered_classes['TestClass'] == TestClass
|
||||
|
||||
def test_subclass_json_mixin_alt_names(self):
|
||||
"""
|
||||
Test that a class is `registered` using the specified names when subclassing JSONMixin
|
||||
"""
|
||||
# GIVEN: The JSONMixin class
|
||||
# WHEN: Subclassing it with custom names
|
||||
class TestClass(JSONMixin, register_names=('AltName1', 'AltName2')):
|
||||
pass
|
||||
|
||||
# THEN: The TestClass should have been registered with only those names
|
||||
assert 'TestClass' not in _registered_classes
|
||||
assert _registered_classes['AltName1'] == TestClass
|
||||
assert _registered_classes['AltName2'] == TestClass
|
||||
|
||||
def test_encoding_json_mixin_subclass(self):
|
||||
"""
|
||||
Test that an instance of a JSONMixin subclass is properly serialized to a JSON string
|
||||
"""
|
||||
# GIVEN: A instance of a subclass of the JSONMixin class
|
||||
class TestClass(TestClassBase, JSONMixin):
|
||||
_json_keys = ['a', 'b']
|
||||
|
||||
instance = TestClass(a=1, c=2)
|
||||
|
||||
# WHEN: Serializing the instance
|
||||
json_string = json.dumps(instance, cls=OpenLPJSONEncoder)
|
||||
|
||||
# THEN: Only the attributes specified by `_json_keys` should be serialized, and only if they have been set
|
||||
assert json_string == '{"a": 1, "json_meta": {"class": "TestClass", "version": 1}}'
|
||||
|
||||
def test_decoding_json_mixin_subclass(self):
|
||||
"""
|
||||
Test that an instance of a JSONMixin subclass is properly deserialized from a JSON string
|
||||
"""
|
||||
# GIVEN: A subclass of the JSONMixin class
|
||||
class TestClass(TestClassBase, JSONMixin):
|
||||
_json_keys = ['a', 'b']
|
||||
|
||||
# WHEN: Deserializing a JSON representation of the TestClass
|
||||
instance = json.loads(
|
||||
'{"a": 1, "c": 2, "json_meta": {"class": "TestClass", "version": 1}}', cls=OpenLPJSONDecoder)
|
||||
|
||||
# THEN: Only the attributes specified by `_json_keys` should have been set
|
||||
assert instance.__class__ == TestClass
|
||||
assert instance.a == 1
|
||||
assert instance.b is None
|
||||
assert instance.c is None
|
||||
|
||||
def test_encoding_json_mixin_subclass_custom_name(self):
|
||||
"""
|
||||
Test that an instance of a JSONMixin subclass is properly serialized to a JSON string when using a custom name
|
||||
"""
|
||||
# GIVEN: A instance of a subclass of the JSONMixin class with a custom name
|
||||
class TestClass(TestClassBase, JSONMixin, register_names=('AltName', )):
|
||||
_json_keys = ['a', 'b']
|
||||
_name = 'AltName'
|
||||
_version = 2
|
||||
|
||||
instance = TestClass(a=1, c=2)
|
||||
|
||||
# WHEN: Serializing the instance
|
||||
json_string = json.dumps(instance, cls=OpenLPJSONEncoder)
|
||||
|
||||
# THEN: Only the attributes specified by `_json_keys` should be serialized, and only if they have been set
|
||||
assert json_string == '{"a": 1, "json_meta": {"class": "AltName", "version": 2}}'
|
||||
|
||||
def test_decoding_json_mixin_subclass_custom_name(self):
|
||||
"""
|
||||
Test that an instance of a JSONMixin subclass is properly deserialized from a JSON string when using a custom
|
||||
name
|
||||
"""
|
||||
# GIVEN: A instance of a subclass of the JSONMixin class with a custom name
|
||||
class TestClass(TestClassBase, JSONMixin, register_names=('AltName', )):
|
||||
_json_keys = ['a', 'b']
|
||||
_name = 'AltName'
|
||||
_version = 2
|
||||
|
||||
# WHEN: Deserializing a JSON representation of the TestClass
|
||||
instance = json.loads(
|
||||
'{"a": 1, "c": 2, "json_meta": {"class": "AltName", "version": 2}}', cls=OpenLPJSONDecoder)
|
||||
|
||||
# THEN: Only the attributes specified by `_json_keys` should have been set
|
||||
assert instance.__class__ == TestClass
|
||||
assert instance.a == 1
|
||||
assert instance.b is None
|
||||
assert instance.c is None
|
||||
|
||||
|
||||
class TestOpenLPJSONDecoder(TestCase):
|
||||
"""
|
||||
Test the OpenLPJsonDecoder class
|
||||
"""
|
||||
@ -39,10 +159,10 @@ class TestOpenLPJsonDecoder(TestCase):
|
||||
Test the object_hook method when called with a decoded Path JSON object
|
||||
"""
|
||||
# GIVEN: An instance of OpenLPJsonDecoder
|
||||
instance = OpenLPJsonDecoder()
|
||||
instance = OpenLPJSONDecoder()
|
||||
|
||||
# WHEN: Calling the object_hook method with a decoded JSON object which contains a Path
|
||||
result = instance.object_hook({'__Path__': ['test', 'path']})
|
||||
result = instance.object_hook({'parts': ['test', 'path'], "json_meta": {"class": "Path", "version": 1}})
|
||||
|
||||
# THEN: A Path object should be returned
|
||||
assert result == Path('test', 'path')
|
||||
@ -52,7 +172,7 @@ class TestOpenLPJsonDecoder(TestCase):
|
||||
Test the object_hook method when called with a decoded JSON object
|
||||
"""
|
||||
# GIVEN: An instance of OpenLPJsonDecoder
|
||||
instance = OpenLPJsonDecoder()
|
||||
instance = OpenLPJSONDecoder()
|
||||
|
||||
# WHEN: Calling the object_hook method with a decoded JSON object which contains a Path
|
||||
with patch('openlp.core.common.json.Path') as mocked_path:
|
||||
@ -67,31 +187,32 @@ class TestOpenLPJsonDecoder(TestCase):
|
||||
Test the OpenLPJsonDecoder when decoding a JSON string
|
||||
"""
|
||||
# GIVEN: A JSON encoded string
|
||||
json_string = '[{"__Path__": ["test", "path1"]}, {"__Path__": ["test", "path2"]}]'
|
||||
json_string = '[{"parts": ["test", "path1"], "json_meta": {"class": "Path", "version": 1}}, ' \
|
||||
'{"parts": ["test", "path2"], "json_meta": {"class": "Path", "version": 1}}]'
|
||||
|
||||
# WHEN: Decoding the string using the OpenLPJsonDecoder class
|
||||
obj = json.loads(json_string, cls=OpenLPJsonDecoder)
|
||||
obj = json.loads(json_string, cls=OpenLPJSONDecoder)
|
||||
|
||||
# THEN: The object returned should be a python version of the JSON string
|
||||
assert obj == [Path('test', 'path1'), Path('test', 'path2')]
|
||||
|
||||
|
||||
class TestOpenLPJsonEncoder(TestCase):
|
||||
class TestOpenLPJSONEncoder(TestCase):
|
||||
"""
|
||||
Test the OpenLPJsonEncoder class
|
||||
Test the OpenLPJSONEncoder class
|
||||
"""
|
||||
def test_default_path_object(self):
|
||||
"""
|
||||
Test the default method when called with a Path object
|
||||
"""
|
||||
# GIVEN: An instance of OpenLPJsonEncoder
|
||||
instance = OpenLPJsonEncoder()
|
||||
# GIVEN: An instance of OpenLPJSONEncoder
|
||||
instance = OpenLPJSONEncoder()
|
||||
|
||||
# WHEN: Calling the default method with a Path object
|
||||
result = instance.default(Path('test', 'path'))
|
||||
|
||||
# THEN: A dictionary object that can be JSON encoded should be returned
|
||||
assert result == {'__Path__': ('test', 'path')}
|
||||
assert result == {'parts': ('test', 'path'), "json_meta": {"class": "Path", "version": 1}}
|
||||
|
||||
def test_default_non_path_object(self):
|
||||
"""
|
||||
@ -99,8 +220,8 @@ class TestOpenLPJsonEncoder(TestCase):
|
||||
"""
|
||||
with patch('openlp.core.common.json.JSONEncoder.default') as mocked_super_default:
|
||||
|
||||
# GIVEN: An instance of OpenLPJsonEncoder
|
||||
instance = OpenLPJsonEncoder()
|
||||
# GIVEN: An instance of OpenLPJSONEncoder
|
||||
instance = OpenLPJSONEncoder()
|
||||
|
||||
# WHEN: Calling the default method with a object other than a Path object
|
||||
instance.default('invalid object')
|
||||
@ -115,8 +236,65 @@ class TestOpenLPJsonEncoder(TestCase):
|
||||
# GIVEN: A list of Path objects
|
||||
obj = [Path('test', 'path1'), Path('test', 'path2')]
|
||||
|
||||
# WHEN: Encoding the object using the OpenLPJsonEncoder class
|
||||
json_string = json.dumps(obj, cls=OpenLPJsonEncoder)
|
||||
# WHEN: Encoding the object using the OpenLPJSONEncoder class
|
||||
json_string = json.dumps(obj, cls=OpenLPJSONEncoder)
|
||||
|
||||
# THEN: The JSON string return should be a representation of the object encoded
|
||||
assert json_string == '[{"__Path__": ["test", "path1"]}, {"__Path__": ["test", "path2"]}]'
|
||||
assert json_string == '[{"parts": ["test", "path1"], "json_meta": {"class": "Path", "version": 1}}, ' \
|
||||
'{"parts": ["test", "path2"], "json_meta": {"class": "Path", "version": 1}}]'
|
||||
|
||||
|
||||
class TestPathSerializer(TestCase):
|
||||
|
||||
def test_path_encode_json(self):
|
||||
"""
|
||||
Test that `Path.encode_json` returns a Path object from a dictionary representation of a Path object decoded
|
||||
from JSON
|
||||
"""
|
||||
# GIVEN: A Path object from openlp.core.common.path
|
||||
# WHEN: Calling encode_json, with a dictionary representation
|
||||
path = PathSerializer.encode_json(
|
||||
{'parts': ['path', 'to', 'fi.le'], "json_meta": {"class": "Path", "version": 1}}, extra=1, args=2)
|
||||
|
||||
# THEN: A Path object should have been returned
|
||||
assert path == Path('path', 'to', 'fi.le')
|
||||
|
||||
def test_path_encode_json_base_path(self):
|
||||
"""
|
||||
Test that `Path.encode_json` returns a Path object from a dictionary representation of a Path object decoded
|
||||
from JSON when the base_path arg is supplied.
|
||||
"""
|
||||
# GIVEN: A Path object from openlp.core.common.path
|
||||
# WHEN: Calling encode_json, with a dictionary representation
|
||||
path = PathSerializer.encode_json(
|
||||
{'parts': ['path', 'to', 'fi.le'], "json_meta": {"class": "Path", "version": 1}}, base_path=Path('/base'))
|
||||
|
||||
# THEN: A Path object should have been returned with an absolute path
|
||||
assert path == Path('/', 'base', 'path', 'to', 'fi.le')
|
||||
|
||||
def test_path_json_object(self):
|
||||
"""
|
||||
Test that `Path.json_object` creates a JSON decode-able object from a Path object
|
||||
"""
|
||||
# GIVEN: A Path object from openlp.core.common.path
|
||||
path = Path('/base', 'path', 'to', 'fi.le')
|
||||
|
||||
# WHEN: Calling json_object
|
||||
obj = PathSerializer().json_object(path, extra=1, args=2)
|
||||
|
||||
# THEN: A JSON decodeable object should have been returned.
|
||||
assert obj == {'parts': (os.sep, 'base', 'path', 'to', 'fi.le'), "json_meta": {"class": "Path", "version": 1}}
|
||||
|
||||
def test_path_json_object_base_path(self):
|
||||
"""
|
||||
Test that `Path.json_object` creates a JSON decode-able object from a Path object, that is relative to the
|
||||
base_path
|
||||
"""
|
||||
# GIVEN: A Path object from openlp.core.common.path
|
||||
path = Path('/base', 'path', 'to', 'fi.le')
|
||||
|
||||
# WHEN: Calling json_object with a base_path
|
||||
obj = PathSerializer().json_object(path, base_path=Path('/', 'base'))
|
||||
|
||||
# THEN: A JSON decodable object should have been returned.
|
||||
assert obj == {'parts': ('path', 'to', 'fi.le'), "json_meta": {"class": "Path", "version": 1}}
|
||||
|
@ -23,11 +23,11 @@
|
||||
Package to test the openlp.core.common.path package.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest.mock import ANY, MagicMock, patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.common.path import Path, copy, copyfile, copytree, create_paths, files_to_paths, path_to_str, \
|
||||
replace_params, str_to_path, which
|
||||
from openlp.core.common.path import create_paths, files_to_paths, path_to_str, replace_params, str_to_path, which
|
||||
|
||||
|
||||
class TestShutil(TestCase):
|
||||
@ -66,139 +66,6 @@ class TestShutil(TestCase):
|
||||
assert result_args == (1, '2')
|
||||
assert result_kwargs == {'arg3': '3', 'arg4': 4}
|
||||
|
||||
def test_copy(self):
|
||||
"""
|
||||
Test :func:`openlp.core.common.path.copy`
|
||||
"""
|
||||
# GIVEN: A mocked `shutil.copy` which returns a test path as a string
|
||||
with patch('openlp.core.common.path.shutil.copy', return_value=os.path.join('destination', 'test', 'path')) \
|
||||
as mocked_shutil_copy:
|
||||
|
||||
# WHEN: Calling :func:`openlp.core.common.path.copy` with the src and dst parameters as Path object types
|
||||
result = copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path'))
|
||||
|
||||
# THEN: :func:`shutil.copy` should have been called with the str equivalents of the Path objects.
|
||||
# :func:`openlp.core.common.path.copy` should return the str type result of calling
|
||||
# :func:`shutil.copy` as a Path object.
|
||||
mocked_shutil_copy.assert_called_once_with(os.path.join('source', 'test', 'path'),
|
||||
os.path.join('destination', 'test', 'path'))
|
||||
assert result == Path('destination', 'test', 'path')
|
||||
|
||||
def test_copy_follow_optional_params(self):
|
||||
"""
|
||||
Test :func:`openlp.core.common.path.copy` when follow_symlinks is set to false
|
||||
"""
|
||||
# GIVEN: A mocked `shutil.copy`
|
||||
with patch('openlp.core.common.path.shutil.copy', return_value='') as mocked_shutil_copy:
|
||||
|
||||
# WHEN: Calling :func:`openlp.core.common.path.copy` with :param:`follow_symlinks` set to False
|
||||
copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False)
|
||||
|
||||
# THEN: :func:`shutil.copy` should have been called with :param:`follow_symlinks` set to false
|
||||
mocked_shutil_copy.assert_called_once_with(ANY, ANY, follow_symlinks=False)
|
||||
|
||||
def test_copyfile(self):
|
||||
"""
|
||||
Test :func:`openlp.core.common.path.copyfile`
|
||||
"""
|
||||
# GIVEN: A mocked :func:`shutil.copyfile` which returns a test path as a string
|
||||
with patch('openlp.core.common.path.shutil.copyfile',
|
||||
return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copyfile:
|
||||
|
||||
# WHEN: Calling :func:`openlp.core.common.path.copyfile` with the src and dst parameters as Path object
|
||||
# types
|
||||
result = copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path'))
|
||||
|
||||
# THEN: :func:`shutil.copyfile` should have been called with the str equivalents of the Path objects.
|
||||
# :func:`openlp.core.common.path.copyfile` should return the str type result of calling
|
||||
# :func:`shutil.copyfile` as a Path object.
|
||||
mocked_shutil_copyfile.assert_called_once_with(os.path.join('source', 'test', 'path'),
|
||||
os.path.join('destination', 'test', 'path'))
|
||||
assert result == Path('destination', 'test', 'path')
|
||||
|
||||
def test_copyfile_optional_params(self):
|
||||
"""
|
||||
Test :func:`openlp.core.common.path.copyfile` when follow_symlinks is set to false
|
||||
"""
|
||||
# GIVEN: A mocked :func:`shutil.copyfile`
|
||||
with patch('openlp.core.common.path.shutil.copyfile', return_value='') as mocked_shutil_copyfile:
|
||||
|
||||
# WHEN: Calling :func:`openlp.core.common.path.copyfile` with :param:`follow_symlinks` set to False
|
||||
copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False)
|
||||
|
||||
# THEN: :func:`shutil.copyfile` should have been called with the optional parameters, with out any of the
|
||||
# values being modified
|
||||
mocked_shutil_copyfile.assert_called_once_with(ANY, ANY, follow_symlinks=False)
|
||||
|
||||
def test_copytree(self):
|
||||
"""
|
||||
Test :func:`openlp.core.common.path.copytree`
|
||||
"""
|
||||
# GIVEN: A mocked :func:`shutil.copytree` which returns a test path as a string
|
||||
with patch('openlp.core.common.path.shutil.copytree',
|
||||
return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copytree:
|
||||
|
||||
# WHEN: Calling :func:`openlp.core.common.path.copytree` with the src and dst parameters as Path object
|
||||
# types
|
||||
result = copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path'))
|
||||
|
||||
# THEN: :func:`shutil.copytree` should have been called with the str equivalents of the Path objects.
|
||||
# :func:`openlp.core.common.path.copytree` should return the str type result of calling
|
||||
# :func:`shutil.copytree` as a Path object.
|
||||
mocked_shutil_copytree.assert_called_once_with(os.path.join('source', 'test', 'path'),
|
||||
os.path.join('destination', 'test', 'path'))
|
||||
assert result == Path('destination', 'test', 'path')
|
||||
|
||||
def test_copytree_optional_params(self):
|
||||
"""
|
||||
Test :func:`openlp.core.common.path.copytree` when optional parameters are passed
|
||||
"""
|
||||
# GIVEN: A mocked :func:`shutil.copytree`
|
||||
with patch('openlp.core.common.path.shutil.copytree', return_value='') as mocked_shutil_copytree:
|
||||
mocked_ignore = MagicMock()
|
||||
mocked_copy_function = MagicMock()
|
||||
|
||||
# WHEN: Calling :func:`openlp.core.common.path.copytree` with the optional parameters set
|
||||
copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), symlinks=True,
|
||||
ignore=mocked_ignore, copy_function=mocked_copy_function, ignore_dangling_symlinks=True)
|
||||
|
||||
# THEN: :func:`shutil.copytree` should have been called with the optional parameters, with out any of the
|
||||
# values being modified
|
||||
mocked_shutil_copytree.assert_called_once_with(ANY, ANY, symlinks=True, ignore=mocked_ignore,
|
||||
copy_function=mocked_copy_function,
|
||||
ignore_dangling_symlinks=True)
|
||||
|
||||
def test_rmtree(self):
|
||||
"""
|
||||
Test :func:`rmtree`
|
||||
"""
|
||||
# GIVEN: A mocked :func:`shutil.rmtree` and a test Path object
|
||||
with patch('openlp.core.common.path.shutil.rmtree', return_value=None) as mocked_shutil_rmtree:
|
||||
path = Path('test', 'path')
|
||||
|
||||
# WHEN: Calling :func:`openlp.core.common.path.rmtree` with the path parameter as Path object type
|
||||
path.rmtree()
|
||||
|
||||
# THEN: :func:`shutil.rmtree` should have been called with the the Path object.
|
||||
mocked_shutil_rmtree.assert_called_once_with(Path('test', 'path'), False, None)
|
||||
|
||||
def test_rmtree_optional_params(self):
|
||||
"""
|
||||
Test :func:`openlp.core.common.path.rmtree` when optional parameters are passed
|
||||
"""
|
||||
# GIVEN: A mocked :func:`shutil.rmtree` and a test Path object.
|
||||
with patch('openlp.core.common.path.shutil.rmtree', return_value=None) as mocked_shutil_rmtree:
|
||||
path = Path('test', 'path')
|
||||
mocked_on_error = MagicMock()
|
||||
|
||||
# WHEN: Calling :func:`openlp.core.common.path.rmtree` with :param:`ignore_errors` set to True and
|
||||
# :param:`onerror` set to a mocked object
|
||||
path.rmtree(ignore_errors=True, onerror=mocked_on_error)
|
||||
|
||||
# THEN: :func:`shutil.rmtree` should have been called with the optional parameters, with out any of the
|
||||
# values being modified
|
||||
mocked_shutil_rmtree.assert_called_once_with(path, True, mocked_on_error)
|
||||
|
||||
def test_which_no_command(self):
|
||||
"""
|
||||
Test :func:`openlp.core.common.path.which` when the command is not found.
|
||||
@ -243,7 +110,18 @@ class TestPath(TestCase):
|
||||
# WHEN: Calling `path_to_str` with an invalid Type
|
||||
# THEN: A TypeError should have been raised
|
||||
with self.assertRaises(TypeError):
|
||||
path_to_str(str())
|
||||
path_to_str(57)
|
||||
|
||||
def test_path_to_str_wth_str(self):
|
||||
"""
|
||||
Test that `path_to_str` just returns a str when given a str
|
||||
"""
|
||||
# GIVEN: The `path_to_str` function
|
||||
# WHEN: Calling `path_to_str` with a str
|
||||
result = path_to_str('/usr/bin')
|
||||
|
||||
# THEN: The string should be returned
|
||||
assert result == '/usr/bin'
|
||||
|
||||
def test_path_to_str_none(self):
|
||||
"""
|
||||
@ -287,57 +165,6 @@ class TestPath(TestCase):
|
||||
# THEN: `path_to_str` should return None
|
||||
assert result is None
|
||||
|
||||
def test_path_encode_json(self):
|
||||
"""
|
||||
Test that `Path.encode_json` returns a Path object from a dictionary representation of a Path object decoded
|
||||
from JSON
|
||||
"""
|
||||
# GIVEN: A Path object from openlp.core.common.path
|
||||
# WHEN: Calling encode_json, with a dictionary representation
|
||||
path = Path.encode_json({'__Path__': ['path', 'to', 'fi.le']}, extra=1, args=2)
|
||||
|
||||
# THEN: A Path object should have been returned
|
||||
assert path == Path('path', 'to', 'fi.le')
|
||||
|
||||
def test_path_encode_json_base_path(self):
|
||||
"""
|
||||
Test that `Path.encode_json` returns a Path object from a dictionary representation of a Path object decoded
|
||||
from JSON when the base_path arg is supplied.
|
||||
"""
|
||||
# GIVEN: A Path object from openlp.core.common.path
|
||||
# WHEN: Calling encode_json, with a dictionary representation
|
||||
path = Path.encode_json({'__Path__': ['path', 'to', 'fi.le']}, base_path=Path('/base'))
|
||||
|
||||
# THEN: A Path object should have been returned with an absolute path
|
||||
assert path == Path('/', 'base', 'path', 'to', 'fi.le')
|
||||
|
||||
def test_path_json_object(self):
|
||||
"""
|
||||
Test that `Path.json_object` creates a JSON decode-able object from a Path object
|
||||
"""
|
||||
# GIVEN: A Path object from openlp.core.common.path
|
||||
path = Path('/base', 'path', 'to', 'fi.le')
|
||||
|
||||
# WHEN: Calling json_object
|
||||
obj = path.json_object(extra=1, args=2)
|
||||
|
||||
# THEN: A JSON decodable object should have been returned.
|
||||
assert obj == {'__Path__': (os.sep, 'base', 'path', 'to', 'fi.le')}
|
||||
|
||||
def test_path_json_object_base_path(self):
|
||||
"""
|
||||
Test that `Path.json_object` creates a JSON decode-able object from a Path object, that is relative to the
|
||||
base_path
|
||||
"""
|
||||
# GIVEN: A Path object from openlp.core.common.path
|
||||
path = Path('/base', 'path', 'to', 'fi.le')
|
||||
|
||||
# WHEN: Calling json_object with a base_path
|
||||
obj = path.json_object(base_path=Path('/', 'base'))
|
||||
|
||||
# THEN: A JSON decodable object should have been returned.
|
||||
assert obj == {'__Path__': ('path', 'to', 'fi.le')}
|
||||
|
||||
def test_create_paths_dir_exists(self):
|
||||
"""
|
||||
Test the create_paths() function when the path already exists
|
||||
|
@ -277,7 +277,8 @@ class TestSettings(TestCase, TestMixin):
|
||||
"""Test the Settings._convert_value() method when a setting is JSON and represents a Path object"""
|
||||
# GIVEN: A settings object
|
||||
# WHEN: _convert_value() is run
|
||||
result = Settings()._convert_value('{"__Path__": ["openlp", "core"]}', None)
|
||||
result = Settings()._convert_value(
|
||||
'{"parts": ["openlp", "core"], "json_meta": {"class": "Path", "version": 1}}', None)
|
||||
|
||||
# THEN: The result should be a Path object
|
||||
assert isinstance(result, Path), 'The result should be a Path object'
|
||||
|
@ -23,6 +23,7 @@
|
||||
Package to test the openlp.core.lib package.
|
||||
"""
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from tempfile import mkdtemp
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
@ -31,7 +32,6 @@ from sqlalchemy import MetaData
|
||||
from sqlalchemy.orm.scoping import ScopedSession
|
||||
from sqlalchemy.pool import NullPool
|
||||
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.lib.db import delete_database, get_upgrade_op, init_db, upgrade_db
|
||||
|
||||
|
||||
|
@ -22,14 +22,16 @@
|
||||
"""
|
||||
Package to test the openlp.core.lib package.
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from PyQt5 import QtCore, QtGui
|
||||
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.lib import build_icon, check_item_selected, create_separated_list, create_thumb, \
|
||||
get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb
|
||||
from openlp.core.lib import DataType, build_icon, check_item_selected, create_separated_list, create_thumb, \
|
||||
get_text_file_string, image_to_byte, read_or_fail, read_int, resize_image, seek_or_fail, str_to_bool, validate_thumb
|
||||
from tests.utils.constants import RESOURCE_PATH
|
||||
|
||||
|
||||
@ -680,3 +682,179 @@ class TestLib(TestCase):
|
||||
# THEN: We should have "Author 1, Author 2 and Author 3"
|
||||
assert string_result == 'Author 1, Author 2 and Author 3', \
|
||||
'The string should be "Author 1, Author 2, and Author 3".'
|
||||
|
||||
def test_read_or_fail_fail(self):
|
||||
"""
|
||||
Test the :func:`read_or_fail` function when attempting to read more data than the buffer contains.
|
||||
"""
|
||||
# GIVEN: Some test data
|
||||
test_data = io.BytesIO(b'test data')
|
||||
|
||||
# WHEN: Attempting to read past the end of the buffer
|
||||
# THEN: An OSError should be raised.
|
||||
with self.assertRaises(OSError):
|
||||
read_or_fail(test_data, 15)
|
||||
|
||||
def test_read_or_fail_success(self):
|
||||
"""
|
||||
Test the :func:`read_or_fail` function when reading data that is in the buffer.
|
||||
"""
|
||||
# GIVEN: Some test data
|
||||
test_data = io.BytesIO(b'test data')
|
||||
|
||||
# WHEN: Attempting to read data that should exist.
|
||||
result = read_or_fail(test_data, 4)
|
||||
|
||||
# THEN: The data of the requested length should be returned
|
||||
assert result == b'test'
|
||||
|
||||
def test_read_int_u8_big(self):
|
||||
"""
|
||||
Test the :func:`read_int` function when reading an unsigned 8-bit int using 'big' endianness.
|
||||
"""
|
||||
# GIVEN: Some test data
|
||||
test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
|
||||
|
||||
# WHEN: Reading a an unsigned 8-bit int
|
||||
result = read_int(test_data, DataType.U8, 'big')
|
||||
|
||||
# THEN: The an int should have been returned of the expected value
|
||||
assert result == 15
|
||||
|
||||
def test_read_int_u8_little(self):
|
||||
"""
|
||||
Test the :func:`read_int` function when reading an unsigned 8-bit int using 'little' endianness.
|
||||
"""
|
||||
# GIVEN: Some test data
|
||||
test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
|
||||
|
||||
# WHEN: Reading a an unsigned 8-bit int
|
||||
result = read_int(test_data, DataType.U8, 'little')
|
||||
|
||||
# THEN: The an int should have been returned of the expected value
|
||||
assert result == 15
|
||||
|
||||
def test_read_int_u16_big(self):
|
||||
"""
|
||||
Test the :func:`read_int` function when reading an unsigned 16-bit int using 'big' endianness.
|
||||
"""
|
||||
# GIVEN: Some test data
|
||||
test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
|
||||
|
||||
# WHEN: Reading a an unsigned 16-bit int
|
||||
result = read_int(test_data, DataType.U16, 'big')
|
||||
|
||||
# THEN: The an int should have been returned of the expected value
|
||||
assert result == 4080
|
||||
|
||||
def test_read_int_u16_little(self):
|
||||
"""
|
||||
Test the :func:`read_int` function when reading an unsigned 16-bit int using 'little' endianness.
|
||||
"""
|
||||
# GIVEN: Some test data
|
||||
test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
|
||||
|
||||
# WHEN: Reading a an unsigned 16-bit int
|
||||
result = read_int(test_data, DataType.U16, 'little')
|
||||
|
||||
# THEN: The an int should have been returned of the expected value
|
||||
assert result == 61455
|
||||
|
||||
def test_read_int_u32_big(self):
|
||||
"""
|
||||
Test the :func:`read_int` function when reading an unsigned 32-bit int using 'big' endianness.
|
||||
"""
|
||||
# GIVEN: Some test data
|
||||
test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
|
||||
|
||||
# WHEN: Reading a an unsigned 32-bit int
|
||||
result = read_int(test_data, DataType.U32, 'big')
|
||||
|
||||
# THEN: The an int should have been returned of the expected value
|
||||
assert result == 267390960
|
||||
|
||||
def test_read_int_u32_little(self):
|
||||
"""
|
||||
Test the :func:`read_int` function when reading an unsigned 32-bit int using 'little' endianness.
|
||||
"""
|
||||
# GIVEN: Some test data
|
||||
test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')
|
||||
|
||||
# WHEN: Reading a an unsigned 32-bit int
|
||||
result = read_int(test_data, DataType.U32, 'little')
|
||||
|
||||
# THEN: The an int should have been returned of the expected value
|
||||
assert result == 4027576335
|
||||
|
||||
def test_seek_or_fail_default_method(self):
|
||||
"""
|
||||
Test the :func:`seek_or_fail` function when using the default value for the :arg:`how`
|
||||
"""
|
||||
# GIVEN: A mocked_file_like_object
|
||||
mocked_file_like_object = MagicMock(**{'seek.return_value': 5, 'tell.return_value': 0})
|
||||
|
||||
# WHEN: Calling seek_or_fail with out the how arg set
|
||||
seek_or_fail(mocked_file_like_object, 5)
|
||||
|
||||
# THEN: seek should be called using the os.SEEK_SET constant
|
||||
mocked_file_like_object.seek.assert_called_once_with(5, os.SEEK_SET)
|
||||
|
||||
def test_seek_or_fail_os_end(self):
|
||||
"""
|
||||
Test the :func:`seek_or_fail` function when called with an unsupported seek operation.
|
||||
"""
|
||||
# GIVEN: A Mocked object
|
||||
# WHEN: Attempting to seek relative to the end
|
||||
# THEN: An NotImplementedError should have been raised
|
||||
with self.assertRaises(NotImplementedError):
|
||||
seek_or_fail(MagicMock(), 1, os.SEEK_END)
|
||||
|
||||
def test_seek_or_fail_valid_seek_set(self):
|
||||
"""
|
||||
Test that :func:`seek_or_fail` successfully seeks to the correct position.
|
||||
"""
|
||||
# GIVEN: A mocked file-like object
|
||||
mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 5})
|
||||
|
||||
# WHEN: Attempting to seek from the beginning
|
||||
result = seek_or_fail(mocked_file_like_object, 5, os.SEEK_SET)
|
||||
|
||||
# THEN: The new position should be 5 from the beginning
|
||||
assert result == 5
|
||||
|
||||
def test_seek_or_fail_invalid_seek_set(self):
|
||||
"""
|
||||
Test that :func:`seek_or_fail` raises an exception when seeking past the end.
|
||||
"""
|
||||
# GIVEN: A Mocked file-like object
|
||||
mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 10})
|
||||
|
||||
# WHEN: Attempting to seek from the beginning past the end
|
||||
# THEN: An OSError should have been raised
|
||||
with self.assertRaises(OSError):
|
||||
seek_or_fail(mocked_file_like_object, 15, os.SEEK_SET)
|
||||
|
||||
def test_seek_or_fail_valid_seek_cur(self):
|
||||
"""
|
||||
Test that :func:`seek_or_fail` successfully seeks to the correct position.
|
||||
"""
|
||||
# GIVEN: A mocked file_like object
|
||||
mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 8})
|
||||
|
||||
# WHEN: Attempting to seek from the current position
|
||||
result = seek_or_fail(mocked_file_like_object, 5, os.SEEK_CUR)
|
||||
|
||||
# THEN: The new position should be 8 (5 from its starting position)
|
||||
assert result == 8
|
||||
|
||||
def test_seek_or_fail_invalid_seek_cur(self):
|
||||
"""
|
||||
Test that :func:`seek_or_fail` raises an exception when seeking past the end.
|
||||
"""
|
||||
# GIVEN: A mocked file_like object
|
||||
mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 10})
|
||||
|
||||
# WHEN: Attempting to seek from the current position pas the end.
|
||||
# THEN: An OSError should have been raised
|
||||
with self.assertRaises(OSError):
|
||||
seek_or_fail(mocked_file_like_object, 15, os.SEEK_CUR)
|
||||
|
@ -23,12 +23,12 @@
|
||||
Package to test the openlp.core.lib package.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.state import State
|
||||
from openlp.core.common import md5_hash
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib.formattingtags import FormattingTags
|
||||
|
@ -19,10 +19,10 @@
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.server import Server
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
@ -27,7 +27,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.ui.media.mediacontroller import MediaController
|
||||
from openlp.core.ui.media.vlcplayer import VlcPlayer
|
||||
from openlp.core.ui.media import ItemMediaInfo
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
from tests.utils.constants import RESOURCE_PATH
|
||||
@ -43,26 +43,6 @@ class TestMediaController(TestCase, TestMixin):
|
||||
Registry.create()
|
||||
Registry().register('service_manager', MagicMock())
|
||||
|
||||
def test_generate_extensions_lists(self):
|
||||
"""
|
||||
Test that the extensions are create correctly
|
||||
"""
|
||||
# GIVEN: A MediaController and an active player with audio and video extensions
|
||||
media_controller = MediaController()
|
||||
media_controller.vlc_player = VlcPlayer(None)
|
||||
media_controller.vlc_player.is_active = True
|
||||
media_controller.vlc_player.audio_extensions_list = ['*.mp3', '*.wav', '*.wma', '*.ogg']
|
||||
media_controller.vlc_player.video_extensions_list = ['*.mp4', '*.mov', '*.avi', '*.ogm']
|
||||
|
||||
# WHEN: calling _generate_extensions_lists
|
||||
media_controller._generate_extensions_lists()
|
||||
|
||||
# THEN: extensions list should have been copied from the player to the mediacontroller
|
||||
assert media_controller.video_extensions_list == media_controller.video_extensions_list, \
|
||||
'Video extensions should be the same'
|
||||
assert media_controller.audio_extensions_list == media_controller.audio_extensions_list, \
|
||||
'Audio extensions should be the same'
|
||||
|
||||
def test_resize(self):
|
||||
"""
|
||||
Test that the resize method is called correctly
|
||||
@ -78,7 +58,7 @@ class TestMediaController(TestCase, TestMixin):
|
||||
# THEN: The player's resize method should be called correctly
|
||||
mocked_player.resize.assert_called_with(mocked_display)
|
||||
|
||||
def test_check_file_type(self):
|
||||
def test_check_file_type_null(self):
|
||||
"""
|
||||
Test that we don't try to play media when no players available
|
||||
"""
|
||||
@ -92,7 +72,47 @@ class TestMediaController(TestCase, TestMixin):
|
||||
ret = media_controller._check_file_type(mocked_controller, mocked_display)
|
||||
|
||||
# THEN: it should return False
|
||||
assert ret is False, '_check_file_type should return False when no mediaplayers are available.'
|
||||
assert ret is False, '_check_file_type should return False when no media file matches.'
|
||||
|
||||
def test_check_file_video(self):
|
||||
"""
|
||||
Test that we process a file that is valid
|
||||
"""
|
||||
# GIVEN: A mocked UiStrings, get_used_players, controller, display and service_item
|
||||
media_controller = MediaController()
|
||||
mocked_controller = MagicMock()
|
||||
mocked_display = MagicMock()
|
||||
media_controller.media_players = MagicMock()
|
||||
mocked_controller.media_info = ItemMediaInfo()
|
||||
mocked_controller.media_info.file_info = [TEST_PATH / 'mp3_file.mp3']
|
||||
media_controller.current_media_players = {}
|
||||
media_controller.vlc_player = MagicMock()
|
||||
|
||||
# WHEN: calling _check_file_type when no players exists
|
||||
ret = media_controller._check_file_type(mocked_controller, mocked_display)
|
||||
|
||||
# THEN: it should return False
|
||||
assert ret is True, '_check_file_type should return True when audio file is present and matches.'
|
||||
|
||||
def test_check_file_audio(self):
|
||||
"""
|
||||
Test that we process a file that is valid
|
||||
"""
|
||||
# GIVEN: A mocked UiStrings, get_used_players, controller, display and service_item
|
||||
media_controller = MediaController()
|
||||
mocked_controller = MagicMock()
|
||||
mocked_display = MagicMock()
|
||||
media_controller.media_players = MagicMock()
|
||||
mocked_controller.media_info = ItemMediaInfo()
|
||||
mocked_controller.media_info.file_info = [TEST_PATH / 'mp4_file.mp4']
|
||||
media_controller.current_media_players = {}
|
||||
media_controller.vlc_player = MagicMock()
|
||||
|
||||
# WHEN: calling _check_file_type when no players exists
|
||||
ret = media_controller._check_file_type(mocked_controller, mocked_display)
|
||||
|
||||
# THEN: it should return False
|
||||
assert ret is True, '_check_file_type should return True when media file is present and matches.'
|
||||
|
||||
def test_media_play_msg(self):
|
||||
"""
|
||||
|
@ -30,7 +30,7 @@ from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.ui.media import MediaState, MediaType
|
||||
from openlp.core.ui.media.vlcplayer import AUDIO_EXT, VIDEO_EXT, VlcPlayer, get_vlc
|
||||
from openlp.core.ui.media.vlcplayer import VlcPlayer, get_vlc
|
||||
from tests.helpers import MockDateTime
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
@ -95,8 +95,6 @@ class TestVLCPlayer(TestCase, TestMixin):
|
||||
assert '&VLC' == vlc_player.display_name
|
||||
assert vlc_player.parent is None
|
||||
assert vlc_player.can_folder is True
|
||||
assert AUDIO_EXT == vlc_player.audio_extensions_list
|
||||
assert VIDEO_EXT == vlc_player.video_extensions_list
|
||||
|
||||
@patch('openlp.core.ui.media.vlcplayer.is_win')
|
||||
@patch('openlp.core.ui.media.vlcplayer.is_macosx')
|
||||
@ -958,20 +956,3 @@ class TestVLCPlayer(TestCase, TestMixin):
|
||||
mocked_controller.seek_slider.setSliderPosition.assert_called_with(300)
|
||||
expected_calls = [call(True), call(False)]
|
||||
assert expected_calls == mocked_controller.seek_slider.blockSignals.call_args_list
|
||||
|
||||
@patch('openlp.core.ui.media.vlcplayer.translate')
|
||||
def test_get_info(self, mocked_translate):
|
||||
"""
|
||||
Test that get_info() returns some information about the VLC player
|
||||
"""
|
||||
# GIVEN: A VlcPlayer
|
||||
mocked_translate.side_effect = lambda *x: x[1]
|
||||
vlc_player = VlcPlayer(None)
|
||||
|
||||
# WHEN: get_info() is run
|
||||
info = vlc_player.get_info()
|
||||
|
||||
# THEN: The information should be correct
|
||||
assert 'VLC is an external player which supports a number of different formats.<br/> ' \
|
||||
'<strong>Audio</strong><br/>' + str(AUDIO_EXT) + '<br/><strong>Video</strong><br/>' + \
|
||||
str(VIDEO_EXT) + '<br/>' == info
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user