~tomasgroth/openlp/portable-path

« back to all changes in this revision

Viewing changes to openlp/core/display/renderer.py

  • Committer: Tomas Groth
  • Date: 2019-04-30 19:02:42 UTC
  • mfrom: (2829.2.32 openlp)
  • Revision ID: tomasgroth@yahoo.dk-20190430190242-6zwjk8724tyux70m
trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
3
 
 
4
 
###############################################################################
5
 
# OpenLP - Open Source Lyrics Projection                                      #
6
 
# --------------------------------------------------------------------------- #
7
 
# Copyright (c) 2008-2018 OpenLP Developers                                   #
8
 
# --------------------------------------------------------------------------- #
9
 
# This program is free software; you can redistribute it and/or modify it     #
10
 
# under the terms of the GNU General Public License as published by the Free  #
11
 
# Software Foundation; version 2 of the License.                              #
12
 
#                                                                             #
13
 
# This program is distributed in the hope that it will be useful, but WITHOUT #
14
 
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
15
 
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
16
 
# more details.                                                               #
17
 
#                                                                             #
18
 
# You should have received a copy of the GNU General Public License along     #
19
 
# with this program; if not, write to the Free Software Foundation, Inc., 59  #
20
 
# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
21
 
###############################################################################
22
 
 
23
 
import re
24
 
from string import Template
25
 
 
26
 
from PyQt5 import QtGui, QtCore, QtWebKitWidgets
27
 
 
28
 
from openlp.core.common import ThemeLevel
29
 
from openlp.core.common.mixins import LogMixin, RegistryProperties
30
 
from openlp.core.common.path import path_to_str
31
 
from openlp.core.common.registry import Registry, RegistryBase
32
 
from openlp.core.common.settings import Settings
33
 
from openlp.core.display.screens import ScreenList
34
 
from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ServiceItem, expand_tags, build_chords_css, \
35
 
    build_lyrics_format_css, build_lyrics_outline_css
36
 
from openlp.core.ui.maindisplay import MainDisplay
37
 
 
38
 
VERSE = 'The Lord said to {r}Noah{/r}: \n' \
39
 
    'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n' \
40
 
    'The Lord said to {g}Noah{/g}:\n' \
41
 
    'There\'s gonna be a {st}floody{/st}, {it}floody{/it}\n' \
42
 
    'Get those children out of the muddy, muddy \n' \
43
 
    '{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}' \
44
 
    'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
45
 
VERSE_FOR_LINE_COUNT = '\n'.join(map(str, range(100)))
46
 
FOOTER = ['Arky Arky (Unknown)', 'Public Domain', 'CCLI 123456']
47
 
 
48
 
 
49
 
class Renderer(RegistryBase, LogMixin, RegistryProperties):
50
 
    """
51
 
    Class to pull all Renderer interactions into one place. The plugins will call helper methods to do the rendering but
52
 
    this class will provide display defense code.
53
 
    """
54
 
 
55
 
    def __init__(self):
56
 
        """
57
 
        Initialise the renderer.
58
 
        """
59
 
        super(Renderer, self).__init__(None)
60
 
        # Need live behaviour if this is also working as a pseudo MainDisplay.
61
 
        self.screens = ScreenList()
62
 
        self.theme_level = ThemeLevel.Global
63
 
        self.global_theme_name = ''
64
 
        self.service_theme_name = ''
65
 
        self.item_theme_name = ''
66
 
        self.force_page = False
67
 
        self._theme_dimensions = {}
68
 
        self._calculate_default()
69
 
        self.web = QtWebKitWidgets.QWebView()
70
 
        self.web.setVisible(False)
71
 
        self.web_frame = self.web.page().mainFrame()
72
 
        Registry().register_function('theme_update_global', self.set_global_theme)
73
 
 
74
 
    def bootstrap_initialise(self):
75
 
        """
76
 
        Initialise functions
77
 
        """
78
 
        self.display = MainDisplay(self)
79
 
        self.display.setup()
80
 
 
81
 
    def update_display(self):
82
 
        """
83
 
        Updates the renderer's information about the current screen.
84
 
        """
