openlp/openlp/core/lib/renderer.py

512 lines
21 KiB
Python
Raw Normal View History

2010-03-21 23:58:01 +00:00
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
2010-12-26 11:04:47 +00:00
# Copyright (c) 2008-2011 Raoul Snyman #
2011-05-26 16:25:54 +00:00
# Portions copyright (c) 2008-2011 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Michael Gorven, Scott Guerrieri, Matthias Hub, Meinert Jordan, #
2011-05-26 17:11:22 +00:00
# Armin Köhler, Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias #
2011-06-12 16:02:52 +00:00
# Põldaru, Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
2011-06-12 15:41:01 +00:00
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Frode Woldsund #
# --------------------------------------------------------------------------- #
# 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 #
###############################################################################
2011-03-28 18:12:46 +00:00
import logging
from PyQt4 import QtCore, QtWebKit
2011-05-05 11:51:47 +00:00
from openlp.core.lib import ServiceItem, expand_tags, \
2011-03-28 18:12:46 +00:00
build_lyrics_format_css, build_lyrics_outline_css, Receiver, \
ItemCapabilities
from openlp.core.lib.theme import ThemeLevel
from openlp.core.ui import MainDisplay, ScreenList
2011-03-28 18:12:46 +00:00
log = logging.getLogger(__name__)
VERSE = u'The Lord said to {r}Noah{/r}: \n' \
'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n' \
'The Lord said to {g}Noah{/g}:\n' \
'There\'s gonna be a {st}floody{/st}, {it}floody{/it}\n' \
'Get those children out of the muddy, muddy \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'
FOOTER = [u'Arky Arky (Unknown)', u'Public Domain', u'CCLI 123456']
class Renderer(object):
"""
Class to pull all Renderer interactions into one place. The plugins will
call helper methods to do the rendering but this class will provide
display defense code.
"""
2011-03-28 18:56:39 +00:00
log.info(u'Renderer Loaded')
2011-03-28 18:12:46 +00:00
def __init__(self, imageManager, themeManager):
2011-03-28 18:12:46 +00:00
"""
Initialise the render manager.
2011-05-07 20:55:11 +00:00
``imageManager``
2011-05-07 20:55:11 +00:00
A ImageManager instance which takes care of e. g. caching and resizing
images.
``themeManager``
2011-05-07 20:55:11 +00:00
The ThemeManager instance, used to get the current theme details.
2011-03-28 18:12:46 +00:00
"""
2011-06-01 07:38:19 +00:00
log.debug(u'Initialisation started')
self.themeManager = themeManager
self.imageManager = imageManager
2011-05-05 11:51:47 +00:00
self.screens = ScreenList.get_instance()
2011-03-28 18:12:46 +00:00
self.service_theme = u''
self.theme_level = u''
self.override_background = None
self.theme_data = None
2011-05-05 11:51:47 +00:00
self.bg_frame = None
2011-03-28 18:12:46 +00:00
self.force_page = False
self.display = MainDisplay(None, self.imageManager, False)
2011-05-05 11:51:47 +00:00
self.display.setup()
2011-03-28 18:12:46 +00:00
def update_display(self):
"""
Updates the render manager's information about the current screen.
"""
log.debug(u'Update Display')
self._calculate_default(self.screens.current[u'size'])
if self.display:
self.display.close()
self.display = MainDisplay(None, self.imageManager, False)
2011-03-28 18:12:46 +00:00
self.display.setup()
self.bg_frame = None
self.theme_data = None
def set_global_theme(self, global_theme, theme_level=ThemeLevel.Global):
"""
Set the global-level theme and the theme level.
``global_theme``
The global-level theme to be set.
``theme_level``
2011-06-13 09:47:18 +00:00
Defaults to ``ThemeLevel.Global``. The theme level, can be
2011-03-28 18:12:46 +00:00
``ThemeLevel.Global``, ``ThemeLevel.Service`` or
``ThemeLevel.Song``.
"""
self.global_theme = global_theme
self.theme_level = theme_level
self.global_theme_data = \
self.themeManager.getThemeData(self.global_theme)
2011-03-28 18:12:46 +00:00
self.theme_data = None
def set_service_theme(self, service_theme):
"""
Set the service-level theme.
``service_theme``
The service-level theme to be set.
"""
self.service_theme = service_theme
self.theme_data = None
2011-04-03 06:19:03 +00:00
def set_override_theme(self, override_theme, override_levels=False):
2011-03-28 18:12:46 +00:00
"""
Set the appropriate theme depending on the theme level.
Called by the service item when building a display frame
``theme``
The name of the song-level theme. None means the service
item wants to use the given value.
2011-04-01 04:48:09 +00:00
``override_levels``
2011-03-28 18:12:46 +00:00
Used to force the theme data passed in to be used.
"""
2011-04-03 06:19:03 +00:00
log.debug(u'set override theme to %s', override_theme)
2011-03-28 18:12:46 +00:00
theme_level = self.theme_level
2011-04-01 04:48:09 +00:00
if override_levels:
2011-03-28 18:12:46 +00:00
theme_level = ThemeLevel.Song
if theme_level == ThemeLevel.Global:
2011-04-03 06:19:03 +00:00
theme = self.global_theme
2011-03-28 18:12:46 +00:00
elif theme_level == ThemeLevel.Service:
if self.service_theme == u'':
2011-04-03 06:19:03 +00:00
theme = self.global_theme
2011-03-28 18:12:46 +00:00
else:
2011-04-03 06:19:03 +00:00
theme = self.service_theme
2011-03-28 18:12:46 +00:00
else:
# Images have a theme of -1
2011-04-03 06:19:03 +00:00
if override_theme and override_theme != -1:
theme = override_theme
2011-03-28 18:12:46 +00:00
elif theme_level == ThemeLevel.Song or \
theme_level == ThemeLevel.Service:
if self.service_theme == u'':
2011-04-03 06:19:03 +00:00
theme = self.global_theme
2011-03-28 18:12:46 +00:00
else:
2011-04-03 06:19:03 +00:00
theme = self.service_theme
2011-03-28 18:12:46 +00:00
else:
2011-04-03 06:19:03 +00:00
theme = self.global_theme
log.debug(u'theme is now %s', theme)
2011-03-28 18:12:46 +00:00
# Force the theme to be the one passed in.
2011-04-01 04:48:09 +00:00
if override_levels:
2011-04-03 06:19:03 +00:00
self.theme_data = override_theme
2011-03-28 18:12:46 +00:00
else:
self.theme_data = self.themeManager.getThemeData(theme)
2011-03-28 18:12:46 +00:00
self._calculate_default(self.screens.current[u'size'])
self._build_text_rectangle(self.theme_data)
# if No file do not update cache
if self.theme_data.background_filename:
self.imageManager.add_image(self.theme_data.theme_name,
self.theme_data.background_filename)
2011-03-28 18:12:46 +00:00
return self._rect, self._rect_footer
def generate_preview(self, theme_data, force_page=False):
"""
Generate a preview of a theme.
``theme_data``
The theme to generated a preview for.
``force_page``
Flag to tell message lines per page need to be generated.
"""
log.debug(u'generate preview')
# save value for use in format_slide
self.force_page = force_page
# set the default image size for previews
2011-03-29 16:25:48 +00:00
self._calculate_default(self.screens.preview[u'size'])
2011-03-28 18:12:46 +00:00
# build a service item to generate preview
serviceItem = ServiceItem()
serviceItem.theme = theme_data
if self.force_page:
# make big page for theme edit dialog to get line count
2011-05-11 22:32:25 +00:00
serviceItem.add_from_text(u'', VERSE + VERSE + VERSE)
2011-03-28 18:12:46 +00:00
else:
self.imageManager.del_image(theme_data.theme_name)
2011-05-11 22:32:25 +00:00
serviceItem.add_from_text(u'', VERSE)
2011-03-29 16:25:48 +00:00
serviceItem.renderer = self
2011-03-28 18:12:46 +00:00
serviceItem.raw_footer = FOOTER
serviceItem.render(True)
if not self.force_page:
self.display.buildHtml(serviceItem)
raw_html = serviceItem.get_rendered_frame(0)
preview = self.display.text(raw_html)
# Reset the real screen size for subsequent render requests
self._calculate_default(self.screens.current[u'size'])
return preview
2011-06-13 09:47:18 +00:00
self.force_page = False
2011-03-28 18:12:46 +00:00
2011-06-13 08:16:50 +00:00
def format_slide(self, text, item):
2011-03-28 18:12:46 +00:00
"""
Calculate how much text can fit on a slide.
2011-04-03 06:19:03 +00:00
``text``
2011-03-28 18:12:46 +00:00
The words to go on the slides.
2011-06-13 08:16:50 +00:00
``item``
2011-06-13 09:47:18 +00:00
The :class:`~openlp.core.lib.serviceitem.ServiceItem` item object.
2011-03-28 18:12:46 +00:00
"""
log.debug(u'format slide')
2011-06-13 08:16:50 +00:00
# Add line endings after each line of text used for bibles.
line_end = u'<br>'
2011-06-13 08:16:50 +00:00
if item.is_capable(ItemCapabilities.NoLineBreaks):
line_end = u' '
# Bibles
if item.is_capable(ItemCapabilities.AllowsWordSplit):
pages = self._paginate_slide_words(text, line_end)
else:
# Clean up line endings.
lines = self._lines_split(text)
pages = self._paginate_slide(lines, line_end)
if len(pages) > 1:
# Songs and Custom
if item.is_capable(ItemCapabilities.AllowsVirtualSplit):
# Do not forget the line breaks!
slides = text.split(u'[---]')
pages = []
for slide in slides:
lines = slide.strip(u'\n').split(u'\n')
pages.extend(self._paginate_slide(lines, line_end))
2011-06-13 09:05:22 +00:00
new_pages = []
for page in pages:
while page.endswith(u'<br>'):
page = page[:-4]
2011-06-13 09:05:22 +00:00
new_pages.append(page)
return new_pages
2011-03-28 18:12:46 +00:00
def _calculate_default(self, screen):
"""
Calculate the default dimentions of the screen.
``screen``
2011-06-13 08:16:50 +00:00
The screen to calculate the default of.
2011-03-28 18:12:46 +00:00
"""
2011-06-06 17:35:47 +00:00
log.debug(u'_calculate default %s', screen)
2011-03-28 18:12:46 +00:00
self.width = screen.width()
self.height = screen.height()
self.screen_ratio = float(self.height) / float(self.width)
log.debug(u'calculate default %d, %d, %f',
self.width, self.height, self.screen_ratio)
# 90% is start of footer
self.footer_start = int(self.height * 0.90)
def _build_text_rectangle(self, theme):
"""
Builds a text block using the settings in ``theme``
and the size of the display screen.height.
2011-04-25 06:44:04 +00:00
Note the system has a 10 pixel border round the screen
2011-03-28 18:12:46 +00:00
``theme``
The theme to build a text block for.
"""
log.debug(u'_build_text_rectangle')
main_rect = None
footer_rect = None
if not theme.font_main_override:
main_rect = QtCore.QRect(10, 0, self.width - 20, self.footer_start)
else:
main_rect = QtCore.QRect(theme.font_main_x, theme.font_main_y,
theme.font_main_width - 1, theme.font_main_height - 1)
if not theme.font_footer_override:
footer_rect = QtCore.QRect(10, self.footer_start, self.width - 20,
self.height - self.footer_start)
else:
footer_rect = QtCore.QRect(theme.font_footer_x,
theme.font_footer_y, theme.font_footer_width - 1,
theme.font_footer_height - 1)
self._set_text_rectangle(main_rect, footer_rect)
def _set_text_rectangle(self, rect_main, rect_footer):
"""
Sets the rectangle within which text should be rendered.
``rect_main``
The main text block.
``rect_footer``
The footer text block.
"""
2011-06-06 17:35:47 +00:00
log.debug(u'_set_text_rectangle %s , %s' % (rect_main, rect_footer))
2011-03-28 18:12:46 +00:00
self._rect = rect_main
self._rect_footer = rect_footer
self.page_width = self._rect.width()
self.page_height = self._rect.height()
if self.theme_data.font_main_shadow:
self.page_width -= int(self.theme_data.font_main_shadow_size)
self.page_height -= int(self.theme_data.font_main_shadow_size)
self.web = QtWebKit.QWebView()
self.web.setVisible(False)
self.web.resize(self.page_width, self.page_height)
self.web_frame = self.web.page().mainFrame()
# Adjust width and height to account for shadow. outline done in css
2011-07-30 06:33:51 +00:00
html = u"""<!DOCTYPE html><html><head><script>
function show_text(newtext) {
var main = document.getElementById('main');
main.innerHTML = newtext;
// We have to return something, otherwise the renderer does not
// work as expected.
return document.all.main.offsetHeight;
}
</script><style>*{margin:0; padding:0; border:0;}
#main {position:absolute; top:0px; %s %s}</style></head><body>
<div id="main"></div></body></html>""" % \
2011-03-28 18:12:46 +00:00
(build_lyrics_format_css(self.theme_data, self.page_width,
self.page_height), build_lyrics_outline_css(self.theme_data))
2011-07-30 06:33:51 +00:00
self.web.setHtml(html)
2011-03-28 18:12:46 +00:00
2011-06-13 08:16:50 +00:00
def _paginate_slide(self, lines, line_end):
2011-03-28 18:12:46 +00:00
"""
Figure out how much text can appear on a slide, using the current
theme settings.
``lines``
2011-06-13 08:16:50 +00:00
The text to be fitted on the slide split into lines.
2011-03-28 18:12:46 +00:00
2011-06-13 08:16:50 +00:00
``line_end``
The text added after each line. Either ``u' '`` or ``u'<br>``.
2011-03-28 18:12:46 +00:00
"""
2011-04-26 17:03:19 +00:00
log.debug(u'_paginate_slide - Start')
2011-03-28 18:12:46 +00:00
formatted = []
2011-06-12 19:27:19 +00:00
previous_html = u''
previous_raw = u''
separator = u'<br>'
2011-06-12 19:27:19 +00:00
html_lines = map(expand_tags, lines)
2011-06-12 18:38:04 +00:00
# Text too long so go to next page.
2011-07-30 06:33:51 +00:00
if self._text_fits_on_slide(separator.join(html_lines)):
2011-06-13 08:16:50 +00:00
html_text, previous_raw = self._binary_chop(formatted,
previous_html, previous_raw, html_lines, lines, separator, u'')
2011-06-12 18:38:04 +00:00
else:
previous_raw = separator.join(lines)
2011-06-12 19:27:19 +00:00
if previous_raw:
formatted.append(previous_raw)
2011-04-26 17:03:19 +00:00
log.debug(u'_paginate_slide - End')
2011-03-28 18:12:46 +00:00
return formatted
2011-06-13 08:16:50 +00:00
def _paginate_slide_words(self, text, line_end):
2011-04-01 04:48:09 +00:00
"""
Figure out how much text can appear on a slide, using the current
2011-04-03 06:19:03 +00:00
theme settings. This version is to handle text which needs to be split
into words to get it to fit.
2011-04-01 04:48:09 +00:00
2011-04-03 06:19:03 +00:00
``text``
2011-04-01 04:48:09 +00:00
The words to be fitted on the slide split into lines.
2011-04-03 06:19:03 +00:00
2011-06-13 08:16:50 +00:00
``line_end``
The text added after each line. Either ``u' '`` or ``u'<br>``.
2011-06-29 07:38:04 +00:00
This is needed for bibles.
2011-04-01 04:48:09 +00:00
"""
2011-04-26 17:03:19 +00:00
log.debug(u'_paginate_slide_words - Start')
2011-04-01 04:48:09 +00:00
formatted = []
2011-04-26 17:03:19 +00:00
previous_html = u''
previous_raw = u''
2011-04-29 14:31:17 +00:00
lines = text.split(u'\n')
2011-04-01 04:48:09 +00:00
for line in lines:
2011-05-23 17:28:52 +00:00
line = line.strip()
2011-06-13 09:05:22 +00:00
html_line = expand_tags(line)
2011-05-23 18:41:49 +00:00
# Text too long so go to next page.
2011-07-30 06:33:51 +00:00
if self._text_fits_on_slide(previous_html + html_line):
# Check if there was a verse before the current one and append
# it, when it fits on the page.
if previous_html:
2011-07-30 06:33:51 +00:00
if not self._text_fits_on_slide(previous_html):
formatted.append(previous_raw)
previous_html = u''
previous_raw = u''
# Now check if the current verse will fit, if it does
# not we have to start to process the verse word by
# word.
2011-07-30 06:33:51 +00:00
if not self._text_fits_on_slide(html_line):
2011-06-13 09:05:22 +00:00
previous_html = html_line + line_end
previous_raw = line + line_end
continue
2011-06-13 09:05:22 +00:00
# Figure out how many words of the line will fit on screen as
# the line will not fit as a whole.
2011-05-23 17:28:52 +00:00
raw_words = self._words_split(line)
html_words = map(expand_tags, raw_words)
2011-06-13 08:16:50 +00:00
previous_html, previous_raw = self._binary_chop(
2011-06-12 19:27:19 +00:00
formatted, previous_html, previous_raw, html_words,
raw_words, u' ', line_end)
2011-04-09 08:11:46 +00:00
else:
2011-06-13 09:05:22 +00:00
previous_html += html_line + line_end
2011-04-26 17:03:19 +00:00
previous_raw += line + line_end
formatted.append(previous_raw)
log.debug(u'_paginate_slide_words - End')
2011-04-01 04:48:09 +00:00
return formatted
2011-06-12 19:27:19 +00:00
def _binary_chop(self, formatted, previous_html, previous_raw, html_list,
raw_list, separator, line_end):
2011-06-12 18:38:04 +00:00
"""
2011-06-13 12:59:52 +00:00
This implements the binary chop algorithm for faster rendering. This
algorithm works line based (line by line) and word based (word by word).
2011-06-13 14:36:18 +00:00
It is assumed that this method is **only** called, when the lines/words
2011-06-23 14:34:43 +00:00
to be rendered do **not** fit as a whole.
2011-06-12 18:38:04 +00:00
``formatted``
2011-06-13 12:59:52 +00:00
The list to append any slides.
2011-06-12 18:38:04 +00:00
``previous_html``
The html text which is know to fit on a slide, but is not yet added
2011-06-12 19:27:19 +00:00
to the list of slides. (unicode string)
2011-06-12 18:38:04 +00:00
``previous_raw``
The raw text (with formatting tags) which is know to fit on a slide,
2011-06-12 19:27:19 +00:00
but is not yet added to the list of slides. (unicode string)
``html_list``
2011-06-13 12:59:52 +00:00
The elements which do not fit on a slide and needs to be processed
2011-06-13 09:47:18 +00:00
using the binary chop. The text contains html.
2011-06-12 18:38:04 +00:00
2011-06-12 19:27:19 +00:00
``raw_list``
2011-06-13 12:59:52 +00:00
The elements which do not fit on a slide and needs to be processed
using the binary chop. The elements can contain formatting tags.
2011-06-12 18:38:04 +00:00
``separator``
The separator for the elements. For lines this is ``u'<br>'`` and
2011-06-23 14:34:43 +00:00
for words this is ``u' '``.
2011-06-12 18:38:04 +00:00
``line_end``
The text added after each "element line". Either ``u' '`` or
``u'<br>``. This is needed for bibles.
2011-06-12 18:38:04 +00:00
"""
smallest_index = 0
2011-06-12 19:27:19 +00:00
highest_index = len(html_list) - 1
2011-06-12 18:38:04 +00:00
index = int(highest_index / 2)
while True:
2011-07-30 06:33:51 +00:00
if self._text_fits_on_slide(
previous_html + separator.join(html_list[:index + 1]).strip()):
2011-06-12 18:38:04 +00:00
# We know that it does not fit, so change/calculate the
# new index and highest_index accordingly.
highest_index = index
index = int(index - (index - smallest_index) / 2)
else:
smallest_index = index
index = int(index + (highest_index - index) / 2)
# We found the number of words which will fit.
if smallest_index == index or highest_index == index:
index = smallest_index
formatted.append(previous_raw.rstrip(u'<br>') +
separator.join(raw_list[:index + 1]))
2011-06-12 18:38:04 +00:00
previous_html = u''
previous_raw = u''
2011-06-13 08:16:50 +00:00
# Stop here as the theme line count was requested.
if self.force_page:
Receiver.send_message(u'theme_line_count', index + 1)
break
2011-06-12 18:38:04 +00:00
else:
continue
2011-06-13 12:59:52 +00:00
# Check if the remaining elements fit on the slide.
2011-07-30 06:33:51 +00:00
if not self._text_fits_on_slide(
separator.join(html_list[index + 1:]).strip()):
previous_html = separator.join(
html_list[index + 1:]).strip() + line_end
previous_raw = separator.join(
raw_list[index + 1:]).strip() + line_end
2011-06-12 18:38:04 +00:00
break
else:
2011-06-13 12:59:52 +00:00
# The remaining elements do not fit, thus reset the indexes,
# create a new list and continue.
2011-06-12 19:27:19 +00:00
raw_list = raw_list[index + 1:]
html_list = html_list[index + 1:]
2011-06-12 18:38:04 +00:00
smallest_index = 0
2011-06-12 19:27:19 +00:00
highest_index = len(html_list) - 1
2011-06-12 18:38:04 +00:00
index = int(highest_index / 2)
2011-06-13 08:16:50 +00:00
return previous_html, previous_raw
2011-06-12 18:38:04 +00:00
2011-07-30 06:33:51 +00:00
def _text_fits_on_slide(self, text):
"""
Checks if the given ``text`` fits on a slide. If it does, ``True`` is
returned, otherwise ``False``.
``text``
The text to check. It can contain HTML tags.
"""
self.web_frame.evaluateJavaScript(u'show_text("%s")' %
text.replace(u'\\', u'\\\\').replace(u'\"', u'\\\"'))
return self.web_frame.contentsSize().height() > self.page_height
2011-04-26 19:09:56 +00:00
def _words_split(self, line):
2011-03-28 18:12:46 +00:00
"""
Split the slide up by word so can wrap better
"""
2011-04-08 15:35:33 +00:00
# this parse we are to be wordy
2011-04-26 19:09:56 +00:00
line = line.replace(u'\n', u' ')
return line.split(u' ')
2011-04-08 15:35:33 +00:00
def _lines_split(self, text):
"""
Split the slide up by physical line
"""
# this parse we do not want to use this so remove it
2011-04-27 19:44:21 +00:00
text = text.replace(u'\n[---]', u'')
2011-06-14 06:58:49 +00:00
text = text.replace(u'[---]', u'')
return text.split(u'\n')