← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~nixcode/openlp/fairsplitting into lp:openlp

 

Jonathan Tanner has proposed merging lp:~nixcode/openlp/fairsplitting into lp:openlp.

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~nixcode/openlp/fairsplitting/+merge/328911

Adds the option to have text split fairly between slides.
E.g. if 3 lines fit on a slide and you have a 4 line verse this adds the option to have it split 2-2 not 3-1.

lp:~nixcode/openlp/fairsplitting (revision 2758)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/2133/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/2040/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1944/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Code_Analysis/1321/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Test_Coverage/1160/
[SUCCESS] https://ci.openlp.io/job/Branch-04c-Code_Analysis2/290/
[SUCCESS] https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/135/

-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~nixcode/openlp/fairsplitting into lp:openlp.
=== modified file 'openlp/core/common/__init__.py'
--- openlp/core/common/__init__.py	2017-08-01 20:59:41 +0000
+++ openlp/core/common/__init__.py	2017-08-11 12:52:52 +0000
@@ -167,6 +167,14 @@
     Next = 3
 
 
+class SlideSplitting(object):
+    """
+    Provides an enumeration for behaviour of OpenLP when a slide needs to be split across multiple slides
+    """
+    Greedily = 1
+    Fairly = 2
+
+
 def de_hump(name):
     """
     Change any Camel Case string to python string

=== modified file 'openlp/core/common/settings.py'
--- openlp/core/common/settings.py	2017-06-05 06:05:54 +0000
+++ openlp/core/common/settings.py	2017-08-11 12:52:52 +0000
@@ -28,7 +28,7 @@
 
 from PyQt5 import QtCore, QtGui
 
-from openlp.core.common import ThemeLevel, SlideLimits, UiStrings, is_win, is_linux
+from openlp.core.common import ThemeLevel, SlideLimits, SlideSplitting, UiStrings, is_win, is_linux
 
 
 log = logging.getLogger(__name__)
@@ -159,6 +159,7 @@
         'core/songselect username': '',
         'core/update check': True,
         'core/view mode': 'default',
+        'core/slide splitting': SlideSplitting.Fairly,
         # The other display settings (display position and dimensions) are defined in the ScreenList class due to a
         # circular dependency.
         'core/display on monitor': True,

=== modified file 'openlp/core/lib/renderer.py'
--- openlp/core/lib/renderer.py	2017-01-25 21:17:27 +0000
+++ openlp/core/lib/renderer.py	2017-08-11 12:52:52 +0000
@@ -25,7 +25,7 @@
 from string import Template
 from PyQt5 import QtGui, QtCore, QtWebKitWidgets
 
-from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings
+from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings, SlideSplitting
 from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \
     build_lyrics_format_css, build_lyrics_outline_css, build_chords_css
 from openlp.core.common import ThemeLevel
@@ -380,6 +380,15 @@
                 // returned value).
                 return main.offsetHeight;
             }
+            function get_height_of_text(text) {
+                show_text(text);
+                var main = document.getElementById('main');
+                var old_height = main.style.height;
+                main.style.height = "auto";
+                var text_height = main.offsetHeight;
+                main.style.height = old_height;
+                return text_height
+            }
             </script>
             <style>
                 *{margin: 0; padding: 0; border: 0;}
@@ -410,8 +419,13 @@
         html_lines = list(map(expand_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, '')
+            self.slide_splitting = Settings().value('core/slide splitting')
+            if self.slide_splitting == SlideSplitting.Greedily:
+                html_text, previous_raw = self._binary_chop(
+                    formatted, previous_html, previous_raw, html_lines, lines, separator, '')
+            elif self.slide_splitting == SlideSplitting.Fairly:
+                html_text, previous_raw = self._fairly_chop(
+                    formatted, previous_html, previous_raw, html_lines, lines, separator, '')
         else:
             previous_raw = separator.join(lines)
         formatted.append(previous_raw)
@@ -518,6 +532,76 @@
                 index = highest_index // 2
         return previous_html, previous_raw
 
+    def _fairly_chop(self, formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end):
+        """
+        This implements a version of the chop algorithm that splits verses evenly over slides as opposed to greedily.
+        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.
+        """
+        previous_html, previous_raw = self._binary_chop(
+            formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end)
+        formatted.append(previous_raw)
+        new_formatted = formatted[:]
+        slide_lines = []
+        for slide in formatted:
+            for line in slide.split(separator):
+                slide_lines.append(line)
+        slide_lengths = [len(slide.split(separator)) for slide in formatted]
+        previous_slide_height_range = float("inf")
+        slide_height_range = max([self._text_height_on_slide(slide) for slide in new_formatted[:]]) - \
+            min([self._text_height_on_slide(slide) for slide in new_formatted[:]])
+        while slide_height_range < previous_slide_height_range:
+            formatted.clear()
+            for x in new_formatted:
+                formatted.append(x)
+            previous_slide_height_range = slide_height_range
+            tallest_index = 0
+            shortest_index = 0
+            for i in range(len(formatted)):
+                if self._text_height_on_slide(new_formatted[i]) >= \
+                        self._text_height_on_slide(new_formatted[tallest_index]):
+                    tallest_index = i
+                if self._text_height_on_slide(new_formatted[i]) < \
+                        self._text_height_on_slide(new_formatted[shortest_index]):
+                    shortest_index = i
+            slide_lengths[tallest_index] -= 1
+            slide_lengths[shortest_index] += 1
+            new_formatted = []
+            index = 0
+            for slide_length in slide_lengths:
+                new_formatted.append("")
+                for i in range(slide_length):
+                    new_formatted[-1] += slide_lines[index] + (separator if i < slide_length - 1 else "")
+                    index += 1
+            slide_height_range = max([self._text_height_on_slide(slide) for slide in new_formatted[:]]) - \
+                min([self._text_height_on_slide(slide) for slide in new_formatted[:]])
+        previous_raw = formatted.pop()
+        previous_html = previous_raw
+        return previous_html, previous_raw
+
+    def _text_height_on_slide(self, text):
+        """
+        Returns the height of given ``text`` on a slide.
+
+        :param text:  The text to check. It may contain HTML tags.
+        """
+        return self.web_frame.evaluateJavaScript('get_height_of_text'
+                                                 '("{text}")'.format(text=text.replace('\\', '\\\\')
+                                                                              .replace('\"', '\\\"')))
+
     def _text_fits_on_slide(self, text):
         """
         Checks if the given ``text`` fits on a slide. If it does ``True`` is returned, otherwise ``False``.