85
 
        self._calculate_default()
86
 
        if self.display:
87
 
            self.display.close()
88
 
        self.display = MainDisplay(self)
89
 
        self.display.setup()
90
 
        self._theme_dimensions = {}
91
 
 
92
 
    def update_theme(self, theme_name, old_theme_name=None, only_delete=False):
93
 
        """
94
 
        This method updates the theme in ``_theme_dimensions`` when a theme has been edited or renamed.
95
 
 
96
 
        :param theme_name: The current theme name.
97
 
        :param old_theme_name: The old theme name. Has only to be passed, when the theme has been renamed.
98
 
            Defaults to *None*.
99
 
        :param only_delete: Only remove the given ``theme_name`` from the ``_theme_dimensions`` list. This can be
100
 
            used when a theme is permanently deleted.
101
 
        """
102
 
        if old_theme_name is not None and old_theme_name in self._theme_dimensions:
103
 
            del self._theme_dimensions[old_theme_name]
104
 
        if theme_name in self._theme_dimensions:
105
 
            del self._theme_dimensions[theme_name]
106
 
        if not only_delete and theme_name:
107
 
            self._set_theme(theme_name)
108
 
 
109
 
    def _set_theme(self, theme_name):
110
 
        """
111
 
        Helper method to save theme names and theme data.
112
 
 
113
 
        :param theme_name: The theme name
114
 
        """
115
 
        self.log_debug("_set_theme with theme {theme}".format(theme=theme_name))
116
 
        if theme_name not in self._theme_dimensions:
117
 
            theme_data = self.theme_manager.get_theme_data(theme_name)
118
 
            main_rect = self.get_main_rectangle(theme_data)
119
 
            footer_rect = self.get_footer_rectangle(theme_data)
120
 
            self._theme_dimensions[theme_name] = [theme_data, main_rect, footer_rect]
121
 
        else:
122
 
            theme_data, main_rect, footer_rect = self._theme_dimensions[theme_name]
123
 
        # if No file do not update cache
124
 
        if theme_data.background_filename:
