2010-09-10 19:47:33 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2012-12-28 22:06:43 +00:00
|
|
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
2012-12-27 16:27:59 +00:00
|
|
|
|
2010-09-10 19:47:33 +00:00
|
|
|
###############################################################################
|
|
|
|
# OpenLP - Open Source Lyrics Projection #
|
|
|
|
# --------------------------------------------------------------------------- #
|
2016-12-31 11:01:36 +00:00
|
|
|
# Copyright (c) 2008-2017 OpenLP Developers #
|
2010-09-10 19:47:33 +00:00
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
# This program is free software; you can redistribute it and/or modify it #
|
|
|
|
# under the terms of the GNU General Public License as published by the Free #
|
|
|
|
# Software Foundation; version 2 of the License. #
|
|
|
|
# #
|
|
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
|
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
|
|
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
|
|
|
# more details. #
|
|
|
|
# #
|
|
|
|
# You should have received a copy of the GNU General Public License along #
|
|
|
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
|
|
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
|
|
|
###############################################################################
|
|
|
|
"""
|
|
|
|
The :mod:`lib` module contains most of the components and libraries that make
|
|
|
|
OpenLP work.
|
|
|
|
"""
|
2017-05-17 20:06:45 +00:00
|
|
|
import html
|
2010-09-10 19:47:33 +00:00
|
|
|
import logging
|
2012-04-12 14:16:12 +00:00
|
|
|
import os
|
2016-01-03 17:01:44 +00:00
|
|
|
import re
|
2016-07-25 20:07:07 +00:00
|
|
|
import math
|
2010-09-10 19:47:33 +00:00
|
|
|
|
2017-09-15 19:01:09 +00:00
|
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
2015-11-07 00:49:40 +00:00
|
|
|
|
2013-10-13 20:36:42 +00:00
|
|
|
from openlp.core.common import translate
|
2017-09-15 19:01:09 +00:00
|
|
|
from openlp.core.common.path import Path
|
2013-10-13 20:36:42 +00:00
|
|
|
|
2014-04-12 20:19:22 +00:00
|
|
|
log = logging.getLogger(__name__ + '.__init__')
|
2010-09-10 19:47:33 +00:00
|
|
|
|
2017-05-17 20:06:45 +00:00
|
|
|
SLIMCHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
|
|
|
|
|
2013-03-23 07:07:06 +00:00
|
|
|
|
2012-10-20 08:20:11 +00:00
|
|
|
class ServiceItemContext(object):
|
|
|
|
"""
|
|
|
|
The context in which a Service Item is being generated
|
|
|
|
"""
|
|
|
|
Preview = 0
|
|
|
|
Live = 1
|
|
|
|
Service = 2
|
|
|
|
|
2012-07-01 18:41:59 +00:00
|
|
|
|
|
|
|
class ImageSource(object):
|
|
|
|
"""
|
2013-03-07 12:30:24 +00:00
|
|
|
This enumeration class represents different image sources. An image sources states where an image is used. This
|
|
|
|
enumeration class is need in the context of the :class:~openlp.core.lib.imagemanager`.
|
2012-07-01 19:41:12 +00:00
|
|
|
|
|
|
|
``ImagePlugin``
|
|
|
|
This states that an image is being used by the image plugin.
|
|
|
|
|
|
|
|
``Theme``
|
|
|
|
This says, that the image is used by a theme.
|
2016-05-05 18:57:32 +00:00
|
|
|
|
2016-05-05 03:57:04 +00:00
|
|
|
``CommandPlugins``
|
|
|
|
This states that an image is being used by a command plugin.
|
2012-07-01 18:41:59 +00:00
|
|
|
"""
|
|
|
|
ImagePlugin = 1
|
|
|
|
Theme = 2
|
2016-05-05 03:57:04 +00:00
|
|
|
CommandPlugins = 3
|
2012-07-01 18:41:59 +00:00
|
|
|
|
|
|
|
|
2011-08-23 21:48:46 +00:00
|
|
|
class MediaType(object):
|
|
|
|
"""
|
|
|
|
An enumeration class for types of media.
|
|
|
|
"""
|
|
|
|
Audio = 1
|
|
|
|
Video = 2
|
2012-01-29 22:13:51 +00:00
|
|
|
|
|
|
|
|
2012-01-28 08:07:54 +00:00
|
|
|
class ServiceItemAction(object):
|
|
|
|
"""
|
2013-03-07 12:30:24 +00:00
|
|
|
Provides an enumeration for the required action moving between service items by left/right arrow keys
|
2012-01-28 08:07:54 +00:00
|
|
|
"""
|
|
|
|
Previous = 1
|
|
|
|
PreviousLastSlide = 2
|
|
|
|
Next = 3
|
2011-08-23 21:48:46 +00:00
|
|
|
|
2012-04-22 19:50:18 +00:00
|
|
|
|
2017-08-12 17:45:56 +00:00
|
|
|
def get_text_file_string(text_file_path):
|
2010-09-10 19:47:33 +00:00
|
|
|
"""
|
2017-08-12 17:45:56 +00:00
|
|
|
Open a file and return its content as a string. If the supplied file path is not a file then the function
|
2013-03-07 12:30:24 +00:00
|
|
|
returns False. If there is an error loading the file or the content can't be decoded then the function will return
|
|
|
|
None.
|
2010-09-10 19:47:33 +00:00
|
|
|
|
2017-08-25 20:03:25 +00:00
|
|
|
:param openlp.core.common.path.Path text_file_path: The path to the file.
|
2017-08-12 17:45:56 +00:00
|
|
|
: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
|
2010-09-10 19:47:33 +00:00
|
|
|
"""
|
2017-08-12 17:45:56 +00:00
|
|
|
if not text_file_path.is_file():
|
2010-09-10 19:47:33 +00:00
|
|
|
return False
|
2013-03-27 09:25:39 +00:00
|
|
|
content = None
|
2010-09-10 19:47:33 +00:00
|
|
|
try:
|
2017-08-12 17:45:56 +00:00
|
|
|
with text_file_path.open('r', encoding='utf-8') as file_handle:
|
|
|
|
if file_handle.read(3) != '\xEF\xBB\xBF':
|
|
|
|
# no BOM was found
|
|
|
|
file_handle.seek(0)
|
|
|
|
content = file_handle.read()
|
2010-09-10 19:47:33 +00:00
|
|
|
except (IOError, UnicodeError):
|
2017-08-12 17:45:56 +00:00
|
|
|
log.exception('Failed to open text file {text}'.format(text=text_file_path))
|
2013-03-27 09:25:39 +00:00
|
|
|
return content
|
2010-09-10 19:47:33 +00:00
|
|
|
|
2012-04-22 19:50:18 +00:00
|
|
|
|
2013-03-07 12:30:24 +00:00
|
|
|
def str_to_bool(string_value):
|
2010-09-10 19:47:33 +00:00
|
|
|
"""
|
|
|
|
Convert a string version of a boolean into a real boolean.
|
|
|
|
|
2014-01-01 09:33:07 +00:00
|
|
|
:param string_value: The string value to examine and convert to a boolean type.
|
2015-09-08 19:13:59 +00:00
|
|
|
:return: The correct boolean value
|
2010-09-10 19:47:33 +00:00
|
|
|
"""
|
2013-03-07 12:30:24 +00:00
|
|
|
if isinstance(string_value, bool):
|
|
|
|
return string_value
|
2013-08-31 18:17:38 +00:00
|
|
|
return str(string_value).strip().lower() in ('true', 'yes', 'y')
|
2010-09-10 19:47:33 +00:00
|
|
|
|
2012-04-22 19:50:18 +00:00
|
|
|
|
2010-09-10 19:47:33 +00:00
|
|
|
def build_icon(icon):
|
|
|
|
"""
|
2013-03-07 12:30:24 +00:00
|
|
|
Build a QIcon instance from an existing QIcon, a resource location, or a physical file location. If the icon is a
|
|
|
|
QIcon instance, that icon is simply returned. If not, it builds a QIcon instance from the resource or file name.
|
2010-09-10 19:47:33 +00:00
|
|
|
|
2017-09-15 19:01:09 +00:00
|
|
|
:param QtGui.QIcon | Path | QtGui.QIcon | str icon:
|
|
|
|
The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file path
|
|
|
|
location like ``Path(/path/to/file.png)``. However, the **recommended** way is to specify a resource string.
|
2015-09-08 19:13:59 +00:00
|
|
|
:return: The build icon.
|
2017-09-15 19:01:09 +00:00
|
|
|
:rtype: QtGui.QIcon
|
2010-09-10 19:47:33 +00:00
|
|
|
"""
|
|
|
|
if isinstance(icon, QtGui.QIcon):
|
2016-10-27 17:45:50 +00:00
|
|
|
return icon
|
2016-10-30 08:29:22 +00:00
|
|
|
pix_map = None
|
2016-10-27 17:45:50 +00:00
|
|
|
button_icon = QtGui.QIcon()
|
|
|
|
if isinstance(icon, str):
|
2016-10-30 08:29:22 +00:00
|
|
|
pix_map = QtGui.QPixmap(icon)
|
2017-09-15 19:01:09 +00:00
|
|
|
elif isinstance(icon, Path):
|
|
|
|
pix_map = QtGui.QPixmap(str(icon))
|
2010-09-10 19:47:33 +00:00
|
|
|
elif isinstance(icon, QtGui.QImage):
|
2016-10-27 17:45:50 +00:00
|
|
|
pix_map = QtGui.QPixmap.fromImage(icon)
|
2016-10-30 08:29:22 +00:00
|
|
|
if pix_map:
|
|
|
|
button_icon.addPixmap(pix_map, QtGui.QIcon.Normal, QtGui.QIcon.Off)
|
2010-09-10 19:47:33 +00:00
|
|
|
return button_icon
|
|
|
|
|
2012-04-22 19:50:18 +00:00
|
|
|
|
2013-10-29 15:38:28 +00:00
|
|
|
def image_to_byte(image, base_64=True):
|
2010-09-10 19:47:33 +00:00
|
|
|
"""
|
2013-03-07 12:30:24 +00:00
|
|
|
Resize an image to fit on the current screen for the web and returns it as a byte stream.
|
2010-09-10 19:47:33 +00:00
|
|
|
|
2014-01-01 09:33:07 +00:00
|
|
|
:param image: The image to converted.
|
2014-01-09 19:52:20 +00:00
|
|
|
:param base_64: If True returns the image as Base64 bytes, otherwise the image is returned as a byte array.
|
2013-10-29 15:38:28 +00:00
|
|
|
To preserve original intention, this defaults to True
|
2010-09-10 19:47:33 +00:00
|
|
|
"""
|
2013-08-31 18:17:38 +00:00
|
|
|
log.debug('image_to_byte - start')
|
2010-09-10 19:47:33 +00:00
|
|
|
byte_array = QtCore.QByteArray()
|
|
|
|
# use buffer to store pixmap into byteArray
|
|
|
|
buffie = QtCore.QBuffer(byte_array)
|
|
|
|
buffie.open(QtCore.QIODevice.WriteOnly)
|
2010-10-15 15:33:06 +00:00
|
|
|
image.save(buffie, "PNG")
|
2013-08-31 18:17:38 +00:00
|
|
|
log.debug('image_to_byte - end')
|
2013-10-29 15:38:28 +00:00
|
|
|
if not base_64:
|
|
|
|
return byte_array
|
2010-09-10 19:47:33 +00:00
|
|
|
# convert to base64 encoding so does not get missed!
|
2013-03-27 09:41:01 +00:00
|
|
|
return bytes(byte_array.toBase64()).decode('utf-8')
|
2010-09-10 19:47:33 +00:00
|
|
|
|
2012-04-22 19:50:18 +00:00
|
|
|
|
2011-06-12 15:59:46 +00:00
|
|
|
def create_thumb(image_path, thumb_path, return_icon=True, size=None):
|
2011-06-12 15:17:01 +00:00
|
|
|
"""
|
2013-03-07 12:30:24 +00:00
|
|
|
Create a thumbnail from the given image path and depending on ``return_icon`` it returns an icon from this thumb.
|
2011-06-12 15:17:01 +00:00
|
|
|
|
2014-01-01 09:33:07 +00:00
|
|
|
:param image_path: The image file to create the icon from.
|
|
|
|
:param 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.
|
2015-09-08 19:13:59 +00:00
|
|
|
:return: The final icon.
|
2011-06-12 15:17:01 +00:00
|
|
|
"""
|
|
|
|
ext = os.path.splitext(thumb_path)[1].lower()
|
|
|
|
reader = QtGui.QImageReader(image_path)
|
2011-06-12 15:59:46 +00:00
|
|
|
if size is None:
|
2016-05-04 12:10:42 +00:00
|
|
|
# No size given; use default height of 88
|
2016-05-16 12:35:58 +00:00
|
|
|
if reader.size().isEmpty():
|
|
|
|
ratio = 1
|
|
|
|
else:
|
|
|
|
ratio = reader.size().width() / reader.size().height()
|
2011-06-12 15:59:46 +00:00
|
|
|
reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
|
2016-05-04 12:10:42 +00:00
|
|
|
elif size.isValid():
|
|
|
|
# Complete size given
|
2011-06-12 15:59:46 +00:00
|
|
|
reader.setScaledSize(size)
|
2016-05-04 12:10:42 +00:00
|
|
|
else:
|
|
|
|
# Invalid size given
|
2016-05-16 12:35:58 +00:00
|
|
|
if reader.size().isEmpty():
|
|
|
|
ratio = 1
|
|
|
|
else:
|
|
|
|
ratio = reader.size().width() / reader.size().height()
|
2016-05-04 12:10:42 +00:00
|
|
|
if size.width() >= 0:
|
|
|
|
# Valid width; scale height
|
|
|
|
reader.setScaledSize(QtCore.QSize(size.width(), int(size.width() / ratio)))
|
|
|
|
elif size.height() >= 0:
|
|
|
|
# Valid height; scale width
|
|
|
|
reader.setScaledSize(QtCore.QSize(int(ratio * size.height()), size.height()))
|
|
|
|
else:
|
|
|
|
# Invalid; use default height of 88
|
|
|
|
reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
|
2011-06-12 15:17:01 +00:00
|
|
|
thumb = reader.read()
|
|
|
|
thumb.save(thumb_path, ext[1:])
|
|
|
|
if not return_icon:
|
|
|
|
return
|
|
|
|
if os.path.exists(thumb_path):
|
2013-10-28 21:23:17 +00:00
|
|
|
return build_icon(thumb_path)
|
2011-06-12 15:17:01 +00:00
|
|
|
# Fallback for files with animation support.
|
2013-10-28 21:23:17 +00:00
|
|
|
return build_icon(image_path)
|
2011-06-12 15:17:01 +00:00
|
|
|
|
2012-04-22 19:50:18 +00:00
|
|
|
|
2011-06-12 15:59:46 +00:00
|
|
|
def validate_thumb(file_path, thumb_path):
|
2011-06-12 15:17:01 +00:00
|
|
|
"""
|
2013-03-07 12:30:24 +00:00
|
|
|
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.
|
2011-06-12 15:17:01 +00:00
|
|
|
|
2014-01-01 09:33:07 +00:00
|
|
|
:param file_path: The path to the file. The file **must** exist!
|
|
|
|
:param thumb_path: The path to the thumb.
|
2015-09-08 19:13:59 +00:00
|
|
|
:return: True, False if the image has changed since the thumb was created.
|
2011-06-12 15:17:01 +00:00
|
|
|
"""
|
2017-09-15 19:01:09 +00:00
|
|
|
file_path = Path(file_path)
|
|
|
|
thumb_path = Path(thumb_path)
|
|
|
|
if not thumb_path.exists():
|
2011-06-12 15:17:01 +00:00
|
|
|
return False
|
2017-09-15 19:01:09 +00:00
|
|
|
image_date = file_path.stat().st_mtime
|
|
|
|
thumb_date = thumb_path.stat().st_mtime
|
2011-06-12 15:17:01 +00:00
|
|
|
return image_date <= thumb_date
|
|
|
|
|
2012-04-22 19:50:18 +00:00
|
|
|
|
2017-04-30 08:47:01 +00:00
|
|
|
def resize_image(image_path, width, height, background='#000000', ignore_aspect_ratio=False):
|
2010-09-10 19:47:33 +00:00
|
|
|
"""
|
|
|
|
Resize an image to fit on the current screen.
|
|
|
|
|
2011-09-20 17:53:09 +00:00
|
|
|
DO NOT REMOVE THE DEFAULT BACKGROUND VALUE!
|
2014-01-01 09:33:07 +00:00
|
|
|
|
|
|
|
:param image_path: The path to the image to resize.
|
|
|
|
:param width: The new image width.
|
|
|
|
:param height: The new image height.
|
|
|
|
:param background: The background colour. Defaults to black.
|
2010-09-10 19:47:33 +00:00
|
|
|
"""
|
2013-08-31 18:17:38 +00:00
|
|
|
log.debug('resize_image - start')
|
2011-06-09 10:41:02 +00:00
|
|
|
reader = QtGui.QImageReader(image_path)
|
2011-06-07 07:30:50 +00:00
|
|
|
# The image's ratio.
|
2013-04-24 19:05:34 +00:00
|
|
|
image_ratio = reader.size().width() / reader.size().height()
|
|
|
|
resize_ratio = width / height
|
2011-06-07 07:30:50 +00:00
|
|
|
# Figure out the size we want to resize the image to (keep aspect ratio).
|
2017-04-30 08:47:01 +00:00
|
|
|
if image_ratio == resize_ratio or ignore_aspect_ratio:
|
2011-06-07 07:30:50 +00:00
|
|
|
size = QtCore.QSize(width, height)
|
|
|
|
elif image_ratio < resize_ratio:
|
|
|
|
# Use the image's height as reference for the new size.
|
|
|
|
size = QtCore.QSize(image_ratio * height, height)
|
2010-10-15 15:33:06 +00:00
|
|
|
else:
|
2011-06-07 07:30:50 +00:00
|
|
|
# Use the image's width as reference for the new size.
|
|
|
|
size = QtCore.QSize(width, 1 / (image_ratio / width))
|
|
|
|
reader.setScaledSize(size)
|
|
|
|
preview = reader.read()
|
|
|
|
if image_ratio == resize_ratio:
|
|
|
|
# We neither need to centre the image nor add "bars" to the image.
|
|
|
|
return preview
|
2012-07-01 18:45:14 +00:00
|
|
|
real_width = preview.width()
|
|
|
|
real_height = preview.height()
|
2010-09-10 19:47:33 +00:00
|
|
|
# and move it to the centre of the preview space
|
2012-12-28 22:06:43 +00:00
|
|
|
new_image = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32_Premultiplied)
|
2010-09-10 19:47:33 +00:00
|
|
|
painter = QtGui.QPainter(new_image)
|
2011-09-20 15:23:29 +00:00
|
|
|
painter.fillRect(new_image.rect(), QtGui.QColor(background))
|
2013-04-24 19:05:34 +00:00
|
|
|
painter.drawImage((width - real_width) // 2, (height - real_height) // 2, preview)
|
2010-09-10 19:47:33 +00:00
|
|
|
return new_image
|
|
|
|
|
2012-04-22 19:50:18 +00:00
|
|
|
|
2010-09-10 19:47:33 +00:00
|
|
|
def check_item_selected(list_widget, message):
|
|
|
|
"""
|
|
|
|
Check if a list item is selected so an action may be performed on it
|
|
|
|
|
2014-01-01 09:33:07 +00:00
|
|
|
:param list_widget: The list to check for selected items
|
|
|
|
:param message: The message to give the user if no item is selected
|
2010-09-10 19:47:33 +00:00
|
|
|
"""
|
|
|
|
if not list_widget.selectedIndexes():
|
2015-11-07 00:49:40 +00:00
|
|
|
QtWidgets.QMessageBox.information(list_widget.parent(),
|
|
|
|
translate('OpenLP.MediaManagerItem', 'No Items Selected'), message)
|
2010-09-10 19:47:33 +00:00
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
2012-04-22 19:50:18 +00:00
|
|
|
|
2016-07-21 20:37:25 +00:00
|
|
|
def clean_tags(text, remove_chords=False):
|
2010-09-10 19:47:33 +00:00
|
|
|
"""
|
|
|
|
Remove Tags from text for display
|
2014-01-01 09:33:07 +00:00
|
|
|
|
|
|
|
:param text: Text to be cleaned
|
2016-07-21 20:37:25 +00:00
|
|
|
:param remove_chords: Clean ChordPro tags
|
2010-09-10 19:47:33 +00:00
|
|
|
"""
|
2013-08-31 18:17:38 +00:00
|
|
|
text = text.replace('<br>', '\n')
|
|
|
|
text = text.replace('{br}', '\n')
|
2017-01-29 21:25:52 +00:00
|
|
|
text = text.replace(' ', ' ')
|
2011-07-30 07:34:37 +00:00
|
|
|
for tag in FormattingTags.get_html_tags():
|
2013-08-31 18:17:38 +00:00
|
|
|
text = text.replace(tag['start tag'], '')
|
|
|
|
text = text.replace(tag['end tag'], '')
|
2016-01-03 17:01:44 +00:00
|
|
|
# Remove ChordPro tags
|
2016-07-21 20:37:25 +00:00
|
|
|
if remove_chords:
|
2016-01-03 17:01:44 +00:00
|
|
|
text = re.sub(r'\[.+?\]', r'', text)
|
2010-09-10 19:47:33 +00:00
|
|
|
return text
|
|
|
|
|
2012-04-22 19:50:18 +00:00
|
|
|
|
2017-04-25 14:41:35 +00:00
|
|
|
def expand_tags(text, expand_chord_tags=False, for_printing=False):
|
2010-09-10 19:47:33 +00:00
|
|
|
"""
|
|
|
|
Expand tags HTML for display
|
2014-01-01 09:33:07 +00:00
|
|
|
|
|
|
|
:param text: The text to be expanded.
|
2010-09-10 19:47:33 +00:00
|
|
|
"""
|
2017-04-25 14:41:35 +00:00
|
|
|
if expand_chord_tags:
|
|
|
|
if for_printing:
|
|
|
|
text = expand_chords_for_printing(text, '{br}')
|
|
|
|
else:
|
|
|
|
text = expand_chords(text)
|
2011-07-30 07:34:37 +00:00
|
|
|
for tag in FormattingTags.get_html_tags():
|
2013-08-31 18:17:38 +00:00
|
|
|
text = text.replace(tag['start tag'], tag['start html'])
|
|
|
|
text = text.replace(tag['end tag'], tag['end html'])
|
2010-09-10 19:47:33 +00:00
|
|
|
return text
|
|
|
|
|
2013-10-13 20:36:42 +00:00
|
|
|
|
2017-02-19 21:35:40 +00:00
|
|
|
def expand_and_align_chords_in_line(match):
|
|
|
|
"""
|
|
|
|
Expand the chords in the line and align them using whitespaces.
|
|
|
|
NOTE: There is equivalent javascript code in chords.js, in the updateSlide function. Make sure to update both!
|
|
|
|
|
|
|
|
:param match:
|
|
|
|
:return: The line with expanded html-chords
|
|
|
|
"""
|
|
|
|
whitespaces = ''
|
|
|
|
chordlen = 0
|
|
|
|
taillen = 0
|
|
|
|
# The match could be "[G]sweet the " from a line like "A[D]mazing [D7]grace! How [G]sweet the [D]sound!"
|
|
|
|
# The actual chord, would be "G" in match "[G]sweet the "
|
|
|
|
chord = match.group(1)
|
|
|
|
# The tailing word of the chord, would be "sweet" in match "[G]sweet the "
|
|
|
|
tail = match.group(2)
|
|
|
|
# The remainder of the line, until line end or next chord. Would be " the " in match "[G]sweet the "
|
|
|
|
remainder = match.group(3)
|
|
|
|
# Line end if found, else None
|
|
|
|
end = match.group(4)
|
|
|
|
# Based on char width calculate width of chord
|
|
|
|
for chord_char in chord:
|
2017-03-17 21:12:29 +00:00
|
|
|
if chord_char not in SLIMCHARS:
|
2017-02-19 21:35:40 +00:00
|
|
|
chordlen += 2
|
|
|
|
else:
|
|
|
|
chordlen += 1
|
|
|
|
# Based on char width calculate width of tail
|
|
|
|
for tail_char in tail:
|
2017-03-17 21:12:29 +00:00
|
|
|
if tail_char not in SLIMCHARS:
|
2017-02-19 21:35:40 +00:00
|
|
|
taillen += 2
|
|
|
|
else:
|
|
|
|
taillen += 1
|
|
|
|
# Based on char width calculate width of remainder
|
|
|
|
for remainder_char in remainder:
|
2017-03-17 21:12:29 +00:00
|
|
|
if remainder_char not in SLIMCHARS:
|
2017-02-19 21:35:40 +00:00
|
|
|
taillen += 2
|
|
|
|
else:
|
|
|
|
taillen += 1
|
|
|
|
# If the chord is wider than the tail+remainder and the line goes on, some padding is needed
|
|
|
|
if chordlen >= taillen and end is None:
|
|
|
|
# Decide if the padding should be "_" for drawing out words or spaces
|
|
|
|
if tail:
|
|
|
|
if not remainder:
|
2017-03-17 21:12:29 +00:00
|
|
|
for c in range(math.ceil((chordlen - taillen) / 2) + 2):
|
2017-02-19 21:35:40 +00:00
|
|
|
whitespaces += '_'
|
|
|
|
else:
|
2017-03-17 21:12:29 +00:00
|
|
|
for c in range(chordlen - taillen + 1):
|
2017-02-19 21:35:40 +00:00
|
|
|
whitespaces += ' '
|
|
|
|
else:
|
|
|
|
if not remainder:
|
|
|
|
for c in range(math.floor((chordlen - taillen) / 2)):
|
|
|
|
whitespaces += '_'
|
|
|
|
else:
|
|
|
|
for c in range(chordlen - taillen + 1):
|
|
|
|
whitespaces += ' '
|
|
|
|
else:
|
|
|
|
if not tail and remainder and remainder[0] == ' ':
|
|
|
|
for c in range(chordlen):
|
|
|
|
whitespaces += ' '
|
|
|
|
if whitespaces:
|
2017-02-20 21:22:17 +00:00
|
|
|
if '_' in whitespaces:
|
|
|
|
ws_length = len(whitespaces)
|
|
|
|
if ws_length == 1:
|
2017-02-23 21:40:22 +00:00
|
|
|
whitespaces = '–'
|
2017-02-20 21:22:17 +00:00
|
|
|
else:
|
|
|
|
wsl_mod = ws_length // 2
|
|
|
|
ws_right = ws_left = ' ' * wsl_mod
|
2017-02-23 21:40:22 +00:00
|
|
|
whitespaces = ws_left + '–' + ws_right
|
2017-02-19 21:35:40 +00:00
|
|
|
whitespaces = '<span class="ws">' + whitespaces + '</span>'
|
2017-05-17 20:06:45 +00:00
|
|
|
return '<span class="chord"><span><strong>' + html.escape(chord) + '</strong></span></span>' + html.escape(tail) + \
|
|
|
|
whitespaces + html.escape(remainder)
|
2017-02-19 21:35:40 +00:00
|
|
|
|
|
|
|
|
|
|
|
def expand_chords(text):
|
|
|
|
"""
|
|
|
|
Expand ChordPro tags
|
|
|
|
|
|
|
|
:param text:
|
|
|
|
"""
|
|
|
|
text_lines = text.split('{br}')
|
|
|
|
expanded_text_lines = []
|
|
|
|
chords_on_prev_line = False
|
|
|
|
for line in text_lines:
|
|
|
|
# If a ChordPro is detected in the line, replace it with a html-span tag and wrap the line in a span tag.
|
|
|
|
if '[' in line and ']' in line:
|
|
|
|
if chords_on_prev_line:
|
|
|
|
new_line = '<span class="chordline">'
|
|
|
|
else:
|
|
|
|
new_line = '<span class="chordline firstchordline">'
|
|
|
|
chords_on_prev_line = True
|
|
|
|
# Matches a chord, a tail, a remainder and a line end. See expand_and_align_chords_in_line() for more info.
|
2017-02-26 21:14:49 +00:00
|
|
|
new_line += re.sub(r'\[(.*?)\]([\u0080-\uFFFF,\w]*)'
|
2017-02-19 21:35:40 +00:00
|
|
|
'([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(\Z)?',
|
|
|
|
expand_and_align_chords_in_line, line)
|
|
|
|
new_line += '</span>'
|
|
|
|
expanded_text_lines.append(new_line)
|
|
|
|
else:
|
|
|
|
chords_on_prev_line = False
|
2017-05-17 20:06:45 +00:00
|
|
|
expanded_text_lines.append(html.escape(line))
|
2017-02-19 21:35:40 +00:00
|
|
|
return '{br}'.join(expanded_text_lines)
|
|
|
|
|
|
|
|
|
2017-01-28 22:04:16 +00:00
|
|
|
def compare_chord_lyric(chord, lyric):
|
2016-07-25 20:07:07 +00:00
|
|
|
"""
|
2017-02-20 21:22:17 +00:00
|
|
|
Compare the width of chord and lyrics. If chord width is greater than the lyric width the diff is returned.
|
2016-07-26 19:02:35 +00:00
|
|
|
|
2017-01-28 22:04:16 +00:00
|
|
|
:param chord:
|
|
|
|
:param lyric:
|
|
|
|
:return:
|
2016-07-25 20:07:07 +00:00
|
|
|
"""
|
|
|
|
chordlen = 0
|
2017-01-28 22:04:16 +00:00
|
|
|
if chord == ' ':
|
|
|
|
return 0
|
2017-02-02 20:32:35 +00:00
|
|
|
chord = re.sub(r'\{.*?\}', r'', chord)
|
|
|
|
lyric = re.sub(r'\{.*?\}', r'', lyric)
|
2016-07-25 20:07:07 +00:00
|
|
|
for chord_char in chord:
|
2017-01-28 22:04:16 +00:00
|
|
|
if chord_char not in SLIMCHARS:
|
2016-07-25 20:07:07 +00:00
|
|
|
chordlen += 2
|
|
|
|
else:
|
|
|
|
chordlen += 1
|
2017-01-28 22:04:16 +00:00
|
|
|
lyriclen = 0
|
|
|
|
for lyric_char in lyric:
|
|
|
|
if lyric_char not in SLIMCHARS:
|
|
|
|
lyriclen += 2
|
2016-07-25 20:07:07 +00:00
|
|
|
else:
|
2017-01-28 22:04:16 +00:00
|
|
|
lyriclen += 1
|
|
|
|
if chordlen > lyriclen:
|
|
|
|
return chordlen - lyriclen
|
2016-07-25 20:07:07 +00:00
|
|
|
else:
|
2017-01-28 22:04:16 +00:00
|
|
|
return 0
|
2016-07-25 20:07:07 +00:00
|
|
|
|
|
|
|
|
2017-02-02 20:32:35 +00:00
|
|
|
def find_formatting_tags(text, active_formatting_tags):
|
|
|
|
"""
|
|
|
|
Look for formatting tags in lyrics and adds/removes them to/from the given list. Returns the update list.
|
|
|
|
|
|
|
|
:param text:
|
|
|
|
:param active_formatting_tags:
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
if not re.search(r'\{.*?\}', text):
|
|
|
|
return active_formatting_tags
|
|
|
|
word_it = iter(text)
|
|
|
|
# Loop through lyrics to find any formatting tags
|
|
|
|
for char in word_it:
|
|
|
|
if char == '{':
|
|
|
|
tag = ''
|
|
|
|
char = next(word_it)
|
|
|
|
start_tag = True
|
|
|
|
if char == '/':
|
|
|
|
start_tag = False
|
2017-04-11 12:44:28 +00:00
|
|
|
char = next(word_it)
|
2017-02-02 20:32:35 +00:00
|
|
|
while char != '}':
|
|
|
|
tag += char
|
|
|
|
char = next(word_it)
|
|
|
|
# See if the found tag has an end tag
|
|
|
|
for formatting_tag in FormattingTags.get_html_tags():
|
|
|
|
if formatting_tag['start tag'] == '{' + tag + '}':
|
|
|
|
if formatting_tag['end tag']:
|
|
|
|
if start_tag:
|
|
|
|
# prepend the new tag to the list of active formatting tags
|
2017-04-11 12:44:28 +00:00
|
|
|
active_formatting_tags[:0] = [tag]
|
2017-02-02 20:32:35 +00:00
|
|
|
else:
|
|
|
|
# remove the tag from the list
|
|
|
|
active_formatting_tags.remove(tag)
|
2017-04-11 12:44:28 +00:00
|
|
|
# Break out of the loop matching the found tag against the tag list.
|
|
|
|
break
|
2017-02-02 20:32:35 +00:00
|
|
|
return active_formatting_tags
|
|
|
|
|
|
|
|
|
2017-02-19 21:35:40 +00:00
|
|
|
def expand_chords_for_printing(text, line_split):
|
2016-01-03 17:01:44 +00:00
|
|
|
"""
|
|
|
|
Expand ChordPro tags
|
|
|
|
|
|
|
|
:param text:
|
2017-02-02 20:32:35 +00:00
|
|
|
:param line_split:
|
2016-01-03 17:01:44 +00:00
|
|
|
"""
|
2017-02-26 21:14:49 +00:00
|
|
|
if not re.search(r'\[.*?\]', text):
|
2017-01-25 21:08:30 +00:00
|
|
|
return text
|
2017-02-02 20:32:35 +00:00
|
|
|
text_lines = text.split(line_split)
|
2016-01-03 17:01:44 +00:00
|
|
|
expanded_text_lines = []
|
|
|
|
for line in text_lines:
|
2017-01-25 21:08:30 +00:00
|
|
|
# If a ChordPro is detected in the line, build html tables.
|
|
|
|
new_line = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td>'
|
2017-02-02 20:32:35 +00:00
|
|
|
active_formatting_tags = []
|
2017-02-26 21:14:49 +00:00
|
|
|
if re.search(r'\[.*?\]', line):
|
2017-01-25 21:08:30 +00:00
|
|
|
words = line.split(' ')
|
2017-02-02 20:32:35 +00:00
|
|
|
in_chord = False
|
2017-01-25 21:08:30 +00:00
|
|
|
for word in words:
|
2017-01-28 22:04:16 +00:00
|
|
|
chords = []
|
2017-01-25 21:08:30 +00:00
|
|
|
lyrics = []
|
|
|
|
new_line += '<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left">'
|
2017-02-02 20:32:35 +00:00
|
|
|
# If the word contains a chord, we need to handle it.
|
2017-02-26 21:14:49 +00:00
|
|
|
if re.search(r'\[.*?\]', word):
|
2017-02-02 20:32:35 +00:00
|
|
|
chord = ''
|
|
|
|
lyric = ''
|
|
|
|
# Loop over each character of the word
|
2017-01-25 21:08:30 +00:00
|
|
|
for char in word:
|
|
|
|
if char == '[':
|
2017-02-02 20:32:35 +00:00
|
|
|
in_chord = True
|
2017-01-25 21:08:30 +00:00
|
|
|
if lyric != '':
|
2017-01-28 22:04:16 +00:00
|
|
|
if chord == '':
|
|
|
|
chord = ' '
|
|
|
|
chords.append(chord)
|
2017-01-25 21:08:30 +00:00
|
|
|
lyrics.append(lyric)
|
2017-01-28 22:04:16 +00:00
|
|
|
chord = ''
|
2017-01-25 21:08:30 +00:00
|
|
|
lyric = ''
|
2017-02-02 20:32:35 +00:00
|
|
|
elif char == ']' and in_chord:
|
|
|
|
in_chord = False
|
|
|
|
elif in_chord:
|
2017-01-28 22:04:16 +00:00
|
|
|
chord += char
|
2017-01-25 21:08:30 +00:00
|
|
|
else:
|
|
|
|
lyric += char
|
2017-01-28 22:04:16 +00:00
|
|
|
if lyric != '' or chord != '':
|
|
|
|
if chord == '':
|
|
|
|
chord = ' '
|
2017-01-25 21:08:30 +00:00
|
|
|
if lyric == '':
|
|
|
|
lyric = ' '
|
2017-01-28 22:04:16 +00:00
|
|
|
chords.append(chord)
|
2017-01-25 21:08:30 +00:00
|
|
|
lyrics.append(lyric)
|
2017-02-02 21:55:05 +00:00
|
|
|
new_chord_line = '<tr class="chordrow">'
|
2017-01-28 22:04:16 +00:00
|
|
|
new_lyric_line = '</tr><tr>'
|
2017-01-26 21:14:19 +00:00
|
|
|
for i in range(len(lyrics)):
|
2017-01-28 22:04:16 +00:00
|
|
|
spacer = compare_chord_lyric(chords[i], lyrics[i])
|
2017-02-02 20:32:35 +00:00
|
|
|
# Handle formatting tags
|
|
|
|
start_formatting_tags = ''
|
|
|
|
if active_formatting_tags:
|
|
|
|
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
|
|
|
|
# Update list of active formatting tags
|
|
|
|
active_formatting_tags = find_formatting_tags(lyrics[i], active_formatting_tags)
|
|
|
|
end_formatting_tags = ''
|
|
|
|
if active_formatting_tags:
|
|
|
|
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
|
|
|
|
new_chord_line += '<td class="chord">%s</td>' % chords[i]
|
2017-04-18 19:20:36 +00:00
|
|
|
# Check if this is the last column, if so skip spacing calc and instead insert a single space
|
2017-01-26 21:14:19 +00:00
|
|
|
if i + 1 == len(lyrics):
|
2017-02-19 21:35:40 +00:00
|
|
|
new_lyric_line += '<td class="lyrics">{starttags}{lyrics} {endtags}</td>'.format(
|
|
|
|
starttags=start_formatting_tags, lyrics=lyrics[i], endtags=end_formatting_tags)
|
2017-01-28 22:04:16 +00:00
|
|
|
else:
|
2017-02-02 20:32:35 +00:00
|
|
|
spacing = ''
|
2017-01-28 22:04:16 +00:00
|
|
|
if spacer > 0:
|
|
|
|
space = ' ' * int(math.ceil(spacer / 2))
|
2017-02-02 20:32:35 +00:00
|
|
|
spacing = '<span class="chordspacing">%s-%s</span>' % (space, space)
|
2017-02-19 21:35:40 +00:00
|
|
|
new_lyric_line += '<td class="lyrics">{starttags}{lyrics}{spacing}{endtags}</td>'.format(
|
|
|
|
starttags=start_formatting_tags, lyrics=lyrics[i], spacing=spacing,
|
|
|
|
endtags=end_formatting_tags)
|
2017-01-28 22:04:16 +00:00
|
|
|
new_line += new_chord_line + new_lyric_line + '</tr>'
|
2017-01-25 21:08:30 +00:00
|
|
|
else:
|
2017-02-02 20:32:35 +00:00
|
|
|
start_formatting_tags = ''
|
|
|
|
if active_formatting_tags:
|
|
|
|
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
|
|
|
|
active_formatting_tags = find_formatting_tags(word, active_formatting_tags)
|
|
|
|
end_formatting_tags = ''
|
|
|
|
if active_formatting_tags:
|
|
|
|
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
|
2017-02-19 21:35:40 +00:00
|
|
|
new_line += '<tr class="chordrow"><td class="chord"> </td></tr><tr><td class="lyrics">' \
|
|
|
|
'{starttags}{lyrics} {endtags}</td></tr>'.format(
|
|
|
|
starttags=start_formatting_tags, lyrics=word, endtags=end_formatting_tags)
|
2017-01-25 21:08:30 +00:00
|
|
|
new_line += '</table>'
|
2016-01-03 17:01:44 +00:00
|
|
|
else:
|
2017-01-25 21:08:30 +00:00
|
|
|
new_line += line
|
|
|
|
new_line += '</td></tr></table>'
|
|
|
|
expanded_text_lines.append(new_line)
|
2017-02-02 20:32:35 +00:00
|
|
|
# the {br} tag used to split lines is not inserted again since the style of the line-tables makes them redundant.
|
2017-01-29 21:25:52 +00:00
|
|
|
return ''.join(expanded_text_lines)
|
2016-01-03 17:01:44 +00:00
|
|
|
|
|
|
|
|
2013-10-13 17:23:52 +00:00
|
|
|
def create_separated_list(string_list):
|
2012-02-16 20:36:35 +00:00
|
|
|
"""
|
2016-10-04 00:03:15 +00:00
|
|
|
Returns a string that represents a join of a list of strings with a localized separator.
|
|
|
|
Localized separation will be done via the translate() function by the translators.
|
2014-01-01 09:33:07 +00:00
|
|
|
|
2016-10-02 18:57:38 +00:00
|
|
|
:param string_list: List of unicode strings
|
|
|
|
:return: Formatted string
|
2012-02-16 20:36:35 +00:00
|
|
|
"""
|
2016-10-02 18:57:38 +00:00
|
|
|
list_length = len(string_list)
|
|
|
|
if list_length == 1:
|
2016-10-17 19:42:07 +00:00
|
|
|
list_to_string = string_list[0]
|
2016-10-02 18:57:38 +00:00
|
|
|
elif list_length == 2:
|
2016-10-17 19:42:07 +00:00
|
|
|
list_to_string = translate('OpenLP.core.lib', '{one} and {two}').format(one=string_list[0], two=string_list[1])
|
2016-10-02 18:57:38 +00:00
|
|
|
elif list_length > 2:
|
2016-11-21 20:56:48 +00:00
|
|
|
list_to_string = translate('OpenLP.core.lib', '{first} and {last}').format(first=', '.join(string_list[:-1]),
|
|
|
|
last=string_list[-1])
|
2012-02-16 20:36:35 +00:00
|
|
|
else:
|
2016-10-17 19:42:07 +00:00
|
|
|
list_to_string = ''
|
|
|
|
return list_to_string
|
2013-08-31 18:17:38 +00:00
|
|
|
|
|
|
|
|
2017-08-04 17:40:57 +00:00
|
|
|
def replace_params(args, kwargs, params):
|
|
|
|
"""
|
|
|
|
Apply a transformation function to the specified args or kwargs
|
|
|
|
|
2017-08-25 20:03:25 +00:00
|
|
|
:param tuple args: Positional arguments
|
|
|
|
:param dict kwargs: Key Word arguments
|
2017-08-04 17:40:57 +00:00
|
|
|
:param params: A tuple of tuples with the position and the key word to replace.
|
|
|
|
:return: The modified positional and keyword arguments
|
2017-08-25 20:03:25 +00:00
|
|
|
:rtype: tuple[tuple, dict]
|
2017-08-04 17:40:57 +00:00
|
|
|
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
Take a method with the following signature, and assume we which to apply the str function to arg2:
|
|
|
|
def method(arg1=None, arg2=None, arg3=None)
|
|
|
|
|
|
|
|
As arg2 can be specified postitionally as the second argument (1 with a zero index) or as a keyword, the we
|
|
|
|
would call this function as follows:
|
|
|
|
|
|
|
|
replace_params(args, kwargs, ((1, 'arg2', str),))
|
|
|
|
"""
|
|
|
|
args = list(args)
|
|
|
|
for position, key_word, transform in params:
|
|
|
|
if len(args) > position:
|
|
|
|
args[position] = transform(args[position])
|
|
|
|
elif key_word in kwargs:
|
|
|
|
kwargs[key_word] = transform(kwargs[key_word])
|
|
|
|
return tuple(args), kwargs
|
|
|
|
|
|
|
|
|
2015-02-13 21:47:06 +00:00
|
|
|
from .exceptions import ValidationError
|
2013-08-31 18:17:38 +00:00
|
|
|
from .screen import ScreenList
|
|
|
|
from .formattingtags import FormattingTags
|
|
|
|
from .plugin import PluginStatus, StringContent, Plugin
|
|
|
|
from .pluginmanager import PluginManager
|
|
|
|
from .settingstab import SettingsTab
|
|
|
|
from .serviceitem import ServiceItem, ServiceItemType, ItemCapabilities
|
2016-07-21 20:37:25 +00:00
|
|
|
from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css, build_chords_css
|
2013-08-31 18:17:38 +00:00
|
|
|
from .imagemanager import ImageManager
|
|
|
|
from .renderer import Renderer
|
|
|
|
from .mediamanageritem import MediaManagerItem
|
2014-10-06 19:10:03 +00:00
|
|
|
from .projector.db import ProjectorDB, Projector
|
2017-08-06 07:23:26 +00:00
|
|
|
from .projector.pjlink import PJLink
|
2014-10-06 19:10:03 +00:00
|
|
|
from .projector.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING
|