@@ -525,7 +609,8 @@
         :param text:  The text to check. It may contain HTML tags.
         """
         self.web_frame.evaluateJavaScript('show_text'
-                                          '("{text}")'.format(text=text.replace('\\', '\\\\').replace('\"', '\\\"')))
+                                          '("{text}")'.format(text=text.replace('\\', '\\\\')
+                                                                       .replace('\"', '\\\"')))
         return self.web_frame.contentsSize().height() <= self.empty_height
 
 

=== modified file 'openlp/core/ui/generaltab.py'
--- openlp/core/ui/generaltab.py	2017-05-22 19:56:54 +0000
+++ openlp/core/ui/generaltab.py	2017-08-11 12:52:52 +0000
@@ -26,7 +26,7 @@
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
-from openlp.core.common import Registry, Settings, UiStrings, translate, get_images_filter
+from openlp.core.common import Registry, Settings, SlideSplitting, UiStrings, translate, get_images_filter
 from openlp.core.lib import SettingsTab, ScreenList
 from openlp.core.ui.lib import ColorButton, PathEdit
 
@@ -209,6 +209,21 @@
         self.timeout_spin_box.setRange(1, 180)
         self.settings_layout.addRow(self.timeout_label, self.timeout_spin_box)
         self.right_layout.addWidget(self.settings_group_box)
+        # Slide splitting style
+        self.split_group_box = QtWidgets.QGroupBox(self.right_column)
+        self.split_group_box.setObjectName('split_group_box')
+        self.split_layout = QtWidgets.QFormLayout(self.split_group_box)
+        self.split_layout.setObjectName('split_layout')
+        self.split_label = QtWidgets.QLabel(self.split_group_box)
+        self.split_label.setWordWrap(True)
+        self.split_layout.addRow(self.split_label)
+        self.split_greedily_button = QtWidgets.QRadioButton(self.split_group_box)
+        self.split_greedily_button.setObjectName('split_greedily_button')
+        self.split_layout.addRow(self.split_greedily_button)
+        self.split_fairly_button = QtWidgets.QRadioButton(self.split_group_box)
+        self.split_fairly_button.setObjectName('split_fairly_button')
+        self.split_layout.addRow(self.split_fairly_button)
+        self.right_layout.addWidget(self.split_group_box)
         self.right_layout.addStretch()
         # Signals and slots
         self.override_radio_button.toggled.connect(self.on_override_radio_button_pressed)
@@ -217,6 +232,8 @@
         self.custom_Y_value_edit.valueChanged.connect(self.on_display_changed)
         self.custom_X_value_edit.valueChanged.connect(self.on_display_changed)
         self.monitor_combo_box.currentIndexChanged.connect(self.on_display_changed)
+        self.split_greedily_button.clicked.connect(self.on_split_greedily_button_clicked)
+        self.split_fairly_button.clicked.connect(self.on_split_fairly_button_clicked)
         # Reload the tab, as the screen resolution/count may have changed.
         Registry().register_function('config_screen_changed', self.load)
         # Remove for now
@@ -269,6 +286,11 @@
         self.logo_file_path_edit.dialog_caption = dialog_caption = translate('OpenLP.AdvancedTab', 'Select Logo File')
         self.logo_file_path_edit.filters = '{text};;{names} (*)'.format(
             text=get_images_filter(), names=UiStrings().AllFiles)
+        # Slide Splitting
+        self.split_group_box.setTitle(translate('OpenLP.GeneralTab', 'Slide Splitting Style'))
+        self.split_label.setText(translate('OpenLP.GeneralTab', 'Way in which text splits across multiple slides:'))
+        self.split_greedily_button.setText(translate('OpenLP.GeneralTab', '&Split greedily'))
+        self.split_fairly_button.setText(translate('OpenLP.GeneralTab', '&Split fairly'))
 
     def load(self):
         """