125
 
            self.image_manager.add_image(path_to_str(theme_data.background_filename),
126
 
                                         ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
127
 
 
128
 
    def pre_render(self, override_theme_data=None):
129
 
        """
130
 
        Set up the theme to be used before rendering an item.
131
 
 
132
 
        :param override_theme_data: The theme data should be passed, when we want to use our own theme data, regardless
133
 
         of the theme level. This should for example be used in the theme manager. **Note**, this is **not** to
134
 
         be mixed up with the ``set_item_theme`` method.
135
 
        """
136
 
        # Just assume we use the global theme.
137
 
        theme_to_use = self.global_theme_name
138
 
        # The theme level is either set to Service or Item. Use the service theme if one is set. We also have to use the
139
 
        # service theme, even when the theme level is set to Item, because the item does not necessarily have to have a
140
 
        # theme.
141
 
        if self.theme_level != ThemeLevel.Global:
142
 
            # When the theme level is at Service and we actually have a service theme then use it.
143
 
            if self.service_theme_name:
144
 
                theme_to_use = self.service_theme_name
145
 
        # If we have Item level and have an item theme then use it.
146
 
        if self.theme_level == ThemeLevel.Song and self.item_theme_name:
147
 
            theme_to_use = self.item_theme_name
148
 
        if override_theme_data is None:
149
 
            if theme_to_use not in self._theme_dimensions:
150
 
                self._set_theme(theme_to_use)
151
 
            theme_data, main_rect, footer_rect = self._theme_dimensions[theme_to_use]
152
 
        else:
153
 
            # Ignore everything and use own theme data.
154
 
            theme_data = override_theme_data
155
 
            main_rect = self.get_main_rectangle(override_theme_data)
156
 
            footer_rect = self.get_footer_rectangle(override_theme_data)
157
 
        self._set_text_rectangle(theme_data, main_rect, footer_rect)
158
 
        return theme_data, self._rect, self._rect_footer
159
 
 
160
 
    def set_theme_level(self, theme_level):
161
 
        """
162
 
        Sets the theme level.
163
 
 
164
 
        :param theme_level: The theme level to be used.
165
 
        """
166
 
        self.theme_level = theme_level
167
 
 
168
 
    def set_global_theme(self):
169
 
        """
170
 
        Set the global-level theme name.
171
 
        """
172
 
        global_theme_name = Settings().value('themes/global theme')
173
 
        self._set_theme(global_theme_name)
174
 
        self.global_theme_name = global_theme_name
175
 
 
176
 
    def set_service_theme(self, service_theme_name):
177
 
        """
178
 
        Set the service-level theme.
179
 
 
180
 
        :param service_theme_name: The service level theme's name.
181
 
        """
182
 
        self._set_theme(service_theme_name)
183
 
        self.service_theme_name = service_theme_name
184
 
 
185
 
    def set_item_theme(self, item_theme_name):
186
 
        """
187
 
        Set the item-level theme. **Note**, this has to be done for each item we are rendering.
188
 
 
189
 
        :param item_theme_name: The item theme's name.
190
 
        """
191
 
        self.log_debug("set_item_theme with theme {theme}".format(theme=item_theme_name))
192
 
        self._set_theme(item_theme_name)
193
 
        self.item_theme_name = item_theme_name
194
 
 
195
 
    def generate_preview(self, theme_data, force_page=False):
196
 
        """
197
 
        Generate a preview of a theme.
198
 
 
199
 
        :param theme_data:  The theme to generated a preview for.
200
 
        :param force_page: Flag to tell message lines per page need to be generated.
201
 
        :rtype: QtGui.QPixmap
202
 
        """
203
 
        # save value for use in format_slide
204
 
        self.force_page = force_page
205
 
        # build a service item to generate preview
206
 
        service_item = ServiceItem()
207
 
        if self.force_page:
208
 
            # make big page for theme edit dialog to get line count
209
 
            service_item.add_from_text(VERSE_FOR_LINE_COUNT)
210
 
        else:
211
 
            service_item.add_from_text(VERSE)
212
 
        service_item.raw_footer = FOOTER
213
 
        # if No file do not update cache
214
 
        if theme_data.background_filename:
215
 
            self.image_manager.add_image(path_to_str(theme_data.background_filename),
216
 
                                         ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
217
 
        theme_data, main, footer = self.pre_render(theme_data)
218
 
        service_item.theme_data = theme_data
219
 
        service_item.main = main
220
 
        service_item.footer = footer
221
 
        service_item.render(True)
222
 
        if not self.force_page:
223
 
            self.display.build_html(service_item)
224
 
            raw_html = service_item.get_rendered_frame(0)
225
 
            self.display.text(raw_html, False)
226
 
            return self.display.preview()
227
 
        self.force_page = False
228
 
 
229
 
    def format_slide(self, text, item):
230
 
        """
231
 
        Calculate how much text can fit on a slide.
232
 
 
233
 
        :param text:  The words to go on the slides.
234
 
        :param item: The :class:`~openlp.core.lib.serviceitem.ServiceItem` item object.
235
 
 
236
 
        """
237
 
        self.log_debug('format slide')
238
 
        # Add line endings after each line of text used for bibles.
239
 
        line_end = '<br>'
240
 
        if item.is_capable(ItemCapabilities.NoLineBreaks):
241
 
            line_end = ' '
242
 
        # Bibles
243
 
        if item.is_capable(ItemCapabilities.CanWordSplit):
244
 
            pages = self._paginate_slide_words(text.split('\n'), line_end)
245
 
        # Songs and Custom
246
 
        elif item.is_capable(ItemCapabilities.CanSoftBreak):
247
 
            pages = []
248
 
            if '[---]' in text:
249
 
                # Remove Overflow split if at start of the text
250
 
                if text.startswith('[---]'):
251
 
                    text = text[5:]
252
 
                # Remove two or more option slide breaks next to each other (causing infinite loop).
253
 
                while '\n[---]\n[---]\n' in text:
254
 
                    text = text.replace('\n[---]\n[---]\n', '\n[---]\n')
255
 
                while ' [---]' in text:
256
 
                    text = text.replace(' [---]', '[---]')
257
 
                while '[---] ' in text:
258
 
                    text = text.replace('[---] ', '[---]')
259
 
                count = 0
260
 
                # only loop 5 times as there will never be more than 5 incorrect logical splits on a single slide.
261
 
                while True and count < 5:
262
 
                    slides = text.split('\n[---]\n', 2)
263
 
                    # If there are (at least) two occurrences of [---] we use the first two slides (and neglect the last
264
 
                    # for now).
265
 
                    if len(slides) == 3:
266
 
                        html_text = expand_tags('\n'.join(slides[:2]))
267
 
                    # We check both slides to determine if the optional split is needed (there is only one optional
268
 
                    # split).
269
 
                    else:
270
 
                        html_text = expand_tags('\n'.join(slides))
271
 
                    html_text = html_text.replace('\n', '<br>')
272
 
                    if self._text_fits_on_slide(html_text):
273
 
                        # The first two optional slides fit (as a whole) on one slide. Replace the first occurrence
274
 
                        # of [---].
275
 
                        text = text.replace('\n[---]', '', 1)
276
 
                    else:
277
 
                        # The first optional slide fits, which means we have to render the first optional slide.
278
 
                        text_contains_split = '[---]' in text
279
 
                        if text_contains_split:
280
 
                            try:
281
 
                                text_to_render, text = text.split('\n[---]\n', 1)
282
 
                            except ValueError:
283
 
                                text_to_render = text.split('\n[---]\n')[0]
284
 
                                text = ''
285
 
                            text_to_render, raw_tags, html_tags = get_start_tags(text_to_render)
286
 
                            if text:
287
 
                                text = raw_tags + text
288
 
                        else:
289
 
                            text_to_render = text
290
 
                            text = ''
291
 
                        lines = text_to_render.strip('\n').split('\n')
292
 
                        slides = self._paginate_slide(lines, line_end)
293
 
                        if len(slides) > 1 and text:
294
 
                            # Add all slides apart from the last one the list.
295
 
                            pages.extend(slides[:-1])
296
 
                            if text_contains_split:
297
 
                                text = slides[-1] + '\n[---]\n' + text
298
 
                            else:
299
 
                                text = slides[-1] + '\n' + text
300
 
                            text = text.replace('<br>', '\n')
301
 
                        else:
302
 
                            pages.extend(slides)
303
 
                    if '[---]' not in text:
304
 
                        lines = text.strip('\n').split('\n')
305
 
                        pages.extend(self._paginate_slide(lines, line_end))
306
 
                        break
307
 
                    count += 1
308
 
            else:
309
 
                # Clean up line endings.
310
 
                pages = self._paginate_slide(text.split('\n'), line_end)
311
 
        else:
312
 
            pages = self._paginate_slide(text.split('\n'), line_end)
313
 
        new_pages = []
314
 
        for page in pages:
315
 
            while page.endswith('<br>'):
316
 
                page = page[:-4]
317
 
            new_pages.append(page)
318
 
        return new_pages
319
 
 
320
 
    def _calculate_default(self):
321
 
        """
322
 
        Calculate the default dimensions of the screen.
323
 
        """
324
 
        screen_size = self.screens.current['size']
325
 
        self.width = screen_size.width()
326
 
        self.height = screen_size.height()
327
 
        self.screen_ratio = self.height / self.width
328
 
        self.log_debug('_calculate default {size}, {ratio:f}'.format(size=screen_size, ratio=self.screen_ratio))
329
 
        # 90% is start of footer
330
 
        self.footer_start = int(self.height * 0.90)
331
 
 
332
 
    def get_main_rectangle(self, theme_data):
333
 
        """
334
 
        Calculates the placement and size of the main rectangle.
335
 
 
336
 
        :param theme_data: The theme information
337
 
        """
338
 
        if not theme_data.font_main_override:
339
 
            return QtCore.QRect(10, 0, self.width - 20, self.footer_start)
340
 
        else:
341
 
            return QtCore.QRect(theme_data.font_main_x, theme_data.font_main_y,
342
 
                                theme_data.font_main_width - 1, theme_data.font_main_height - 1)
343
 
 
344
 
    def get_footer_rectangle(self, theme_data):
345
 
        """
346
 
        Calculates the placement and size of the footer rectangle.
347
 
 
348
 
        :param theme_data: The theme data.
349
 
        """
350
 
        if not theme_data.font_footer_override:
351
 
            return QtCore.QRect(10, self.footer_start, self.width - 20, self.height - self.footer_start)
352
 
        else:
353
 
            return QtCore.QRect(theme_data.font_footer_x,
354
 
                                theme_data.font_footer_y, theme_data.font_footer_width - 1,
355
 
                                theme_data.font_footer_height - 1)
356
 
 
357
 
    def _set_text_rectangle(self, theme_data, rect_main, rect_footer):
358
 
        """
359
 
        Sets the rectangle within which text should be rendered.
360
 
 
361
 
        :param theme_data: The theme data.
362
 
        :param rect_main: The main text block.
363
 
        :param rect_footer: The footer text block.
364
 
        """
365
 
        self.log_debug('_set_text_rectangle {main} , {footer}'.format(main=rect_main, footer=rect_footer))
366
 
        self._rect = rect_main
367
 
        self._rect_footer = rect_footer
368
 
        self.page_width = self._rect.width()
369
 
        self.page_height = self._rect.height()
370
 
        if theme_data.font_main_shadow:
371
 
            self.page_width -= int(theme_data.font_main_shadow_size)
372
 
            self.page_height -= int(theme_data.font_main_shadow_size)
373
 
        # For the life of my I don't know why we have to completely kill the QWebView in order for the display to work
374
 
        # properly, but we do. See bug #1041366 for an example of what happens if we take this out.
375
 
        self.web = None
376
 
        self.web = QtWebKitWidgets.QWebView()
377
 
        self.web.setVisible(False)
378
 
        self.web.resize(self.page_width, self.page_height)
379
 
        self.web_frame = self.web.page().mainFrame()
380
 
        # Adjust width and height to account for shadow. outline done in css.
381
 
        html = Template("""<!DOCTYPE html><html><head><script>
382
 
            function show_text(newtext) {
383
 
                var main = document.getElementById('main');
384
 
                main.innerHTML = newtext;
385
 
                // We need to be sure that the page is loaded, that is why we
386
 
                // return the element's height (even though we do not use the
387
 
                // returned value).
388
 
                return main.offsetHeight;
389
 
            }
390
 
            </script>
391
 
            <style>
392
 
                *{margin: 0; padding: 0; border: 0;}
393
 
                #main {position: absolute; top: 0px; ${format_css} ${outline_css}} ${chords_css}
394
 
            </style></head>
395
 
            <body><div id="main"></div></body></html>""")
396
 
        self.web.setHtml(html.substitute(format_css=build_lyrics_format_css(theme_data,
397
 
                                                                            self.page_width,
398
 
                                                                            self.page_height),
399
 
                                         outline_css=build_lyrics_outline_css(theme_data),
400
 
                                         chords_css=build_chords_css()))
401
 
        self.empty_height = self.web_frame.contentsSize().height()
402
 
 
403
 
    def _paginate_slide(self, lines, line_end):
404
 
        """
405
 
        Figure out how much text can appear on a slide, using the current theme settings.
406
 
 
407
 
        **Note:** The smallest possible "unit" of text for a slide is one line. If the line is too long it will be cut
408
 
        off when displayed.
409
 
 
410
 
        :param lines: The text to be fitted on the slide split into lines.
411
 
        :param line_end: The text added after each line. Either ``' '`` or ``'<br>``.
412
 
        """
413
 
        formatted = []
414
 
        previous_html = ''
415
 
        previous_raw = ''
416
 
        separator = '<br>'
417
 
        html_lines = list(map(expand_tags, lines))
418
 
        # Text too long so go to next page.
419
 
        if not self._text_fits_on_slide(separator.join(html_lines)):
420
 
            html_text, previous_raw = self._binary_chop(
421
 
                formatted, previous_html, previous_raw, html_lines, lines, separator, '')
422
 
        else:
423
 
            previous_raw = separator.join(lines)
424
 
        formatted.append(previous_raw)
425
 
        return formatted
426
 
 
427
 
    def _paginate_slide_words(self, lines, line_end):
428
 
        """
429
 
        Figure out how much text can appear on a slide, using the current theme settings.
430
 
 
431
 
        **Note:** The smallest possible "unit" of text for a slide is one word. If one line is too long it will be
432
 
        processed word by word. This is sometimes need for **bible** verses.
433
 
 
434
 
        :param lines: The text to be fitted on the slide split into lines.
435
 
        :param line_end: The text added after each line. Either ``' '`` or ``'<br>``. This is needed for **bibles**.
436
 
        """
437
 
        formatted = []
438
 
        previous_html = ''
439
 
        previous_raw = ''
440
 
        for line in lines:
441
 
            line = line.strip()
442
 
            html_line = expand_tags(line)
443
 
            # Text too long so go to next page.
444
 
            if not self._text_fits_on_slide(previous_html + html_line):
445
 
                # Check if there was a verse before the current one and append it, when it fits on the page.
446
 
                if previous_html:
447
 
                    if self._text_fits_on_slide(previous_html):
448
 
                        formatted.append(previous_raw)
449
 
                        previous_html = ''
450
 
                        previous_raw = ''
451
 
                        # Now check if the current verse will fit, if it does not we have to start to process the verse
452
 
                        # word by word.
453
 
                        if self._text_fits_on_slide(html_line):
454
 
                            previous_html = html_line + line_end
455
 
                            previous_raw = line + line_end
456
 
                            continue
457
 
                # Figure out how many words of the line will fit on screen as the line will not fit as a whole.
458
 
                raw_words = words_split(line)
459
 
                html_words = list(map(expand_tags, raw_words))
460
 
                previous_html, previous_raw = \
461
 
                    self._binary_chop(formatted, previous_html, previous_raw, html_words, raw_words, ' ', line_end)
462
 
            else:
463
 
                previous_html += html_line + line_end
464
 
                previous_raw += line + line_end
465
 
        formatted.append(previous_raw)
466
 
        return formatted
467
 
 
468
 
    def _binary_chop(self, formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end):
469
 
        """
470
 
        This implements the binary chop algorithm for faster rendering. This algorithm works line based (line by line)
471
 
        and word based (word by word). It is assumed that this method is **only** called, when the lines/words to be
472
 
        rendered do **not** fit as a whole.
473
 
 
474
 
        :param formatted: The list to append any slides.
475
 
        :param previous_html: The html text which is know to fit on a slide, but is not yet added to the list of
476
 
        slides. (unicode string)
477
 
        :param previous_raw: The raw text (with formatting tags) which is know to fit on a slide, but is not yet added
478
 
        to the list of slides. (unicode string)
479
 
        :param html_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
480
 
        The text contains html.
481
 
        :param raw_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
482
 
        The elements can contain formatting tags.
483
 
        :param separator: The separator for the elements. For lines this is ``'<br>'`` and for words this is ``' '``.
484
 
        :param line_end: The text added after each "element line". Either ``' '`` or ``'<br>``. This is needed for
485
 
         bibles.
486
 
        """
487
 
        smallest_index = 0
488
 
        highest_index = len(html_list) - 1
489
 
        index = highest_index // 2
490
 
        while True:
491
 
            if not self._text_fits_on_slide(previous_html + separator.join(html_list[:index + 1]).strip()):
492
 
                # We know that it does not fit, so change/calculate the new index and highest_index accordingly.
493
 
                highest_index = index
494
 
                index = index - (index - smallest_index) // 2
495
 
            else:
496
 
                smallest_index = index
497
 
                index = index + (highest_index - index) // 2
498
 
            # We found the number of words which will fit.
499
 
            if smallest_index == index or highest_index == index:
500
 
                index = smallest_index
501
 
                text = previous_raw.rstrip('<br>') + separator.join(raw_list[:index + 1])
502
 
                text, raw_tags, html_tags = get_start_tags(text)
503
 
                formatted.append(text)
504
 
                previous_html = ''
505
 
                previous_raw = ''
506
 
                # Stop here as the theme line count was requested.
507
 
                if self.force_page:
508
 
                    Registry().execute('theme_line_count', index + 1)
509
 
                    break
510
 
            else:
511
 
                continue
512
 
            # Check if the remaining elements fit on the slide.
513
 
            if self._text_fits_on_slide(html_tags + separator.join(html_list[index + 1:]).strip()):
514
 
                previous_html = html_tags + separator.join(html_list[index + 1:]).strip() + line_end
515
 
                previous_raw = raw_tags + separator.join(raw_list[index + 1:]).strip() + line_end
516
 
                break
517
 
            else:
518
 
                # The remaining elements do not fit, thus reset the indexes, create a new list and continue.
519
 
                raw_list = raw_list[index + 1:]
520
 
                raw_list[0] = raw_tags + raw_list[0]
521
 
                html_list = html_list[index + 1:]
522
 
                html_list[0] = html_tags + html_list[0]
523
 
                smallest_index = 0
524
 
                highest_index = len(html_list) - 1
525
 
                index = highest_index // 2
526
 
        return previous_html, previous_raw
527
 
 
528
 
    def _text_fits_on_slide(self, text):
529
 
        """
530
 
        Checks if the given ``text`` fits on a slide. If it does ``True`` is returned, otherwise ``False``.
531
 
 
532
 
        :param text:  The text to check. It may contain HTML tags.
533
 
        """
534
 
        self.web_frame.evaluateJavaScript('show_text'
535
 
                                          '("{text}")'.format(text=text.replace('\\', '\\\\').replace('\"', '\\\"')))
536
 
        return self.web_frame.contentsSize().height() <= self.empty_height
537
 
 
538
 
 
539
 
def words_split(line):
540
 
    """
541
 
    Split the slide up by word so can wrap better
542
 
 
543
 
    :param line: Line to be split
544
 
    """
545
 
    # this parse we are to be wordy
546
 
    return re.split(r'\s+', line)
547
 
 
548
 
 
549
 
def get_start_tags(raw_text):
550
 
    """
551
 
    Tests the given text for not closed formatting tags and returns a tuple consisting of three unicode strings::
552
 
 
553
 
        ('{st}{r}Text text text{/r}{/st}', '{st}{r}', '<strong><span style="-webkit-text-fill-color:red">')
554
 
 
555
 
    The first unicode string is the text, with correct closing tags. The second unicode string are OpenLP's opening
556
 
    formatting tags and the third unicode string the html opening formatting tags.
557
 
 
558
 
    :param raw_text: The text to test. The text must **not** contain html tags, only OpenLP formatting tags
559
 
    are allowed::
560
 
            {st}{r}Text text text
561
 
    """
562
 
    raw_tags = []
563
 
    html_tags = []
564
 
    for tag in FormattingTags.get_html_tags():
565
 
        if tag['start tag'] == '{br}':
566
 
            continue
567
 
        if raw_text.count(tag['start tag']) != raw_text.count(tag['end tag']):
568
 
            raw_tags.append((raw_text.find(tag['start tag']), tag['start tag'], tag['end tag']))
569
 
            html_tags.append((raw_text.find(tag['start tag']), tag['start html']))
570
 
    # Sort the lists, so that the tags which were opened first on the first slide (the text we are checking) will be
571
 
    # opened first on the next slide as well.
572
 
    raw_tags.sort(key=lambda tag: tag[0])
573
 
    html_tags.sort(key=lambda tag: tag[0])
574
 
    # Create a list with closing tags for the raw_text.
575
 
    end_tags = []
576
 
    start_tags = []
577
 
    for tag in raw_tags:
578
 
        start_tags.append(tag[1])
579
 
        end_tags.append(tag[2])
580
 
    end_tags.reverse()
581
 
    # Remove the indexes.
582
 
    html_tags = [tag[1] for tag in html_tags]
583
 
    return raw_text + ''.join(end_tags), ''.join(start_tags), ''.join(html_tags)