openlp/openlp/core/lib/theme.py

524 lines
18 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2019-04-13 13:00:22 +00:00
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
2022-02-01 10:10:57 +00:00
# Copyright (c) 2008-2022 OpenLP Developers #
2019-04-13 13:00:22 +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, 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/>. #
##########################################################################
2010-06-19 17:31:42 +00:00
"""
Provide the theme XML and handling functions for OpenLP v2 themes.
"""
2013-10-13 13:51:13 +00:00
import json
2017-09-26 16:39:13 +00:00
import logging
import copy
from lxml import etree, objectify
2017-10-07 07:05:07 +00:00
from openlp.core.common import de_hump
from openlp.core.common.applocation import AppLocation
from openlp.core.common.json import OpenLPJSONDecoder, OpenLPJSONEncoder
2017-10-10 07:08:44 +00:00
from openlp.core.display.screens import ScreenList
from openlp.core.lib import get_text_file_string, str_to_bool, image_to_data_uri
2018-10-02 04:39:42 +00:00
2010-10-09 10:59:49 +00:00
log = logging.getLogger(__name__)
2013-02-01 19:58:18 +00:00
2010-10-16 13:54:57 +00:00
class BackgroundType(object):
2011-02-02 15:52:17 +00:00
"""
Type enumeration for backgrounds.
"""
2010-10-16 13:54:57 +00:00
Solid = 0
Gradient = 1
Image = 2
2012-01-04 17:19:49 +00:00
Transparent = 3
2016-04-30 15:40:23 +00:00
Video = 4
2019-01-20 21:54:26 +00:00
Stream = 5
2010-10-16 13:54:57 +00:00
@staticmethod
2011-02-02 15:52:17 +00:00
def to_string(background_type):
"""
Return a string representation of a background type.
"""
if background_type == BackgroundType.Solid:
2013-08-31 18:17:38 +00:00
return 'solid'
2011-02-02 15:52:17 +00:00
elif background_type == BackgroundType.Gradient:
2013-08-31 18:17:38 +00:00
return 'gradient'
2011-02-02 15:52:17 +00:00
elif background_type == BackgroundType.Image:
2013-08-31 18:17:38 +00:00
return 'image'
2012-01-04 17:19:49 +00:00
elif background_type == BackgroundType.Transparent:
2013-08-31 18:17:38 +00:00
return 'transparent'
2016-04-30 15:40:23 +00:00
elif background_type == BackgroundType.Video:
return 'video'
2019-01-20 21:54:26 +00:00
elif background_type == BackgroundType.Stream:
return 'stream'
2010-10-16 13:54:57 +00:00
@staticmethod
def from_string(type_string):
2011-02-02 15:52:17 +00:00
"""
Return a background type for the given string.
"""
2013-08-31 18:17:38 +00:00
if type_string == 'solid':
2010-10-16 13:54:57 +00:00
return BackgroundType.Solid
2013-08-31 18:17:38 +00:00
elif type_string == 'gradient':
2010-10-16 13:54:57 +00:00
return BackgroundType.Gradient
2013-08-31 18:17:38 +00:00
elif type_string == 'image':
2010-10-16 13:54:57 +00:00
return BackgroundType.Image
2013-08-31 18:17:38 +00:00
elif type_string == 'transparent':
2012-01-04 17:19:49 +00:00
return BackgroundType.Transparent
2016-04-30 15:40:23 +00:00
elif type_string == 'video':
return BackgroundType.Video
2019-01-20 21:54:26 +00:00
elif type_string == 'stream':
return BackgroundType.Stream
2010-10-16 13:54:57 +00:00
2011-02-25 17:27:06 +00:00
2010-10-16 13:54:57 +00:00
class BackgroundGradientType(object):
2011-02-02 15:52:17 +00:00
"""
Type enumeration for background gradients.
"""
2010-10-16 13:54:57 +00:00
Horizontal = 0
Vertical = 1
Circular = 2
LeftTop = 3
LeftBottom = 4
@staticmethod
2011-02-02 15:52:17 +00:00
def to_string(gradient_type):
"""
Return a string representation of a background gradient type.
"""
if gradient_type == BackgroundGradientType.Horizontal:
2013-08-31 18:17:38 +00:00
return 'horizontal'
2011-02-02 15:52:17 +00:00
elif gradient_type == BackgroundGradientType.Vertical:
2013-08-31 18:17:38 +00:00
return 'vertical'
2011-02-02 15:52:17 +00:00
elif gradient_type == BackgroundGradientType.Circular:
2013-08-31 18:17:38 +00:00
return 'circular'
2011-02-02 15:52:17 +00:00
elif gradient_type == BackgroundGradientType.LeftTop:
2013-08-31 18:17:38 +00:00
return 'leftTop'
2011-02-02 15:52:17 +00:00
elif gradient_type == BackgroundGradientType.LeftBottom:
2013-08-31 18:17:38 +00:00
return 'leftBottom'
2010-10-16 13:54:57 +00:00
@staticmethod
def from_string(type_string):
2011-02-02 15:52:17 +00:00
"""
Return a background gradient type for the given string.
"""
2013-08-31 18:17:38 +00:00
if type_string == 'horizontal':
2010-10-16 13:54:57 +00:00
return BackgroundGradientType.Horizontal
2013-08-31 18:17:38 +00:00
elif type_string == 'vertical':
2010-10-16 13:54:57 +00:00
return BackgroundGradientType.Vertical
2013-08-31 18:17:38 +00:00
elif type_string == 'circular':
2010-10-16 13:54:57 +00:00
return BackgroundGradientType.Circular
2013-08-31 18:17:38 +00:00
elif type_string == 'leftTop':
2010-10-16 13:54:57 +00:00
return BackgroundGradientType.LeftTop
2013-08-31 18:17:38 +00:00
elif type_string == 'leftBottom':
2010-10-16 13:54:57 +00:00
return BackgroundGradientType.LeftBottom
class TransitionType(object):
"""
Type enumeration for transition types.
"""
Fade = 0
Slide = 1
Convex = 2
Concave = 3
Zoom = 4
@staticmethod
def to_string(transition_type):
"""
Return a string representation of a transition type.
"""
if transition_type == TransitionType.Fade:
return 'fade'
elif transition_type == TransitionType.Slide:
return 'slide'
elif transition_type == TransitionType.Convex:
return 'convex'
elif transition_type == TransitionType.Concave:
return 'concave'
elif transition_type == TransitionType.Zoom:
return 'zoom'
@staticmethod
def from_string(type_string):
"""
Return a transition type for the given string.
"""
if type_string == 'fade':
return TransitionType.Fade
elif type_string == 'slide':
return TransitionType.Slide
elif type_string == 'convex':
return TransitionType.Convex
elif type_string == 'concave':
return TransitionType.Concave
elif type_string == 'zoom':
return TransitionType.Zoom
class TransitionSpeed(object):
"""
Type enumeration for transition types.
"""
Normal = 0
Fast = 1
Slow = 2
@staticmethod
def to_string(transition_speed):
"""
Return a string representation of a transition type.
"""
if transition_speed == TransitionSpeed.Normal:
return 'normal'
elif transition_speed == TransitionSpeed.Fast:
return 'fast'
elif transition_speed == TransitionSpeed.Slow:
return 'slow'
@staticmethod
def from_string(type_string):
"""
Return a transition type for the given string.
"""
if type_string == 'normal':
return TransitionSpeed.Normal
if type_string == 'fast':
return TransitionSpeed.Fast
elif type_string == 'slow':
return TransitionSpeed.Slow
2019-12-14 11:44:42 +00:00
class TransitionDirection(object):
"""
Type enumeration for transition types.
"""
Horizontal = 0
Vertical = 1
@staticmethod
def to_string(transition_direction):
"""
Return a string representation of a transition type.
"""
if transition_direction == TransitionDirection.Horizontal:
return 'horizontal'
elif transition_direction == TransitionDirection.Vertical:
return 'vertical'
@staticmethod
def from_string(type_string):
"""
Return a transition type for the given string.
"""
if type_string == 'horizontal':
return TransitionDirection.Horizontal
if type_string == 'vertical':
return TransitionDirection.Vertical
2010-10-17 18:58:42 +00:00
class HorizontalType(object):
2011-02-02 15:52:17 +00:00
"""
Type enumeration for horizontal alignment.
"""
2010-10-17 18:58:42 +00:00
Left = 0
Right = 1
2011-02-18 03:15:09 +00:00
Center = 2
Justify = 3
2010-10-17 18:58:42 +00:00
2013-08-31 18:17:38 +00:00
Names = ['left', 'right', 'center', 'justify']
@staticmethod
def to_string(align):
"""
Return a string representation of the alignment
"""
return HorizontalType.Names[align]
@staticmethod
def from_string(align):
"""
Return an alignment for a given string
"""
return HorizontalType.Names.index(align)
2010-10-17 18:58:42 +00:00
class VerticalType(object):
2011-02-02 15:52:17 +00:00
"""
Type enumeration for vertical alignment.
"""
2010-10-17 18:58:42 +00:00
Top = 0
Middle = 1
Bottom = 2
2013-08-31 18:17:38 +00:00
Names = ['top', 'middle', 'bottom']
@staticmethod
def to_string(align):
"""
Return a string representation of the alignment
"""
return VerticalType.Names[align]
@staticmethod
def from_string(align):
"""
Return an alignment for a given string
"""
return VerticalType.Names.index(align)
2019-12-14 11:44:42 +00:00
BOOLEAN_LIST = ['bold', 'italics', 'override', 'outline', 'shadow', 'slide_transition', 'slide_transition_reverse']
2010-09-11 06:59:36 +00:00
2013-08-31 18:17:38 +00:00
INTEGER_LIST = ['size', 'line_adjustment', 'x', 'height', 'y', 'width', 'shadow_size', 'outline_size',
2019-12-14 11:44:42 +00:00
'horizontal_align', 'vertical_align', 'wrap_style', 'slide_transition_type', 'slide_transition_speed',
'slide_transition_direction']
2010-09-11 06:59:36 +00:00
2011-02-25 17:27:06 +00:00
2017-05-13 07:47:22 +00:00
class Theme(object):
"""
A class to encapsulate the Theme XML.
"""
def __init__(self):
"""
Initialise the theme object.
"""
2013-10-13 15:52:04 +00:00
# basic theme object with defaults
json_path = AppLocation.get_directory(AppLocation.AppDir) / 'core' / 'lib' / 'json' / 'theme.json'
jsn = get_text_file_string(json_path)
2017-09-26 16:39:13 +00:00
self.load_theme(jsn)
self.set_default_header_footer()
2017-09-26 16:39:13 +00:00
self.background_filename = None
self.background_source = None
self.version = 2
2013-10-18 18:10:47 +00:00
def expand_json(self, var, prev=None):
"""
Expand the json objects and make into variables.
2014-03-17 19:05:55 +00:00
:param var: The array list to be processed.
:param prev: The preceding string to add to the key to make the variable.
2013-10-18 18:10:47 +00:00
"""
for key, value in var.items():
if prev:
key = prev + "_" + key
if isinstance(value, dict):
self.expand_json(value, key)
else:
setattr(self, key, value)
def extend_image_filename(self, path):
"""
Add the path name to the image name so the background can be rendered.
:param pathlib.Path path: The path name to be added.
2017-09-26 16:39:13 +00:00
:rtype: None
"""
2016-04-30 15:40:23 +00:00
if self.background_type == 'image' or self.background_type == 'video':
2010-10-11 16:43:14 +00:00
if self.background_filename and path:
self.theme_name = self.theme_name.strip()
2017-09-26 16:39:13 +00:00
self.background_filename = path / self.theme_name / self.background_filename
def set_default_header_footer(self):
2012-05-16 13:14:32 +00:00
"""
Set the default header and footer size to match the current primary screen.
Obeys theme override variables.
2012-05-16 13:14:32 +00:00
"""
if not self.font_main_override:
self.set_default_header()
if not self.font_footer_override:
self.set_default_footer()
2019-10-26 10:00:07 +00:00
def set_default_header(self):
"""
Sets the default header position and size, ignores font_main_override
"""
current_screen_geometry = ScreenList().current.display_geometry
2019-10-26 10:00:07 +00:00
self.font_main_x = 10
self.font_main_y = 0
self.font_main_width = current_screen_geometry.width() - 20
self.font_main_height = current_screen_geometry.height() * 9 / 10
2019-10-26 10:00:07 +00:00
def set_default_footer(self):
"""
Sets the default footer position and size, ignores font_footer_override
"""
2019-10-26 10:00:07 +00:00
current_screen_geometry = ScreenList().current.display_geometry
self.font_footer_x = 10
self.font_footer_y = current_screen_geometry.height() * 9 / 10
2019-10-26 10:00:07 +00:00
self.font_footer_width = current_screen_geometry.width() - 20
self.font_footer_height = current_screen_geometry.height() / 10
2012-05-16 13:14:32 +00:00
2017-09-26 16:39:13 +00:00
def load_theme(self, theme, theme_path=None):
"""
2017-05-24 19:31:48 +00:00
Convert the JSON file and expand it.
2017-05-24 19:55:30 +00:00
2017-05-13 07:47:22 +00:00
:param theme: the theme string
:param pathlib.Path theme_path: The path to the theme
2017-09-26 16:39:13 +00:00
:rtype: None
"""
2017-09-26 16:39:13 +00:00
if theme_path:
jsn = json.loads(theme, cls=OpenLPJSONDecoder, base_path=theme_path)
2017-09-26 16:39:13 +00:00
else:
jsn = json.loads(theme, cls=OpenLPJSONDecoder)
2017-05-13 07:47:22 +00:00
self.expand_json(jsn)
2019-06-21 22:09:36 +00:00
def export_theme(self, theme_path=None, is_js=False):
2017-05-24 19:31:48 +00:00
"""
Loop through the fields and build a dictionary of them
2019-06-21 20:53:42 +00:00
:param pathlib.Path | None theme_path:
2019-06-21 22:09:36 +00:00
:param bool is_js: For internal use, for example with the theme js code.
2019-06-21 20:53:42 +00:00
:return str: The json encoded theme object
2017-05-24 19:31:48 +00:00
"""
theme_data = {}
for attr, value in self.__dict__.items():
theme_data["{attr}".format(attr=attr)] = value
2019-06-21 22:09:36 +00:00
return json.dumps(theme_data, cls=OpenLPJSONEncoder, base_path=theme_path, is_js=is_js)
2017-05-24 19:31:48 +00:00
def export_theme_self_contained(self, is_js=True):
"""
Get a self contained theme dictionary
Same as export theme, but images is turned into a data uri
:param is_js: For internal use, for example with the theme js code.
:return str: The json encoded theme object
"""
theme_copy = copy.deepcopy(self)
if self.background_type == 'image':
image = image_to_data_uri(self.background_filename)
theme_copy.background_filename = image
current_screen_geometry = ScreenList().current.display_geometry
theme_copy.display_size_width = current_screen_geometry.width()
theme_copy.display_size_height = current_screen_geometry.height()
theme_copy.background_source = ''
exported_theme = theme_copy.export_theme(is_js=is_js)
return exported_theme
def parse(self, xml):
"""
Read in an XML string and parse it.
2014-03-17 19:05:55 +00:00
:param xml: The XML string to parse.
"""
2013-08-31 18:17:38 +00:00
self.parse_xml(str(xml))
def parse_xml(self, xml):
"""
Parse an XML string.
2014-03-17 19:05:55 +00:00
:param xml: The XML string to parse.
"""
# remove encoding string
2013-08-31 18:17:38 +00:00
line = xml.find('?>')
2010-10-09 10:59:49 +00:00
if line:
xml = xml[line + 2:]
try:
2010-11-24 01:51:08 +00:00
theme_xml = objectify.fromstring(xml)
except etree.XMLSyntaxError:
log.exception('Invalid xml {text}'.format(text=xml))
2010-10-09 10:59:49 +00:00
return
2010-05-29 19:50:50 +00:00
xml_iter = theme_xml.getiterator()
for element in xml_iter:
2013-08-31 18:17:38 +00:00
master = ''
if element.tag == 'background':
2012-01-04 17:19:49 +00:00
if element.attrib:
for attr in element.attrib:
2012-12-28 22:06:43 +00:00
self._create_attr(element.tag, attr, element.attrib[attr])
2012-01-04 17:19:49 +00:00
parent = element.getparent()
if parent is not None:
2013-08-31 18:17:38 +00:00
if parent.tag == 'font':
master = parent.tag + '_' + parent.attrib['type']
2010-10-11 16:14:36 +00:00
# set up Outline and Shadow Tags and move to font_main
2013-08-31 18:17:38 +00:00
if parent.tag == 'display':
if element.tag.startswith('shadow') or element.tag.startswith('outline'):
self._create_attr('font_main', element.tag, element.text)
2012-01-04 17:19:49 +00:00
master = parent.tag
2013-08-31 18:17:38 +00:00
if parent.tag == 'background':
2012-01-04 17:19:49 +00:00
master = parent.tag
if master:
2010-10-16 07:21:24 +00:00
self._create_attr(master, element.tag, element.text)
if element.attrib:
for attr in element.attrib:
2010-10-07 05:15:02 +00:00
base_element = attr
# correction for the shadow and outline tags
2013-08-31 18:17:38 +00:00
if element.tag == 'shadow' or element.tag == 'outline':
2010-10-07 05:15:02 +00:00
if not attr.startswith(element.tag):
2013-08-31 18:17:38 +00:00
base_element = element.tag + '_' + attr
2012-12-28 22:06:43 +00:00
self._create_attr(master, base_element, element.attrib[attr])
else:
2013-08-31 18:17:38 +00:00
if element.tag == 'name':
self._create_attr('theme', element.tag, element.text)
2010-09-11 06:59:36 +00:00
2017-05-13 07:47:22 +00:00
@staticmethod
def _translate_tags(master, element, value):
2010-10-11 16:14:36 +00:00
"""
Clean up XML removing and redefining tags
"""
master = master.strip().lstrip()
element = element.strip().lstrip()
2013-08-31 18:17:38 +00:00
value = str(value).strip().lstrip()
if master == 'display':
if element == 'wrapStyle':
2010-10-11 16:14:36 +00:00
return True, None, None, None
2013-08-31 18:17:38 +00:00
if element.startswith('shadow') or element.startswith('outline'):
master = 'font_main'
2010-10-11 16:14:36 +00:00
# fix bold font
ret_value = None
2013-08-31 18:17:38 +00:00
if element == 'weight':
element = 'bold'
if value == 'Normal':
2016-07-01 21:17:20 +00:00
ret_value = False
2010-10-11 16:14:36 +00:00
else:
2016-07-01 21:17:20 +00:00
ret_value = True
2013-08-31 18:17:38 +00:00
if element == 'proportion':
element = 'size'
return False, master, element, ret_value if ret_value is not None else value
2010-10-11 16:14:36 +00:00
2012-04-03 17:58:42 +00:00
def _create_attr(self, master, element, value):
2010-09-11 11:11:19 +00:00
"""
Create the attributes with the correct data types and name format
"""
2012-12-28 22:06:43 +00:00
reject, master, element, value = self._translate_tags(master, element, value)
2010-10-11 16:14:36 +00:00
if reject:
return
2013-12-13 19:44:17 +00:00
field = de_hump(element)
2013-08-31 18:17:38 +00:00
tag = master + '_' + field
2011-02-02 15:52:17 +00:00
if field in BOOLEAN_LIST:
setattr(self, tag, str_to_bool(value))
2011-02-02 15:52:17 +00:00
elif field in INTEGER_LIST:
setattr(self, tag, int(value))
2010-09-11 06:59:36 +00:00
else:
# make string value unicode
2013-08-31 18:17:38 +00:00
if not isinstance(value, str):
value = str(value, 'utf-8')
2010-10-29 06:44:38 +00:00
# None means an empty string so lets have one.
2013-08-31 18:17:38 +00:00
if value == 'None':
value = ''
setattr(self, tag, str(value).strip().lstrip())
2011-01-08 13:57:03 +00:00
def __str__(self):
"""
Return a string representation of this object.
"""
theme_strings = []
for key in dir(self):
2013-08-31 18:17:38 +00:00
if key[0:1] != '_':
theme_strings.append('{key:>30}: {value}'.format(key=key, value=getattr(self, key)))
2013-08-31 18:17:38 +00:00
return '\n'.join(theme_strings)