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

Proposed by Jonathan Tanner
Status: Needs review
Proposed branch: lp:~nixcode/openlp/fairsplitting
Merge into: lp:openlp
Diff against target: 355 lines (+202/-6)
7 files modified
openlp/core/common/__init__.py (+8/-0)
openlp/core/common/settings.py (+2/-1)
openlp/core/lib/renderer.py (+89/-4)
openlp/core/ui/generaltab.py (+41/-1)
tests/functional/openlp_core_lib/test_image_manager.py (+2/-0)
tests/functional/openlp_core_lib/test_projector_db.py (+1/-0)
tests/functional/openlp_core_lib/test_renderer.py (+59/-0)
To merge this branch: bzr merge lp:~nixcode/openlp/fairsplitting
Reviewer Review Type Date Requested Status
Tomas Groth Needs Information
Review via email: mp+328911@code.launchpad.net

Description of the change

To post a comment you must log in.
Revision history for this message
Tomas Groth (tomasgroth) wrote :

Hi Jonathan,

I think this is actually a great idea, but my main concern is how this will fit in with the new render that is being worked on in a separate branch/repo. I expect Raoul to be able to answer this.

review: Needs Information

Unmerged revisions

2758. By Jonathan Tanner

Replaced math.inf with float(inf) for pre Python3.5 compatibility

2757. By Jonathan Tanner

Fixed PEP8 errors

2756. By Jonathan Tanner

