Readded the Renderer class for calculating slide sizes.

This commit is contained in:
Tomas Groth 2018-09-28 21:33:40 +02:00
parent 393823c0ab
commit 12a2a90ea6
7 changed files with 470 additions and 110 deletions

View File

@ -291,6 +291,13 @@ var Display = {
$(".slides")[0].innerHTML = ""; $(".slides")[0].innerHTML = "";
this._slides = {}; this._slides = {};
}, },
/**
* Checks if the present slide content fits within the slide
*/
doesContentFit: function () {
console.debug("scrollHeight: " + $(".slides")[0].scrollHeight);
return $(".slides")[0].clientHeight >= $(".slides")[0].scrollHeight;
},
/** /**
* Add a slides. If the slide exists but the HTML is different, update the slide. * Add a slides. If the slide exists but the HTML is different, update the slide.
* @param {string} verse - The verse number, e.g. "v1" * @param {string} verse - The verse number, e.g. "v1"

View File

@ -27,7 +27,14 @@ import logging
import math import math
import re import re
from PyQt5 import QtWidgets
from openlp.core.lib.formattingtags import FormattingTags from openlp.core.lib.formattingtags import FormattingTags
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.display.window import DisplayWindow
from openlp.core.display.screens import ScreenList
from openlp.core.lib import ItemCapabilities
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -354,3 +361,318 @@ def render_tags(text, can_render_chords=False, is_printing=False):
text = text.replace(tag['start tag'], tag['start html']) text = text.replace(tag['start tag'], tag['start html'])
text = text.replace(tag['end tag'], tag['end html']) text = text.replace(tag['end tag'], tag['end html'])
return text return text
def words_split(line):
"""
Split the slide up by word so can wrap better
:param line: Line to be split
"""
# this parse we are to be wordy
return re.split(r'\s+', line)
def get_start_tags(raw_text):
"""
Tests the given text for not closed formatting tags and returns a tuple consisting of three unicode strings::
('{st}{r}Text text text{/r}{/st}', '{st}{r}', '<strong><span style="-webkit-text-fill-color:red">')
The first unicode string is the text, with correct closing tags. The second unicode string are OpenLP's opening
formatting tags and the third unicode string the html opening formatting tags.
:param raw_text: The text to test. The text must **not** contain html tags, only OpenLP formatting tags
are allowed::
{st}{r}Text text text
"""
raw_tags = []
html_tags = []
for tag in FormattingTags.get_html_tags():
if tag['start tag'] == '{br}':
continue
if raw_text.count(tag['start tag']) != raw_text.count(tag['end tag']):
raw_tags.append((raw_text.find(tag['start tag']), tag['start tag'], tag['end tag']))
html_tags.append((raw_text.find(tag['start tag']), tag['start html']))
# Sort the lists, so that the tags which were opened first on the first slide (the text we are checking) will be
# opened first on the next slide as well.
raw_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.
end_tags = []
start_tags = []
for tag in raw_tags:
start_tags.append(tag[1])
end_tags.append(tag[2])
end_tags.reverse()
# Remove the indexes.
html_tags = [tag[1] for tag in html_tags]
return raw_text + ''.join(end_tags), ''.join(start_tags), ''.join(html_tags)
class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
"""
A virtual display used for rendering thumbnails and other offscreen tasks
"""
def __init__(self, *args, **kwargs):
"""
Constructor
"""
super().__init__(*args, **kwargs)
self.force_page = False
for screen in ScreenList():
if screen.is_display:
self.setGeometry(screen.geometry.x(), screen.geometry.y(),
screen.geometry.width(), screen.geometry.height())
break
# If the display is not show'ed and hidden like this webegine will not render
self.show()
self.hide()
self.theme_height = 0
def calculate_line_count(self):
"""
Calculate the number of lines that fits on one slide
"""
return self.run_javascript('Display.calculateLineCount();', is_sync=True)
def clear_slides(self):
"""
Clear slides
"""
return self.run_javascript('Display.clearSlides();')
def format_slide(self, text, item):
"""
Calculate how much text can fit on a slide.
:param text: The words to go on the slides.
:param item: The :class:`~openlp.core.lib.serviceitem.ServiceItem` item object.
"""
while not self._is_initialised:
QtWidgets.QApplication.instance().processEvents()
self.log_debug('format slide')
theme_name = item.theme if item.theme else Registry().get('theme_manager').global_theme
theme_data = Registry().get('theme_manager').get_theme_data(theme_name)
self.theme_height = theme_data.font_main_height
# Set theme for preview
self.set_theme(theme_data)
# Add line endings after each line of text used for bibles.
line_end = '<br>'
if item.is_capable(ItemCapabilities.NoLineBreaks):
line_end = ' '
# Bibles
if item.is_capable(ItemCapabilities.CanWordSplit):
pages = self._paginate_slide_words(text.split('\n'), line_end)
# Songs and Custom
elif item.is_capable(ItemCapabilities.CanSoftBreak):
pages = []
if '[---]' in text:
# Remove Overflow split if at start of the text
if text.startswith('[---]'):
text = text[5:]
# Remove two or more option slide breaks next to each other (causing infinite loop).
while '\n[---]\n[---]\n' in text:
text = text.replace('\n[---]\n[---]\n', '\n[---]\n')
while ' [---]' in text:
text = text.replace(' [---]', '[---]')
while '[---] ' in text:
text = text.replace('[---] ', '[---]')
count = 0
# only loop 5 times as there will never be more than 5 incorrect logical splits on a single slide.
while True and count < 5:
slides = text.split('\n[---]\n', 2)
# If there are (at least) two occurrences of [---] we use the first two slides (and neglect the last
# for now).
if len(slides) == 3:
html_text = expand_tags('\n'.join(slides[:2]))
# We check both slides to determine if the optional split is needed (there is only one optional
# split).
else:
html_text = expand_tags('\n'.join(slides))
html_text = html_text.replace('\n', '<br>')
if self._text_fits_on_slide(html_text):
# The first two optional slides fit (as a whole) on one slide. Replace the first occurrence
# of [---].
text = text.replace('\n[---]', '', 1)
else:
# The first optional slide fits, which means we have to render the first optional slide.
text_contains_split = '[---]' in text
if text_contains_split:
try:
text_to_render, text = text.split('\n[---]\n', 1)
except ValueError:
text_to_render = text.split('\n[---]\n')[0]
text = ''
text_to_render, raw_tags, html_tags = get_start_tags(text_to_render)
if text:
text = raw_tags + text
else:
text_to_render = text
text = ''
lines = text_to_render.strip('\n').split('\n')
slides = self._paginate_slide(lines, line_end)
if len(slides) > 1 and text:
# Add all slides apart from the last one the list.
pages.extend(slides[:-1])
if text_contains_split:
text = slides[-1] + '\n[---]\n' + text
else:
text = slides[-1] + '\n' + text
text = text.replace('<br>', '\n')
else:
pages.extend(slides)
if '[---]' not in text:
lines = text.strip('\n').split('\n')
pages.extend(self._paginate_slide(lines, line_end))
break
count += 1
else:
# Clean up line endings.
pages = self._paginate_slide(text.split('\n'), line_end)
else:
pages = self._paginate_slide(text.split('\n'), line_end)
new_pages = []
for page in pages:
while page.endswith('<br>'):
page = page[:-4]
new_pages.append(page)
return new_pages
def _paginate_slide(self, lines, line_end):
"""
Figure out how much text can appear on a slide, using the current 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
off when displayed.
:param lines: The text to be fitted on the slide split into lines.
:param line_end: The text added after each line. Either ``' '`` or ``'<br>``.
"""
formatted = []
previous_html = ''
previous_raw = ''
separator = '<br>'
html_lines = list(map(render_tags, lines))
# Text too long so go to next page.
if not self._text_fits_on_slide(separator.join(html_lines)):
html_text, previous_raw = self._binary_chop(
formatted, previous_html, previous_raw, html_lines, lines, separator, '')
else:
previous_raw = separator.join(lines)
formatted.append(previous_raw)
return formatted
def _paginate_slide_words(self, lines, line_end):
"""
Figure out how much text can appear on a slide, using the current theme settings.
**Note:** The smallest possible "unit" of text for a slide is one word. If one line is too long it will be
processed word by word. This is sometimes need for **bible** verses.
:param lines: The text to be fitted on the slide split into lines.
:param line_end: The text added after each line. Either ``' '`` or ``'<br>``. This is needed for **bibles**.
"""
formatted = []
previous_html = ''
previous_raw = ''
for line in lines:
line = line.strip()
html_line = expand_tags(line)
# Text too long so go to next page.
if not 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:
if self._text_fits_on_slide(previous_html):
formatted.append(previous_raw)
previous_html = ''
previous_raw = ''
# Now check if the current verse will fit, if it does not we have to start to process the verse
# word by word.
if self._text_fits_on_slide(html_line):
previous_html = html_line + line_end
previous_raw = line + line_end
continue
# Figure out how many words of the line will fit on screen as the line will not fit as a whole.
raw_words = words_split(line)
html_words = list(map(expand_tags, raw_words))
previous_html, previous_raw = \
self._binary_chop(formatted, previous_html, previous_raw, html_words, raw_words, ' ', line_end)
else:
previous_html += html_line + line_end
previous_raw += line + line_end
formatted.append(previous_raw)
return formatted
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 algorithm works line based (line by line)
and word based (word by word). It is assumed that this method is **only** called, when the lines/words to be
rendered do **not** fit as a whole.
:param formatted: The list to append any slides.
:param previous_html: The html text which is know to fit on a slide, but is not yet added to the list of
slides. (unicode string)
:param previous_raw: The raw text (with formatting tags) which is know to fit on a slide, but is not yet added
to the list of slides. (unicode string)
:param html_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
The text contains html.
:param raw_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
The elements can contain formatting tags.
:param separator: The separator for the elements. For lines this is ``'<br>'`` and for words this is ``' '``.
:param line_end: The text added after each "element line". Either ``' '`` or ``'<br>``. This is needed for
bibles.
"""
smallest_index = 0
highest_index = len(html_list) - 1
index = highest_index // 2
while True:
if not self._text_fits_on_slide(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.
highest_index = index
index = index - (index - smallest_index) // 2
else:
smallest_index = index
index = 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
text = previous_raw.rstrip('<br>') + separator.join(raw_list[:index + 1])
text, raw_tags, html_tags = get_start_tags(text)
formatted.append(text)
previous_html = ''
previous_raw = ''
# Stop here as the theme line count was requested.
if self.force_page:
Registry().execute('theme_line_count', index + 1)
break
else:
continue
# Check if the remaining elements fit on the slide.
if self._text_fits_on_slide(html_tags + separator.join(html_list[index + 1:]).strip()):
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
break
else:
# The remaining elements do not fit, thus reset the indexes, create a new list and continue.
raw_list = raw_list[index + 1:]
raw_list[0] = raw_tags + raw_list[0]
html_list = html_list[index + 1:]
html_list[0] = html_tags + html_list[0]
smallest_index = 0
highest_index = len(html_list) - 1
index = highest_index // 2
return previous_html, previous_raw
def _text_fits_on_slide(self, text):
"""
Checks if the given ``text`` fits on a slide. If it does ``True`` is returned, otherwise ``False``.
:param text: The text to check. It may contain HTML tags.
"""
self.clear_slides()
self.run_javascript('Display.addTextSlide("v1", "{text}");'.format(text=text), is_sync=True)
does_text_fits = self.run_javascript('Display.doesContentFit();', is_sync=True)
return does_text_fits

View File

@ -81,9 +81,13 @@ class Screen(object):
:return: A Screen object with the values from screen_dict :return: A Screen object with the values from screen_dict
:rtype: openlp.core.display.screens.Screen :rtype: openlp.core.display.screens.Screen
""" """
screen_dict['geometry'] = QtCore.QRect(**screen_dict['geometry']) screen_dict['geometry'] = QtCore.QRect(screen_dict['geometry']['x'], screen_dict['geometry']['y'],
screen_dict['geometry']['width'], screen_dict['geometry']['height'])
if 'display_geometry' in screen_dict: if 'display_geometry' in screen_dict:
screen_dict['display_geometry'] = QtCore.QRect(**screen_dict['display_geometry']) screen_dict['display_geometry'] = QtCore.QRect(screen_dict['display_geometry']['x'],
screen_dict['display_geometry']['y'],
screen_dict['display_geometry']['width'],
screen_dict['display_geometry']['height'])
return cls(**screen_dict) return cls(**screen_dict)
def to_dict(self): def to_dict(self):

View File

@ -154,6 +154,7 @@ class DisplayWindow(QtWidgets.QWidget):
Add stuff after page initialisation Add stuff after page initialisation
""" """
self.run_javascript('Display.init();') self.run_javascript('Display.init();')
self._is_initialised = True
def run_javascript(self, script, is_sync=False): def run_javascript(self, script, is_sync=False):
""" """

View File

@ -80,6 +80,103 @@ class ServiceItemAction(object):
Next = 3 Next = 3
class ItemCapabilities(object):
"""
Provides an enumeration of a service item's capabilities
``CanPreview``
The capability to allow the ServiceManager to add to the preview tab when making the previous item live.
``CanEdit``
The capability to allow the ServiceManager to allow the item to be edited
``CanMaintain``
The capability to allow the ServiceManager to allow the item to be reordered.
``RequiresMedia``
Determines is the service_item needs a Media Player
``CanLoop``
The capability to allow the SlideController to allow the loop processing.
``CanAppend``
The capability to allow the ServiceManager to add leaves to the
item
``NoLineBreaks``
The capability to remove lines breaks in the renderer
``OnLoadUpdate``
The capability to update MediaManager when a service Item is loaded.
``AddIfNewItem``
Not Used
``ProvidesOwnDisplay``
The capability to tell the SlideController the service Item has a different display.
``HasDetailedTitleDisplay``
Being Removed and decommissioned.
``HasVariableStartTime``
The capability to tell the ServiceManager that a change to start time is possible.
``CanSoftBreak``
The capability to tell the renderer that Soft Break is allowed
``CanWordSplit``
The capability to tell the renderer that it can split words is
allowed
``HasBackgroundAudio``
That a audio file is present with the text.
``CanAutoStartForLive``
The capability to ignore the do not play if display blank flag.
``CanEditTitle``
The capability to edit the title of the item
``IsOptical``
Determines is the service_item is based on an optical device
``HasDisplayTitle``
The item contains 'displaytitle' on every frame which should be
preferred over 'title' when displaying the item
``HasNotes``
The item contains 'notes'
``HasThumbnails``
The item has related thumbnails available
``HasMetaData``
The item has Meta Data about item
"""
CanPreview = 1
CanEdit = 2
CanMaintain = 3
RequiresMedia = 4
CanLoop = 5
CanAppend = 6
NoLineBreaks = 7
OnLoadUpdate = 8
AddIfNewItem = 9
ProvidesOwnDisplay = 10
# HasDetailedTitleDisplay = 11
HasVariableStartTime = 12
CanSoftBreak = 13
CanWordSplit = 14
HasBackgroundAudio = 15
CanAutoStartForLive = 16
CanEditTitle = 17
IsOptical = 18
HasDisplayTitle = 19
HasNotes = 20
HasThumbnails = 21
HasMetaData = 22
def get_text_file_string(text_file_path): def get_text_file_string(text_file_path):
""" """
Open a file and return its content as a string. If the supplied file path is not a file then the function Open a file and return its content as a string. If the supplied file path is not a file then the function

View File

@ -40,7 +40,7 @@ from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.path import Path from openlp.core.common.path import Path
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.display.render import remove_tags, render_tags from openlp.core.display.render import remove_tags, render_tags
from openlp.core.lib import ImageSource, build_icon from openlp.core.lib import ImageSource, ItemCapabilities, build_icon
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -54,103 +54,6 @@ class ServiceItemType(object):
Command = 3 Command = 3
class ItemCapabilities(object):
"""
Provides an enumeration of a service item's capabilities
``CanPreview``
The capability to allow the ServiceManager to add to the preview tab when making the previous item live.
``CanEdit``
The capability to allow the ServiceManager to allow the item to be edited
``CanMaintain``
The capability to allow the ServiceManager to allow the item to be reordered.
``RequiresMedia``
Determines is the service_item needs a Media Player
``CanLoop``
The capability to allow the SlideController to allow the loop processing.
``CanAppend``
The capability to allow the ServiceManager to add leaves to the
item
``NoLineBreaks``
The capability to remove lines breaks in the renderer
``OnLoadUpdate``
The capability to update MediaManager when a service Item is loaded.
``AddIfNewItem``
Not Used
``ProvidesOwnDisplay``
The capability to tell the SlideController the service Item has a different display.
``HasDetailedTitleDisplay``
Being Removed and decommissioned.
``HasVariableStartTime``
The capability to tell the ServiceManager that a change to start time is possible.
``CanSoftBreak``
The capability to tell the renderer that Soft Break is allowed
``CanWordSplit``
The capability to tell the renderer that it can split words is
allowed
``HasBackgroundAudio``
That a audio file is present with the text.
``CanAutoStartForLive``
The capability to ignore the do not play if display blank flag.
``CanEditTitle``
The capability to edit the title of the item
``IsOptical``
Determines is the service_item is based on an optical device
``HasDisplayTitle``
The item contains 'displaytitle' on every frame which should be
preferred over 'title' when displaying the item
``HasNotes``
The item contains 'notes'
``HasThumbnails``
The item has related thumbnails available
``HasMetaData``
The item has Meta Data about item
"""
CanPreview = 1
CanEdit = 2
CanMaintain = 3
RequiresMedia = 4
CanLoop = 5
CanAppend = 6
NoLineBreaks = 7
OnLoadUpdate = 8
AddIfNewItem = 9
ProvidesOwnDisplay = 10
# HasDetailedTitleDisplay = 11
HasVariableStartTime = 12
CanSoftBreak = 13
CanWordSplit = 14
HasBackgroundAudio = 15
CanAutoStartForLive = 16
CanEditTitle = 17
IsOptical = 18
HasDisplayTitle = 19
HasNotes = 20
HasThumbnails = 21
HasMetaData = 22
class ServiceItem(RegistryProperties): class ServiceItem(RegistryProperties):
""" """
The service item is a base class for the plugins to use to interact with The service item is a base class for the plugins to use to interact with
@ -249,17 +152,45 @@ class ServiceItem(RegistryProperties):
else: else:
self.icon = UiIcons().clone self.icon = UiIcons().clone
def _create_slides(self):
"""
Create frames for rendering and display
"""
self._rendered_slides = []
self._display_slides = []
# Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
# the dict instead of rendering them again.
previous_pages = {}
#for slide in self._raw_frames:
for raw_slide in self.slides:
verse_tag = raw_slide['verse']
if verse_tag in previous_pages and previous_pages[verse_tag][0] == raw_slide:
pages = previous_pages[verse_tag][1]
else:
pages = self.renderer.format_slide(raw_slide['text'], self)
previous_pages[verse_tag] = (raw_slide, pages)
for page in pages:
rendered_slide = {
'title': raw_slide['title'],
'text': render_tags(page),
'verse': verse_tag,
}
self._rendered_slides.append(rendered_slide)
display_slide = {
'title': raw_slide['title'],
'text': remove_tags(page),
'verse': verse_tag,
}
self._display_slides.append(display_slide)
@property @property
def rendered_slides(self): def rendered_slides(self):
""" """
Render the frames and return them Render the frames and return them
""" """
if not self._rendered_slides: if not self._rendered_slides:
self._rendered_slides = [] self._create_slides()
for raw_slide in self.slides:
rendered_slide = deepcopy(raw_slide)
rendered_slide['text'] = render_tags(rendered_slide['text'])
self._rendered_slides.append(rendered_slide)
return self._rendered_slides return self._rendered_slides
@property @property
@ -268,11 +199,7 @@ class ServiceItem(RegistryProperties):
Render the frames and return them Render the frames and return them
""" """
if not self._display_slides: if not self._display_slides:
self._display_slides = [] self._create_slides()
for raw_slide in self.slides:
display_slide = deepcopy(raw_slide)
display_slide['text'] = remove_tags(display_slide['text'])
self._display_slides.append(display_slide)
return self._display_slides return self._display_slides
# def render(self, provides_own_theme_data=False): # def render(self, provides_own_theme_data=False):

View File

@ -43,6 +43,7 @@ from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList from openlp.core.display.screens import ScreenList
from openlp.core.lib.imagemanager import ImageManager from openlp.core.lib.imagemanager import ImageManager
from openlp.core.display.render import Renderer
from openlp.core.lib.plugin import PluginStatus from openlp.core.lib.plugin import PluginStatus
from openlp.core.lib.pluginmanager import PluginManager from openlp.core.lib.pluginmanager import PluginManager
from openlp.core.lib.ui import create_action from openlp.core.lib.ui import create_action
@ -509,6 +510,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
# Set up the path with plugins # Set up the path with plugins
PluginManager(self) PluginManager(self)
ImageManager() ImageManager()
Renderer()
# Set up the interface # Set up the interface
self.setup_ui(self) self.setup_ui(self)
# Define the media Dock Manager # Define the media Dock Manager