updated renderer comments/docs

This commit is contained in:
Andreas Preikschat 2013-03-01 11:46:46 +01:00
parent daed497a9e
commit 0086d102bd

View File

@ -51,22 +51,14 @@ FOOTER = [u'Arky Arky (Unknown)', u'Public Domain', u'CCLI 123456']
class Renderer(object): class Renderer(object):
""" """
Class to pull all Renderer interactions into one place. The plugins will Class to pull all Renderer interactions into one place. The plugins will call helper methods to do the rendering but
call helper methods to do the rendering but this class will provide this class will provide display defense code.
display defense code.
""" """
log.info(u'Renderer Loaded') log.info(u'Renderer Loaded')
def __init__(self): def __init__(self):
""" """
Initialise the renderer. Initialise the renderer.
``image_manager``
A image_manager instance which takes care of e. g. caching and
resizing images.
``theme_manager``
The theme_manager instance, used to get the current theme details.
""" """
log.debug(u'Initialisation started') log.debug(u'Initialisation started')
self.screens = ScreenList() self.screens = ScreenList()
@ -99,19 +91,17 @@ class Renderer(object):
def update_theme(self, theme_name, old_theme_name=None, only_delete=False): def update_theme(self, theme_name, old_theme_name=None, only_delete=False):
""" """
This method updates the theme in ``_theme_dimensions`` when a theme This method updates the theme in ``_theme_dimensions`` when a theme has been edited or renamed.
has been edited or renamed.
``theme_name`` ``theme_name``
The current theme name. The current theme name.
``old_theme_name`` ``old_theme_name``
The old theme name. Has only to be passed, when the theme has been The old theme name. Has only to be passed, when the theme has been renamed. Defaults to *None*.
renamed. Defaults to *None*.
``only_delete`` ``only_delete``
Only remove the given ``theme_name`` from the ``_theme_dimensions`` Only remove the given ``theme_name`` from the ``_theme_dimensions`` list. This can be used when a theme is
list. This can be used when a theme is permanently deleted. permanently deleted.
""" """
if old_theme_name is not None and old_theme_name in self._theme_dimensions: if old_theme_name is not None and old_theme_name in self._theme_dimensions:
del self._theme_dimensions[old_theme_name] del self._theme_dimensions[old_theme_name]
@ -144,20 +134,17 @@ class Renderer(object):
Set up the theme to be used before rendering an item. Set up the theme to be used before rendering an item.
``override_theme_data`` ``override_theme_data``
The theme data should be passed, when we want to use our own theme The theme data should be passed, when we want to use our own theme data, regardless of the theme level. This
data, regardless of the theme level. This should for example be used should for example be used in the theme manager. **Note**, this is **not** to be mixed up with the
in the theme manager. **Note**, this is **not** to be mixed up with ``set_item_theme`` method.
the ``set_item_theme`` method.
""" """
# Just assume we use the global theme. # Just assume we use the global theme.
theme_to_use = self.global_theme_name theme_to_use = self.global_theme_name
# The theme level is either set to Service or Item. Use the service # The theme level is either set to Service or Item. Use the service theme if one is set. We also have to use the
# theme if one is set. We also have to use the service theme, even when # service theme, even when the theme level is set to Item, because the item does not necessarily have to have a
# the theme level is set to Item, because the item does not necessarily # theme.
# have to have a theme.
if self.theme_level != ThemeLevel.Global: if self.theme_level != ThemeLevel.Global:
# When the theme level is at Service and we actually have a service # When the theme level is at Service and we actually have a service theme then use it.
# theme then use it.
if self.service_theme_name: if self.service_theme_name:
theme_to_use = self.service_theme_name theme_to_use = self.service_theme_name
# If we have Item level and have an item theme then use it. # If we have Item level and have an item theme then use it.
@ -206,8 +193,7 @@ class Renderer(object):
def set_item_theme(self, item_theme_name): def set_item_theme(self, item_theme_name):
""" """
Set the item-level theme. **Note**, this has to be done for each item we Set the item-level theme. **Note**, this has to be done for each item we are rendering.
are rendering.
``item_theme_name`` ``item_theme_name``
The item theme's name. The item theme's name.
@ -238,9 +224,8 @@ class Renderer(object):
serviceItem.raw_footer = FOOTER serviceItem.raw_footer = FOOTER
# if No file do not update cache # if No file do not update cache
if theme_data.background_filename: if theme_data.background_filename:
self.image_manager.add_image(theme_data.background_filename, self.image_manager.add_image(
ImageSource.Theme, theme_data.background_filename, ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
QtGui.QColor(theme_data.background_border_color))
theme_data, main, footer = self.pre_render(theme_data) theme_data, main, footer = self.pre_render(theme_data)
serviceItem.themedata = theme_data serviceItem.themedata = theme_data
serviceItem.main = main serviceItem.main = main
@ -278,22 +263,21 @@ class Renderer(object):
if u'[---]' in text: if u'[---]' in text:
while True: while True:
slides = text.split(u'\n[---]\n', 2) slides = text.split(u'\n[---]\n', 2)
# If there are (at least) two occurrences of [---] we use # If there are (at least) two occurrences of [---] we use the first two slides (and neglect the last
# the first two slides (and neglect the last for now). # for now).
if len(slides) == 3: if len(slides) == 3:
html_text = expand_tags(u'\n'.join(slides[:2])) html_text = expand_tags(u'\n'.join(slides[:2]))
# We check both slides to determine if the optional split is # We check both slides to determine if the optional split is needed (there is only one optional
# needed (there is only one optional split). # split).
else: else:
html_text = expand_tags(u'\n'.join(slides)) html_text = expand_tags(u'\n'.join(slides))
html_text = html_text.replace(u'\n', u'<br>') html_text = html_text.replace(u'\n', u'<br>')
if self._text_fits_on_slide(html_text): if self._text_fits_on_slide(html_text):
# The first two optional slides fit (as a whole) on one # The first two optional slides fit (as a whole) on one slide. Replace the first occurrence
# slide. Replace the first occurrence of [---]. # of [---].
text = text.replace(u'\n[---]', u'', 1) text = text.replace(u'\n[---]', u'', 1)
else: else:
# The first optional slide fits, which means we have to # The first optional slide fits, which means we have to render the first optional slide.
# render the first optional slide.
text_contains_split = u'[---]' in text text_contains_split = u'[---]' in text
if text_contains_split: if text_contains_split:
try: try:
@ -343,8 +327,7 @@ class Renderer(object):
self.width = screen_size.width() self.width = screen_size.width()
self.height = screen_size.height() self.height = screen_size.height()
self.screen_ratio = float(self.height) / float(self.width) self.screen_ratio = float(self.height) / float(self.width)
log.debug(u'_calculate default %s, %f' % (screen_size, log.debug(u'_calculate default %s, %f' % (screen_size, self.screen_ratio))
self.screen_ratio))
# 90% is start of footer # 90% is start of footer
self.footer_start = int(self.height * 0.90) self.footer_start = int(self.height * 0.90)
@ -369,12 +352,10 @@ class Renderer(object):
The theme data. The theme data.
""" """
if not theme_data.font_footer_override: if not theme_data.font_footer_override:
return QtCore.QRect(10, self.footer_start, self.width - 20, return QtCore.QRect(10, self.footer_start, self.width - 20, self.height - self.footer_start)
self.height - self.footer_start)
else: else:
return QtCore.QRect(theme_data.font_footer_x, return QtCore.QRect(theme_data.font_footer_x,
theme_data.font_footer_y, theme_data.font_footer_width - 1, theme_data.font_footer_y, theme_data.font_footer_width - 1, theme_data.font_footer_height - 1)
theme_data.font_footer_height - 1)
def _set_text_rectangle(self, theme_data, rect_main, rect_footer): def _set_text_rectangle(self, theme_data, rect_main, rect_footer):
""" """
@ -397,9 +378,8 @@ class Renderer(object):
if theme_data.font_main_shadow: if theme_data.font_main_shadow:
self.page_width -= int(theme_data.font_main_shadow_size) self.page_width -= int(theme_data.font_main_shadow_size)
self.page_height -= int(theme_data.font_main_shadow_size) self.page_height -= int(theme_data.font_main_shadow_size)
# For the life of my I don't know why we have to completely kill the # For the life of my I don't know why we have to completely kill the QWebView in order for the display to work
# QWebView in order for the display to work properly, but we do. See # properly, but we do. See bug #1041366 for an example of what happens if we take this out.
# bug #1041366 for an example of what happens if we take this out.
self.web = None self.web = None
self.web = QtWebKit.QWebView() self.web = QtWebKit.QWebView()
self.web.setVisible(False) self.web.setVisible(False)
@ -425,10 +405,9 @@ class Renderer(object):
def _paginate_slide(self, lines, line_end): def _paginate_slide(self, lines, line_end):
""" """
Figure out how much text can appear on a slide, using the current Figure out how much text can appear on a slide, using the current theme settings.
theme settings. **Note:** The smallest possible "unit" of text for a slide is one line. If the line is too long it will be cut
**Note:** The smallest possible "unit" of text for a slide is one line. off when displayed.
If the line is too long it will be cut off when displayed.
``lines`` ``lines``
The text to be fitted on the slide split into lines. The text to be fitted on the slide split into lines.
@ -444,8 +423,8 @@ class Renderer(object):
html_lines = map(expand_tags, lines) html_lines = map(expand_tags, lines)
# Text too long so go to next page. # Text too long so go to next page.
if not self._text_fits_on_slide(separator.join(html_lines)): if not self._text_fits_on_slide(separator.join(html_lines)):
html_text, previous_raw = self._binary_chop(formatted, html_text, previous_raw = self._binary_chop(
previous_html, previous_raw, html_lines, lines, separator, u'') formatted, previous_html, previous_raw, html_lines, lines, separator, u'')
else: else:
previous_raw = separator.join(lines) previous_raw = separator.join(lines)
formatted.append(previous_raw) formatted.append(previous_raw)
@ -454,18 +433,15 @@ class Renderer(object):
def _paginate_slide_words(self, lines, line_end): def _paginate_slide_words(self, lines, line_end):
""" """
Figure out how much text can appear on a slide, using the current Figure out how much text can appear on a slide, using the current theme settings.
theme settings. **Note:** The smallest possible "unit" of text for a slide is one word. If one line is too long it will be
**Note:** The smallest possible "unit" of text for a slide is one word. processed word by word. This is sometimes need for **bible** verses.
If one line is too long it will be processed word by word. This is
sometimes need for **bible** verses.
``lines`` ``lines``
The text to be fitted on the slide split into lines. The text to be fitted on the slide split into lines.
``line_end`` ``line_end``
The text added after each line. Either ``u' '`` or ``u'<br>``. The text added after each line. Either ``u' '`` or ``u'<br>``. This is needed for **bibles**.
This is needed for **bibles**.
""" """
log.debug(u'_paginate_slide_words - Start') log.debug(u'_paginate_slide_words - Start')
formatted = [] formatted = []
@ -476,22 +452,19 @@ class Renderer(object):
html_line = expand_tags(line) html_line = expand_tags(line)
# Text too long so go to next page. # Text too long so go to next page.
if not self._text_fits_on_slide(previous_html + html_line): if not self._text_fits_on_slide(previous_html + html_line):
# Check if there was a verse before the current one and append # Check if there was a verse before the current one and append it, when it fits on the page.
# it, when it fits on the page.
if previous_html: if previous_html:
if self._text_fits_on_slide(previous_html): if self._text_fits_on_slide(previous_html):
formatted.append(previous_raw) formatted.append(previous_raw)
previous_html = u'' previous_html = u''
previous_raw = u'' previous_raw = u''
# Now check if the current verse will fit, if it does # Now check if the current verse will fit, if it does not we have to start to process the verse
# not we have to start to process the verse word by # word by word.
# word.
if self._text_fits_on_slide(html_line): if self._text_fits_on_slide(html_line):
previous_html = html_line + line_end previous_html = html_line + line_end
previous_raw = line + line_end previous_raw = line + line_end
continue continue
# Figure out how many words of the line will fit on screen as # Figure out how many words of the line will fit on screen as the line will not fit as a whole.
# the line will not fit as a whole.
raw_words = self._words_split(line) raw_words = self._words_split(line)
html_words = map(expand_tags, raw_words) html_words = map(expand_tags, raw_words)
previous_html, previous_raw = \ previous_html, previous_raw = \
@ -505,19 +478,15 @@ class Renderer(object):
def _get_start_tags(self, raw_text): def _get_start_tags(self, raw_text):
""" """
Tests the given text for not closed formatting tags and returns a tuple Tests the given text for not closed formatting tags and returns a tuple consisting of three unicode strings::
consisting of three unicode strings::
(u'{st}{r}Text text text{/r}{/st}', u'{st}{r}', u'<strong> (u'{st}{r}Text text text{/r}{/st}', u'{st}{r}', u'<strong><span style="-webkit-text-fill-color:red">')
<span style="-webkit-text-fill-color:red">')
The first unicode string is the text, with correct closing tags. The The first unicode string is the text, with correct closing tags. The second unicode string are OpenLP's opening
second unicode string are OpenLP's opening formatting tags and the third formatting tags and the third unicode string the html opening formatting tags.
unicode string the html opening formatting tags.
``raw_text`` ``raw_text``
The text to test. The text must **not** contain html tags, only The text to test. The text must **not** contain html tags, only OpenLP formatting tags are allowed::
OpenLP formatting tags are allowed::
{st}{r}Text text text {st}{r}Text text text
""" """
@ -529,9 +498,8 @@ class Renderer(object):
if raw_text.count(tag[u'start tag']) != raw_text.count(tag[u'end tag']): if raw_text.count(tag[u'start tag']) != raw_text.count(tag[u'end tag']):
raw_tags.append((raw_text.find(tag[u'start tag']), tag[u'start tag'], tag[u'end tag'])) raw_tags.append((raw_text.find(tag[u'start tag']), tag[u'start tag'], tag[u'end tag']))
html_tags.append((raw_text.find(tag[u'start tag']), tag[u'start html'])) html_tags.append((raw_text.find(tag[u'start tag']), tag[u'start html']))
# Sort the lists, so that the tags which were opened first on the first # Sort the lists, so that the tags which were opened first on the first slide (the text we are checking) will be
# slide (the text we are checking) will be opened first on the next # opened first on the next slide as well.
# slide as well.
raw_tags.sort(key=lambda tag: tag[0]) raw_tags.sort(key=lambda tag: tag[0])
html_tags.sort(key=lambda tag: tag[0]) html_tags.sort(key=lambda tag: tag[0])
# Create a list with closing tags for the raw_text. # Create a list with closing tags for the raw_text.
@ -547,46 +515,40 @@ class Renderer(object):
def _binary_chop(self, formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end): def _binary_chop(self, formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end):
""" """
This implements the binary chop algorithm for faster rendering. This This implements the binary chop algorithm for faster rendering. This algorithm works line based (line by line)
algorithm works line based (line by line) and word based (word by word). and word based (word by word). It is assumed that this method is **only** called, when the lines/words to be
It is assumed that this method is **only** called, when the lines/words rendered do **not** fit as a whole.
to be rendered do **not** fit as a whole.
``formatted`` ``formatted``
The list to append any slides. The list to append any slides.
``previous_html`` ``previous_html``
The html text which is know to fit on a slide, but is not yet added The html text which is know to fit on a slide, but is not yet added to the list of slides. (unicode string)
to the list of slides. (unicode string)
``previous_raw`` ``previous_raw``
The raw text (with formatting tags) which is know to fit on a slide, The raw text (with formatting tags) which is know to fit on a slide, but is not yet added to the list of
but is not yet added to the list of slides. (unicode string) slides. (unicode string)
``html_list`` ``html_list``
The elements which do not fit on a slide and needs to be processed The elements which do not fit on a slide and needs to be processed using the binary chop. The text contains
using the binary chop. The text contains html. html.
``raw_list`` ``raw_list``
The elements which do not fit on a slide and needs to be processed The elements which do not fit on a slide and needs to be processed using the binary chop. The elements can
using the binary chop. The elements can contain formatting tags. contain formatting tags.
``separator`` ``separator``
The separator for the elements. For lines this is ``u'<br>'`` and The separator for the elements. For lines this is ``u'<br>'`` and for words this is ``u' '``.
for words this is ``u' '``.
``line_end`` ``line_end``
The text added after each "element line". Either ``u' '`` or The text added after each "element line". Either ``u' '`` or ``u'<br>``. This is needed for bibles.
``u'<br>``. This is needed for bibles.
""" """
smallest_index = 0 smallest_index = 0
highest_index = len(html_list) - 1 highest_index = len(html_list) - 1
index = int(highest_index / 2) index = int(highest_index / 2)
while True: while True:
if not self._text_fits_on_slide( if not self._text_fits_on_slide(previous_html + separator.join(html_list[:index + 1]).strip()):
previous_html + separator.join(html_list[:index + 1]).strip()): # We know that it does not fit, so change/calculate the new index and highest_index accordingly.
# We know that it does not fit, so change/calculate the
# new index and highest_index accordingly.
highest_index = index highest_index = index
index = int(index - (index - smallest_index) / 2) index = int(index - (index - smallest_index) / 2)
else: else:
@ -607,14 +569,12 @@ class Renderer(object):
else: else:
continue continue
# Check if the remaining elements fit on the slide. # Check if the remaining elements fit on the slide.
if self._text_fits_on_slide( if self._text_fits_on_slide(html_tags + separator.join(html_list[index + 1:]).strip()):
html_tags + separator.join(html_list[index + 1:]).strip()):
previous_html = html_tags + separator.join(html_list[index + 1:]).strip() + line_end previous_html = html_tags + separator.join(html_list[index + 1:]).strip() + line_end
previous_raw = raw_tags + separator.join(raw_list[index + 1:]).strip() + line_end previous_raw = raw_tags + separator.join(raw_list[index + 1:]).strip() + line_end
break break
else: else:
# The remaining elements do not fit, thus reset the indexes, # The remaining elements do not fit, thus reset the indexes, create a new list and continue.
# create a new list and continue.
raw_list = raw_list[index + 1:] raw_list = raw_list[index + 1:]
raw_list[0] = raw_tags + raw_list[0] raw_list[0] = raw_tags + raw_list[0]
html_list = html_list[index + 1:] html_list = html_list[index + 1:]
@ -626,8 +586,7 @@ class Renderer(object):
def _text_fits_on_slide(self, text): def _text_fits_on_slide(self, text):
""" """
Checks if the given ``text`` fits on a slide. If it does ``True`` is Checks if the given ``text`` fits on a slide. If it does ``True`` is returned, otherwise ``False``.
returned, otherwise ``False``.
``text`` ``text``
The text to check. It may contain HTML tags. The text to check. It may contain HTML tags.