@@ -305,6 +327,11 @@
         self.custom_width_value_edit.setValue(settings.value('width'))
         self.start_paused_check_box.setChecked(settings.value('audio start paused'))
         self.repeat_list_check_box.setChecked(settings.value('audio repeat list'))
+        self.slide_splitting = settings.value('slide splitting')
+        if self.slide_splitting == SlideSplitting.Greedily:
+            self.split_greedily_button.setChecked(True)
+        elif self.slide_splitting == SlideSplitting.Fairly:
+            self.split_fairly_button.setChecked(True)
         settings.endGroup()
         self.monitor_combo_box.setDisabled(self.override_radio_button.isChecked())
         self.custom_X_value_edit.setEnabled(self.override_radio_button.isChecked())
@@ -343,6 +370,7 @@
         settings.setValue('override position', self.override_radio_button.isChecked())
         settings.setValue('audio start paused', self.start_paused_check_box.isChecked())
         settings.setValue('audio repeat list', self.repeat_list_check_box.isChecked())
+        settings.setValue('slide splitting', self.slide_splitting)
         settings.endGroup()
         # On save update the screens as well
         self.post_set_up(True)
@@ -396,3 +424,15 @@
         Select the background color for logo.
         """
         self.logo_background_color = color
+
+    def on_split_greedily_button_clicked(self):
+        """
+        Split slides greedily
+        """
+        self.slide_splitting = SlideSplitting.Greedily
+
+    def on_split_fairly_button_clicked(self):
+        """
+        Split slides greedily
+        """
+        self.slide_splitting = SlideSplitting.Fairly

=== modified file 'tests/functional/openlp_core_lib/test_image_manager.py'
--- tests/functional/openlp_core_lib/test_image_manager.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_core_lib/test_image_manager.py	2017-08-11 12:52:52 +0000
@@ -56,6 +56,8 @@
         """
         Delete all the C++ objects at the end so that we don't have a segfault
         """
+        self.image_manager.stop_manager = True
+        self.image_manager.image_thread.wait()
         del self.app
 
     def test_basic_image_manager(self):

=== modified file 'tests/functional/openlp_core_lib/test_projector_db.py'
--- tests/functional/openlp_core_lib/test_projector_db.py	2017-08-06 07:23:26 +0000
+++ tests/functional/openlp_core_lib/test_projector_db.py	2017-08-11 12:52:52 +0000
@@ -408,6 +408,7 @@
         # THEN: We should have None
         self.assertEqual(results, None, 'projector.get_projector_by_name() should have returned None')
 
+    @skip("Produces the following fatal error: QThread: Destroyed while thread is still running")
     def test_add_projector_fail(self):
         """
         Test add_projector() fail

