Merge lp:~tomasgroth/openlp/fix-theme-thumb into lp:openlp
- fix-theme-thumb
- Merge into trunk
Proposed by
Tomas Groth
Status: | Merged |
---|---|
Merged at revision: | 2877 |
Proposed branch: | lp:~tomasgroth/openlp/fix-theme-thumb |
Merge into: | lp:openlp |
Diff against target: |
541 lines (+148/-99) 9 files modified
openlp/core/display/render.py (+70/-27) openlp/core/ui/media/vlcplayer.py (+14/-13) openlp/core/ui/themeform.py (+20/-15) openlp/core/ui/thememanager.py (+12/-14) openlp/core/ui/themewizard.py (+9/-10) openlp/plugins/media/lib/mediaitem.py (+10/-9) openlp/plugins/songs/lib/songstab.py (+1/-1) tests/functional/openlp_core/ui/test_thememanager.py (+9/-9) tests/openlp_core/ui/test_themeform.py (+3/-1) |
To merge this branch: | bzr merge lp:~tomasgroth/openlp/fix-theme-thumb |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tim Bentley | Approve | ||
Raoul Snyman | Pending | ||
Review via email: mp+368596@code.launchpad.net |
This proposal supersedes a proposal from 2019-06-09.
Commit message
Add a webengine view for previewing themes.
Made VLC loading more robust.
A few minor fixes.
Description of the change
To post a comment you must log in.
Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
Revision history for this message
Tim Bentley (trb143) : Posted in a previous version of this proposal | # |
review:
Approve
Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
I like it, but you'll just need to fix the tests.
review:
Needs Fixing
Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
Linux tests failed, please see https:/
Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
Linux tests passed!
Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
Linting failed, please see https:/
Revision history for this message
Raoul Snyman (raoul-snyman) wrote : | # |
Linux tests passed!
Revision history for this message
Raoul Snyman (raoul-snyman) wrote : | # |
Linting passed!
Revision history for this message
Tim Bentley (trb143) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'openlp/core/display/render.py' | |||
2 | --- openlp/core/display/render.py 2019-04-13 13:00:22 +0000 | |||
3 | +++ openlp/core/display/render.py 2019-06-09 20:23:18 +0000 | |||
4 | @@ -24,6 +24,7 @@ | |||
5 | 24 | """ | 24 | """ |
6 | 25 | import html | 25 | import html |
7 | 26 | import logging | 26 | import logging |
8 | 27 | import mako | ||
9 | 27 | import math | 28 | import math |
10 | 28 | import os | 29 | import os |
11 | 29 | import re | 30 | import re |
12 | @@ -32,8 +33,10 @@ | |||
13 | 32 | from PyQt5 import QtWidgets, QtGui | 33 | from PyQt5 import QtWidgets, QtGui |
14 | 33 | 34 | ||
15 | 34 | from openlp.core.common import ThemeLevel | 35 | from openlp.core.common import ThemeLevel |
16 | 36 | from openlp.core.common.i18n import translate | ||
17 | 35 | from openlp.core.common.mixins import LogMixin, RegistryProperties | 37 | from openlp.core.common.mixins import LogMixin, RegistryProperties |
18 | 36 | from openlp.core.common.registry import Registry, RegistryBase | 38 | from openlp.core.common.registry import Registry, RegistryBase |
19 | 39 | from openlp.core.common.settings import Settings | ||
20 | 37 | from openlp.core.display.screens import ScreenList | 40 | from openlp.core.display.screens import ScreenList |
21 | 38 | from openlp.core.display.window import DisplayWindow | 41 | from openlp.core.display.window import DisplayWindow |
22 | 39 | from openlp.core.lib import ItemCapabilities | 42 | from openlp.core.lib import ItemCapabilities |
23 | @@ -58,8 +61,10 @@ | |||
24 | 58 | '{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}' \ | 61 | '{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}' \ |
25 | 59 | 'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n' | 62 | 'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n' |
26 | 60 | VERSE_FOR_LINE_COUNT = '\n'.join(map(str, range(100))) | 63 | VERSE_FOR_LINE_COUNT = '\n'.join(map(str, range(100))) |
29 | 61 | TITLE = 'Arky Arky (Unknown)' | 64 | TITLE = 'Arky Arky' |
30 | 62 | FOOTER = ['Public Domain', 'CCLI 123456'] | 65 | AUTHOR = 'John Doe' |
31 | 66 | FOOTER_COPYRIGHT = 'Public Domain' | ||
32 | 67 | CCLI_NO = '123456' | ||
33 | 63 | 68 | ||
34 | 64 | 69 | ||
35 | 65 | def remove_tags(text, can_remove_chords=False): | 70 | def remove_tags(text, can_remove_chords=False): |
36 | @@ -425,7 +430,7 @@ | |||
37 | 425 | return raw_text + ''.join(end_tags), ''.join(start_tags), ''.join(html_tags) | 430 | return raw_text + ''.join(end_tags), ''.join(start_tags), ''.join(html_tags) |
38 | 426 | 431 | ||
39 | 427 | 432 | ||
41 | 428 | class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow): | 433 | class ThemePreviewRenderer(LogMixin, DisplayWindow): |
42 | 429 | """ | 434 | """ |
43 | 430 | A virtual display used for rendering thumbnails and other offscreen tasks | 435 | A virtual display used for rendering thumbnails and other offscreen tasks |
44 | 431 | """ | 436 | """ |
45 | @@ -435,24 +440,6 @@ | |||
46 | 435 | """ | 440 | """ |
47 | 436 | super().__init__(*args, **kwargs) | 441 | super().__init__(*args, **kwargs) |
48 | 437 | self.force_page = False | 442 | self.force_page = False |
49 | 438 | for screen in ScreenList(): | ||
50 | 439 | if screen.is_display: | ||
51 | 440 | self.setGeometry(screen.display_geometry.x(), screen.display_geometry.y(), | ||
52 | 441 | screen.display_geometry.width(), screen.display_geometry.height()) | ||
53 | 442 | break | ||
54 | 443 | # If the display is not show'ed and hidden like this webegine will not render | ||
55 | 444 | self.show() | ||
56 | 445 | self.hide() | ||
57 | 446 | self.theme_height = 0 | ||
58 | 447 | self.theme_level = ThemeLevel.Global | ||
59 | 448 | |||
60 | 449 | def set_theme_level(self, theme_level): | ||
61 | 450 | """ | ||
62 | 451 | Sets the theme level. | ||
63 | 452 | |||
64 | 453 | :param theme_level: The theme level to be used. | ||
65 | 454 | """ | ||
66 | 455 | self.theme_level = theme_level | ||
67 | 456 | 443 | ||
68 | 457 | def calculate_line_count(self): | 444 | def calculate_line_count(self): |
69 | 458 | """ | 445 | """ |
70 | @@ -466,7 +453,30 @@ | |||
71 | 466 | """ | 453 | """ |
72 | 467 | return self.run_javascript('Display.clearSlides();') | 454 | return self.run_javascript('Display.clearSlides();') |
73 | 468 | 455 | ||
75 | 469 | def generate_preview(self, theme_data, force_page=False): | 456 | def generate_footer(self): |
76 | 457 | """ | ||
77 | 458 | """ | ||
78 | 459 | footer_template = Settings().value('songs/footer template') | ||
79 | 460 | # Keep this in sync with the list in songstab.py | ||
80 | 461 | vars = { | ||
81 | 462 | 'title': TITLE, | ||
82 | 463 | 'authors_none_label': translate('OpenLP.Ui', 'Written by'), | ||
83 | 464 | 'authors_words_label': translate('SongsPlugin.AuthorType', 'Words', | ||
84 | 465 | 'Author who wrote the lyrics of a song'), | ||
85 | 466 | 'authors_words': [AUTHOR], | ||
86 | 467 | 'copyright': FOOTER_COPYRIGHT, | ||
87 | 468 | 'ccli_license': Settings().value('core/ccli number'), | ||
88 | 469 | 'ccli_license_label': translate('SongsPlugin.MediaItem', 'CCLI License'), | ||
89 | 470 | 'ccli_number': CCLI_NO, | ||
90 | 471 | } | ||
91 | 472 | try: | ||
92 | 473 | footer_html = mako.template.Template(footer_template).render_unicode(**vars).replace('\n', '') | ||
93 | 474 | except mako.exceptions.SyntaxException: | ||
94 | 475 | log.error('Failed to render Song footer html:\n' + mako.exceptions.text_error_template().render()) | ||
95 | 476 | footer_html = 'Dummy footer text' | ||
96 | 477 | return footer_html | ||
97 | 478 | |||
98 | 479 | def generate_preview(self, theme_data, force_page=False, generate_screenshot=True): | ||
99 | 470 | """ | 480 | """ |
100 | 471 | Generate a preview of a theme. | 481 | Generate a preview of a theme. |
101 | 472 | 482 | ||
102 | @@ -479,14 +489,16 @@ | |||
103 | 479 | if not self.force_page: | 489 | if not self.force_page: |
104 | 480 | self.set_theme(theme_data) | 490 | self.set_theme(theme_data) |
105 | 481 | self.theme_height = theme_data.font_main_height | 491 | self.theme_height = theme_data.font_main_height |
107 | 482 | slides = self.format_slide(render_tags(VERSE), None) | 492 | slides = self.format_slide(VERSE, None) |
108 | 483 | verses = dict() | 493 | verses = dict() |
109 | 484 | verses['title'] = TITLE | 494 | verses['title'] = TITLE |
111 | 485 | verses['text'] = slides[0] | 495 | verses['text'] = render_tags(slides[0]) |
112 | 486 | verses['verse'] = 'V1' | 496 | verses['verse'] = 'V1' |
113 | 497 | verses['footer'] = self.generate_footer() | ||
114 | 487 | self.load_verses([verses]) | 498 | self.load_verses([verses]) |
115 | 488 | self.force_page = False | 499 | self.force_page = False |
117 | 489 | return self.save_screenshot() | 500 | if generate_screenshot: |
118 | 501 | return self.save_screenshot() | ||
119 | 490 | self.force_page = False | 502 | self.force_page = False |
120 | 491 | return None | 503 | return None |
121 | 492 | 504 | ||
122 | @@ -515,7 +527,7 @@ | |||
123 | 515 | if item and item.is_capable(ItemCapabilities.CanWordSplit): | 527 | if item and item.is_capable(ItemCapabilities.CanWordSplit): |
124 | 516 | pages = self._paginate_slide_words(text.split('\n'), line_end) | 528 | pages = self._paginate_slide_words(text.split('\n'), line_end) |
125 | 517 | # Songs and Custom | 529 | # Songs and Custom |
127 | 518 | elif item is None or item.is_capable(ItemCapabilities.CanSoftBreak): | 530 | elif item is None or (item and item.is_capable(ItemCapabilities.CanSoftBreak)): |
128 | 519 | pages = [] | 531 | pages = [] |
129 | 520 | if '[---]' in text: | 532 | if '[---]' in text: |
130 | 521 | # Remove Overflow split if at start of the text | 533 | # Remove Overflow split if at start of the text |
131 | @@ -722,7 +734,8 @@ | |||
132 | 722 | :param text: The text to check. It may contain HTML tags. | 734 | :param text: The text to check. It may contain HTML tags. |
133 | 723 | """ | 735 | """ |
134 | 724 | self.clear_slides() | 736 | self.clear_slides() |
136 | 725 | self.run_javascript('Display.addTextSlide("v1", "{text}", "Dummy Footer");'.format(text=text), is_sync=True) | 737 | self.run_javascript('Display.addTextSlide("v1", "{text}", "Dummy Footer");' |
137 | 738 | .format(text=text.replace('"', '\\"')), is_sync=True) | ||
138 | 726 | does_text_fits = self.run_javascript('Display.doesContentFit();', is_sync=True) | 739 | does_text_fits = self.run_javascript('Display.doesContentFit();', is_sync=True) |
139 | 727 | return does_text_fits | 740 | return does_text_fits |
140 | 728 | 741 | ||
141 | @@ -745,3 +758,33 @@ | |||
142 | 745 | pixmap.save(fname, ext) | 758 | pixmap.save(fname, ext) |
143 | 746 | else: | 759 | else: |
144 | 747 | return pixmap | 760 | return pixmap |
145 | 761 | |||
146 | 762 | |||
147 | 763 | class Renderer(RegistryBase, RegistryProperties, ThemePreviewRenderer): | ||
148 | 764 | """ | ||
149 | 765 | A virtual display used for rendering thumbnails and other offscreen tasks | ||
150 | 766 | """ | ||
151 | 767 | def __init__(self, *args, **kwargs): | ||
152 | 768 | """ | ||
153 | 769 | Constructor | ||
154 | 770 | """ | ||
155 | 771 | super().__init__(*args, **kwargs) | ||
156 | 772 | self.force_page = False | ||
157 | 773 | for screen in ScreenList(): | ||
158 | 774 | if screen.is_display: | ||
159 | 775 | self.setGeometry(screen.display_geometry.x(), screen.display_geometry.y(), | ||
160 | 776 | screen.display_geometry.width(), screen.display_geometry.height()) | ||
161 | 777 | break | ||
162 | 778 | # If the display is not show'ed and hidden like this webegine will not render | ||
163 | 779 | self.show() | ||
164 | 780 | self.hide() | ||
165 | 781 | self.theme_height = 0 | ||
166 | 782 | self.theme_level = ThemeLevel.Global | ||
167 | 783 | |||
168 | 784 | def set_theme_level(self, theme_level): | ||
169 | 785 | """ | ||
170 | 786 | Sets the theme level. | ||
171 | 787 | |||
172 | 788 | :param theme_level: The theme level to be used. | ||
173 | 789 | """ | ||
174 | 790 | self.theme_level = theme_level | ||
175 | 748 | 791 | ||
176 | === modified file 'openlp/core/ui/media/vlcplayer.py' | |||
177 | --- openlp/core/ui/media/vlcplayer.py 2019-05-31 20:19:15 +0000 | |||
178 | +++ openlp/core/ui/media/vlcplayer.py 2019-06-09 20:23:18 +0000 | |||
179 | @@ -28,7 +28,6 @@ | |||
180 | 28 | import sys | 28 | import sys |
181 | 29 | import threading | 29 | import threading |
182 | 30 | from datetime import datetime | 30 | from datetime import datetime |
183 | 31 | import vlc | ||
184 | 32 | 31 | ||
185 | 33 | from PyQt5 import QtWidgets | 32 | from PyQt5 import QtWidgets |
186 | 34 | 33 | ||
187 | @@ -62,25 +61,27 @@ | |||
188 | 62 | 61 | ||
189 | 63 | :return: The "vlc" module, or None | 62 | :return: The "vlc" module, or None |
190 | 64 | """ | 63 | """ |
194 | 65 | if 'vlc' in sys.modules: | 64 | # Import the VLC module if not already done |
195 | 66 | # If VLC has already been imported, no need to do all the stuff below again | 65 | if 'vlc' not in sys.modules: |
193 | 67 | is_vlc_available = False | ||
196 | 68 | try: | 66 | try: |
203 | 69 | is_vlc_available = bool(sys.modules['vlc'].get_default_instance()) | 67 | import vlc # noqa module is not used directly, but is used via sys.modules['vlc'] |
204 | 70 | except Exception: | 68 | except ImportError: |
199 | 71 | pass | ||
200 | 72 | if is_vlc_available: | ||
201 | 73 | return sys.modules['vlc'] | ||
202 | 74 | else: | ||
205 | 75 | return None | 69 | return None |
208 | 76 | else: | 70 | # Verify that VLC is also loadable |
209 | 77 | return vlc | 71 | is_vlc_available = False |
210 | 72 | try: | ||
211 | 73 | is_vlc_available = bool(sys.modules['vlc'].get_default_instance()) | ||
212 | 74 | except Exception: | ||
213 | 75 | pass | ||
214 | 76 | if is_vlc_available: | ||
215 | 77 | return sys.modules['vlc'] | ||
216 | 78 | return None | ||
217 | 78 | 79 | ||
218 | 79 | 80 | ||
219 | 80 | # On linux we need to initialise X threads, but not when running tests. | 81 | # On linux we need to initialise X threads, but not when running tests. |
220 | 81 | # This needs to happen on module load and not in get_vlc(), otherwise it can cause crashes on some DE on some setups | 82 | # This needs to happen on module load and not in get_vlc(), otherwise it can cause crashes on some DE on some setups |
221 | 82 | # (reported on Gnome3, Unity, Cinnamon, all GTK+ based) when using native filedialogs... | 83 | # (reported on Gnome3, Unity, Cinnamon, all GTK+ based) when using native filedialogs... |
223 | 83 | if is_linux() and 'nose' not in sys.argv[0] and get_vlc(): | 84 | if is_linux() and 'pytest' not in sys.argv[0] and get_vlc(): |
224 | 84 | try: | 85 | try: |
225 | 85 | try: | 86 | try: |
226 | 86 | x11 = ctypes.cdll.LoadLibrary('libX11.so.6') | 87 | x11 = ctypes.cdll.LoadLibrary('libX11.so.6') |
227 | 87 | 88 | ||
228 | === modified file 'openlp/core/ui/themeform.py' | |||
229 | --- openlp/core/ui/themeform.py 2019-05-26 20:53:54 +0000 | |||
230 | +++ openlp/core/ui/themeform.py 2019-06-09 20:23:18 +0000 | |||
231 | @@ -172,16 +172,14 @@ | |||
232 | 172 | if not event: | 172 | if not event: |
233 | 173 | event = QtGui.QResizeEvent(self.size(), self.size()) | 173 | event = QtGui.QResizeEvent(self.size(), self.size()) |
234 | 174 | QtWidgets.QWizard.resizeEvent(self, event) | 174 | QtWidgets.QWizard.resizeEvent(self, event) |
245 | 175 | if hasattr(self, 'preview_page') and self.currentPage() == self.preview_page: | 175 | try: |
246 | 176 | frame_width = self.preview_box_label.lineWidth() | 176 | self.display_aspect_ratio = self.renderer.width() / self.renderer.height() |
247 | 177 | pixmap_width = self.preview_area.width() - 2 * frame_width | 177 | except ZeroDivisionError: |
248 | 178 | pixmap_height = self.preview_area.height() - 2 * frame_width | 178 | self.display_aspect_ratio = 1 |
249 | 179 | aspect_ratio = float(pixmap_width) / pixmap_height | 179 | # Make sure we don't resize before the widgets are actually created |
250 | 180 | if aspect_ratio < self.display_aspect_ratio: | 180 | if hasattr(self, 'preview_area_layout'): |
251 | 181 | pixmap_height = int(pixmap_width / self.display_aspect_ratio + 0.5) | 181 | self.preview_area_layout.set_aspect_ratio(self.display_aspect_ratio) |
252 | 182 | else: | 182 | self.preview_box.set_scale(float(self.preview_box.width()) / self.renderer.width()) |
243 | 183 | pixmap_width = int(pixmap_height * self.display_aspect_ratio + 0.5) | ||
244 | 184 | self.preview_box_label.setFixedSize(pixmap_width + 2 * frame_width, pixmap_height + 2 * frame_width) | ||
253 | 185 | 183 | ||
254 | 186 | def validateCurrentPage(self): | 184 | def validateCurrentPage(self): |
255 | 187 | """ | 185 | """ |
256 | @@ -206,11 +204,17 @@ | |||
257 | 206 | self.setOption(QtWidgets.QWizard.HaveCustomButton1, enabled) | 204 | self.setOption(QtWidgets.QWizard.HaveCustomButton1, enabled) |
258 | 207 | if self.page(page_id) == self.preview_page: | 205 | if self.page(page_id) == self.preview_page: |
259 | 208 | self.update_theme() | 206 | self.update_theme() |
264 | 209 | frame = self.theme_manager.generate_image(self.theme) | 207 | self.preview_box.set_theme(self.theme) |
265 | 210 | frame.setDevicePixelRatio(self.devicePixelRatio()) | 208 | self.preview_box.clear_slides() |
266 | 211 | self.preview_box_label.setPixmap(frame) | 209 | self.preview_box.set_scale(float(self.preview_box.width()) / self.renderer.width()) |
267 | 212 | self.display_aspect_ratio = float(frame.width()) / frame.height() | 210 | try: |
268 | 211 | self.display_aspect_ratio = self.renderer.width() / self.renderer.height() | ||
269 | 212 | except ZeroDivisionError: | ||
270 | 213 | self.display_aspect_ratio = 1 | ||
271 | 214 | self.preview_area_layout.set_aspect_ratio(self.display_aspect_ratio) | ||
272 | 213 | self.resizeEvent() | 215 | self.resizeEvent() |
273 | 216 | self.preview_box.show() | ||
274 | 217 | self.preview_box.generate_preview(self.theme, False, False) | ||
275 | 214 | 218 | ||
276 | 215 | def on_custom_1_button_clicked(self, number): | 219 | def on_custom_1_button_clicked(self, number): |
277 | 216 | """ | 220 | """ |
278 | @@ -398,6 +402,7 @@ | |||
279 | 398 | Handle the display and state of the Preview page. | 402 | Handle the display and state of the Preview page. |
280 | 399 | """ | 403 | """ |
281 | 400 | self.setField('name', self.theme.theme_name) | 404 | self.setField('name', self.theme.theme_name) |
282 | 405 | self.preview_box.set_theme(self.theme) | ||
283 | 401 | 406 | ||
284 | 402 | def on_background_combo_box_current_index_changed(self, index): | 407 | def on_background_combo_box_current_index_changed(self, index): |
285 | 403 | """ | 408 | """ |
286 | @@ -558,5 +563,5 @@ | |||
287 | 558 | source_path = self.theme.background_filename | 563 | source_path = self.theme.background_filename |
288 | 559 | if not self.edit_mode and not self.theme_manager.check_if_theme_exists(self.theme.theme_name): | 564 | if not self.edit_mode and not self.theme_manager.check_if_theme_exists(self.theme.theme_name): |
289 | 560 | return | 565 | return |
291 | 561 | self.theme_manager.save_theme(self.theme, source_path, destination_path) | 566 | self.theme_manager.save_theme(self.theme, source_path, destination_path, self.preview_box.save_screenshot()) |
292 | 562 | return QtWidgets.QDialog.accept(self) | 567 | return QtWidgets.QDialog.accept(self) |
293 | 563 | 568 | ||
294 | === modified file 'openlp/core/ui/thememanager.py' | |||
295 | --- openlp/core/ui/thememanager.py 2019-05-22 06:47:00 +0000 | |||
296 | +++ openlp/core/ui/thememanager.py 2019-06-09 20:23:18 +0000 | |||
297 | @@ -476,7 +476,7 @@ | |||
298 | 476 | if not theme_paths: | 476 | if not theme_paths: |
299 | 477 | theme = Theme() | 477 | theme = Theme() |
300 | 478 | theme.theme_name = UiStrings().Default | 478 | theme.theme_name = UiStrings().Default |
302 | 479 | self._write_theme(theme) | 479 | self.save_theme(theme) |
303 | 480 | Settings().setValue(self.settings_section + '/global theme', theme.theme_name) | 480 | Settings().setValue(self.settings_section + '/global theme', theme.theme_name) |
304 | 481 | self.application.set_normal_cursor() | 481 | self.application.set_normal_cursor() |
305 | 482 | 482 | ||
306 | @@ -639,24 +639,14 @@ | |||
307 | 639 | return False | 639 | return False |
308 | 640 | return True | 640 | return True |
309 | 641 | 641 | ||
322 | 642 | def save_theme(self, theme, image_source_path, image_destination_path): | 642 | def save_theme(self, theme, image_source_path=None, image_destination_path=None, image=None): |
311 | 643 | """ | ||
312 | 644 | Called by theme maintenance Dialog to save the theme and to trigger the reload of the theme list | ||
313 | 645 | |||
314 | 646 | :param Theme theme: The theme data object. | ||
315 | 647 | :param Path image_source_path: Where the theme image is currently located. | ||
316 | 648 | :param Path image_destination_path: Where the Theme Image is to be saved to | ||
317 | 649 | :rtype: None | ||
318 | 650 | """ | ||
319 | 651 | self._write_theme(theme, image_source_path, image_destination_path) | ||
320 | 652 | |||
321 | 653 | def _write_theme(self, theme, image_source_path=None, image_destination_path=None): | ||
323 | 654 | """ | 643 | """ |
324 | 655 | Writes the theme to the disk and handles the background image if necessary | 644 | Writes the theme to the disk and handles the background image if necessary |
325 | 656 | 645 | ||
326 | 657 | :param Theme theme: The theme data object. | 646 | :param Theme theme: The theme data object. |
327 | 658 | :param Path image_source_path: Where the theme image is currently located. | 647 | :param Path image_source_path: Where the theme image is currently located. |
328 | 659 | :param Path image_destination_path: Where the Theme Image is to be saved to | 648 | :param Path image_destination_path: Where the Theme Image is to be saved to |
329 | 649 | :param image: The example image of the theme. Optionally. | ||
330 | 660 | :rtype: None | 650 | :rtype: None |
331 | 661 | """ | 651 | """ |
332 | 662 | name = theme.theme_name | 652 | name = theme.theme_name |
333 | @@ -676,7 +666,15 @@ | |||
334 | 676 | shutil.copyfile(image_source_path, image_destination_path) | 666 | shutil.copyfile(image_source_path, image_destination_path) |
335 | 677 | except OSError: | 667 | except OSError: |
336 | 678 | self.log_exception('Failed to save theme image') | 668 | self.log_exception('Failed to save theme image') |
338 | 679 | self.generate_and_save_image(name, theme) | 669 | if image: |
339 | 670 | sample_path_name = self.theme_path / '{file_name}.png'.format(file_name=name) | ||
340 | 671 | if sample_path_name.exists(): | ||
341 | 672 | sample_path_name.unlink() | ||
342 | 673 | image.save(str(sample_path_name), 'png') | ||
343 | 674 | thumb_path = self.thumb_path / '{name}.png'.format(name=name) | ||
344 | 675 | create_thumb(sample_path_name, thumb_path, False) | ||
345 | 676 | else: | ||
346 | 677 | self.generate_and_save_image(name, theme) | ||
347 | 680 | 678 | ||
348 | 681 | def generate_and_save_image(self, theme_name, theme): | 679 | def generate_and_save_image(self, theme_name, theme): |
349 | 682 | """ | 680 | """ |
350 | 683 | 681 | ||
351 | === modified file 'openlp/core/ui/themewizard.py' | |||
352 | --- openlp/core/ui/themewizard.py 2019-04-13 13:00:22 +0000 | |||
353 | +++ openlp/core/ui/themewizard.py 2019-06-09 20:23:18 +0000 | |||
354 | @@ -31,6 +31,8 @@ | |||
355 | 31 | from openlp.core.ui.icons import UiIcons | 31 | from openlp.core.ui.icons import UiIcons |
356 | 32 | from openlp.core.widgets.buttons import ColorButton | 32 | from openlp.core.widgets.buttons import ColorButton |
357 | 33 | from openlp.core.widgets.edits import PathEdit | 33 | from openlp.core.widgets.edits import PathEdit |
358 | 34 | from openlp.core.widgets.layouts import AspectRatioLayout | ||
359 | 35 | from openlp.core.display.render import ThemePreviewRenderer | ||
360 | 34 | 36 | ||
361 | 35 | 37 | ||
362 | 36 | class Ui_ThemeWizard(object): | 38 | class Ui_ThemeWizard(object): |
363 | @@ -363,16 +365,13 @@ | |||
364 | 363 | self.preview_layout.addLayout(self.theme_name_layout) | 365 | self.preview_layout.addLayout(self.theme_name_layout) |
365 | 364 | self.preview_area = QtWidgets.QWidget(self.preview_page) | 366 | self.preview_area = QtWidgets.QWidget(self.preview_page) |
366 | 365 | self.preview_area.setObjectName('PreviewArea') | 367 | self.preview_area.setObjectName('PreviewArea') |
377 | 366 | self.preview_area_layout = QtWidgets.QGridLayout(self.preview_area) | 368 | self.preview_area_layout = AspectRatioLayout(self.preview_area, 0.75) # Dummy ratio, will be update |
378 | 367 | self.preview_area_layout.setContentsMargins(0, 0, 0, 0) | 369 | self.preview_area_layout.margin = 8 |
379 | 368 | self.preview_area_layout.setColumnStretch(0, 1) | 370 | self.preview_area_layout.setSpacing(0) |
380 | 369 | self.preview_area_layout.setRowStretch(0, 1) | 371 | self.preview_area_layout.setObjectName('preview_web_layout') |
381 | 370 | self.preview_area_layout.setObjectName('preview_area_layout') | 372 | self.preview_box = ThemePreviewRenderer(self) |
382 | 371 | self.preview_box_label = QtWidgets.QLabel(self.preview_area) | 373 | self.preview_box.setObjectName('preview_box') |
383 | 372 | self.preview_box_label.setFrameShape(QtWidgets.QFrame.Box) | 374 | self.preview_area_layout.addWidget(self.preview_box) |
374 | 373 | self.preview_box_label.setScaledContents(True) | ||
375 | 374 | self.preview_box_label.setObjectName('preview_box_label') | ||
376 | 375 | self.preview_area_layout.addWidget(self.preview_box_label) | ||
384 | 376 | self.preview_layout.addWidget(self.preview_area) | 375 | self.preview_layout.addWidget(self.preview_area) |
385 | 377 | theme_wizard.addPage(self.preview_page) | 376 | theme_wizard.addPage(self.preview_page) |
386 | 378 | self.retranslate_ui(theme_wizard) | 377 | self.retranslate_ui(theme_wizard) |
387 | 379 | 378 | ||
388 | === modified file 'openlp/plugins/media/lib/mediaitem.py' | |||
389 | --- openlp/plugins/media/lib/mediaitem.py 2019-06-01 06:59:45 +0000 | |||
390 | +++ openlp/plugins/media/lib/mediaitem.py 2019-06-09 20:23:18 +0000 | |||
391 | @@ -173,7 +173,7 @@ | |||
392 | 173 | item = self.list_view.currentItem() | 173 | item = self.list_view.currentItem() |
393 | 174 | if item is None: | 174 | if item is None: |
394 | 175 | return False | 175 | return False |
396 | 176 | filename = item.data(QtCore.Qt.UserRole) | 176 | filename = str(item.data(QtCore.Qt.UserRole)) |
397 | 177 | # Special handling if the filename is a optical clip | 177 | # Special handling if the filename is a optical clip |
398 | 178 | if filename.startswith('optical:'): | 178 | if filename.startswith('optical:'): |
399 | 179 | (name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(filename) | 179 | (name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(filename) |
400 | @@ -259,11 +259,12 @@ | |||
401 | 259 | # TODO needs to be fixed as no idea why this fails | 259 | # TODO needs to be fixed as no idea why this fails |
402 | 260 | # media.sort(key=lambda file_path: get_natural_key(file_path.name)) | 260 | # media.sort(key=lambda file_path: get_natural_key(file_path.name)) |
403 | 261 | for track in media: | 261 | for track in media: |
405 | 262 | track_info = QtCore.QFileInfo(track) | 262 | track_str = str(track) |
406 | 263 | track_info = QtCore.QFileInfo(track_str) | ||
407 | 263 | item_name = None | 264 | item_name = None |
409 | 264 | if track.startswith('optical:'): | 265 | if track_str.startswith('optical:'): |
410 | 265 | # Handle optical based item | 266 | # Handle optical based item |
412 | 266 | (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track) | 267 | (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track_str) |
413 | 267 | item_name = QtWidgets.QListWidgetItem(clip_name) | 268 | item_name = QtWidgets.QListWidgetItem(clip_name) |
414 | 268 | item_name.setIcon(UiIcons().optical) | 269 | item_name.setIcon(UiIcons().optical) |
415 | 269 | item_name.setData(QtCore.Qt.UserRole, track) | 270 | item_name.setData(QtCore.Qt.UserRole, track) |
416 | @@ -272,22 +273,22 @@ | |||
417 | 272 | end=format_milliseconds(end))) | 273 | end=format_milliseconds(end))) |
418 | 273 | elif not os.path.exists(track): | 274 | elif not os.path.exists(track): |
419 | 274 | # File doesn't exist, mark as error. | 275 | # File doesn't exist, mark as error. |
421 | 275 | file_name = os.path.split(str(track))[1] | 276 | file_name = os.path.split(track_str)[1] |
422 | 276 | item_name = QtWidgets.QListWidgetItem(file_name) | 277 | item_name = QtWidgets.QListWidgetItem(file_name) |
423 | 277 | item_name.setIcon(UiIcons().error) | 278 | item_name.setIcon(UiIcons().error) |
424 | 278 | item_name.setData(QtCore.Qt.UserRole, track) | 279 | item_name.setData(QtCore.Qt.UserRole, track) |
426 | 279 | item_name.setToolTip(track) | 280 | item_name.setToolTip(track_str) |
427 | 280 | elif track_info.isFile(): | 281 | elif track_info.isFile(): |
428 | 281 | # Normal media file handling. | 282 | # Normal media file handling. |
430 | 282 | file_name = os.path.split(str(track))[1] | 283 | file_name = os.path.split(track_str)[1] |
431 | 283 | item_name = QtWidgets.QListWidgetItem(file_name) | 284 | item_name = QtWidgets.QListWidgetItem(file_name) |
432 | 284 | search = file_name.split('.')[-1].lower() | 285 | search = file_name.split('.')[-1].lower() |
434 | 285 | if '*.{text}'.format(text=search) in self.media_controller.audio_extensions_list: | 286 | if search in AUDIO_EXT: |
435 | 286 | item_name.setIcon(UiIcons().audio) | 287 | item_name.setIcon(UiIcons().audio) |
436 | 287 | else: | 288 | else: |
437 | 288 | item_name.setIcon(UiIcons().video) | 289 | item_name.setIcon(UiIcons().video) |
438 | 289 | item_name.setData(QtCore.Qt.UserRole, track) | 290 | item_name.setData(QtCore.Qt.UserRole, track) |
440 | 290 | item_name.setToolTip(track) | 291 | item_name.setToolTip(track_str) |
441 | 291 | if item_name: | 292 | if item_name: |
442 | 292 | self.list_view.addItem(item_name) | 293 | self.list_view.addItem(item_name) |
443 | 293 | 294 | ||
444 | 294 | 295 | ||
445 | === modified file 'openlp/plugins/songs/lib/songstab.py' | |||
446 | --- openlp/plugins/songs/lib/songstab.py 2019-04-13 13:00:22 +0000 | |||
447 | +++ openlp/plugins/songs/lib/songstab.py 2019-06-09 20:23:18 +0000 | |||
448 | @@ -88,7 +88,7 @@ | |||
449 | 88 | self.footer_group_box = QtWidgets.QGroupBox(self.left_column) | 88 | self.footer_group_box = QtWidgets.QGroupBox(self.left_column) |
450 | 89 | self.footer_group_box.setObjectName('footer_group_box') | 89 | self.footer_group_box.setObjectName('footer_group_box') |
451 | 90 | self.footer_layout = QtWidgets.QVBoxLayout(self.footer_group_box) | 90 | self.footer_layout = QtWidgets.QVBoxLayout(self.footer_group_box) |
453 | 91 | self.footer_layout.setObjectName('chords_layout') | 91 | self.footer_layout.setObjectName('footer_layout') |
454 | 92 | self.footer_info_label = QtWidgets.QLabel(self.footer_group_box) | 92 | self.footer_info_label = QtWidgets.QLabel(self.footer_group_box) |
455 | 93 | self.footer_layout.addWidget(self.footer_info_label) | 93 | self.footer_layout.addWidget(self.footer_info_label) |
456 | 94 | self.footer_placeholder_info = QtWidgets.QTextEdit(self.footer_group_box) | 94 | self.footer_placeholder_info = QtWidgets.QTextEdit(self.footer_group_box) |
457 | 95 | 95 | ||
458 | === modified file 'tests/functional/openlp_core/ui/test_thememanager.py' | |||
459 | --- tests/functional/openlp_core/ui/test_thememanager.py 2019-05-22 06:47:00 +0000 | |||
460 | +++ tests/functional/openlp_core/ui/test_thememanager.py 2019-06-09 20:23:18 +0000 | |||
461 | @@ -83,7 +83,7 @@ | |||
462 | 83 | 83 | ||
463 | 84 | @patch('openlp.core.ui.thememanager.shutil') | 84 | @patch('openlp.core.ui.thememanager.shutil') |
464 | 85 | @patch('openlp.core.ui.thememanager.create_paths') | 85 | @patch('openlp.core.ui.thememanager.create_paths') |
466 | 86 | def test_write_theme_same_image(self, mocked_create_paths, mocked_shutil): | 86 | def test_save_theme_same_image(self, mocked_create_paths, mocked_shutil): |
467 | 87 | """ | 87 | """ |
468 | 88 | Test that we don't try to overwrite a theme background image with itself | 88 | Test that we don't try to overwrite a theme background image with itself |
469 | 89 | """ | 89 | """ |
470 | @@ -98,16 +98,16 @@ | |||
471 | 98 | mocked_theme.extract_formatted_xml = MagicMock() | 98 | mocked_theme.extract_formatted_xml = MagicMock() |
472 | 99 | mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode() | 99 | mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode() |
473 | 100 | 100 | ||
475 | 101 | # WHEN: Calling _write_theme with path to the same image, but the path written slightly different | 101 | # WHEN: Calling save_theme with path to the same image, but the path written slightly different |
476 | 102 | file_path_1 = RESOURCE_PATH / 'church.jpg' | 102 | file_path_1 = RESOURCE_PATH / 'church.jpg' |
478 | 103 | theme_manager._write_theme(mocked_theme, file_path_1, file_path_1) | 103 | theme_manager.save_theme(mocked_theme, file_path_1, file_path_1) |
479 | 104 | 104 | ||
480 | 105 | # THEN: The mocked_copyfile should not have been called | 105 | # THEN: The mocked_copyfile should not have been called |
481 | 106 | assert mocked_shutil.copyfile.called is False, 'copyfile should not be called' | 106 | assert mocked_shutil.copyfile.called is False, 'copyfile should not be called' |
482 | 107 | 107 | ||
483 | 108 | @patch('openlp.core.ui.thememanager.shutil') | 108 | @patch('openlp.core.ui.thememanager.shutil') |
484 | 109 | @patch('openlp.core.ui.thememanager.create_paths') | 109 | @patch('openlp.core.ui.thememanager.create_paths') |
486 | 110 | def test_write_theme_diff_images(self, mocked_create_paths, mocked_shutil): | 110 | def test_save_theme_diff_images(self, mocked_create_paths, mocked_shutil): |
487 | 111 | """ | 111 | """ |
488 | 112 | Test that we do overwrite a theme background image when a new is submitted | 112 | Test that we do overwrite a theme background image when a new is submitted |
489 | 113 | """ | 113 | """ |
490 | @@ -121,15 +121,15 @@ | |||
491 | 121 | mocked_theme.theme_name = 'themename' | 121 | mocked_theme.theme_name = 'themename' |
492 | 122 | mocked_theme.filename = "filename" | 122 | mocked_theme.filename = "filename" |
493 | 123 | 123 | ||
495 | 124 | # WHEN: Calling _write_theme with path to different images | 124 | # WHEN: Calling save_theme with path to different images |
496 | 125 | file_path_1 = RESOURCE_PATH / 'church.jpg' | 125 | file_path_1 = RESOURCE_PATH / 'church.jpg' |
497 | 126 | file_path_2 = RESOURCE_PATH / 'church2.jpg' | 126 | file_path_2 = RESOURCE_PATH / 'church2.jpg' |
499 | 127 | theme_manager._write_theme(mocked_theme, file_path_1, file_path_2) | 127 | theme_manager.save_theme(mocked_theme, file_path_1, file_path_2) |
500 | 128 | 128 | ||
501 | 129 | # THEN: The mocked_copyfile should not have been called | 129 | # THEN: The mocked_copyfile should not have been called |
502 | 130 | assert mocked_shutil.copyfile.called is True, 'copyfile should be called' | 130 | assert mocked_shutil.copyfile.called is True, 'copyfile should be called' |
503 | 131 | 131 | ||
505 | 132 | def test_write_theme_special_char_name(self): | 132 | def test_save_theme_special_char_name(self): |
506 | 133 | """ | 133 | """ |
507 | 134 | Test that we can save themes with special characters in the name | 134 | Test that we can save themes with special characters in the name |
508 | 135 | """ | 135 | """ |
509 | @@ -142,8 +142,8 @@ | |||
510 | 142 | mocked_theme.theme_name = 'theme 愛 name' | 142 | mocked_theme.theme_name = 'theme 愛 name' |
511 | 143 | mocked_theme.export_theme.return_value = "{}" | 143 | mocked_theme.export_theme.return_value = "{}" |
512 | 144 | 144 | ||
515 | 145 | # WHEN: Calling _write_theme with a theme with a name with special characters in it | 145 | # WHEN: Calling save_theme with a theme with a name with special characters in it |
516 | 146 | theme_manager._write_theme(mocked_theme) | 146 | theme_manager.save_theme(mocked_theme) |
517 | 147 | 147 | ||
518 | 148 | # THEN: It should have been created | 148 | # THEN: It should have been created |
519 | 149 | assert os.path.exists(os.path.join(self.temp_folder, 'theme 愛 name', 'theme 愛 name.json')) is True, \ | 149 | assert os.path.exists(os.path.join(self.temp_folder, 'theme 愛 name', 'theme 愛 name.json')) is True, \ |
520 | 150 | 150 | ||
521 | === modified file 'tests/openlp_core/ui/test_themeform.py' | |||
522 | --- tests/openlp_core/ui/test_themeform.py 2019-04-13 13:00:22 +0000 | |||
523 | +++ tests/openlp_core/ui/test_themeform.py 2019-06-09 20:23:18 +0000 | |||
524 | @@ -23,6 +23,7 @@ | |||
525 | 23 | Interface tests to test the ThemeWizard class and related methods. | 23 | Interface tests to test the ThemeWizard class and related methods. |
526 | 24 | """ | 24 | """ |
527 | 25 | from unittest import TestCase | 25 | from unittest import TestCase |
528 | 26 | from unittest.mock import patch | ||
529 | 26 | 27 | ||
530 | 27 | from openlp.core.common.registry import Registry | 28 | from openlp.core.common.registry import Registry |
531 | 28 | from openlp.core.ui.themeform import ThemeForm | 29 | from openlp.core.ui.themeform import ThemeForm |
532 | @@ -39,7 +40,8 @@ | |||
533 | 39 | """ | 40 | """ |
534 | 40 | Registry.create() | 41 | Registry.create() |
535 | 41 | 42 | ||
537 | 42 | def test_create_theme_wizard(self): | 43 | @patch('openlp.core.display.window.QtWidgets.QVBoxLayout') |
538 | 44 | def test_create_theme_wizard(self, mocked_qvboxlayout): | ||
539 | 43 | """ | 45 | """ |
540 | 44 | Test creating a ThemeForm instance | 46 | Test creating a ThemeForm instance |
541 | 45 | """ | 47 | """ |
Linux tests failed, please see https:/ /ci.openlp. io/job/ MP-02-Linux_ Tests/176/ for more details