added slide splitting controls

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== 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 @@
167 Next = 3167 Next = 3
168168
169169
170class SlideSplitting(object):
171 """
172 Provides an enumeration for behaviour of OpenLP when a slide needs to be split across multiple slides
173 """
174 Greedily = 1
175 Fairly = 2
176
177
170def de_hump(name):178def de_hump(name):
171 """179 """
172 Change any Camel Case string to python string180 Change any Camel Case string to python string
173181
=== 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 @@
2828
29from PyQt5 import QtCore, QtGui29from PyQt5 import QtCore, QtGui
3030
31from openlp.core.common import ThemeLevel, SlideLimits, UiStrings, is_win, is_linux31from openlp.core.common import ThemeLevel, SlideLimits, SlideSplitting, UiStrings, is_win, is_linux
3232
3333
34log = logging.getLogger(__name__)34log = logging.getLogger(__name__)
@@ -159,6 +159,7 @@
159 'core/songselect username': '',159 'core/songselect username': '',
160 'core/update check': True,160 'core/update check': True,
161 'core/view mode': 'default',161 'core/view mode': 'default',
162 'core/slide splitting': SlideSplitting.Fairly,
162 # The other display settings (display position and dimensions) are defined in the ScreenList class due to a163 # The other display settings (display position and dimensions) are defined in the ScreenList class due to a
163 # circular dependency.164 # circular dependency.
164 'core/display on monitor': True,165 'core/display on monitor': True,
165166
=== 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 @@
25from string import Template25from string import Template
26from PyQt5 import QtGui, QtCore, QtWebKitWidgets26from PyQt5 import QtGui, QtCore, QtWebKitWidgets
2727
28from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings28from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings, SlideSplitting
29from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \29from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \
30 build_lyrics_format_css, build_lyrics_outline_css, build_chords_css30 build_lyrics_format_css, build_lyrics_outline_css, build_chords_css
31from openlp.core.common import ThemeLevel31from openlp.core.common import ThemeLevel
@@ -380,6 +380,15 @@
380 // returned value).380 // returned value).
381 return main.offsetHeight;381 return main.offsetHeight;
382 }382 }
383 function get_height_of_text(text) {
384 show_text(text);
385 var main = document.getElementById('main');
386 var old_height = main.style.height;
387 main.style.height = "auto";
388 var text_height = main.offsetHeight;
389 main.style.height = old_height;
390 return text_height
391 }
383 </script>392 </script>
384 <style>393 <style>
385 *{margin: 0; padding: 0; border: 0;}394 *{margin: 0; padding: 0; border: 0;}
@@ -410,8 +419,13 @@
410 html_lines = list(map(expand_tags, lines))419 html_lines = list(map(expand_tags, lines))
411 # Text too long so go to next page.420 # Text too long so go to next page.
412 if not self._text_fits_on_slide(separator.join(html_lines)):421 if not self._text_fits_on_slide(separator.join(html_lines)):
413 html_text, previous_raw = self._binary_chop(422 self.slide_splitting = Settings().value('core/slide splitting')
414 formatted, previous_html, previous_raw, html_lines, lines, separator, '')423 if self.slide_splitting == SlideSplitting.Greedily:
424 html_text, previous_raw = self._binary_chop(
425 formatted, previous_html, previous_raw, html_lines, lines, separator, '')
426 elif self.slide_splitting == SlideSplitting.Fairly:
427 html_text, previous_raw = self._fairly_chop(
428 formatted, previous_html, previous_raw, html_lines, lines, separator, '')
415 else:429 else:
416 previous_raw = separator.join(lines)430 previous_raw = separator.join(lines)
417 formatted.append(previous_raw)431 formatted.append(previous_raw)
@@ -518,6 +532,76 @@
518 index = highest_index // 2532 index = highest_index // 2
519 return previous_html, previous_raw533 return previous_html, previous_raw
520534
535 def _fairly_chop(self, formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end):
536 """
537 This implements a version of the chop algorithm that splits verses evenly over slides as opposed to greedily.
538 This algorithm works line based (line by line) and word based (word by word). It is assumed that this method
539 is **only** called, when the lines/words to be rendered do **not** fit as a whole.
540
541 :param formatted: The list to append any slides.
542 :param previous_html: The html text which is know to fit on a slide, but is not yet added to the list of
543 slides. (unicode string)
544 :param previous_raw: The raw text (with formatting tags) which is know to fit on a slide, but is not yet added
545 to the list of slides. (unicode string)
546 :param html_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
547 The text contains html.
548 :param raw_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
549 The elements can contain formatting tags.
550 :param separator: The separator for the elements. For lines this is ``'<br>'`` and for words this is ``' '``.
551 :param line_end: The text added after each "element line". Either ``' '`` or ``'<br>``. This is needed for
552 bibles.
553 """
554 previous_html, previous_raw = self._binary_chop(
555 formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end)
556 formatted.append(previous_raw)
557 new_formatted = formatted[:]
558 slide_lines = []
559 for slide in formatted:
560 for line in slide.split(separator):
561 slide_lines.append(line)
562 slide_lengths = [len(slide.split(separator)) for slide in formatted]
563 previous_slide_height_range = float("inf")
564 slide_height_range = max([self._text_height_on_slide(slide) for slide in new_formatted[:]]) - \
565 min([self._text_height_on_slide(slide) for slide in new_formatted[:]])
566 while slide_height_range < previous_slide_height_range:
567 formatted.clear()
568 for x in new_formatted:
569 formatted.append(x)
570 previous_slide_height_range = slide_height_range
571 tallest_index = 0
572 shortest_index = 0
573 for i in range(len(formatted)):
574 if self._text_height_on_slide(new_formatted[i]) >= \
575 self._text_height_on_slide(new_formatted[tallest_index]):
576 tallest_index = i
577 if self._text_height_on_slide(new_formatted[i]) < \
578 self._text_height_on_slide(new_formatted[shortest_index]):
579 shortest_index = i
580 slide_lengths[tallest_index] -= 1
581 slide_lengths[shortest_index] += 1
582 new_formatted = []
583 index = 0
584 for slide_length in slide_lengths:
585 new_formatted.append("")
586 for i in range(slide_length):
587 new_formatted[-1] += slide_lines[index] + (separator if i < slide_length - 1 else "")
588 index += 1
589 slide_height_range = max([self._text_height_on_slide(slide) for slide in new_formatted[:]]) - \
590 min([self._text_height_on_slide(slide) for slide in new_formatted[:]])
591 previous_raw = formatted.pop()
592 previous_html = previous_raw
593 return previous_html, previous_raw
594
595 def _text_height_on_slide(self, text):
596 """
597 Returns the height of given ``text`` on a slide.
598
599 :param text: The text to check. It may contain HTML tags.
600 """
601 return self.web_frame.evaluateJavaScript('get_height_of_text'
602 '("{text}")'.format(text=text.replace('\\', '\\\\')
603 .replace('\"', '\\\"')))
604
521 def _text_fits_on_slide(self, text):605 def _text_fits_on_slide(self, text):
522 """606 """
523 Checks if the given ``text`` fits on a slide. If it does ``True`` is returned, otherwise ``False``.607 Checks if the given ``text`` fits on a slide. If it does ``True`` is returned, otherwise ``False``.
@@ -525,7 +609,8 @@
525 :param text: The text to check. It may contain HTML tags.609 :param text: The text to check. It may contain HTML tags.
526 """610 """
527 self.web_frame.evaluateJavaScript('show_text'611 self.web_frame.evaluateJavaScript('show_text'
528 '("{text}")'.format(text=text.replace('\\', '\\\\').replace('\"', '\\\"')))612 '("{text}")'.format(text=text.replace('\\', '\\\\')
613 .replace('\"', '\\\"')))
529 return self.web_frame.contentsSize().height() <= self.empty_height614 return self.web_frame.contentsSize().height() <= self.empty_height
530615
531616
532617
=== 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 @@
2626
27from PyQt5 import QtCore, QtGui, QtWidgets27from PyQt5 import QtCore, QtGui, QtWidgets
2828
29from openlp.core.common import Registry, Settings, UiStrings, translate, get_images_filter29from openlp.core.common import Registry, Settings, SlideSplitting, UiStrings, translate, get_images_filter
30from openlp.core.lib import SettingsTab, ScreenList30from openlp.core.lib import SettingsTab, ScreenList
31from openlp.core.ui.lib import ColorButton, PathEdit31from openlp.core.ui.lib import ColorButton, PathEdit
3232
@@ -209,6 +209,21 @@
209 self.timeout_spin_box.setRange(1, 180)209 self.timeout_spin_box.setRange(1, 180)
210 self.settings_layout.addRow(self.timeout_label, self.timeout_spin_box)210 self.settings_layout.addRow(self.timeout_label, self.timeout_spin_box)
211 self.right_layout.addWidget(self.settings_group_box)211 self.right_layout.addWidget(self.settings_group_box)
212 # Slide splitting style
213 self.split_group_box = QtWidgets.QGroupBox(self.right_column)
214 self.split_group_box.setObjectName('split_group_box')
215 self.split_layout = QtWidgets.QFormLayout(self.split_group_box)
216 self.split_layout.setObjectName('split_layout')
217 self.split_label = QtWidgets.QLabel(self.split_group_box)
218 self.split_label.setWordWrap(True)
219 self.split_layout.addRow(self.split_label)
220 self.split_greedily_button = QtWidgets.QRadioButton(self.split_group_box)
221 self.split_greedily_button.setObjectName('split_greedily_button')
222 self.split_layout.addRow(self.split_greedily_button)
223 self.split_fairly_button = QtWidgets.QRadioButton(self.split_group_box)
224 self.split_fairly_button.setObjectName('split_fairly_button')
225 self.split_layout.addRow(self.split_fairly_button)
226 self.right_layout.addWidget(self.split_group_box)
212 self.right_layout.addStretch()227 self.right_layout.addStretch()
213 # Signals and slots228 # Signals and slots
214 self.override_radio_button.toggled.connect(self.on_override_radio_button_pressed)229 self.override_radio_button.toggled.connect(self.on_override_radio_button_pressed)
@@ -217,6 +232,8 @@
217 self.custom_Y_value_edit.valueChanged.connect(self.on_display_changed)232 self.custom_Y_value_edit.valueChanged.connect(self.on_display_changed)
218 self.custom_X_value_edit.valueChanged.connect(self.on_display_changed)233 self.custom_X_value_edit.valueChanged.connect(self.on_display_changed)
219 self.monitor_combo_box.currentIndexChanged.connect(self.on_display_changed)234 self.monitor_combo_box.currentIndexChanged.connect(self.on_display_changed)
235 self.split_greedily_button.clicked.connect(self.on_split_greedily_button_clicked)
236 self.split_fairly_button.clicked.connect(self.on_split_fairly_button_clicked)
220 # Reload the tab, as the screen resolution/count may have changed.237 # Reload the tab, as the screen resolution/count may have changed.
221 Registry().register_function('config_screen_changed', self.load)238 Registry().register_function('config_screen_changed', self.load)
222 # Remove for now239 # Remove for now
@@ -269,6 +286,11 @@
269 self.logo_file_path_edit.dialog_caption = dialog_caption = translate('OpenLP.AdvancedTab', 'Select Logo File')286 self.logo_file_path_edit.dialog_caption = dialog_caption = translate('OpenLP.AdvancedTab', 'Select Logo File')
270 self.logo_file_path_edit.filters = '{text};;{names} (*)'.format(287 self.logo_file_path_edit.filters = '{text};;{names} (*)'.format(
271 text=get_images_filter(), names=UiStrings().AllFiles)288 text=get_images_filter(), names=UiStrings().AllFiles)
289 # Slide Splitting
290 self.split_group_box.setTitle(translate('OpenLP.GeneralTab', 'Slide Splitting Style'))
291 self.split_label.setText(translate('OpenLP.GeneralTab', 'Way in which text splits across multiple slides:'))
292 self.split_greedily_button.setText(translate('OpenLP.GeneralTab', '&Split greedily'))
293 self.split_fairly_button.setText(translate('OpenLP.GeneralTab', '&Split fairly'))
272294
273 def load(self):295 def load(self):
274 """296 """
@@ -305,6 +327,11 @@
305 self.custom_width_value_edit.setValue(settings.value('width'))327 self.custom_width_value_edit.setValue(settings.value('width'))
306 self.start_paused_check_box.setChecked(settings.value('audio start paused'))328 self.start_paused_check_box.setChecked(settings.value('audio start paused'))
307 self.repeat_list_check_box.setChecked(settings.value('audio repeat list'))329 self.repeat_list_check_box.setChecked(settings.value('audio repeat list'))
330 self.slide_splitting = settings.value('slide splitting')
331 if self.slide_splitting == SlideSplitting.Greedily:
332 self.split_greedily_button.setChecked(True)
333 elif self.slide_splitting == SlideSplitting.Fairly:
334 self.split_fairly_button.setChecked(True)
308 settings.endGroup()335 settings.endGroup()
309 self.monitor_combo_box.setDisabled(self.override_radio_button.isChecked())336 self.monitor_combo_box.setDisabled(self.override_radio_button.isChecked())
310 self.custom_X_value_edit.setEnabled(self.override_radio_button.isChecked())337 self.custom_X_value_edit.setEnabled(self.override_radio_button.isChecked())
@@ -343,6 +370,7 @@
343 settings.setValue('override position', self.override_radio_button.isChecked())370 settings.setValue('override position', self.override_radio_button.isChecked())
344 settings.setValue('audio start paused', self.start_paused_check_box.isChecked())371 settings.setValue('audio start paused', self.start_paused_check_box.isChecked())
345 settings.setValue('audio repeat list', self.repeat_list_check_box.isChecked())372 settings.setValue('audio repeat list', self.repeat_list_check_box.isChecked())
373 settings.setValue('slide splitting', self.slide_splitting)
346 settings.endGroup()374 settings.endGroup()
347 # On save update the screens as well375 # On save update the screens as well
348 self.post_set_up(True)376 self.post_set_up(True)
@@ -396,3 +424,15 @@
396 Select the background color for logo.424 Select the background color for logo.
397 """425 """
398 self.logo_background_color = color426 self.logo_background_color = color
427
428 def on_split_greedily_button_clicked(self):
429 """
430 Split slides greedily
431 """
432 self.slide_splitting = SlideSplitting.Greedily
433
434 def on_split_fairly_button_clicked(self):
435 """
436 Split slides greedily
437 """
438 self.slide_splitting = SlideSplitting.Fairly
399439
=== 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 @@
56 """56 """
57 Delete all the C++ objects at the end so that we don't have a segfault57 Delete all the C++ objects at the end so that we don't have a segfault
58 """58 """
59 self.image_manager.stop_manager = True
60 self.image_manager.image_thread.wait()
59 del self.app61 del self.app
6062
61 def test_basic_image_manager(self):63 def test_basic_image_manager(self):
6264
=== 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 @@
408 # THEN: We should have None408 # THEN: We should have None
409 self.assertEqual(results, None, 'projector.get_projector_by_name() should have returned None')409 self.assertEqual(results, None, 'projector.get_projector_by_name() should have returned None')
410410
411 @skip("Produces the following fatal error: QThread: Destroyed while thread is still running")
411 def test_add_projector_fail(self):412 def test_add_projector_fail(self):
412 """413 """
413 Test add_projector() fail414 Test add_projector() fail
414415
=== 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 @@
205205
206 # THEN: QtWebKitWidgets should be called with the proper string206 # THEN: QtWebKitWidgets should be called with the proper string
207 mock_webview.setHtml.called_with(CSS_TEST_ONE, 'Should be the same')207 mock_webview.setHtml.called_with(CSS_TEST_ONE, 'Should be the same')
208
209 @patch('openlp.core.lib.renderer.build_lyrics_format_css')
210 @patch('openlp.core.lib.renderer.build_lyrics_outline_css')
211 @patch('openlp.core.lib.renderer.build_chords_css')
212 def test_text_height_on_slide(self, mock_build_chords_css, mock_outline_css, mock_lyrics_css):
213 """
214 Test _text_height_on_slide returns a larger value for 2 lines than one
215 """
216 # GIVEN: test object and data
217 mock_lyrics_css.return_value = ' FORMAT CSS; '
218 mock_outline_css.return_value = ' OUTLINE CSS; '
219 mock_build_chords_css.return_value = ' CHORDS CSS; '
220 theme_data = Theme()
221 theme_data.font_main_name = 'Arial'
222 theme_data.font_main_size = 20
223 theme_data.font_main_color = '#FFFFFF'
224 theme_data.font_main_outline_color = '#FFFFFF'
225 main = QtCore.QRect(10, 10, 1280, 900)
226 foot = QtCore.QRect(10, 1000, 1260, 24)
227 renderer = Renderer()
228 short_text = "test"
229 long_text = "test<br>test"
230
231 # WHEN: Calling method
232 renderer._set_text_rectangle(theme_data=theme_data, rect_main=main, rect_footer=foot)
233 short_height = renderer._text_height_on_slide(short_text)
234 long_height = renderer._text_height_on_slide(long_text)
235
236 # THEN: long_height should be greater than short_height
237 self.assertGreater(long_height, short_height)
238
239 @patch('openlp.core.lib.renderer.Renderer._text_height_on_slide')
240 @patch('openlp.core.lib.renderer.Renderer._binary_chop')
241 def test_fairly_chop(self, mock_binary_chop, mock_text_height_on_slide):
242 """
243 Test fairly_chop splits a 4 line verse 2-2 not 3-1
244 """
245 # GIVEN: test object and data
246 mock_binary_chop.side_effect = lambda formatted, previous_html, previous_raw, \
247 html_list, raw_list, separator, line_end: \
248 (previous_html, previous_raw)
249 mock_text_height_on_slide.side_effect = lambda text: text.count(separator) + 1
250 formatted = ["test<br>test<br>test"]
251 expected = ["test<br>test", "test<br>test"]
252 previous_html = "test"
253 previous_raw = "test"
254 html_list = None
255 raw_list = None
256 separator = "<br>"
257 line_end = ''
258 renderer = Renderer()
259
260 # WHEN: Calling method
261 previous_html, previous_raw = renderer._fairly_chop(
262 formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end)
263
264 # THEN: long_height should be greater than short_height
265 formatted.append(previous_raw)
266 self.assertListEqual(formatted, expected)