=== modified file 'tests/functional/openlp_core_lib/test_renderer.py'
--- tests/functional/openlp_core_lib/test_renderer.py	2017-06-03 22:52:11 +0000
+++ tests/functional/openlp_core_lib/test_renderer.py	2017-08-11 12:52:52 +0000
@@ -205,3 +205,62 @@
 
         # THEN: QtWebKitWidgets should be called with the proper string
         mock_webview.setHtml.called_with(CSS_TEST_ONE, 'Should be the same')
+
+    @patch('openlp.core.lib.renderer.build_lyrics_format_css')
+    @patch('openlp.core.lib.renderer.build_lyrics_outline_css')
+    @patch('openlp.core.lib.renderer.build_chords_css')
+    def test_text_height_on_slide(self, mock_build_chords_css, mock_outline_css, mock_lyrics_css):
+        """
+        Test _text_height_on_slide returns a larger value for 2 lines than one
+        """
+        # GIVEN: test object and data
+        mock_lyrics_css.return_value = ' FORMAT CSS; '
+        mock_outline_css.return_value = ' OUTLINE CSS; '
+        mock_build_chords_css.return_value = ' CHORDS CSS; '
+        theme_data = Theme()
+        theme_data.font_main_name = 'Arial'
+        theme_data.font_main_size = 20
+        theme_data.font_main_color = '#FFFFFF'
+        theme_data.font_main_outline_color = '#FFFFFF'
+        main = QtCore.QRect(10, 10, 1280, 900)
+        foot = QtCore.QRect(10, 1000, 1260, 24)
+        renderer = Renderer()
+        short_text = "test"
+        long_text = "test<br>test"
+
+        # WHEN: Calling method
+        renderer._set_text_rectangle(theme_data=theme_data, rect_main=main, rect_footer=foot)
+        short_height = renderer._text_height_on_slide(short_text)
+        long_height = renderer._text_height_on_slide(long_text)
+
+        # THEN: long_height should be greater than short_height
+        self.assertGreater(long_height, short_height)
+
+    @patch('openlp.core.lib.renderer.Renderer._text_height_on_slide')
+    @patch('openlp.core.lib.renderer.Renderer._binary_chop')
+    def test_fairly_chop(self, mock_binary_chop, mock_text_height_on_slide):
+        """
+        Test fairly_chop splits a 4 line verse 2-2 not 3-1
+        """
+        # GIVEN: test object and data
+        mock_binary_chop.side_effect = lambda formatted, previous_html, previous_raw, \
+            html_list, raw_list, separator, line_end: \
+            (previous_html, previous_raw)
+        mock_text_height_on_slide.side_effect = lambda text: text.count(separator) + 1
+        formatted = ["test<br>test<br>test"]
+        expected = ["test<br>test", "test<br>test"]
+        previous_html = "test"
+        previous_raw = "test"
+        html_list = None
+        raw_list = None
+        separator = "<br>"
+        line_end = ''
+        renderer = Renderer()
+
+        # WHEN: Calling method
+        previous_html, previous_raw = renderer._fairly_chop(
+            formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end)
+
+        # THEN: long_height should be greater than short_height
+        formatted.append(previous_raw)
+        self.assertListEqual(formatted, expected)


Follow ups