Merge lp:~raoul-snyman/openlp/pyro-impress into lp:openlp
- pyro-impress
- Merge into trunk
Status: | Superseded |
---|---|
Proposed branch: | lp:~raoul-snyman/openlp/pyro-impress |
Merge into: | lp:openlp |
Diff against target: |
2439 lines (+2223/-56) 14 files modified
.bzrignore (+29/-38) openlp/core/common/path.py (+2/-0) openlp/core/ui/media/mediacontroller.py (+2/-0) openlp/plugins/presentations/lib/libreofficeserver.py (+431/-0) openlp/plugins/presentations/lib/maclocontroller.py (+266/-0) openlp/plugins/presentations/lib/presentationcontroller.py (+3/-1) openlp/plugins/presentations/lib/presentationtab.py (+2/-2) openlp/plugins/presentations/lib/serializers.py (+52/-0) openlp/plugins/presentations/lib/vendor/do_not_delete.txt (+5/-0) openlp/plugins/presentations/presentationplugin.py (+16/-14) scripts/check_dependencies.py (+2/-0) tests/functional/openlp_core/common/test_path.py (+12/-1) tests/functional/openlp_plugins/presentations/test_libreofficeserver.py (+948/-0) tests/functional/openlp_plugins/presentations/test_maclocontroller.py (+453/-0) |
To merge this branch: | bzr merge lp:~raoul-snyman/openlp/pyro-impress |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tomas Groth | Needs Fixing | ||
Tim Bentley | Pending | ||
Review via email: mp+367787@code.launchpad.net |
This proposal supersedes a proposal from 2019-05-18.
This proposal has been superseded by a proposal from 2019-06-05.
Commit message
Description of the change
Add presentations through LibreOffice on macOS. Comments and criticisms welcome.
Tim Bentley (trb143) wrote : Posted in a previous version of this proposal | # |
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
Linux tests failed, please see https:/
Tim Bentley (trb143) wrote : Posted in a previous version of this proposal | # |
As commented all the license years are 2016 but that can be fixed with the gpl3 move!
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
This is still a WIP branch. There are some changes that need to be made after merging with trunk.
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
Linux tests failed, please see https:/
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal | # |
Linux tests failed, please see https:/
Raoul Snyman (raoul-snyman) wrote : | # |
Linux tests passed!
Raoul Snyman (raoul-snyman) wrote : | # |
Linting passed!
Raoul Snyman (raoul-snyman) wrote : | # |
macOS tests passed!
Tomas Groth (tomasgroth) wrote : | # |
Headers needs update - see inline.
I tried to test but the MacBook at hand was simply to slow to be of value....
- 2733. By Raoul Snyman
-
HEAD
- 2734. By Raoul Snyman
-
Fix license issues
Unmerged revisions
Preview Diff
1 | === modified file '.bzrignore' | |||
2 | --- .bzrignore 2019-03-25 21:24:51 +0000 | |||
3 | +++ .bzrignore 2019-06-05 04:57:51 +0000 | |||
4 | @@ -1,57 +1,48 @@ | |||
5 | 1 | *.*~ | 1 | *.*~ |
6 | 2 | *.~\?~ | ||
7 | 3 | \#*\# | ||
8 | 4 | build | ||
9 | 5 | .cache | ||
10 | 6 | cover | ||
11 | 7 | .coverage | ||
12 | 8 | coverage | ||
13 | 9 | .directory | ||
14 | 10 | .vscode | ||
15 | 11 | dist | ||
16 | 12 | *.dll | 2 | *.dll |
17 | 13 | documentation/build/doctrees | ||
18 | 14 | documentation/build/html | ||
19 | 15 | *.e4* | 3 | *.e4* |
20 | 16 | *eric[1-9]project | ||
21 | 17 | .git | ||
22 | 18 | env | ||
23 | 19 | # Git files | ||
24 | 20 | .gitignore | ||
25 | 21 | htmlcov | ||
26 | 22 | .idea | ||
27 | 23 | *.kate-swp | 4 | *.kate-swp |
28 | 24 | *.kdev4 | 5 | *.kdev4 |
29 | 25 | .kdev4 | ||
30 | 26 | *.komodoproject | 6 | *.komodoproject |
31 | 27 | .komodotools | ||
32 | 28 | list | ||
33 | 29 | *.log* | 7 | *.log* |
34 | 30 | *.nja | 8 | *.nja |
35 | 31 | openlp.cfg | ||
36 | 32 | openlp/core/resources.py.old | ||
37 | 33 | OpenLP.egg-info | ||
38 | 34 | openlp.org 2.0.e4* | ||
39 | 35 | openlp.pro | ||
40 | 36 | openlp-test-projectordb.sqlite | ||
41 | 37 | *.orig | 9 | *.orig |
42 | 38 | output | ||
43 | 39 | *.pyc | 10 | *.pyc |
44 | 40 | __pycache__ | ||
45 | 41 | .pylint.d | ||
46 | 42 | .pytest_cache | ||
47 | 43 | *.qm | 11 | *.qm |
48 | 44 | *.rej | 12 | *.rej |
49 | 45 | # Rejected diff's | ||
50 | 46 | resources/innosetup/Output | ||
51 | 47 | resources/windows/warnOpenLP.txt | ||
52 | 48 | *.ropeproject | 13 | *.ropeproject |
55 | 49 | tags | 14 | *.~\?~ |
56 | 50 | output | 15 | *eric[1-9]project |
57 | 16 | .cache | ||
58 | 17 | .coverage | ||
59 | 18 | .directory | ||
60 | 19 | .git | ||
61 | 20 | .gitignore | ||
62 | 21 | .idea | ||
63 | 22 | .kdev4 | ||
64 | 23 | .komodotools | ||
65 | 24 | .pylint.d | ||
66 | 25 | .pytest_cache | ||
67 | 26 | .vscode | ||
68 | 27 | OpenLP.egg-info | ||
69 | 28 | \#*\# | ||
70 | 29 | __pycache__ | ||
71 | 30 | build | ||
72 | 31 | cover | ||
73 | 32 | coverage | ||
74 | 33 | dist | ||
75 | 34 | env | ||
76 | 51 | htmlcov | 35 | htmlcov |
77 | 36 | list | ||
78 | 52 | node_modules | 37 | node_modules |
79 | 53 | openlp-test-projectordb.sqlite | 38 | openlp-test-projectordb.sqlite |
80 | 39 | openlp.cfg | ||
81 | 40 | openlp.pro | ||
82 | 41 | openlp/core/resources.py.old | ||
83 | 42 | openlp/plugins/presentations/lib/vendor/Pyro4 | ||
84 | 43 | openlp/plugins/presentations/lib/vendor/serpent.py | ||
85 | 44 | output | ||
86 | 54 | package-lock.json | 45 | package-lock.json |
88 | 55 | .cache | 46 | tags |
89 | 56 | test | 47 | test |
90 | 57 | tests.kdev4 | 48 | tests.kdev4 |
91 | 58 | 49 | ||
92 | === modified file 'openlp/core/common/path.py' | |||
93 | --- openlp/core/common/path.py 2019-05-22 06:47:00 +0000 | |||
94 | +++ openlp/core/common/path.py 2019-06-05 04:57:51 +0000 | |||
95 | @@ -78,6 +78,8 @@ | |||
96 | 78 | :return: An empty string if :param:`path` is None, else a string representation of the :param:`path` | 78 | :return: An empty string if :param:`path` is None, else a string representation of the :param:`path` |
97 | 79 | :rtype: str | 79 | :rtype: str |
98 | 80 | """ | 80 | """ |
99 | 81 | if isinstance(path, str): | ||
100 | 82 | return path | ||
101 | 81 | if not isinstance(path, Path) and path is not None: | 83 | if not isinstance(path, Path) and path is not None: |
102 | 82 | raise TypeError('parameter \'path\' must be of type Path or NoneType') | 84 | raise TypeError('parameter \'path\' must be of type Path or NoneType') |
103 | 83 | if path is None: | 85 | if path is None: |
104 | 84 | 86 | ||
105 | === modified file 'openlp/core/ui/media/mediacontroller.py' | |||
106 | --- openlp/core/ui/media/mediacontroller.py 2019-06-01 06:59:45 +0000 | |||
107 | +++ openlp/core/ui/media/mediacontroller.py 2019-06-05 04:57:51 +0000 | |||
108 | @@ -104,6 +104,8 @@ | |||
109 | 104 | State().update_pre_conditions('mediacontroller', True) | 104 | State().update_pre_conditions('mediacontroller', True) |
110 | 105 | State().update_pre_conditions('media_live', True) | 105 | State().update_pre_conditions('media_live', True) |
111 | 106 | else: | 106 | else: |
112 | 107 | if hasattr(self.main_window, 'splash') and self.main_window.splash.isVisible(): | ||
113 | 108 | self.main_window.splash.hide() | ||
114 | 107 | State().missing_text('media_live', translate('OpenLP.SlideController', | 109 | State().missing_text('media_live', translate('OpenLP.SlideController', |
115 | 108 | 'VLC or pymediainfo are missing, so you are unable to play any media')) | 110 | 'VLC or pymediainfo are missing, so you are unable to play any media')) |
116 | 109 | return True | 111 | return True |
117 | 110 | 112 | ||
118 | === added file 'openlp/plugins/presentations/lib/libreofficeserver.py' | |||
119 | --- openlp/plugins/presentations/lib/libreofficeserver.py 1970-01-01 00:00:00 +0000 | |||
120 | +++ openlp/plugins/presentations/lib/libreofficeserver.py 2019-06-05 04:57:51 +0000 | |||
121 | @@ -0,0 +1,431 @@ | |||
122 | 1 | # -*- coding: utf-8 -*- | ||
123 | 2 | # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 | ||
124 | 3 | |||
125 | 4 | ########################################################################## | ||
126 | 5 | # OpenLP - Open Source Lyrics Projection # | ||
127 | 6 | # ---------------------------------------------------------------------- # | ||
128 | 7 | # Copyright (c) 2008-2019 OpenLP Developers # | ||
129 | 8 | # ---------------------------------------------------------------------- # | ||
130 | 9 | # This program is free software: you can redistribute it and/or modify # | ||
131 | 10 | # it under the terms of the GNU General Public License as published by # | ||
132 | 11 | # the Free Software Foundation, either version 3 of the License, or # | ||
133 | 12 | # (at your option) any later version. # | ||
134 | 13 | # # | ||
135 | 14 | # This program is distributed in the hope that it will be useful, # | ||
136 | 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of # | ||
137 | 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # | ||
138 | 17 | # GNU General Public License for more details. # | ||
139 | 18 | # # | ||
140 | 19 | # You should have received a copy of the GNU General Public License # | ||
141 | 20 | # along with this program. If not, see <https://www.gnu.org/licenses/>. # | ||
142 | 21 | ########################################################################## | ||
143 | 22 | """ | ||
144 | 23 | This module runs a Pyro4 server using LibreOffice's version of Python | ||
145 | 24 | |||
146 | 25 | Please Note: This intentionally uses os.path over pathlib because we don't know which version of Python is shipped with | ||
147 | 26 | the version of LibreOffice on the user's computer. | ||
148 | 27 | """ | ||
149 | 28 | from subprocess import Popen | ||
150 | 29 | import sys | ||
151 | 30 | import os | ||
152 | 31 | import logging | ||
153 | 32 | import time | ||
154 | 33 | |||
155 | 34 | |||
156 | 35 | if sys.platform.startswith('darwin'): | ||
157 | 36 | # Only make the log file on OS X when running as a server | ||
158 | 37 | logfile = os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp', 'libreofficeserver.log') | ||
159 | 38 | print('Setting up log file: {logfile}'.format(logfile=logfile)) | ||
160 | 39 | logging.basicConfig(filename=logfile, level=logging.INFO) | ||
161 | 40 | |||
162 | 41 | |||
163 | 42 | # Add the current directory to sys.path so that we can load the serializers | ||
164 | 43 | sys.path.append(os.path.join(os.path.dirname(__file__))) | ||
165 | 44 | # Add the vendor directory to sys.path so that we can load Pyro4 | ||
166 | 45 | sys.path.append(os.path.join(os.path.dirname(__file__), 'vendor')) | ||
167 | 46 | |||
168 | 47 | from serializers import register_classes | ||
169 | 48 | from Pyro4 import Daemon, expose | ||
170 | 49 | |||
171 | 50 | try: | ||
172 | 51 | # Wrap these imports in a try so that we can run the tests on macOS | ||
173 | 52 | import uno | ||
174 | 53 | from com.sun.star.beans import PropertyValue | ||
175 | 54 | from com.sun.star.task import ErrorCodeIOException | ||
176 | 55 | except ImportError: | ||
177 | 56 | # But they need to be defined for mocking | ||
178 | 57 | uno = None | ||
179 | 58 | PropertyValue = None | ||
180 | 59 | ErrorCodeIOException = Exception | ||
181 | 60 | |||
182 | 61 | |||
183 | 62 | log = logging.getLogger(__name__) | ||
184 | 63 | register_classes() | ||
185 | 64 | |||
186 | 65 | |||
187 | 66 | class TextType(object): | ||
188 | 67 | """ | ||
189 | 68 | Type Enumeration for Types of Text to request | ||
190 | 69 | """ | ||
191 | 70 | Title = 0 | ||
192 | 71 | SlideText = 1 | ||
193 | 72 | Notes = 2 | ||
194 | 73 | |||
195 | 74 | |||
196 | 75 | class LibreOfficeException(Exception): | ||
197 | 76 | """ | ||
198 | 77 | A specific exception for LO | ||
199 | 78 | """ | ||
200 | 79 | pass | ||
201 | 80 | |||
202 | 81 | |||
203 | 82 | @expose | ||
204 | 83 | class LibreOfficeServer(object): | ||
205 | 84 | """ | ||
206 | 85 | A Pyro4 server which controls LibreOffice | ||
207 | 86 | """ | ||
208 | 87 | def __init__(self): | ||
209 | 88 | """ | ||
210 | 89 | Set up the server | ||
211 | 90 | """ | ||
212 | 91 | self._desktop = None | ||
213 | 92 | self._control = None | ||
214 | 93 | self._document = None | ||
215 | 94 | self._presentation = None | ||
216 | 95 | self._process = None | ||
217 | 96 | self._manager = None | ||
218 | 97 | |||
219 | 98 | def _create_property(self, name, value): | ||
220 | 99 | """ | ||
221 | 100 | Create an OOo style property object which are passed into some Uno methods. | ||
222 | 101 | """ | ||
223 | 102 | log.debug('create property') | ||
224 | 103 | property_object = PropertyValue() | ||
225 | 104 | property_object.Name = name | ||
226 | 105 | property_object.Value = value | ||
227 | 106 | return property_object | ||
228 | 107 | |||
229 | 108 | def _get_text_from_page(self, slide_no, text_type=TextType.SlideText): | ||
230 | 109 | """ | ||
231 | 110 | Return any text extracted from the presentation page. | ||
232 | 111 | |||
233 | 112 | :param slide_no: The slide the notes are required for, starting at 1 | ||
234 | 113 | :param notes: A boolean. If set the method searches the notes of the slide. | ||
235 | 114 | :param text_type: A TextType. Enumeration of the types of supported text. | ||
236 | 115 | """ | ||
237 | 116 | text = '' | ||
238 | 117 | if TextType.Title <= text_type <= TextType.Notes: | ||
239 | 118 | pages = self._document.getDrawPages() | ||
240 | 119 | if 0 < slide_no <= pages.getCount(): | ||
241 | 120 | page = pages.getByIndex(slide_no - 1) | ||
242 | 121 | if text_type == TextType.Notes: | ||
243 | 122 | page = page.getNotesPage() | ||
244 | 123 | for index in range(page.getCount()): | ||
245 | 124 | shape = page.getByIndex(index) | ||
246 | 125 | shape_type = shape.getShapeType() | ||
247 | 126 | if shape.supportsService('com.sun.star.drawing.Text'): | ||
248 | 127 | # if they requested title, make sure it is the title | ||
249 | 128 | if text_type != TextType.Title or shape_type == 'com.sun.star.presentation.TitleTextShape': | ||
250 | 129 | text += shape.getString() + '\n' | ||
251 | 130 | return text | ||
252 | 131 | |||
253 | 132 | def start_process(self): | ||
254 | 133 | """ | ||
255 | 134 | Initialise Impress | ||
256 | 135 | """ | ||
257 | 136 | uno_command = [ | ||
258 | 137 | '/Applications/LibreOffice.app/Contents/MacOS/soffice', | ||
259 | 138 | '--nologo', | ||
260 | 139 | '--norestore', | ||
261 | 140 | '--minimized', | ||
262 | 141 | '--nodefault', | ||
263 | 142 | '--nofirststartwizard', | ||
264 | 143 | '--accept=pipe,name=openlp_maclo;urp;StarOffice.ServiceManager' | ||
265 | 144 | ] | ||
266 | 145 | self._process = Popen(uno_command) | ||
267 | 146 | |||
268 | 147 | @property | ||
269 | 148 | def desktop(self): | ||
270 | 149 | """ | ||
271 | 150 | Set up an UNO desktop instance | ||
272 | 151 | """ | ||
273 | 152 | if self._desktop is not None: | ||
274 | 153 | return self._desktop | ||
275 | 154 | uno_instance = None | ||
276 | 155 | context = uno.getComponentContext() | ||
277 | 156 | resolver = context.ServiceManager.createInstanceWithContext('com.sun.star.bridge.UnoUrlResolver', context) | ||
278 | 157 | loop = 0 | ||
279 | 158 | while uno_instance is None and loop < 3: | ||
280 | 159 | try: | ||
281 | 160 | uno_instance = resolver.resolve('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext') | ||
282 | 161 | except Exception: | ||
283 | 162 | log.exception('Unable to find running instance, retrying...') | ||
284 | 163 | loop += 1 | ||
285 | 164 | try: | ||
286 | 165 | self._manager = uno_instance.ServiceManager | ||
287 | 166 | log.debug('get UNO Desktop Openoffice - createInstanceWithContext - Desktop') | ||
288 | 167 | desktop = self._manager.createInstanceWithContext('com.sun.star.frame.Desktop', uno_instance) | ||
289 | 168 | if not desktop: | ||
290 | 169 | raise Exception('Failed to get UNO desktop') | ||
291 | 170 | self._desktop = desktop | ||
292 | 171 | return desktop | ||
293 | 172 | except Exception: | ||
294 | 173 | log.exception('Failed to get UNO desktop') | ||
295 | 174 | return None | ||
296 | 175 | |||
297 | 176 | def shutdown(self): | ||
298 | 177 | """ | ||
299 | 178 | Shut down the server | ||
300 | 179 | """ | ||
301 | 180 | can_kill = True | ||
302 | 181 | if hasattr(self, '_docs'): | ||
303 | 182 | while self._docs: | ||
304 | 183 | self._docs[0].close_presentation() | ||
305 | 184 | docs = self.desktop.getComponents() | ||
306 | 185 | count = 0 | ||
307 | 186 | if docs.hasElements(): | ||
308 | 187 | list_elements = docs.createEnumeration() | ||
309 | 188 | while list_elements.hasMoreElements(): | ||
310 | 189 | doc = list_elements.nextElement() | ||
311 | 190 | if doc.getImplementationName() != 'com.sun.star.comp.framework.BackingComp': | ||
312 | 191 | count += 1 | ||
313 | 192 | if count > 0: | ||
314 | 193 | log.debug('LibreOffice not terminated as docs are still open') | ||
315 | 194 | can_kill = False | ||
316 | 195 | else: | ||
317 | 196 | try: | ||
318 | 197 | self.desktop.terminate() | ||
319 | 198 | log.debug('LibreOffice killed') | ||
320 | 199 | except Exception: | ||
321 | 200 | log.exception('Failed to terminate LibreOffice') | ||
322 | 201 | if getattr(self, '_process') and can_kill: | ||
323 | 202 | self._process.kill() | ||
324 | 203 | |||
325 | 204 | def load_presentation(self, file_path, screen_number): | ||
326 | 205 | """ | ||
327 | 206 | Load a presentation | ||
328 | 207 | """ | ||
329 | 208 | self._file_path = file_path | ||
330 | 209 | url = uno.systemPathToFileUrl(file_path) | ||
331 | 210 | properties = (self._create_property('Hidden', True),) | ||
332 | 211 | self._document = None | ||
333 | 212 | loop_count = 0 | ||
334 | 213 | while loop_count < 3: | ||
335 | 214 | try: | ||
336 | 215 | self._document = self.desktop.loadComponentFromURL(url, '_blank', 0, properties) | ||
337 | 216 | except Exception: | ||
338 | 217 | log.exception('Failed to load presentation {url}'.format(url=url)) | ||
339 | 218 | if self._document: | ||
340 | 219 | break | ||
341 | 220 | time.sleep(0.5) | ||
342 | 221 | loop_count += 1 | ||
343 | 222 | if loop_count == 3: | ||
344 | 223 | log.error('Looped too many times') | ||
345 | 224 | return False | ||
346 | 225 | self._presentation = self._document.getPresentation() | ||
347 | 226 | self._presentation.Display = screen_number | ||
348 | 227 | self._control = None | ||
349 | 228 | return True | ||
350 | 229 | |||
351 | 230 | def extract_thumbnails(self, temp_folder): | ||
352 | 231 | """ | ||
353 | 232 | Create thumbnails for the presentation | ||
354 | 233 | """ | ||
355 | 234 | thumbnails = [] | ||
356 | 235 | thumb_dir_url = uno.systemPathToFileUrl(temp_folder) | ||
357 | 236 | properties = (self._create_property('FilterName', 'impress_png_Export'),) | ||
358 | 237 | pages = self._document.getDrawPages() | ||
359 | 238 | if not pages: | ||
360 | 239 | return [] | ||
361 | 240 | if not os.path.isdir(temp_folder): | ||
362 | 241 | os.makedirs(temp_folder) | ||
363 | 242 | for index in range(pages.getCount()): | ||
364 | 243 | page = pages.getByIndex(index) | ||
365 | 244 | self._document.getCurrentController().setCurrentPage(page) | ||
366 | 245 | url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1)) | ||
367 | 246 | path = os.path.join(temp_folder, str(index + 1) + '.png') | ||
368 | 247 | try: | ||
369 | 248 | self._document.storeToURL(url_path, properties) | ||
370 | 249 | thumbnails.append(path) | ||
371 | 250 | except ErrorCodeIOException as exception: | ||
372 | 251 | log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode)) | ||
373 | 252 | except Exception: | ||
374 | 253 | log.exception('{path} - Unable to store openoffice preview'.format(path=path)) | ||
375 | 254 | return thumbnails | ||
376 | 255 | |||
377 | 256 | def get_titles_and_notes(self): | ||
378 | 257 | """ | ||
379 | 258 | Extract the titles and the notes from the slides. | ||
380 | 259 | """ | ||
381 | 260 | titles = [] | ||
382 | 261 | notes = [] | ||
383 | 262 | pages = self._document.getDrawPages() | ||
384 | 263 | for slide_no in range(1, pages.getCount() + 1): | ||
385 | 264 | titles.append(self._get_text_from_page(slide_no, TextType.Title).replace('\n', ' ') + '\n') | ||
386 | 265 | note = self._get_text_from_page(slide_no, TextType.Notes) | ||
387 | 266 | if len(note) == 0: | ||
388 | 267 | note = ' ' | ||
389 | 268 | notes.append(note) | ||
390 | 269 | return titles, notes | ||
391 | 270 | |||
392 | 271 | def close_presentation(self): | ||
393 | 272 | """ | ||
394 | 273 | Close presentation and clean up objects. | ||
395 | 274 | """ | ||
396 | 275 | log.debug('close Presentation LibreOffice') | ||
397 | 276 | if self._document: | ||
398 | 277 | if self._presentation: | ||
399 | 278 | try: | ||
400 | 279 | self._presentation.end() | ||
401 | 280 | self._presentation = None | ||
402 | 281 | self._document.dispose() | ||
403 | 282 | except Exception: | ||
404 | 283 | log.exception("Closing presentation failed") | ||
405 | 284 | self._document = None | ||
406 | 285 | |||
407 | 286 | def is_loaded(self): | ||
408 | 287 | """ | ||
409 | 288 | Returns true if a presentation is loaded. | ||
410 | 289 | """ | ||
411 | 290 | log.debug('is loaded LibreOffice') | ||
412 | 291 | if self._presentation is None or self._document is None: | ||
413 | 292 | log.debug("is_loaded: no presentation or document") | ||
414 | 293 | return False | ||
415 | 294 | try: | ||
416 | 295 | if self._document.getPresentation() is None: | ||
417 | 296 | log.debug("getPresentation failed to find a presentation") | ||
418 | 297 | return False | ||
419 | 298 | except Exception: | ||
420 | 299 | log.exception("getPresentation failed to find a presentation") | ||
421 | 300 | return False | ||
422 | 301 | return True | ||
423 | 302 | |||
424 | 303 | def is_active(self): | ||
425 | 304 | """ | ||
426 | 305 | Returns true if a presentation is active and running. | ||
427 | 306 | """ | ||
428 | 307 | log.debug('is active LibreOffice') | ||
429 | 308 | if not self.is_loaded(): | ||
430 | 309 | return False | ||
431 | 310 | return self._control.isRunning() if self._control else False | ||
432 | 311 | |||
433 | 312 | def unblank_screen(self): | ||
434 | 313 | """ | ||
435 | 314 | Unblanks the screen. | ||
436 | 315 | """ | ||
437 | 316 | log.debug('unblank screen LibreOffice') | ||
438 | 317 | return self._control.resume() | ||
439 | 318 | |||
440 | 319 | def blank_screen(self): | ||
441 | 320 | """ | ||
442 | 321 | Blanks the screen. | ||
443 | 322 | """ | ||
444 | 323 | log.debug('blank screen LibreOffice') | ||
445 | 324 | self._control.blankScreen(0) | ||
446 | 325 | |||
447 | 326 | def is_blank(self): | ||
448 | 327 | """ | ||
449 | 328 | Returns true if screen is blank. | ||
450 | 329 | """ | ||
451 | 330 | log.debug('is blank LibreOffice') | ||
452 | 331 | if self._control and self._control.isRunning(): | ||
453 | 332 | return self._control.isPaused() | ||
454 | 333 | else: | ||
455 | 334 | return False | ||
456 | 335 | |||
457 | 336 | def stop_presentation(self): | ||
458 | 337 | """ | ||
459 | 338 | Stop the presentation, remove from screen. | ||
460 | 339 | """ | ||
461 | 340 | log.debug('stop presentation LibreOffice') | ||
462 | 341 | self._presentation.end() | ||
463 | 342 | self._control = None | ||
464 | 343 | |||
465 | 344 | def start_presentation(self): | ||
466 | 345 | """ | ||
467 | 346 | Start the presentation from the beginning. | ||
468 | 347 | """ | ||
469 | 348 | log.debug('start presentation LibreOffice') | ||
470 | 349 | if self._control is None or not self._control.isRunning(): | ||
471 | 350 | window = self._document.getCurrentController().getFrame().getContainerWindow() | ||
472 | 351 | window.setVisible(True) | ||
473 | 352 | self._presentation.start() | ||
474 | 353 | self._control = self._presentation.getController() | ||
475 | 354 | # start() returns before the Component is ready. Try for 15 seconds. | ||
476 | 355 | sleep_count = 1 | ||
477 | 356 | while not self._control and sleep_count < 150: | ||
478 | 357 | time.sleep(0.1) | ||
479 | 358 | sleep_count += 1 | ||
480 | 359 | self._control = self._presentation.getController() | ||
481 | 360 | window.setVisible(False) | ||
482 | 361 | else: | ||
483 | 362 | self._control.activate() | ||
484 | 363 | self.goto_slide(1) | ||
485 | 364 | |||
486 | 365 | def get_slide_number(self): | ||
487 | 366 | """ | ||
488 | 367 | Return the current slide number on the screen, from 1. | ||
489 | 368 | """ | ||
490 | 369 | return self._control.getCurrentSlideIndex() + 1 | ||
491 | 370 | |||
492 | 371 | def get_slide_count(self): | ||
493 | 372 | """ | ||
494 | 373 | Return the total number of slides. | ||
495 | 374 | """ | ||
496 | 375 | return self._document.getDrawPages().getCount() | ||
497 | 376 | |||
498 | 377 | def goto_slide(self, slide_no): | ||
499 | 378 | """ | ||
500 | 379 | Go to a specific slide (from 1). | ||
501 | 380 | |||
502 | 381 | :param slide_no: The slide the text is required for, starting at 1 | ||
503 | 382 | """ | ||
504 | 383 | self._control.gotoSlideIndex(slide_no - 1) | ||
505 | 384 | |||
506 | 385 | def next_step(self): | ||
507 | 386 | """ | ||
508 | 387 | Triggers the next effect of slide on the running presentation. | ||
509 | 388 | """ | ||
510 | 389 | is_paused = self._control.isPaused() | ||
511 | 390 | self._control.gotoNextEffect() | ||
512 | 391 | time.sleep(0.1) | ||
513 | 392 | if not is_paused and self._control.isPaused(): | ||
514 | 393 | self._control.gotoPreviousEffect() | ||
515 | 394 | |||
516 | 395 | def previous_step(self): | ||
517 | 396 | """ | ||
518 | 397 | Triggers the previous slide on the running presentation. | ||
519 | 398 | """ | ||
520 | 399 | self._control.gotoPreviousEffect() | ||
521 | 400 | |||
522 | 401 | def get_slide_text(self, slide_no): | ||
523 | 402 | """ | ||
524 | 403 | Returns the text on the slide. | ||
525 | 404 | |||
526 | 405 | :param slide_no: The slide the text is required for, starting at 1 | ||
527 | 406 | """ | ||
528 | 407 | return self._get_text_from_page(slide_no) | ||
529 | 408 | |||
530 | 409 | def get_slide_notes(self, slide_no): | ||
531 | 410 | """ | ||
532 | 411 | Returns the text in the slide notes. | ||
533 | 412 | |||
534 | 413 | :param slide_no: The slide the notes are required for, starting at 1 | ||
535 | 414 | """ | ||
536 | 415 | return self._get_text_from_page(slide_no, TextType.Notes) | ||
537 | 416 | |||
538 | 417 | |||
539 | 418 | def main(): | ||
540 | 419 | """ | ||
541 | 420 | The main function which runs the server | ||
542 | 421 | """ | ||
543 | 422 | daemon = Daemon(host='localhost', port=4310) | ||
544 | 423 | daemon.register(LibreOfficeServer, 'openlp.libreofficeserver') | ||
545 | 424 | try: | ||
546 | 425 | daemon.requestLoop() | ||
547 | 426 | finally: | ||
548 | 427 | daemon.close() | ||
549 | 428 | |||
550 | 429 | |||
551 | 430 | if __name__ == '__main__': | ||
552 | 431 | main() | ||
553 | 0 | 432 | ||
554 | === added file 'openlp/plugins/presentations/lib/maclocontroller.py' | |||
555 | --- openlp/plugins/presentations/lib/maclocontroller.py 1970-01-01 00:00:00 +0000 | |||
556 | +++ openlp/plugins/presentations/lib/maclocontroller.py 2019-06-05 04:57:51 +0000 | |||
557 | @@ -0,0 +1,266 @@ | |||
558 | 1 | # -*- coding: utf-8 -*- | ||
559 | 2 | # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 | ||
560 | 3 | |||
561 | 4 | ########################################################################## | ||
562 | 5 | # OpenLP - Open Source Lyrics Projection # | ||
563 | 6 | # ---------------------------------------------------------------------- # | ||
564 | 7 | # Copyright (c) 2008-2019 OpenLP Developers # | ||
565 | 8 | # ---------------------------------------------------------------------- # | ||
566 | 9 | # This program is free software: you can redistribute it and/or modify # | ||
567 | 10 | # it under the terms of the GNU General Public License as published by # | ||
568 | 11 | # the Free Software Foundation, either version 3 of the License, or # | ||
569 | 12 | # (at your option) any later version. # | ||
570 | 13 | # # | ||
571 | 14 | # This program is distributed in the hope that it will be useful, # | ||
572 | 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of # | ||
573 | 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # | ||
574 | 17 | # GNU General Public License for more details. # | ||
575 | 18 | # # | ||
576 | 19 | # You should have received a copy of the GNU General Public License # | ||
577 | 20 | # along with this program. If not, see <https://www.gnu.org/licenses/>. # | ||
578 | 21 | ########################################################################## | ||
579 | 22 | |||
580 | 23 | import logging | ||
581 | 24 | from subprocess import Popen | ||
582 | 25 | |||
583 | 26 | from Pyro4 import Proxy | ||
584 | 27 | |||
585 | 28 | from openlp.core.common import delete_file, is_macosx | ||
586 | 29 | from openlp.core.common.applocation import AppLocation | ||
587 | 30 | from openlp.core.common.path import Path | ||
588 | 31 | from openlp.core.common.registry import Registry | ||
589 | 32 | from openlp.core.display.screens import ScreenList | ||
590 | 33 | from openlp.plugins.presentations.lib.serializers import register_classes | ||
591 | 34 | from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument | ||
592 | 35 | |||
593 | 36 | |||
594 | 37 | LIBREOFFICE_PATH = Path('/Applications/LibreOffice.app') | ||
595 | 38 | LIBREOFFICE_PYTHON = LIBREOFFICE_PATH / 'Contents' / 'Resources' / 'python' | ||
596 | 39 | |||
597 | 40 | if is_macosx() and LIBREOFFICE_PATH.exists(): | ||
598 | 41 | macuno_available = True | ||
599 | 42 | else: | ||
600 | 43 | macuno_available = False | ||
601 | 44 | |||
602 | 45 | |||
603 | 46 | log = logging.getLogger(__name__) | ||
604 | 47 | register_classes() | ||
605 | 48 | |||
606 | 49 | |||
607 | 50 | class MacLOController(PresentationController): | ||
608 | 51 | """ | ||
609 | 52 | Class to control interactions with MacLO presentations on Mac OS X via Pyro4. It starts the Pyro4 nameserver, | ||
610 | 53 | starts the LibreOfficeServer, and then controls MacLO via Pyro4. | ||
611 | 54 | """ | ||
612 | 55 | log.info('MacLOController loaded') | ||
613 | 56 | |||
614 | 57 | def __init__(self, plugin): | ||
615 | 58 | """ | ||
616 | 59 | Initialise the class | ||
617 | 60 | """ | ||
618 | 61 | log.debug('Initialising') | ||
619 | 62 | super(MacLOController, self).__init__(plugin, 'maclo', MacLODocument, 'Impress on macOS') | ||
620 | 63 | self.supports = ['odp'] | ||
621 | 64 | self.also_supports = ['ppt', 'pps', 'pptx', 'ppsx', 'pptm'] | ||
622 | 65 | self.server_process = None | ||
623 | 66 | self._client = None | ||
624 | 67 | self._start_server() | ||
625 | 68 | |||
626 | 69 | def _start_server(self): | ||
627 | 70 | """ | ||
628 | 71 | Start a LibreOfficeServer | ||
629 | 72 | """ | ||
630 | 73 | libreoffice_python = Path('/Applications/LibreOffice.app/Contents/Resources/python') | ||
631 | 74 | libreoffice_server = AppLocation.get_directory(AppLocation.PluginsDir).joinpath('presentations', 'lib', | ||
632 | 75 | 'libreofficeserver.py') | ||
633 | 76 | if libreoffice_python.exists(): | ||
634 | 77 | self.server_process = Popen([str(libreoffice_python), str(libreoffice_server)]) | ||
635 | 78 | |||
636 | 79 | @property | ||
637 | 80 | def client(self): | ||
638 | 81 | """ | ||
639 | 82 | Set up a Pyro4 client so that we can talk to the LibreOfficeServer | ||
640 | 83 | """ | ||
641 | 84 | if not self._client: | ||
642 | 85 | self._client = Proxy('PYRO:openlp.libreofficeserver@localhost:4310') | ||
643 | 86 | if not self._client._pyroConnection: | ||
644 | 87 | self._client._pyroReconnect() | ||
645 | 88 | return self._client | ||
646 | 89 | |||
647 | 90 | def check_available(self): | ||
648 | 91 | """ | ||
649 | 92 | MacLO is able to run on this machine. | ||
650 | 93 | """ | ||
651 | 94 | log.debug('check_available') | ||
652 | 95 | return macuno_available | ||
653 | 96 | |||
654 | 97 | def start_process(self): | ||
655 | 98 | """ | ||
656 | 99 | Loads a running version of LibreOffice in the background. It is not displayed to the user but is available to | ||
657 | 100 | the UNO interface when required. | ||
658 | 101 | """ | ||
659 | 102 | log.debug('Started automatically by the Pyro server') | ||
660 | 103 | self.client.start_process() | ||
661 | 104 | |||
662 | 105 | def kill(self): | ||
663 | 106 | """ | ||
664 | 107 | Called at system exit to clean up any running presentations. | ||
665 | 108 | """ | ||
666 | 109 | log.debug('Kill LibreOffice') | ||
667 | 110 | self.client.shutdown() | ||
668 | 111 | self.server_process.kill() | ||
669 | 112 | |||
670 | 113 | |||
671 | 114 | class MacLODocument(PresentationDocument): | ||
672 | 115 | """ | ||
673 | 116 | Class which holds information and controls a single presentation. | ||
674 | 117 | """ | ||
675 | 118 | |||
676 | 119 | def __init__(self, controller, presentation): | ||
677 | 120 | """ | ||
678 | 121 | Constructor, store information about the file and initialise. | ||
679 | 122 | """ | ||
680 | 123 | log.debug('Init Presentation LibreOffice') | ||
681 | 124 | super(MacLODocument, self).__init__(controller, presentation) | ||
682 | 125 | self.client = controller.client | ||
683 | 126 | |||
684 | 127 | def load_presentation(self): | ||
685 | 128 | """ | ||
686 | 129 | Tell the LibreOfficeServer to start the presentation. | ||
687 | 130 | """ | ||
688 | 131 | log.debug('Load Presentation LibreOffice') | ||
689 | 132 | if not self.client.load_presentation(str(self.file_path), ScreenList().current.number + 1): | ||
690 | 133 | return False | ||
691 | 134 | self.create_thumbnails() | ||
692 | 135 | self.create_titles_and_notes() | ||
693 | 136 | return True | ||
694 | 137 | |||
695 | 138 | def create_thumbnails(self): | ||
696 | 139 | """ | ||
697 | 140 | Create thumbnail images for presentation. | ||
698 | 141 | """ | ||
699 | 142 | log.debug('create thumbnails LibreOffice') | ||
700 | 143 | if self.check_thumbnails(): | ||
701 | 144 | return | ||
702 | 145 | temp_thumbnails = self.client.extract_thumbnails(str(self.get_temp_folder())) | ||
703 | 146 | for index, temp_thumb in enumerate(temp_thumbnails): | ||
704 | 147 | temp_thumb = Path(temp_thumb) | ||
705 | 148 | self.convert_thumbnail(temp_thumb, index + 1) | ||
706 | 149 | delete_file(temp_thumb) | ||
707 | 150 | |||
708 | 151 | def create_titles_and_notes(self): | ||
709 | 152 | """ | ||
710 | 153 | Writes the list of titles (one per slide) to 'titles.txt' and the notes to 'slideNotes[x].txt' | ||
711 | 154 | in the thumbnails directory | ||
712 | 155 | """ | ||
713 | 156 | titles, notes = self.client.get_titles_and_notes() | ||
714 | 157 | self.save_titles_and_notes(titles, notes) | ||
715 | 158 | |||
716 | 159 | def close_presentation(self): | ||
717 | 160 | """ | ||
718 | 161 | Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being | ||
719 | 162 | shutdown. | ||
720 | 163 | """ | ||
721 | 164 | log.debug('close Presentation LibreOffice') | ||
722 | 165 | self.client.close_presentation() | ||
723 | 166 | self.controller.remove_doc(self) | ||
724 | 167 | |||
725 | 168 | def is_loaded(self): | ||
726 | 169 | """ | ||
727 | 170 | Returns true if a presentation is loaded. | ||
728 | 171 | """ | ||
729 | 172 | log.debug('is loaded LibreOffice') | ||
730 | 173 | return self.client.is_loaded() | ||
731 | 174 | |||
732 | 175 | def is_active(self): | ||
733 | 176 | """ | ||
734 | 177 | Returns true if a presentation is active and running. | ||
735 | 178 | """ | ||
736 | 179 | log.debug('is active LibreOffice') | ||
737 | 180 | return self.client.is_active() | ||
738 | 181 | |||
739 | 182 | def unblank_screen(self): | ||
740 | 183 | """ | ||
741 | 184 | Unblanks the screen. | ||
742 | 185 | """ | ||
743 | 186 | log.debug('unblank screen LibreOffice') | ||
744 | 187 | return self.client.unblank_screen() | ||
745 | 188 | |||
746 | 189 | def blank_screen(self): | ||
747 | 190 | """ | ||
748 | 191 | Blanks the screen. | ||
749 | 192 | """ | ||
750 | 193 | log.debug('blank screen LibreOffice') | ||
751 | 194 | self.client.blank_screen() | ||
752 | 195 | |||
753 | 196 | def is_blank(self): | ||
754 | 197 | """ | ||
755 | 198 | Returns true if screen is blank. | ||
756 | 199 | """ | ||
757 | 200 | log.debug('is blank LibreOffice') | ||
758 | 201 | return self.client.is_blank() | ||
759 | 202 | |||
760 | 203 | def stop_presentation(self): | ||
761 | 204 | """ | ||
762 | 205 | Stop the presentation, remove from screen. | ||
763 | 206 | """ | ||
764 | 207 | log.debug('stop presentation LibreOffice') | ||
765 | 208 | self.client.stop_presentation() | ||
766 | 209 | |||
767 | 210 | def start_presentation(self): | ||
768 | 211 | """ | ||
769 | 212 | Start the presentation from the beginning. | ||
770 | 213 | """ | ||
771 | 214 | log.debug('start presentation LibreOffice') | ||
772 | 215 | self.client.start_presentation() | ||
773 | 216 | # Make sure impress doesn't steal focus, unless we're on a single screen setup | ||
774 | 217 | if len(ScreenList()) > 1: | ||
775 | 218 | Registry().get('main_window').activateWindow() | ||
776 | 219 | |||
777 | 220 | def get_slide_number(self): | ||
778 | 221 | """ | ||
779 | 222 | Return the current slide number on the screen, from 1. | ||
780 | 223 | """ | ||
781 | 224 | return self.client.get_slide_number() | ||
782 | 225 | |||
783 | 226 | def get_slide_count(self): | ||
784 | 227 | """ | ||
785 | 228 | Return the total number of slides. | ||
786 | 229 | """ | ||
787 | 230 | return self.client.get_slide_count() | ||
788 | 231 | |||
789 | 232 | def goto_slide(self, slide_no): | ||
790 | 233 | """ | ||
791 | 234 | Go to a specific slide (from 1). | ||
792 | 235 | |||
793 | 236 | :param slide_no: The slide the text is required for, starting at 1 | ||
794 | 237 | """ | ||
795 | 238 | self.client.goto_slide(slide_no) | ||
796 | 239 | |||
797 | 240 | def next_step(self): | ||
798 | 241 | """ | ||
799 | 242 | Triggers the next effect of slide on the running presentation. | ||
800 | 243 | """ | ||
801 | 244 | self.client.next_step() | ||
802 | 245 | |||
803 | 246 | def previous_step(self): | ||
804 | 247 | """ | ||
805 | 248 | Triggers the previous slide on the running presentation. | ||
806 | 249 | """ | ||
807 | 250 | self.client.previous_step() | ||
808 | 251 | |||
809 | 252 | def get_slide_text(self, slide_no): | ||
810 | 253 | """ | ||
811 | 254 | Returns the text on the slide. | ||
812 | 255 | |||
813 | 256 | :param slide_no: The slide the text is required for, starting at 1 | ||
814 | 257 | """ | ||
815 | 258 | return self.client.get_slide_text(slide_no) | ||
816 | 259 | |||
817 | 260 | def get_slide_notes(self, slide_no): | ||
818 | 261 | """ | ||
819 | 262 | Returns the text in the slide notes. | ||
820 | 263 | |||
821 | 264 | :param slide_no: The slide the notes are required for, starting at 1 | ||
822 | 265 | """ | ||
823 | 266 | return self.client.get_slide_notes(slide_no) | ||
824 | 0 | 267 | ||
825 | === modified file 'openlp/plugins/presentations/lib/presentationcontroller.py' | |||
826 | --- openlp/plugins/presentations/lib/presentationcontroller.py 2019-05-22 06:47:00 +0000 | |||
827 | +++ openlp/plugins/presentations/lib/presentationcontroller.py 2019-06-05 04:57:51 +0000 | |||
828 | @@ -410,7 +410,8 @@ | |||
829 | 410 | """ | 410 | """ |
830 | 411 | log.info('PresentationController loaded') | 411 | log.info('PresentationController loaded') |
831 | 412 | 412 | ||
833 | 413 | def __init__(self, plugin=None, name='PresentationController', document_class=PresentationDocument): | 413 | def __init__(self, plugin=None, name='PresentationController', document_class=PresentationDocument, |
834 | 414 | display_name=None): | ||
835 | 414 | """ | 415 | """ |
836 | 415 | This is the constructor for the presentationcontroller object. This provides an easy way for descendent plugins | 416 | This is the constructor for the presentationcontroller object. This provides an easy way for descendent plugins |
837 | 416 | 417 | ||
838 | @@ -430,6 +431,7 @@ | |||
839 | 430 | self.docs = [] | 431 | self.docs = [] |
840 | 431 | self.plugin = plugin | 432 | self.plugin = plugin |
841 | 432 | self.name = name | 433 | self.name = name |
842 | 434 | self.display_name = display_name if display_name is not None else name | ||
843 | 433 | self.document_class = document_class | 435 | self.document_class = document_class |
844 | 434 | self.settings_section = self.plugin.settings_section | 436 | self.settings_section = self.plugin.settings_section |
845 | 435 | self.available = None | 437 | self.available = None |
846 | 436 | 438 | ||
847 | === modified file 'openlp/plugins/presentations/lib/presentationtab.py' | |||
848 | --- openlp/plugins/presentations/lib/presentationtab.py 2019-05-22 06:47:00 +0000 | |||
849 | +++ openlp/plugins/presentations/lib/presentationtab.py 2019-06-05 04:57:51 +0000 | |||
850 | @@ -127,10 +127,10 @@ | |||
851 | 127 | 127 | ||
852 | 128 | def set_controller_text(self, checkbox, controller): | 128 | def set_controller_text(self, checkbox, controller): |
853 | 129 | if checkbox.isEnabled(): | 129 | if checkbox.isEnabled(): |
855 | 130 | checkbox.setText(controller.name) | 130 | checkbox.setText(controller.display_name) |
856 | 131 | else: | 131 | else: |
857 | 132 | checkbox.setText(translate('PresentationPlugin.PresentationTab', | 132 | checkbox.setText(translate('PresentationPlugin.PresentationTab', |
859 | 133 | '{name} (unavailable)').format(name=controller.name)) | 133 | '{name} (unavailable)').format(name=controller.display_name)) |
860 | 134 | 134 | ||
861 | 135 | def load(self): | 135 | def load(self): |
862 | 136 | """ | 136 | """ |
863 | 137 | 137 | ||
864 | === added file 'openlp/plugins/presentations/lib/serializers.py' | |||
865 | --- openlp/plugins/presentations/lib/serializers.py 1970-01-01 00:00:00 +0000 | |||
866 | +++ openlp/plugins/presentations/lib/serializers.py 2019-06-05 04:57:51 +0000 | |||
867 | @@ -0,0 +1,52 @@ | |||
868 | 1 | # -*- coding: utf-8 -*- | ||
869 | 2 | # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 | ||
870 | 3 | |||
871 | 4 | ########################################################################## | ||
872 | 5 | # OpenLP - Open Source Lyrics Projection # | ||
873 | 6 | # ---------------------------------------------------------------------- # | ||
874 | 7 | # Copyright (c) 2008-2019 OpenLP Developers # | ||
875 | 8 | # ---------------------------------------------------------------------- # | ||
876 | 9 | # This program is free software: you can redistribute it and/or modify # | ||
877 | 10 | # it under the terms of the GNU General Public License as published by # | ||
878 | 11 | # the Free Software Foundation, either version 3 of the License, or # | ||
879 | 12 | # (at your option) any later version. # | ||
880 | 13 | # # | ||
881 | 14 | # This program is distributed in the hope that it will be useful, # | ||
882 | 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of # | ||
883 | 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # | ||
884 | 17 | # GNU General Public License for more details. # | ||
885 | 18 | # # | ||
886 | 19 | # You should have received a copy of the GNU General Public License # | ||
887 | 20 | # along with this program. If not, see <https://www.gnu.org/licenses/>. # | ||
888 | 21 | ########################################################################## | ||
889 | 22 | """ | ||
890 | 23 | This module contains some helpers for serializing Path objects in Pyro4 | ||
891 | 24 | """ | ||
892 | 25 | try: | ||
893 | 26 | from openlp.core.common.path import Path | ||
894 | 27 | except ImportError: | ||
895 | 28 | from pathlib import Path | ||
896 | 29 | |||
897 | 30 | from Pyro4.util import SerializerBase | ||
898 | 31 | |||
899 | 32 | |||
900 | 33 | def path_class_to_dict(obj): | ||
901 | 34 | """ | ||
902 | 35 | Serialize a Path object for Pyro4 | ||
903 | 36 | """ | ||
904 | 37 | return { | ||
905 | 38 | '__class__': 'Path', | ||
906 | 39 | 'parts': obj.parts | ||
907 | 40 | } | ||
908 | 41 | |||
909 | 42 | |||
910 | 43 | def path_dict_to_class(classname, d): | ||
911 | 44 | return Path(d['parts']) | ||
912 | 45 | |||
913 | 46 | |||
914 | 47 | def register_classes(): | ||
915 | 48 | """ | ||
916 | 49 | Register the serializers | ||
917 | 50 | """ | ||
918 | 51 | SerializerBase.register_class_to_dict(Path, path_class_to_dict) | ||
919 | 52 | SerializerBase.register_dict_to_class('Path', path_dict_to_class) | ||
920 | 0 | 53 | ||
921 | === added directory 'openlp/plugins/presentations/lib/vendor' | |||
922 | === added file 'openlp/plugins/presentations/lib/vendor/do_not_delete.txt' | |||
923 | --- openlp/plugins/presentations/lib/vendor/do_not_delete.txt 1970-01-01 00:00:00 +0000 | |||
924 | +++ openlp/plugins/presentations/lib/vendor/do_not_delete.txt 2019-06-05 04:57:51 +0000 | |||
925 | @@ -0,0 +1,5 @@ | |||
926 | 1 | Vendor Directory | ||
927 | 2 | ================ | ||
928 | 3 | |||
929 | 4 | Do not delete this directory, it is used on Mac OS to place Pyro4 and serpent for use with Impress. | ||
930 | 5 | |||
931 | 0 | 6 | ||
932 | === modified file 'openlp/plugins/presentations/presentationplugin.py' | |||
933 | --- openlp/plugins/presentations/presentationplugin.py 2019-04-13 13:00:22 +0000 | |||
934 | +++ openlp/plugins/presentations/presentationplugin.py 2019-06-05 04:57:51 +0000 | |||
935 | @@ -28,13 +28,13 @@ | |||
936 | 28 | 28 | ||
937 | 29 | from PyQt5 import QtCore | 29 | from PyQt5 import QtCore |
938 | 30 | 30 | ||
939 | 31 | from openlp.core.state import State | ||
940 | 32 | from openlp.core.api.http import register_endpoint | 31 | from openlp.core.api.http import register_endpoint |
941 | 33 | from openlp.core.common import extension_loader | 32 | from openlp.core.common import extension_loader |
942 | 34 | from openlp.core.common.i18n import translate | 33 | from openlp.core.common.i18n import translate |
943 | 35 | from openlp.core.common.settings import Settings | 34 | from openlp.core.common.settings import Settings |
944 | 36 | from openlp.core.lib import build_icon | 35 | from openlp.core.lib import build_icon |
945 | 37 | from openlp.core.lib.plugin import Plugin, StringContent | 36 | from openlp.core.lib.plugin import Plugin, StringContent |
946 | 37 | from openlp.core.state import State | ||
947 | 38 | from openlp.core.ui.icons import UiIcons | 38 | from openlp.core.ui.icons import UiIcons |
948 | 39 | from openlp.plugins.presentations.endpoint import api_presentations_endpoint, presentations_endpoint | 39 | from openlp.plugins.presentations.endpoint import api_presentations_endpoint, presentations_endpoint |
949 | 40 | from openlp.plugins.presentations.lib.presentationcontroller import PresentationController | 40 | from openlp.plugins.presentations.lib.presentationcontroller import PresentationController |
950 | @@ -45,18 +45,20 @@ | |||
951 | 45 | log = logging.getLogger(__name__) | 45 | log = logging.getLogger(__name__) |
952 | 46 | 46 | ||
953 | 47 | 47 | ||
966 | 48 | __default_settings__ = {'presentations/override app': QtCore.Qt.Unchecked, | 48 | __default_settings__ = { |
967 | 49 | 'presentations/enable_pdf_program': QtCore.Qt.Unchecked, | 49 | 'presentations/override app': QtCore.Qt.Unchecked, |
968 | 50 | 'presentations/pdf_program': None, | 50 | 'presentations/enable_pdf_program': QtCore.Qt.Unchecked, |
969 | 51 | 'presentations/Impress': QtCore.Qt.Checked, | 51 | 'presentations/pdf_program': None, |
970 | 52 | 'presentations/Powerpoint': QtCore.Qt.Checked, | 52 | 'presentations/maclo': QtCore.Qt.Checked, |
971 | 53 | 'presentations/Pdf': QtCore.Qt.Checked, | 53 | 'presentations/Impress': QtCore.Qt.Checked, |
972 | 54 | 'presentations/presentations files': [], | 54 | 'presentations/Powerpoint': QtCore.Qt.Checked, |
973 | 55 | 'presentations/thumbnail_scheme': '', | 55 | 'presentations/Pdf': QtCore.Qt.Checked, |
974 | 56 | 'presentations/powerpoint slide click advance': QtCore.Qt.Unchecked, | 56 | 'presentations/presentations files': [], |
975 | 57 | 'presentations/powerpoint control window': QtCore.Qt.Unchecked, | 57 | 'presentations/thumbnail_scheme': '', |
976 | 58 | 'presentations/last directory': None | 58 | 'presentations/powerpoint slide click advance': QtCore.Qt.Unchecked, |
977 | 59 | } | 59 | 'presentations/powerpoint control window': QtCore.Qt.Unchecked, |
978 | 60 | 'presentations/last directory': None | ||
979 | 61 | } | ||
980 | 60 | 62 | ||
981 | 61 | 63 | ||
982 | 62 | class PresentationPlugin(Plugin): | 64 | class PresentationPlugin(Plugin): |
983 | @@ -100,7 +102,7 @@ | |||
984 | 100 | try: | 102 | try: |
985 | 101 | self.controllers[controller].start_process() | 103 | self.controllers[controller].start_process() |
986 | 102 | except Exception: | 104 | except Exception: |
988 | 103 | log.warning('Failed to start controller process') | 105 | log.exception('Failed to start controller process') |
989 | 104 | self.controllers[controller].available = False | 106 | self.controllers[controller].available = False |
990 | 105 | self.media_item.build_file_mask_string() | 107 | self.media_item.build_file_mask_string() |
991 | 106 | 108 | ||
992 | 107 | 109 | ||
993 | === modified file 'scripts/check_dependencies.py' | |||
994 | --- scripts/check_dependencies.py 2019-05-05 08:13:10 +0000 | |||
995 | +++ scripts/check_dependencies.py 2019-06-05 04:57:51 +0000 | |||
996 | @@ -159,6 +159,8 @@ | |||
997 | 159 | w('OK') | 159 | w('OK') |
998 | 160 | except ImportError: | 160 | except ImportError: |
999 | 161 | w('FAIL') | 161 | w('FAIL') |
1000 | 162 | except Exception: | ||
1001 | 163 | w('ERROR') | ||
1002 | 162 | w(os.linesep) | 164 | w(os.linesep) |
1003 | 163 | 165 | ||
1004 | 164 | 166 | ||
1005 | 165 | 167 | ||
1006 | === modified file 'tests/functional/openlp_core/common/test_path.py' | |||
1007 | --- tests/functional/openlp_core/common/test_path.py 2019-05-26 10:30:37 +0000 | |||
1008 | +++ tests/functional/openlp_core/common/test_path.py 2019-06-05 04:57:51 +0000 | |||
1009 | @@ -110,7 +110,18 @@ | |||
1010 | 110 | # WHEN: Calling `path_to_str` with an invalid Type | 110 | # WHEN: Calling `path_to_str` with an invalid Type |
1011 | 111 | # THEN: A TypeError should have been raised | 111 | # THEN: A TypeError should have been raised |
1012 | 112 | with self.assertRaises(TypeError): | 112 | with self.assertRaises(TypeError): |
1014 | 113 | path_to_str(str()) | 113 | path_to_str(57) |
1015 | 114 | |||
1016 | 115 | def test_path_to_str_wth_str(self): | ||
1017 | 116 | """ | ||
1018 | 117 | Test that `path_to_str` just returns a str when given a str | ||
1019 | 118 | """ | ||
1020 | 119 | # GIVEN: The `path_to_str` function | ||
1021 | 120 | # WHEN: Calling `path_to_str` with a str | ||
1022 | 121 | result = path_to_str('/usr/bin') | ||
1023 | 122 | |||
1024 | 123 | # THEN: The string should be returned | ||
1025 | 124 | assert result == '/usr/bin' | ||
1026 | 114 | 125 | ||
1027 | 115 | def test_path_to_str_none(self): | 126 | def test_path_to_str_none(self): |
1028 | 116 | """ | 127 | """ |
1029 | 117 | 128 | ||
1030 | === added file 'tests/functional/openlp_plugins/presentations/test_libreofficeserver.py' | |||
1031 | --- tests/functional/openlp_plugins/presentations/test_libreofficeserver.py 1970-01-01 00:00:00 +0000 | |||
1032 | +++ tests/functional/openlp_plugins/presentations/test_libreofficeserver.py 2019-06-05 04:57:51 +0000 | |||
1033 | @@ -0,0 +1,948 @@ | |||
1034 | 1 | # -*- coding: utf-8 -*- | ||
1035 | 2 | # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 | ||
1036 | 3 | |||
1037 | 4 | ########################################################################## | ||
1038 | 5 | # OpenLP - Open Source Lyrics Projection # | ||
1039 | 6 | # ---------------------------------------------------------------------- # | ||
1040 | 7 | # Copyright (c) 2008-2019 OpenLP Developers # | ||
1041 | 8 | # ---------------------------------------------------------------------- # | ||
1042 | 9 | # This program is free software: you can redistribute it and/or modify # | ||
1043 | 10 | # it under the terms of the GNU General Public License as published by # | ||
1044 | 11 | # the Free Software Foundation, either version 3 of the License, or # | ||
1045 | 12 | # (at your option) any later version. # | ||
1046 | 13 | # # | ||
1047 | 14 | # This program is distributed in the hope that it will be useful, # | ||
1048 | 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of # | ||
1049 | 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # | ||
1050 | 17 | # GNU General Public License for more details. # | ||
1051 | 18 | # # | ||
1052 | 19 | # You should have received a copy of the GNU General Public License # | ||
1053 | 20 | # along with this program. If not, see <https://www.gnu.org/licenses/>. # | ||
1054 | 21 | ########################################################################## | ||
1055 | 22 | """ | ||
1056 | 23 | Functional tests to test the LibreOffice Pyro server | ||
1057 | 24 | """ | ||
1058 | 25 | from unittest.mock import MagicMock, patch, call | ||
1059 | 26 | |||
1060 | 27 | from openlp.plugins.presentations.lib.libreofficeserver import LibreOfficeServer, TextType, main | ||
1061 | 28 | |||
1062 | 29 | |||
1063 | 30 | def test_constructor(): | ||
1064 | 31 | """ | ||
1065 | 32 | Test the Constructor from the server | ||
1066 | 33 | """ | ||
1067 | 34 | # GIVEN: No server | ||
1068 | 35 | # WHEN: The server object is created | ||
1069 | 36 | server = LibreOfficeServer() | ||
1070 | 37 | |||
1071 | 38 | # THEN: The server should have been set up correctly | ||
1072 | 39 | assert server._control is None | ||
1073 | 40 | # assert server._desktop is None | ||
1074 | 41 | assert server._document is None | ||
1075 | 42 | assert server._presentation is None | ||
1076 | 43 | assert server._process is None | ||
1077 | 44 | |||
1078 | 45 | |||
1079 | 46 | @patch('openlp.plugins.presentations.lib.libreofficeserver.Popen') | ||
1080 | 47 | def test_start_process(MockedPopen): | ||
1081 | 48 | """ | ||
1082 | 49 | Test that the correct command is issued to run LibreOffice | ||
1083 | 50 | """ | ||
1084 | 51 | # GIVEN: A LOServer | ||
1085 | 52 | mocked_process = MagicMock() | ||
1086 | 53 | MockedPopen.return_value = mocked_process | ||
1087 | 54 | server = LibreOfficeServer() | ||
1088 | 55 | |||
1089 | 56 | # WHEN: The start_process() method is run | ||
1090 | 57 | server.start_process() | ||
1091 | 58 | |||
1092 | 59 | # THEN: The correct command line should run and the process should have started | ||
1093 | 60 | MockedPopen.assert_called_with([ | ||
1094 | 61 | '/Applications/LibreOffice.app/Contents/MacOS/soffice', | ||
1095 | 62 | '--nologo', | ||
1096 | 63 | '--norestore', | ||
1097 | 64 | '--minimized', | ||
1098 | 65 | '--nodefault', | ||
1099 | 66 | '--nofirststartwizard', | ||
1100 | 67 | '--accept=pipe,name=openlp_maclo;urp;StarOffice.ServiceManager' | ||
1101 | 68 | ]) | ||
1102 | 69 | assert server._process is mocked_process | ||
1103 | 70 | |||
1104 | 71 | |||
1105 | 72 | @patch('openlp.plugins.presentations.lib.libreofficeserver.uno') | ||
1106 | 73 | def test_desktop_already_has_desktop(mocked_uno): | ||
1107 | 74 | """ | ||
1108 | 75 | Test that setup_desktop() exits early when there's already a desktop | ||
1109 | 76 | """ | ||
1110 | 77 | # GIVEN: A LibreOfficeServer instance | ||
1111 | 78 | server = LibreOfficeServer() | ||
1112 | 79 | server._desktop = MagicMock() | ||
1113 | 80 | |||
1114 | 81 | # WHEN: the desktop property is called | ||
1115 | 82 | desktop = server.desktop | ||
1116 | 83 | |||
1117 | 84 | # THEN: setup_desktop() exits early | ||
1118 | 85 | assert desktop is server._desktop | ||
1119 | 86 | assert server._manager is None | ||
1120 | 87 | |||
1121 | 88 | |||
1122 | 89 | @patch('openlp.plugins.presentations.lib.libreofficeserver.uno') | ||
1123 | 90 | def test_desktop_exception(mocked_uno): | ||
1124 | 91 | """ | ||
1125 | 92 | Test that setting up the desktop works correctly when an exception occurs | ||
1126 | 93 | """ | ||
1127 | 94 | # GIVEN: A LibreOfficeServer instance | ||
1128 | 95 | server = LibreOfficeServer() | ||
1129 | 96 | mocked_context = MagicMock() | ||
1130 | 97 | mocked_resolver = MagicMock() | ||
1131 | 98 | mocked_uno_instance = MagicMock() | ||
1132 | 99 | MockedServiceManager = MagicMock() | ||
1133 | 100 | mocked_uno.getComponentContext.return_value = mocked_context | ||
1134 | 101 | mocked_context.ServiceManager.createInstanceWithContext.return_value = mocked_resolver | ||
1135 | 102 | mocked_resolver.resolve.side_effect = [Exception, mocked_uno_instance] | ||
1136 | 103 | mocked_uno_instance.ServiceManager = MockedServiceManager | ||
1137 | 104 | MockedServiceManager.createInstanceWithContext.side_effect = Exception() | ||
1138 | 105 | |||
1139 | 106 | # WHEN: the desktop property is called | ||
1140 | 107 | server.desktop | ||
1141 | 108 | |||
1142 | 109 | # THEN: A desktop object was created | ||
1143 | 110 | mocked_uno.getComponentContext.assert_called_once_with() | ||
1144 | 111 | mocked_context.ServiceManager.createInstanceWithContext.assert_called_once_with( | ||
1145 | 112 | 'com.sun.star.bridge.UnoUrlResolver', mocked_context) | ||
1146 | 113 | expected_calls = [ | ||
1147 | 114 | call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext'), | ||
1148 | 115 | call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext') | ||
1149 | 116 | ] | ||
1150 | 117 | assert mocked_resolver.resolve.call_args_list == expected_calls | ||
1151 | 118 | MockedServiceManager.createInstanceWithContext.assert_called_once_with( | ||
1152 | 119 | 'com.sun.star.frame.Desktop', mocked_uno_instance) | ||
1153 | 120 | assert server._manager is MockedServiceManager | ||
1154 | 121 | assert server._desktop is None | ||
1155 | 122 | |||
1156 | 123 | |||
1157 | 124 | @patch('openlp.plugins.presentations.lib.libreofficeserver.uno') | ||
1158 | 125 | def test_desktop(mocked_uno): | ||
1159 | 126 | """ | ||
1160 | 127 | Test that setting up the desktop works correctly | ||
1161 | 128 | """ | ||
1162 | 129 | # GIVEN: A LibreOfficeServer instance | ||
1163 | 130 | server = LibreOfficeServer() | ||
1164 | 131 | mocked_context = MagicMock() | ||
1165 | 132 | mocked_resolver = MagicMock() | ||
1166 | 133 | mocked_uno_instance = MagicMock() | ||
1167 | 134 | MockedServiceManager = MagicMock() | ||
1168 | 135 | mocked_desktop = MagicMock() | ||
1169 | 136 | mocked_uno.getComponentContext.return_value = mocked_context | ||
1170 | 137 | mocked_context.ServiceManager.createInstanceWithContext.return_value = mocked_resolver | ||
1171 | 138 | mocked_resolver.resolve.side_effect = [Exception, mocked_uno_instance] | ||
1172 | 139 | mocked_uno_instance.ServiceManager = MockedServiceManager | ||
1173 | 140 | MockedServiceManager.createInstanceWithContext.return_value = mocked_desktop | ||
1174 | 141 | |||
1175 | 142 | # WHEN: the desktop property is called | ||
1176 | 143 | server.desktop | ||
1177 | 144 | |||
1178 | 145 | # THEN: A desktop object was created | ||
1179 | 146 | mocked_uno.getComponentContext.assert_called_once_with() | ||
1180 | 147 | mocked_context.ServiceManager.createInstanceWithContext.assert_called_once_with( | ||
1181 | 148 | 'com.sun.star.bridge.UnoUrlResolver', mocked_context) | ||
1182 | 149 | expected_calls = [ | ||
1183 | 150 | call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext'), | ||
1184 | 151 | call('uno:pipe,name=openlp_maclo;urp;StarOffice.ComponentContext') | ||
1185 | 152 | ] | ||
1186 | 153 | assert mocked_resolver.resolve.call_args_list == expected_calls | ||
1187 | 154 | MockedServiceManager.createInstanceWithContext.assert_called_once_with( | ||
1188 | 155 | 'com.sun.star.frame.Desktop', mocked_uno_instance) | ||
1189 | 156 | assert server._manager is MockedServiceManager | ||
1190 | 157 | assert server._desktop is mocked_desktop | ||
1191 | 158 | |||
1192 | 159 | |||
1193 | 160 | @patch('openlp.plugins.presentations.lib.libreofficeserver.PropertyValue') | ||
1194 | 161 | def test_create_property(MockedPropertyValue): | ||
1195 | 162 | """ | ||
1196 | 163 | Test that the _create_property() method works correctly | ||
1197 | 164 | """ | ||
1198 | 165 | # GIVEN: A server amnd property to set | ||
1199 | 166 | server = LibreOfficeServer() | ||
1200 | 167 | name = 'Hidden' | ||
1201 | 168 | value = True | ||
1202 | 169 | |||
1203 | 170 | # WHEN: The _create_property() method is called | ||
1204 | 171 | prop = server._create_property(name, value) | ||
1205 | 172 | |||
1206 | 173 | # THEN: The property should have the correct attributes | ||
1207 | 174 | assert prop.Name == name | ||
1208 | 175 | assert prop.Value == value | ||
1209 | 176 | |||
1210 | 177 | |||
1211 | 178 | def test_get_text_from_page_slide_text(): | ||
1212 | 179 | """ | ||
1213 | 180 | Test that the _get_text_from_page() method gives us nothing for slide text | ||
1214 | 181 | """ | ||
1215 | 182 | # GIVEN: A LibreOfficeServer object and some mocked objects | ||
1216 | 183 | text_type = TextType.SlideText | ||
1217 | 184 | slide_no = 1 | ||
1218 | 185 | server = LibreOfficeServer() | ||
1219 | 186 | server._document = MagicMock() | ||
1220 | 187 | mocked_pages = MagicMock() | ||
1221 | 188 | mocked_page = MagicMock() | ||
1222 | 189 | mocked_shape = MagicMock() | ||
1223 | 190 | server._document.getDrawPages.return_value = mocked_pages | ||
1224 | 191 | mocked_pages.getCount.return_value = 1 | ||
1225 | 192 | mocked_pages.getByIndex.return_value = mocked_page | ||
1226 | 193 | mocked_page.getByIndex.return_value = mocked_shape | ||
1227 | 194 | mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape' | ||
1228 | 195 | mocked_shape.supportsService.return_value = True | ||
1229 | 196 | mocked_shape.getString.return_value = 'Page Text' | ||
1230 | 197 | |||
1231 | 198 | # WHEN: _get_text_from_page() is run for slide text | ||
1232 | 199 | text = server._get_text_from_page(slide_no, text_type) | ||
1233 | 200 | |||
1234 | 201 | # THE: The text is correct | ||
1235 | 202 | assert text == 'Page Text\n' | ||
1236 | 203 | |||
1237 | 204 | |||
1238 | 205 | def test_get_text_from_page_title(): | ||
1239 | 206 | """ | ||
1240 | 207 | Test that the _get_text_from_page() method gives us the text from the titles | ||
1241 | 208 | """ | ||
1242 | 209 | # GIVEN: A LibreOfficeServer object and some mocked objects | ||
1243 | 210 | text_type = TextType.Title | ||
1244 | 211 | slide_no = 1 | ||
1245 | 212 | server = LibreOfficeServer() | ||
1246 | 213 | server._document = MagicMock() | ||
1247 | 214 | mocked_pages = MagicMock() | ||
1248 | 215 | mocked_page = MagicMock() | ||
1249 | 216 | mocked_shape = MagicMock() | ||
1250 | 217 | server._document.getDrawPages.return_value = mocked_pages | ||
1251 | 218 | mocked_pages.getCount.return_value = 1 | ||
1252 | 219 | mocked_pages.getByIndex.return_value = mocked_page | ||
1253 | 220 | mocked_page.getByIndex.return_value = mocked_shape | ||
1254 | 221 | mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape' | ||
1255 | 222 | mocked_shape.supportsService.return_value = True | ||
1256 | 223 | mocked_shape.getString.return_value = 'Page Title' | ||
1257 | 224 | |||
1258 | 225 | # WHEN: _get_text_from_page() is run for titles | ||
1259 | 226 | text = server._get_text_from_page(slide_no, text_type) | ||
1260 | 227 | |||
1261 | 228 | # THEN: The text should be correct | ||
1262 | 229 | assert text == 'Page Title\n' | ||
1263 | 230 | |||
1264 | 231 | |||
1265 | 232 | def test_get_text_from_page_notes(): | ||
1266 | 233 | """ | ||
1267 | 234 | Test that the _get_text_from_page() method gives us the text from the notes | ||
1268 | 235 | """ | ||
1269 | 236 | # GIVEN: A LibreOfficeServer object and some mocked objects | ||
1270 | 237 | text_type = TextType.Notes | ||
1271 | 238 | slide_no = 1 | ||
1272 | 239 | server = LibreOfficeServer() | ||
1273 | 240 | server._document = MagicMock() | ||
1274 | 241 | mocked_pages = MagicMock() | ||
1275 | 242 | mocked_page = MagicMock() | ||
1276 | 243 | mocked_notes_page = MagicMock() | ||
1277 | 244 | mocked_shape = MagicMock() | ||
1278 | 245 | server._document.getDrawPages.return_value = mocked_pages | ||
1279 | 246 | mocked_pages.getCount.return_value = 1 | ||
1280 | 247 | mocked_pages.getByIndex.return_value = mocked_page | ||
1281 | 248 | mocked_page.getNotesPage.return_value = mocked_notes_page | ||
1282 | 249 | mocked_notes_page.getByIndex.return_value = mocked_shape | ||
1283 | 250 | mocked_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape' | ||
1284 | 251 | mocked_shape.supportsService.return_value = True | ||
1285 | 252 | mocked_shape.getString.return_value = 'Page Notes' | ||
1286 | 253 | |||
1287 | 254 | # WHEN: _get_text_from_page() is run for titles | ||
1288 | 255 | text = server._get_text_from_page(slide_no, text_type) | ||
1289 | 256 | |||
1290 | 257 | # THEN: The text should be correct | ||
1291 | 258 | assert text == 'Page Notes\n' | ||
1292 | 259 | |||
1293 | 260 | |||
1294 | 261 | def test_shutdown_other_docs(): | ||
1295 | 262 | """ | ||
1296 | 263 | Test the shutdown method while other documents are open in LibreOffice | ||
1297 | 264 | """ | ||
1298 | 265 | def close_docs(): | ||
1299 | 266 | server._docs = [] | ||
1300 | 267 | |||
1301 | 268 | # GIVEN: An up an running LibreOfficeServer | ||
1302 | 269 | server = LibreOfficeServer() | ||
1303 | 270 | mocked_doc = MagicMock() | ||
1304 | 271 | mocked_desktop = MagicMock() | ||
1305 | 272 | mocked_docs = MagicMock() | ||
1306 | 273 | mocked_list = MagicMock() | ||
1307 | 274 | mocked_element_doc = MagicMock() | ||
1308 | 275 | server._docs = [mocked_doc] | ||
1309 | 276 | server._desktop = mocked_desktop | ||
1310 | 277 | server._process = MagicMock() | ||
1311 | 278 | mocked_doc.close_presentation.side_effect = close_docs | ||
1312 | 279 | mocked_desktop.getComponents.return_value = mocked_docs | ||
1313 | 280 | mocked_docs.hasElements.return_value = True | ||
1314 | 281 | mocked_docs.createEnumeration.return_value = mocked_list | ||
1315 | 282 | mocked_list.hasMoreElements.side_effect = [True, False] | ||
1316 | 283 | mocked_list.nextElement.return_value = mocked_element_doc | ||
1317 | 284 | mocked_element_doc.getImplementationName.side_effect = [ | ||
1318 | 285 | 'org.openlp.Nothing', | ||
1319 | 286 | 'com.sun.star.comp.framework.BackingComp' | ||
1320 | 287 | ] | ||
1321 | 288 | |||
1322 | 289 | # WHEN: shutdown() is called | ||
1323 | 290 | server.shutdown() | ||
1324 | 291 | |||
1325 | 292 | # THEN: The right methods are called and everything works | ||
1326 | 293 | mocked_doc.close_presentation.assert_called_once_with() | ||
1327 | 294 | mocked_desktop.getComponents.assert_called_once_with() | ||
1328 | 295 | mocked_docs.hasElements.assert_called_once_with() | ||
1329 | 296 | mocked_docs.createEnumeration.assert_called_once_with() | ||
1330 | 297 | assert mocked_list.hasMoreElements.call_count == 2 | ||
1331 | 298 | mocked_list.nextElement.assert_called_once_with() | ||
1332 | 299 | mocked_element_doc.getImplementationName.assert_called_once_with() | ||
1333 | 300 | assert mocked_desktop.terminate.call_count == 0 | ||
1334 | 301 | assert server._process.kill.call_count == 0 | ||
1335 | 302 | |||
1336 | 303 | |||
1337 | 304 | def test_shutdown(): | ||
1338 | 305 | """ | ||
1339 | 306 | Test the shutdown method | ||
1340 | 307 | """ | ||
1341 | 308 | def close_docs(): | ||
1342 | 309 | server._docs = [] | ||
1343 | 310 | |||
1344 | 311 | # GIVEN: An up an running LibreOfficeServer | ||
1345 | 312 | server = LibreOfficeServer() | ||
1346 | 313 | mocked_doc = MagicMock() | ||
1347 | 314 | mocked_desktop = MagicMock() | ||
1348 | 315 | mocked_docs = MagicMock() | ||
1349 | 316 | mocked_list = MagicMock() | ||
1350 | 317 | mocked_element_doc = MagicMock() | ||
1351 | 318 | server._docs = [mocked_doc] | ||
1352 | 319 | server._desktop = mocked_desktop | ||
1353 | 320 | server._process = MagicMock() | ||
1354 | 321 | mocked_doc.close_presentation.side_effect = close_docs | ||
1355 | 322 | mocked_desktop.getComponents.return_value = mocked_docs | ||
1356 | 323 | mocked_docs.hasElements.return_value = True | ||
1357 | 324 | mocked_docs.createEnumeration.return_value = mocked_list | ||
1358 | 325 | mocked_list.hasMoreElements.side_effect = [True, False] | ||
1359 | 326 | mocked_list.nextElement.return_value = mocked_element_doc | ||
1360 | 327 | mocked_element_doc.getImplementationName.return_value = 'com.sun.star.comp.framework.BackingComp' | ||
1361 | 328 | |||
1362 | 329 | # WHEN: shutdown() is called | ||
1363 | 330 | server.shutdown() | ||
1364 | 331 | |||
1365 | 332 | # THEN: The right methods are called and everything works | ||
1366 | 333 | mocked_doc.close_presentation.assert_called_once_with() | ||
1367 | 334 | mocked_desktop.getComponents.assert_called_once_with() | ||
1368 | 335 | mocked_docs.hasElements.assert_called_once_with() | ||
1369 | 336 | mocked_docs.createEnumeration.assert_called_once_with() | ||
1370 | 337 | assert mocked_list.hasMoreElements.call_count == 2 | ||
1371 | 338 | mocked_list.nextElement.assert_called_once_with() | ||
1372 | 339 | mocked_element_doc.getImplementationName.assert_called_once_with() | ||
1373 | 340 | mocked_desktop.terminate.assert_called_once_with() | ||
1374 | 341 | server._process.kill.assert_called_once_with() | ||
1375 | 342 | |||
1376 | 343 | |||
1377 | 344 | @patch('openlp.plugins.presentations.lib.libreofficeserver.uno') | ||
1378 | 345 | def test_load_presentation_exception(mocked_uno): | ||
1379 | 346 | """ | ||
1380 | 347 | Test the load_presentation() method when an exception occurs | ||
1381 | 348 | """ | ||
1382 | 349 | # GIVEN: A LibreOfficeServer object | ||
1383 | 350 | presentation_file = '/path/to/presentation.odp' | ||
1384 | 351 | screen_number = 1 | ||
1385 | 352 | server = LibreOfficeServer() | ||
1386 | 353 | mocked_desktop = MagicMock() | ||
1387 | 354 | mocked_uno.systemPathToFileUrl.side_effect = lambda x: x | ||
1388 | 355 | server._desktop = mocked_desktop | ||
1389 | 356 | mocked_desktop.loadComponentFromURL.side_effect = Exception() | ||
1390 | 357 | |||
1391 | 358 | # WHEN: load_presentation() is called | ||
1392 | 359 | with patch.object(server, '_create_property') as mocked_create_property: | ||
1393 | 360 | mocked_create_property.side_effect = lambda x, y: {x: y} | ||
1394 | 361 | result = server.load_presentation(presentation_file, screen_number) | ||
1395 | 362 | |||
1396 | 363 | # THEN: A presentation is loaded | ||
1397 | 364 | assert result is False | ||
1398 | 365 | |||
1399 | 366 | |||
1400 | 367 | @patch('openlp.plugins.presentations.lib.libreofficeserver.uno') | ||
1401 | 368 | def test_load_presentation(mocked_uno): | ||
1402 | 369 | """ | ||
1403 | 370 | Test the load_presentation() method | ||
1404 | 371 | """ | ||
1405 | 372 | # GIVEN: A LibreOfficeServer object | ||
1406 | 373 | presentation_file = '/path/to/presentation.odp' | ||
1407 | 374 | screen_number = 1 | ||
1408 | 375 | server = LibreOfficeServer() | ||
1409 | 376 | mocked_desktop = MagicMock() | ||
1410 | 377 | mocked_document = MagicMock() | ||
1411 | 378 | mocked_presentation = MagicMock() | ||
1412 | 379 | mocked_uno.systemPathToFileUrl.side_effect = lambda x: x | ||
1413 | 380 | server._desktop = mocked_desktop | ||
1414 | 381 | mocked_desktop.loadComponentFromURL.return_value = mocked_document | ||
1415 | 382 | mocked_document.getPresentation.return_value = mocked_presentation | ||
1416 | 383 | |||
1417 | 384 | # WHEN: load_presentation() is called | ||
1418 | 385 | with patch.object(server, '_create_property') as mocked_create_property: | ||
1419 | 386 | mocked_create_property.side_effect = lambda x, y: {x: y} | ||
1420 | 387 | result = server.load_presentation(presentation_file, screen_number) | ||
1421 | 388 | |||
1422 | 389 | # THEN: A presentation is loaded | ||
1423 | 390 | assert result is True | ||
1424 | 391 | mocked_uno.systemPathToFileUrl.assert_called_once_with(presentation_file) | ||
1425 | 392 | mocked_create_property.assert_called_once_with('Hidden', True) | ||
1426 | 393 | mocked_desktop.loadComponentFromURL.assert_called_once_with( | ||
1427 | 394 | presentation_file, '_blank', 0, ({'Hidden': True},)) | ||
1428 | 395 | assert server._document is mocked_document | ||
1429 | 396 | mocked_document.getPresentation.assert_called_once_with() | ||
1430 | 397 | assert server._presentation is mocked_presentation | ||
1431 | 398 | assert server._presentation.Display == screen_number | ||
1432 | 399 | assert server._control is None | ||
1433 | 400 | |||
1434 | 401 | |||
1435 | 402 | @patch('openlp.plugins.presentations.lib.libreofficeserver.uno') | ||
1436 | 403 | def test_extract_thumbnails_no_pages(mocked_uno): | ||
1437 | 404 | """ | ||
1438 | 405 | Test the extract_thumbnails() method when there are no pages | ||
1439 | 406 | """ | ||
1440 | 407 | # GIVEN: A LibreOfficeServer instance | ||
1441 | 408 | temp_folder = '/tmp' | ||
1442 | 409 | server = LibreOfficeServer() | ||
1443 | 410 | mocked_document = MagicMock() | ||
1444 | 411 | server._document = mocked_document | ||
1445 | 412 | mocked_uno.systemPathToFileUrl.side_effect = lambda x: x | ||
1446 | 413 | mocked_document.getDrawPages.return_value = None | ||
1447 | 414 | |||
1448 | 415 | # WHEN: The extract_thumbnails() method is called | ||
1449 | 416 | with patch.object(server, '_create_property') as mocked_create_property: | ||
1450 | 417 | mocked_create_property.side_effect = lambda x, y: {x: y} | ||
1451 | 418 | thumbnails = server.extract_thumbnails(temp_folder) | ||
1452 | 419 | |||
1453 | 420 | # THEN: Thumbnails have been extracted | ||
1454 | 421 | mocked_uno.systemPathToFileUrl.assert_called_once_with(temp_folder) | ||
1455 | 422 | mocked_create_property.assert_called_once_with('FilterName', 'impress_png_Export') | ||
1456 | 423 | mocked_document.getDrawPages.assert_called_once_with() | ||
1457 | 424 | assert thumbnails == [] | ||
1458 | 425 | |||
1459 | 426 | |||
1460 | 427 | @patch('openlp.plugins.presentations.lib.libreofficeserver.uno') | ||
1461 | 428 | @patch('openlp.plugins.presentations.lib.libreofficeserver.os') | ||
1462 | 429 | def test_extract_thumbnails(mocked_os, mocked_uno): | ||
1463 | 430 | """ | ||
1464 | 431 | Test the extract_thumbnails() method | ||
1465 | 432 | """ | ||
1466 | 433 | # GIVEN: A LibreOfficeServer instance | ||
1467 | 434 | temp_folder = '/tmp' | ||
1468 | 435 | server = LibreOfficeServer() | ||
1469 | 436 | mocked_document = MagicMock() | ||
1470 | 437 | mocked_pages = MagicMock() | ||
1471 | 438 | mocked_page_1 = MagicMock() | ||
1472 | 439 | mocked_page_2 = MagicMock() | ||
1473 | 440 | mocked_controller = MagicMock() | ||
1474 | 441 | server._document = mocked_document | ||
1475 | 442 | mocked_uno.systemPathToFileUrl.side_effect = lambda x: x | ||
1476 | 443 | mocked_document.getDrawPages.return_value = mocked_pages | ||
1477 | 444 | mocked_os.path.isdir.return_value = False | ||
1478 | 445 | mocked_pages.getCount.return_value = 2 | ||
1479 | 446 | mocked_pages.getByIndex.side_effect = [mocked_page_1, mocked_page_2] | ||
1480 | 447 | mocked_document.getCurrentController.return_value = mocked_controller | ||
1481 | 448 | mocked_os.path.join.side_effect = lambda *x: '/'.join(x) | ||
1482 | 449 | |||
1483 | 450 | # WHEN: The extract_thumbnails() method is called | ||
1484 | 451 | with patch.object(server, '_create_property') as mocked_create_property: | ||
1485 | 452 | mocked_create_property.side_effect = lambda x, y: {x: y} | ||
1486 | 453 | thumbnails = server.extract_thumbnails(temp_folder) | ||
1487 | 454 | |||
1488 | 455 | # THEN: Thumbnails have been extracted | ||
1489 | 456 | mocked_uno.systemPathToFileUrl.assert_called_once_with(temp_folder) | ||
1490 | 457 | mocked_create_property.assert_called_once_with('FilterName', 'impress_png_Export') | ||
1491 | 458 | mocked_document.getDrawPages.assert_called_once_with() | ||
1492 | 459 | mocked_pages.getCount.assert_called_once_with() | ||
1493 | 460 | assert mocked_pages.getByIndex.call_args_list == [call(0), call(1)] | ||
1494 | 461 | assert mocked_controller.setCurrentPage.call_args_list == \ | ||
1495 | 462 | [call(mocked_page_1), call(mocked_page_2)] | ||
1496 | 463 | assert mocked_document.storeToURL.call_args_list == \ | ||
1497 | 464 | [call('/tmp/1.png', ({'FilterName': 'impress_png_Export'},)), | ||
1498 | 465 | call('/tmp/2.png', ({'FilterName': 'impress_png_Export'},))] | ||
1499 | 466 | assert thumbnails == ['/tmp/1.png', '/tmp/2.png'] | ||
1500 | 467 | |||
1501 | 468 | |||
1502 | 469 | def test_get_titles_and_notes(): | ||
1503 | 470 | """ | ||
1504 | 471 | Test the get_titles_and_notes() method | ||
1505 | 472 | """ | ||
1506 | 473 | # GIVEN: A LibreOfficeServer object and a bunch of mocks | ||
1507 | 474 | server = LibreOfficeServer() | ||
1508 | 475 | mocked_document = MagicMock() | ||
1509 | 476 | mocked_pages = MagicMock() | ||
1510 | 477 | server._document = mocked_document | ||
1511 | 478 | mocked_document.getDrawPages.return_value = mocked_pages | ||
1512 | 479 | mocked_pages.getCount.return_value = 2 | ||
1513 | 480 | |||
1514 | 481 | # WHEN: get_titles_and_notes() is called | ||
1515 | 482 | with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page: | ||
1516 | 483 | mocked_get_text_from_page.side_effect = [ | ||
1517 | 484 | 'OpenLP on Mac OS X', | ||
1518 | 485 | '', | ||
1519 | 486 | '', | ||
1520 | 487 | 'Installing is a drag-and-drop affair' | ||
1521 | 488 | ] | ||
1522 | 489 | titles, notes = server.get_titles_and_notes() | ||
1523 | 490 | |||
1524 | 491 | # THEN: The right calls are made and the right stuff returned | ||
1525 | 492 | mocked_document.getDrawPages.assert_called_once_with() | ||
1526 | 493 | mocked_pages.getCount.assert_called_once_with() | ||
1527 | 494 | assert mocked_get_text_from_page.call_count == 4 | ||
1528 | 495 | expected_calls = [ | ||
1529 | 496 | call(1, TextType.Title), call(1, TextType.Notes), | ||
1530 | 497 | call(2, TextType.Title), call(2, TextType.Notes), | ||
1531 | 498 | ] | ||
1532 | 499 | assert mocked_get_text_from_page.call_args_list == expected_calls | ||
1533 | 500 | assert titles == ['OpenLP on Mac OS X\n', '\n'], titles | ||
1534 | 501 | assert notes == [' ', 'Installing is a drag-and-drop affair'], notes | ||
1535 | 502 | |||
1536 | 503 | |||
1537 | 504 | def test_close_presentation(): | ||
1538 | 505 | """ | ||
1539 | 506 | Test that closing the presentation cleans things up correctly | ||
1540 | 507 | """ | ||
1541 | 508 | # GIVEN: A LibreOfficeServer instance and a bunch of mocks | ||
1542 | 509 | server = LibreOfficeServer() | ||
1543 | 510 | mocked_document = MagicMock() | ||
1544 | 511 | mocked_presentation = MagicMock() | ||
1545 | 512 | server._document = mocked_document | ||
1546 | 513 | server._presentation = mocked_presentation | ||
1547 | 514 | |||
1548 | 515 | # WHEN: close_presentation() is called | ||
1549 | 516 | server.close_presentation() | ||
1550 | 517 | |||
1551 | 518 | # THEN: The presentation and document should be closed | ||
1552 | 519 | mocked_presentation.end.assert_called_once_with() | ||
1553 | 520 | mocked_document.dispose.assert_called_once_with() | ||
1554 | 521 | assert server._document is None | ||
1555 | 522 | assert server._presentation is None | ||
1556 | 523 | |||
1557 | 524 | |||
1558 | 525 | def test_is_loaded_no_objects(): | ||
1559 | 526 | """ | ||
1560 | 527 | Test the is_loaded() method when there's no document or presentation | ||
1561 | 528 | """ | ||
1562 | 529 | # GIVEN: A LibreOfficeServer instance and a bunch of mocks | ||
1563 | 530 | server = LibreOfficeServer() | ||
1564 | 531 | |||
1565 | 532 | # WHEN: The is_loaded() method is called | ||
1566 | 533 | result = server.is_loaded() | ||
1567 | 534 | |||
1568 | 535 | # THEN: The result should be false | ||
1569 | 536 | assert result is False | ||
1570 | 537 | |||
1571 | 538 | |||
1572 | 539 | def test_is_loaded_no_presentation(): | ||
1573 | 540 | """ | ||
1574 | 541 | Test the is_loaded() method when there's no presentation | ||
1575 | 542 | """ | ||
1576 | 543 | # GIVEN: A LibreOfficeServer instance and a bunch of mocks | ||
1577 | 544 | server = LibreOfficeServer() | ||
1578 | 545 | mocked_document = MagicMock() | ||
1579 | 546 | server._document = mocked_document | ||
1580 | 547 | server._presentation = MagicMock() | ||
1581 | 548 | mocked_document.getPresentation.return_value = None | ||
1582 | 549 | |||
1583 | 550 | # WHEN: The is_loaded() method is called | ||
1584 | 551 | result = server.is_loaded() | ||
1585 | 552 | |||
1586 | 553 | # THEN: The result should be false | ||
1587 | 554 | assert result is False | ||
1588 | 555 | mocked_document.getPresentation.assert_called_once_with() | ||
1589 | 556 | |||
1590 | 557 | |||
1591 | 558 | def test_is_loaded_exception(): | ||
1592 | 559 | """ | ||
1593 | 560 | Test the is_loaded() method when an exception is thrown | ||
1594 | 561 | """ | ||
1595 | 562 | # GIVEN: A LibreOfficeServer instance and a bunch of mocks | ||
1596 | 563 | server = LibreOfficeServer() | ||
1597 | 564 | mocked_document = MagicMock() | ||
1598 | 565 | server._document = mocked_document | ||
1599 | 566 | server._presentation = MagicMock() | ||
1600 | 567 | mocked_document.getPresentation.side_effect = Exception() | ||
1601 | 568 | |||
1602 | 569 | # WHEN: The is_loaded() method is called | ||
1603 | 570 | result = server.is_loaded() | ||
1604 | 571 | |||
1605 | 572 | # THEN: The result should be false | ||
1606 | 573 | assert result is False | ||
1607 | 574 | mocked_document.getPresentation.assert_called_once_with() | ||
1608 | 575 | |||
1609 | 576 | |||
1610 | 577 | def test_is_loaded(): | ||
1611 | 578 | """ | ||
1612 | 579 | Test the is_loaded() method | ||
1613 | 580 | """ | ||
1614 | 581 | # GIVEN: A LibreOfficeServer instance and a bunch of mocks | ||
1615 | 582 | server = LibreOfficeServer() | ||
1616 | 583 | mocked_document = MagicMock() | ||
1617 | 584 | mocked_presentation = MagicMock() | ||
1618 | 585 | server._document = mocked_document | ||
1619 | 586 | server._presentation = mocked_presentation | ||
1620 | 587 | mocked_document.getPresentation.return_value = mocked_presentation | ||
1621 | 588 | |||
1622 | 589 | # WHEN: The is_loaded() method is called | ||
1623 | 590 | result = server.is_loaded() | ||
1624 | 591 | |||
1625 | 592 | # THEN: The result should be false | ||
1626 | 593 | assert result is True | ||
1627 | 594 | mocked_document.getPresentation.assert_called_once_with() | ||
1628 | 595 | |||
1629 | 596 | |||
1630 | 597 | def test_is_active_not_loaded(): | ||
1631 | 598 | """ | ||
1632 | 599 | Test is_active() when is_loaded() returns False | ||
1633 | 600 | """ | ||
1634 | 601 | # GIVEN: A LibreOfficeServer instance and a bunch of mocks | ||
1635 | 602 | server = LibreOfficeServer() | ||
1636 | 603 | |||
1637 | 604 | # WHEN: is_active() is called with is_loaded() returns False | ||
1638 | 605 | with patch.object(server, 'is_loaded') as mocked_is_loaded: | ||
1639 | 606 | mocked_is_loaded.return_value = False | ||
1640 | 607 | result = server.is_active() | ||
1641 | 608 | |||
1642 | 609 | # THEN: It should have returned False | ||
1643 | 610 | assert result is False | ||
1644 | 611 | |||
1645 | 612 | |||
1646 | 613 | def test_is_active_no_control(): | ||
1647 | 614 | """ | ||
1648 | 615 | Test is_active() when is_loaded() returns True but there's no control | ||
1649 | 616 | """ | ||
1650 | 617 | # GIVEN: A LibreOfficeServer instance and a bunch of mocks | ||
1651 | 618 | server = LibreOfficeServer() | ||
1652 | 619 | |||
1653 | 620 | # WHEN: is_active() is called with is_loaded() returns False | ||
1654 | 621 | with patch.object(server, 'is_loaded') as mocked_is_loaded: | ||
1655 | 622 | mocked_is_loaded.return_value = True | ||
1656 | 623 | result = server.is_active() | ||
1657 | 624 | |||
1658 | 625 | # THEN: The result should be False | ||
1659 | 626 | assert result is False | ||
1660 | 627 | mocked_is_loaded.assert_called_once_with() | ||
1661 | 628 | |||
1662 | 629 | |||
1663 | 630 | def test_is_active(): | ||
1664 | 631 | """ | ||
1665 | 632 | Test is_active() | ||
1666 | 633 | """ | ||
1667 | 634 | # GIVEN: A LibreOfficeServer instance and a bunch of mocks | ||
1668 | 635 | server = LibreOfficeServer() | ||
1669 | 636 | mocked_control = MagicMock() | ||
1670 | 637 | server._control = mocked_control | ||
1671 | 638 | mocked_control.isRunning.return_value = True | ||
1672 | 639 | |||
1673 | 640 | # WHEN: is_active() is called with is_loaded() returns False | ||
1674 | 641 | with patch.object(server, 'is_loaded') as mocked_is_loaded: | ||
1675 | 642 | mocked_is_loaded.return_value = True | ||
1676 | 643 | result = server.is_active() | ||
1677 | 644 | |||
1678 | 645 | # THEN: The result should be False | ||
1679 | 646 | assert result is True | ||
1680 | 647 | mocked_is_loaded.assert_called_once_with() | ||
1681 | 648 | mocked_control.isRunning.assert_called_once_with() | ||
1682 | 649 | |||
1683 | 650 | |||
1684 | 651 | def test_unblank_screen(): | ||
1685 | 652 | """ | ||
1686 | 653 | Test the unblank_screen() method | ||
1687 | 654 | """ | ||
1688 | 655 | # GIVEN: A LibreOfficeServer instance and a bunch of mocks | ||
1689 | 656 | server = LibreOfficeServer() | ||
1690 | 657 | mocked_control = MagicMock() | ||
1691 | 658 | server._control = mocked_control | ||
1692 | 659 | |||
1693 | 660 | # WHEN: unblank_screen() is run | ||
1694 | 661 | server.unblank_screen() | ||
1695 | 662 | |||
1696 | 663 | # THEN: The resume method should have been called | ||
1697 | 664 | mocked_control.resume.assert_called_once_with() | ||
1698 | 665 | |||
1699 | 666 | |||
1700 | 667 | def test_blank_screen(): | ||
1701 | 668 | """ | ||
1702 | 669 | Test the blank_screen() method | ||
1703 | 670 | """ | ||
1704 | 671 | # GIVEN: A LibreOfficeServer instance and a bunch of mocks | ||
1705 | 672 | server = LibreOfficeServer() | ||
1706 | 673 | mocked_control = MagicMock() | ||
1707 | 674 | server._control = mocked_control | ||
1708 | 675 | |||
1709 | 676 | # WHEN: blank_screen() is run | ||
1710 | 677 | server.blank_screen() | ||
1711 | 678 | |||
1712 | 679 | # THEN: The resume method should have been called | ||
1713 | 680 | mocked_control.blankScreen.assert_called_once_with(0) | ||
1714 | 681 | |||
1715 | 682 | |||
1716 | 683 | def test_is_blank_no_control(): | ||
1717 | 684 | """ | ||
1718 | 685 | Test the is_blank() method when there's no control | ||
1719 | 686 | """ | ||
1720 | 687 | # GIVEN: A LibreOfficeServer instance and a bunch of mocks | ||
1721 | 688 | server = LibreOfficeServer() | ||
1722 | 689 | |||
1723 | 690 | # WHEN: is_blank() is called | ||
1724 | 691 | result = server.is_blank() | ||
1725 | 692 | |||
1726 | 693 | # THEN: It should have returned False | ||
1727 | 694 | assert result is False | ||
1728 | 695 | |||
1729 | 696 | |||
1730 | 697 | def test_is_blank_control_is_running(): | ||
1731 | 698 | """ | ||
1732 | 699 | Test the is_blank() method when the control is running | ||
1733 | 700 | """ | ||
1734 | 701 | # GIVEN: A LibreOfficeServer instance and a bunch of mocks | ||
1735 | 702 | server = LibreOfficeServer() | ||
1736 | 703 | mocked_control = MagicMock() | ||
1737 | 704 | server._control = mocked_control | ||
1738 | 705 | mocked_control.isRunning.return_value = True | ||
1739 | 706 | mocked_control.isPaused.return_value = True | ||
1740 | 707 | |||
1741 | 708 | # WHEN: is_blank() is called | ||
1742 | 709 | result = server.is_blank() | ||
1743 | 710 | |||
1744 | 711 | # THEN: It should have returned False | ||
1745 | 712 | assert result is True | ||
1746 | 713 | mocked_control.isRunning.assert_called_once_with() | ||
1747 | 714 | mocked_control.isPaused.assert_called_once_with() | ||
1748 | 715 | |||
1749 | 716 | |||
1750 | 717 | def test_stop_presentation(): | ||
1751 | 718 | """ | ||
1752 | 719 | Test the stop_presentation() method | ||
1753 | 720 | """ | ||
1754 | 721 | # GIVEN: A LibreOfficeServer instance and a mocked presentation | ||
1755 | 722 | server = LibreOfficeServer() | ||
1756 | 723 | mocked_presentation = MagicMock() | ||
1757 | 724 | mocked_control = MagicMock() | ||
1758 | 725 | server._presentation = mocked_presentation | ||
1759 | 726 | server._control = mocked_control | ||
1760 | 727 | |||
1761 | 728 | # WHEN: stop_presentation() is called | ||
1762 | 729 | server.stop_presentation() | ||
1763 | 730 | |||
1764 | 731 | # THEN: The presentation is ended and the control is removed | ||
1765 | 732 | mocked_presentation.end.assert_called_once_with() | ||
1766 | 733 | assert server._control is None | ||
1767 | 734 | |||
1768 | 735 | |||
1769 | 736 | @patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep') | ||
1770 | 737 | def test_start_presentation_no_control(mocked_sleep): | ||
1771 | 738 | """ | ||
1772 | 739 | Test the start_presentation() method when there's no control | ||
1773 | 740 | """ | ||
1774 | 741 | # GIVEN: A LibreOfficeServer instance and some mocks | ||
1775 | 742 | server = LibreOfficeServer() | ||
1776 | 743 | mocked_control = MagicMock() | ||
1777 | 744 | mocked_document = MagicMock() | ||
1778 | 745 | mocked_presentation = MagicMock() | ||
1779 | 746 | mocked_controller = MagicMock() | ||
1780 | 747 | mocked_frame = MagicMock() | ||
1781 | 748 | mocked_window = MagicMock() | ||
1782 | 749 | server._document = mocked_document | ||
1783 | 750 | server._presentation = mocked_presentation | ||
1784 | 751 | mocked_document.getCurrentController.return_value = mocked_controller | ||
1785 | 752 | mocked_controller.getFrame.return_value = mocked_frame | ||
1786 | 753 | mocked_frame.getContainerWindow.return_value = mocked_window | ||
1787 | 754 | mocked_presentation.getController.side_effect = [None, mocked_control] | ||
1788 | 755 | |||
1789 | 756 | # WHEN: start_presentation() is called | ||
1790 | 757 | server.start_presentation() | ||
1791 | 758 | |||
1792 | 759 | # THEN: The slide number should be correct | ||
1793 | 760 | mocked_document.getCurrentController.assert_called_once_with() | ||
1794 | 761 | mocked_controller.getFrame.assert_called_once_with() | ||
1795 | 762 | mocked_frame.getContainerWindow.assert_called_once_with() | ||
1796 | 763 | mocked_presentation.start.assert_called_once_with() | ||
1797 | 764 | assert mocked_presentation.getController.call_count == 2 | ||
1798 | 765 | mocked_sleep.assert_called_once_with(0.1) | ||
1799 | 766 | assert mocked_window.setVisible.call_args_list == [call(True), call(False)] | ||
1800 | 767 | assert server._control is mocked_control | ||
1801 | 768 | |||
1802 | 769 | |||
1803 | 770 | def test_start_presentation(): | ||
1804 | 771 | """ | ||
1805 | 772 | Test the start_presentation() method when there's a control | ||
1806 | 773 | """ | ||
1807 | 774 | # GIVEN: A LibreOfficeServer instance and some mocks | ||
1808 | 775 | server = LibreOfficeServer() | ||
1809 | 776 | mocked_control = MagicMock() | ||
1810 | 777 | server._control = mocked_control | ||
1811 | 778 | |||
1812 | 779 | # WHEN: start_presentation() is called | ||
1813 | 780 | with patch.object(server, 'goto_slide') as mocked_goto_slide: | ||
1814 | 781 | server.start_presentation() | ||
1815 | 782 | |||
1816 | 783 | # THEN: The control should have been activated and the first slide selected | ||
1817 | 784 | mocked_control.activate.assert_called_once_with() | ||
1818 | 785 | mocked_goto_slide.assert_called_once_with(1) | ||
1819 | 786 | |||
1820 | 787 | |||
1821 | 788 | def test_get_slide_number(): | ||
1822 | 789 | """ | ||
1823 | 790 | Test the get_slide_number() method | ||
1824 | 791 | """ | ||
1825 | 792 | # GIVEN: A LibreOfficeServer instance and some mocks | ||
1826 | 793 | server = LibreOfficeServer() | ||
1827 | 794 | mocked_control = MagicMock() | ||
1828 | 795 | mocked_control.getCurrentSlideIndex.return_value = 3 | ||
1829 | 796 | server._control = mocked_control | ||
1830 | 797 | |||
1831 | 798 | # WHEN: get_slide_number() is called | ||
1832 | 799 | result = server.get_slide_number() | ||
1833 | 800 | |||
1834 | 801 | # THEN: The slide number should be correct | ||
1835 | 802 | assert result == 4 | ||
1836 | 803 | |||
1837 | 804 | |||
1838 | 805 | def test_get_slide_count(): | ||
1839 | 806 | """ | ||
1840 | 807 | Test the get_slide_count() method | ||
1841 | 808 | """ | ||
1842 | 809 | # GIVEN: A LibreOfficeServer instance and some mocks | ||
1843 | 810 | server = LibreOfficeServer() | ||
1844 | 811 | mocked_document = MagicMock() | ||
1845 | 812 | mocked_pages = MagicMock() | ||
1846 | 813 | server._document = mocked_document | ||
1847 | 814 | mocked_document.getDrawPages.return_value = mocked_pages | ||
1848 | 815 | mocked_pages.getCount.return_value = 2 | ||
1849 | 816 | |||
1850 | 817 | # WHEN: get_slide_count() is called | ||
1851 | 818 | result = server.get_slide_count() | ||
1852 | 819 | |||
1853 | 820 | # THEN: The slide count should be correct | ||
1854 | 821 | assert result == 2 | ||
1855 | 822 | |||
1856 | 823 | |||
1857 | 824 | def test_goto_slide(): | ||
1858 | 825 | """ | ||
1859 | 826 | Test the goto_slide() method | ||
1860 | 827 | """ | ||
1861 | 828 | # GIVEN: A LibreOfficeServer instance and some mocks | ||
1862 | 829 | server = LibreOfficeServer() | ||
1863 | 830 | mocked_control = MagicMock() | ||
1864 | 831 | server._control = mocked_control | ||
1865 | 832 | |||
1866 | 833 | # WHEN: goto_slide() is called | ||
1867 | 834 | server.goto_slide(1) | ||
1868 | 835 | |||
1869 | 836 | # THEN: The slide number should be correct | ||
1870 | 837 | mocked_control.gotoSlideIndex.assert_called_once_with(0) | ||
1871 | 838 | |||
1872 | 839 | |||
1873 | 840 | @patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep') | ||
1874 | 841 | def test_next_step_when_paused(mocked_sleep): | ||
1875 | 842 | """ | ||
1876 | 843 | Test the next_step() method when paused | ||
1877 | 844 | """ | ||
1878 | 845 | # GIVEN: A LibreOfficeServer instance and a mocked control | ||
1879 | 846 | server = LibreOfficeServer() | ||
1880 | 847 | mocked_control = MagicMock() | ||
1881 | 848 | server._control = mocked_control | ||
1882 | 849 | mocked_control.isPaused.side_effect = [False, True] | ||
1883 | 850 | |||
1884 | 851 | # WHEN: next_step() is called | ||
1885 | 852 | server.next_step() | ||
1886 | 853 | |||
1887 | 854 | # THEN: The correct call should be made | ||
1888 | 855 | mocked_control.gotoNextEffect.assert_called_once_with() | ||
1889 | 856 | mocked_sleep.assert_called_once_with(0.1) | ||
1890 | 857 | assert mocked_control.isPaused.call_count == 2 | ||
1891 | 858 | mocked_control.gotoPreviousEffect.assert_called_once_with() | ||
1892 | 859 | |||
1893 | 860 | |||
1894 | 861 | @patch('openlp.plugins.presentations.lib.libreofficeserver.time.sleep') | ||
1895 | 862 | def test_next_step(mocked_sleep): | ||
1896 | 863 | """ | ||
1897 | 864 | Test the next_step() method when paused | ||
1898 | 865 | """ | ||
1899 | 866 | # GIVEN: A LibreOfficeServer instance and a mocked control | ||
1900 | 867 | server = LibreOfficeServer() | ||
1901 | 868 | mocked_control = MagicMock() | ||
1902 | 869 | server._control = mocked_control | ||
1903 | 870 | mocked_control.isPaused.side_effect = [True, True] | ||
1904 | 871 | |||
1905 | 872 | # WHEN: next_step() is called | ||
1906 | 873 | server.next_step() | ||
1907 | 874 | |||
1908 | 875 | # THEN: The correct call should be made | ||
1909 | 876 | mocked_control.gotoNextEffect.assert_called_once_with() | ||
1910 | 877 | mocked_sleep.assert_called_once_with(0.1) | ||
1911 | 878 | assert mocked_control.isPaused.call_count == 1 | ||
1912 | 879 | assert mocked_control.gotoPreviousEffect.call_count == 0 | ||
1913 | 880 | |||
1914 | 881 | |||
1915 | 882 | def test_previous_step(): | ||
1916 | 883 | """ | ||
1917 | 884 | Test the previous_step() method | ||
1918 | 885 | """ | ||
1919 | 886 | # GIVEN: A LibreOfficeServer instance and a mocked control | ||
1920 | 887 | server = LibreOfficeServer() | ||
1921 | 888 | mocked_control = MagicMock() | ||
1922 | 889 | server._control = mocked_control | ||
1923 | 890 | |||
1924 | 891 | # WHEN: previous_step() is called | ||
1925 | 892 | server.previous_step() | ||
1926 | 893 | |||
1927 | 894 | # THEN: The correct call should be made | ||
1928 | 895 | mocked_control.gotoPreviousEffect.assert_called_once_with() | ||
1929 | 896 | |||
1930 | 897 | |||
1931 | 898 | def test_get_slide_text(): | ||
1932 | 899 | """ | ||
1933 | 900 | Test the get_slide_text() method | ||
1934 | 901 | """ | ||
1935 | 902 | # GIVEN: A LibreOfficeServer instance | ||
1936 | 903 | server = LibreOfficeServer() | ||
1937 | 904 | |||
1938 | 905 | # WHEN: get_slide_text() is called for a particular slide | ||
1939 | 906 | with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page: | ||
1940 | 907 | mocked_get_text_from_page.return_value = 'OpenLP on Mac OS X' | ||
1941 | 908 | result = server.get_slide_text(5) | ||
1942 | 909 | |||
1943 | 910 | # THEN: The text should be returned | ||
1944 | 911 | mocked_get_text_from_page.assert_called_once_with(5) | ||
1945 | 912 | assert result == 'OpenLP on Mac OS X' | ||
1946 | 913 | |||
1947 | 914 | |||
1948 | 915 | def test_get_slide_notes(): | ||
1949 | 916 | """ | ||
1950 | 917 | Test the get_slide_notes() method | ||
1951 | 918 | """ | ||
1952 | 919 | # GIVEN: A LibreOfficeServer instance | ||
1953 | 920 | server = LibreOfficeServer() | ||
1954 | 921 | |||
1955 | 922 | # WHEN: get_slide_notes() is called for a particular slide | ||
1956 | 923 | with patch.object(server, '_get_text_from_page') as mocked_get_text_from_page: | ||
1957 | 924 | mocked_get_text_from_page.return_value = 'Installing is a drag-and-drop affair' | ||
1958 | 925 | result = server.get_slide_notes(3) | ||
1959 | 926 | |||
1960 | 927 | # THEN: The text should be returned | ||
1961 | 928 | mocked_get_text_from_page.assert_called_once_with(3, TextType.Notes) | ||
1962 | 929 | assert result == 'Installing is a drag-and-drop affair' | ||
1963 | 930 | |||
1964 | 931 | |||
1965 | 932 | @patch('openlp.plugins.presentations.lib.libreofficeserver.Daemon') | ||
1966 | 933 | def test_main(MockedDaemon): | ||
1967 | 934 | """ | ||
1968 | 935 | Test the main() function | ||
1969 | 936 | """ | ||
1970 | 937 | # GIVEN: Mocked out Pyro objects | ||
1971 | 938 | mocked_daemon = MagicMock() | ||
1972 | 939 | MockedDaemon.return_value = mocked_daemon | ||
1973 | 940 | |||
1974 | 941 | # WHEN: main() is run | ||
1975 | 942 | main() | ||
1976 | 943 | |||
1977 | 944 | # THEN: The correct calls are made | ||
1978 | 945 | MockedDaemon.assert_called_once_with(host='localhost', port=4310) | ||
1979 | 946 | mocked_daemon.register.assert_called_once_with(LibreOfficeServer, 'openlp.libreofficeserver') | ||
1980 | 947 | mocked_daemon.requestLoop.assert_called_once_with() | ||
1981 | 948 | mocked_daemon.close.assert_called_once_with() | ||
1982 | 0 | 949 | ||
1983 | === added file 'tests/functional/openlp_plugins/presentations/test_maclocontroller.py' | |||
1984 | --- tests/functional/openlp_plugins/presentations/test_maclocontroller.py 1970-01-01 00:00:00 +0000 | |||
1985 | +++ tests/functional/openlp_plugins/presentations/test_maclocontroller.py 2019-06-05 04:57:51 +0000 | |||
1986 | @@ -0,0 +1,453 @@ | |||
1987 | 1 | # -*- coding: utf-8 -*- | ||
1988 | 2 | # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 | ||
1989 | 3 | |||
1990 | 4 | ########################################################################## | ||
1991 | 5 | # OpenLP - Open Source Lyrics Projection # | ||
1992 | 6 | # ---------------------------------------------------------------------- # | ||
1993 | 7 | # Copyright (c) 2008-2019 OpenLP Developers # | ||
1994 | 8 | # ---------------------------------------------------------------------- # | ||
1995 | 9 | # This program is free software: you can redistribute it and/or modify # | ||
1996 | 10 | # it under the terms of the GNU General Public License as published by # | ||
1997 | 11 | # the Free Software Foundation, either version 3 of the License, or # | ||
1998 | 12 | # (at your option) any later version. # | ||
1999 | 13 | # # | ||
2000 | 14 | # This program is distributed in the hope that it will be useful, # | ||
2001 | 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of # | ||
2002 | 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # | ||
2003 | 17 | # GNU General Public License for more details. # | ||
2004 | 18 | # # | ||
2005 | 19 | # You should have received a copy of the GNU General Public License # | ||
2006 | 20 | # along with this program. If not, see <https://www.gnu.org/licenses/>. # | ||
2007 | 21 | ########################################################################## | ||
2008 | 22 | """ | ||
2009 | 23 | Functional tests to test the Mac LibreOffice class and related methods. | ||
2010 | 24 | """ | ||
2011 | 25 | import shutil | ||
2012 | 26 | from tempfile import mkdtemp | ||
2013 | 27 | from unittest import TestCase | ||
2014 | 28 | from unittest.mock import MagicMock, patch, call | ||
2015 | 29 | |||
2016 | 30 | from openlp.core.common.settings import Settings | ||
2017 | 31 | from openlp.core.common.path import Path | ||
2018 | 32 | from openlp.plugins.presentations.lib.maclocontroller import MacLOController, MacLODocument | ||
2019 | 33 | from openlp.plugins.presentations.presentationplugin import __default_settings__ | ||
2020 | 34 | |||
2021 | 35 | from tests.helpers.testmixin import TestMixin | ||
2022 | 36 | from tests.utils.constants import TEST_RESOURCES_PATH | ||
2023 | 37 | |||
2024 | 38 | |||
2025 | 39 | class TestMacLOController(TestCase, TestMixin): | ||
2026 | 40 | """ | ||
2027 | 41 | Test the MacLOController Class | ||
2028 | 42 | """ | ||
2029 | 43 | |||
2030 | 44 | def setUp(self): | ||
2031 | 45 | """ | ||
2032 | 46 | Set up the patches and mocks need for all tests. | ||
2033 | 47 | """ | ||
2034 | 48 | self.setup_application() | ||
2035 | 49 | self.build_settings() | ||
2036 | 50 | self.mock_plugin = MagicMock() | ||
2037 | 51 | self.temp_folder = mkdtemp() | ||
2038 | 52 | self.mock_plugin.settings_section = self.temp_folder | ||
2039 | 53 | |||
2040 | 54 | def tearDown(self): | ||
2041 | 55 | """ | ||
2042 | 56 | Stop the patches | ||
2043 | 57 | """ | ||
2044 | 58 | self.destroy_settings() | ||
2045 | 59 | shutil.rmtree(self.temp_folder) | ||
2046 | 60 | |||
2047 | 61 | @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') | ||
2048 | 62 | def test_constructor(self, mocked_start_server): | ||
2049 | 63 | """ | ||
2050 | 64 | Test the Constructor from the MacLOController | ||
2051 | 65 | """ | ||
2052 | 66 | # GIVEN: No presentation controller | ||
2053 | 67 | controller = None | ||
2054 | 68 | |||
2055 | 69 | # WHEN: The presentation controller object is created | ||
2056 | 70 | controller = MacLOController(plugin=self.mock_plugin) | ||
2057 | 71 | |||
2058 | 72 | # THEN: The name of the presentation controller should be correct | ||
2059 | 73 | assert controller.name == 'maclo', \ | ||
2060 | 74 | 'The name of the presentation controller should be correct' | ||
2061 | 75 | assert controller.display_name == 'Impress on macOS', \ | ||
2062 | 76 | 'The display name of the presentation controller should be correct' | ||
2063 | 77 | mocked_start_server.assert_called_once_with() | ||
2064 | 78 | |||
2065 | 79 | @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') | ||
2066 | 80 | @patch('openlp.plugins.presentations.lib.maclocontroller.Proxy') | ||
2067 | 81 | def test_client(self, MockedProxy, mocked_start_server): | ||
2068 | 82 | """ | ||
2069 | 83 | Test the client property of the Controller | ||
2070 | 84 | """ | ||
2071 | 85 | # GIVEN: A controller without a client and a mocked out Pyro | ||
2072 | 86 | controller = MacLOController(plugin=self.mock_plugin) | ||
2073 | 87 | mocked_client = MagicMock() | ||
2074 | 88 | MockedProxy.return_value = mocked_client | ||
2075 | 89 | mocked_client._pyroConnection = None | ||
2076 | 90 | |||
2077 | 91 | # WHEN: the client property is called the first time | ||
2078 | 92 | client = controller.client | ||
2079 | 93 | |||
2080 | 94 | # THEN: a client is created | ||
2081 | 95 | assert client == mocked_client | ||
2082 | 96 | MockedProxy.assert_called_once_with('PYRO:openlp.libreofficeserver@localhost:4310') | ||
2083 | 97 | mocked_client._pyroReconnect.assert_called_once_with() | ||
2084 | 98 | |||
2085 | 99 | @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') | ||
2086 | 100 | def test_check_available(self, mocked_start_server): | ||
2087 | 101 | """ | ||
2088 | 102 | Test the check_available() method | ||
2089 | 103 | """ | ||
2090 | 104 | from openlp.plugins.presentations.lib.maclocontroller import macuno_available | ||
2091 | 105 | |||
2092 | 106 | # GIVEN: A controller | ||
2093 | 107 | controller = MacLOController(plugin=self.mock_plugin) | ||
2094 | 108 | |||
2095 | 109 | # WHEN: check_available() is run | ||
2096 | 110 | result = controller.check_available() | ||
2097 | 111 | |||
2098 | 112 | # THEN: it should return false | ||
2099 | 113 | assert result == macuno_available | ||
2100 | 114 | |||
2101 | 115 | @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') | ||
2102 | 116 | def test_start_process(self, mocked_start_server): | ||
2103 | 117 | """ | ||
2104 | 118 | Test the start_process() method | ||
2105 | 119 | """ | ||
2106 | 120 | # GIVEN: A controller and a client | ||
2107 | 121 | controller = MacLOController(plugin=self.mock_plugin) | ||
2108 | 122 | controller._client = MagicMock() | ||
2109 | 123 | |||
2110 | 124 | # WHEN: start_process() is called | ||
2111 | 125 | controller.start_process() | ||
2112 | 126 | |||
2113 | 127 | # THEN: The client's start_process() should have been called | ||
2114 | 128 | controller._client.start_process.assert_called_once_with() | ||
2115 | 129 | |||
2116 | 130 | @patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server') | ||
2117 | 131 | def test_kill(self, mocked_start_server): | ||
2118 | 132 | """ | ||
2119 | 133 | Test the kill() method | ||
2120 | 134 | """ | ||
2121 | 135 | # GIVEN: A controller and a client | ||
2122 | 136 | controller = MacLOController(plugin=self.mock_plugin) | ||
2123 | 137 | controller._client = MagicMock() | ||
2124 | 138 | controller.server_process = MagicMock() | ||
2125 | 139 | |||
2126 | 140 | # WHEN: start_process() is called | ||
2127 | 141 | controller.kill() | ||
2128 | 142 | |||
2129 | 143 | # THEN: The client's start_process() should have been called | ||
2130 | 144 | controller._client.shutdown.assert_called_once_with() | ||
2131 | 145 | controller.server_process.kill.assert_called_once_with() | ||
2132 | 146 | |||
2133 | 147 | |||
2134 | 148 | class TestMacLODocument(TestCase): | ||
2135 | 149 | """ | ||
2136 | 150 | Test the MacLODocument Class | ||
2137 | 151 | """ | ||
2138 | 152 | def setUp(self): | ||
2139 | 153 | mocked_plugin = MagicMock() | ||
2140 | 154 | mocked_plugin.settings_section = 'presentations' | ||
2141 | 155 | Settings().extend_default_settings(__default_settings__) | ||
2142 | 156 | self.file_name = Path(TEST_RESOURCES_PATH) / 'presentations' / 'test.odp' | ||
2143 | 157 | self.mocked_client = MagicMock() | ||
2144 | 158 | with patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server'): | ||
2145 | 159 | self.controller = MacLOController(mocked_plugin) | ||
2146 | 160 | self.controller._client = self.mocked_client | ||
2147 | 161 | self.document = MacLODocument(self.controller, self.file_name) | ||
2148 | 162 | |||
2149 | 163 | @patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList') | ||
2150 | 164 | def test_load_presentation_cannot_load(self, MockedScreenList): | ||
2151 | 165 | """ | ||
2152 | 166 | Test the load_presentation() method when the server can't load the presentation | ||
2153 | 167 | """ | ||
2154 | 168 | # GIVEN: A document and a mocked client | ||
2155 | 169 | mocked_screen_list = MagicMock() | ||
2156 | 170 | MockedScreenList.return_value = mocked_screen_list | ||
2157 | 171 | mocked_screen_list.current.number = 0 | ||
2158 | 172 | self.mocked_client.load_presentation.return_value = False | ||
2159 | 173 | |||
2160 | 174 | # WHEN: load_presentation() is called | ||
2161 | 175 | result = self.document.load_presentation() | ||
2162 | 176 | |||
2163 | 177 | # THEN: Stuff should work right | ||
2164 | 178 | self.mocked_client.load_presentation.assert_called_once_with(str(self.file_name), 1) | ||
2165 | 179 | assert result is False | ||
2166 | 180 | |||
2167 | 181 | @patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList') | ||
2168 | 182 | def test_load_presentation(self, MockedScreenList): | ||
2169 | 183 | """ | ||
2170 | 184 | Test the load_presentation() method | ||
2171 | 185 | """ | ||
2172 | 186 | # GIVEN: A document and a mocked client | ||
2173 | 187 | mocked_screen_list = MagicMock() | ||
2174 | 188 | MockedScreenList.return_value = mocked_screen_list | ||
2175 | 189 | mocked_screen_list.current.number = 0 | ||
2176 | 190 | self.mocked_client.load_presentation.return_value = True | ||
2177 | 191 | |||
2178 | 192 | # WHEN: load_presentation() is called | ||
2179 | 193 | with patch.object(self.document, 'create_thumbnails') as mocked_create_thumbnails, \ | ||
2180 | 194 | patch.object(self.document, 'create_titles_and_notes') as mocked_create_titles_and_notes: | ||
2181 | 195 | result = self.document.load_presentation() | ||
2182 | 196 | |||
2183 | 197 | # THEN: Stuff should work right | ||
2184 | 198 | self.mocked_client.load_presentation.assert_called_once_with(str(self.file_name), 1) | ||
2185 | 199 | mocked_create_thumbnails.assert_called_once_with() | ||
2186 | 200 | mocked_create_titles_and_notes.assert_called_once_with() | ||
2187 | 201 | assert result is True | ||
2188 | 202 | |||
2189 | 203 | def test_create_thumbnails_already_exist(self): | ||
2190 | 204 | """ | ||
2191 | 205 | Test the create_thumbnails() method when thumbnails already exist | ||
2192 | 206 | """ | ||
2193 | 207 | # GIVEN: thumbnails that exist and a mocked client | ||
2194 | 208 | self.document.check_thumbnails = MagicMock(return_value=True) | ||
2195 | 209 | |||
2196 | 210 | # WHEN: create_thumbnails() is called | ||
2197 | 211 | self.document.create_thumbnails() | ||
2198 | 212 | |||
2199 | 213 | # THEN: The method should exit early | ||
2200 | 214 | assert self.mocked_client.extract_thumbnails.call_count == 0 | ||
2201 | 215 | |||
2202 | 216 | @patch('openlp.plugins.presentations.lib.maclocontroller.delete_file') | ||
2203 | 217 | def test_create_thumbnails(self, mocked_delete_file): | ||
2204 | 218 | """ | ||
2205 | 219 | Test the create_thumbnails() method | ||
2206 | 220 | """ | ||
2207 | 221 | # GIVEN: thumbnails that don't exist and a mocked client | ||
2208 | 222 | self.document.check_thumbnails = MagicMock(return_value=False) | ||
2209 | 223 | self.mocked_client.extract_thumbnails.return_value = ['thumb1.png', 'thumb2.png'] | ||
2210 | 224 | |||
2211 | 225 | # WHEN: create_thumbnails() is called | ||
2212 | 226 | with patch.object(self.document, 'convert_thumbnail') as mocked_convert_thumbnail, \ | ||
2213 | 227 | patch.object(self.document, 'get_temp_folder') as mocked_get_temp_folder: | ||
2214 | 228 | mocked_get_temp_folder.return_value = 'temp' | ||
2215 | 229 | self.document.create_thumbnails() | ||
2216 | 230 | |||
2217 | 231 | # THEN: The method should complete successfully | ||
2218 | 232 | self.mocked_client.extract_thumbnails.assert_called_once_with('temp') | ||
2219 | 233 | assert mocked_convert_thumbnail.call_args_list == [ | ||
2220 | 234 | call(Path('thumb1.png'), 1), call(Path('thumb2.png'), 2)] | ||
2221 | 235 | assert mocked_delete_file.call_args_list == [call(Path('thumb1.png')), call(Path('thumb2.png'))] | ||
2222 | 236 | |||
2223 | 237 | def test_create_titles_and_notes(self): | ||
2224 | 238 | """ | ||
2225 | 239 | Test create_titles_and_notes() method | ||
2226 | 240 | """ | ||
2227 | 241 | # GIVEN: mocked client and mocked save_titles_and_notes() method | ||
2228 | 242 | self.mocked_client.get_titles_and_notes.return_value = ('OpenLP', 'This is a note') | ||
2229 | 243 | |||
2230 | 244 | # WHEN: create_titles_and_notes() is called | ||
2231 | 245 | with patch.object(self.document, 'save_titles_and_notes') as mocked_save_titles_and_notes: | ||
2232 | 246 | self.document.create_titles_and_notes() | ||
2233 | 247 | |||
2234 | 248 | # THEN save_titles_and_notes should have been called | ||
2235 | 249 | self.mocked_client.get_titles_and_notes.assert_called_once_with() | ||
2236 | 250 | mocked_save_titles_and_notes.assert_called_once_with('OpenLP', 'This is a note') | ||
2237 | 251 | |||
2238 | 252 | def test_close_presentation(self): | ||
2239 | 253 | """ | ||
2240 | 254 | Test the close_presentation() method | ||
2241 | 255 | """ | ||
2242 | 256 | # GIVEN: A mocked client and mocked remove_doc() method | ||
2243 | 257 | # WHEN: close_presentation() is called | ||
2244 | 258 | with patch.object(self.controller, 'remove_doc') as mocked_remove_doc: | ||
2245 | 259 | self.document.close_presentation() | ||
2246 | 260 | |||
2247 | 261 | # THEN: The presentation should have been closed | ||
2248 | 262 | self.mocked_client.close_presentation.assert_called_once_with() | ||
2249 | 263 | mocked_remove_doc.assert_called_once_with(self.document) | ||
2250 | 264 | |||
2251 | 265 | def test_is_loaded(self): | ||
2252 | 266 | """ | ||
2253 | 267 | Test the is_loaded() method | ||
2254 | 268 | """ | ||
2255 | 269 | # GIVEN: A mocked client | ||
2256 | 270 | self.mocked_client.is_loaded.return_value = True | ||
2257 | 271 | |||
2258 | 272 | # WHEN: is_loaded() is called | ||
2259 | 273 | result = self.document.is_loaded() | ||
2260 | 274 | |||
2261 | 275 | # THEN: Then the result should be correct | ||
2262 | 276 | assert result is True | ||
2263 | 277 | |||
2264 | 278 | def test_is_active(self): | ||
2265 | 279 | """ | ||
2266 | 280 | Test the is_active() method | ||
2267 | 281 | """ | ||
2268 | 282 | # GIVEN: A mocked client | ||
2269 | 283 | self.mocked_client.is_active.return_value = True | ||
2270 | 284 | |||
2271 | 285 | # WHEN: is_active() is called | ||
2272 | 286 | result = self.document.is_active() | ||
2273 | 287 | |||
2274 | 288 | # THEN: Then the result should be correct | ||
2275 | 289 | assert result is True | ||
2276 | 290 | |||
2277 | 291 | def test_unblank_screen(self): | ||
2278 | 292 | """ | ||
2279 | 293 | Test the unblank_screen() method | ||
2280 | 294 | """ | ||
2281 | 295 | # GIVEN: A mocked client | ||
2282 | 296 | self.mocked_client.unblank_screen.return_value = True | ||
2283 | 297 | |||
2284 | 298 | # WHEN: unblank_screen() is called | ||
2285 | 299 | result = self.document.unblank_screen() | ||
2286 | 300 | |||
2287 | 301 | # THEN: Then the result should be correct | ||
2288 | 302 | self.mocked_client.unblank_screen.assert_called_once_with() | ||
2289 | 303 | assert result is True | ||
2290 | 304 | |||
2291 | 305 | def test_blank_screen(self): | ||
2292 | 306 | """ | ||
2293 | 307 | Test the blank_screen() method | ||
2294 | 308 | """ | ||
2295 | 309 | # GIVEN: A mocked client | ||
2296 | 310 | self.mocked_client.blank_screen.return_value = True | ||
2297 | 311 | |||
2298 | 312 | # WHEN: blank_screen() is called | ||
2299 | 313 | self.document.blank_screen() | ||
2300 | 314 | |||
2301 | 315 | # THEN: Then the result should be correct | ||
2302 | 316 | self.mocked_client.blank_screen.assert_called_once_with() | ||
2303 | 317 | |||
2304 | 318 | def test_is_blank(self): | ||
2305 | 319 | """ | ||
2306 | 320 | Test the is_blank() method | ||
2307 | 321 | """ | ||
2308 | 322 | # GIVEN: A mocked client | ||
2309 | 323 | self.mocked_client.is_blank.return_value = True | ||
2310 | 324 | |||
2311 | 325 | # WHEN: is_blank() is called | ||
2312 | 326 | result = self.document.is_blank() | ||
2313 | 327 | |||
2314 | 328 | # THEN: Then the result should be correct | ||
2315 | 329 | assert result is True | ||
2316 | 330 | |||
2317 | 331 | def test_stop_presentation(self): | ||
2318 | 332 | """ | ||
2319 | 333 | Test the stop_presentation() method | ||
2320 | 334 | """ | ||
2321 | 335 | # GIVEN: A mocked client | ||
2322 | 336 | self.mocked_client.stop_presentation.return_value = True | ||
2323 | 337 | |||
2324 | 338 | # WHEN: stop_presentation() is called | ||
2325 | 339 | self.document.stop_presentation() | ||
2326 | 340 | |||
2327 | 341 | # THEN: Then the result should be correct | ||
2328 | 342 | self.mocked_client.stop_presentation.assert_called_once_with() | ||
2329 | 343 | |||
2330 | 344 | @patch('openlp.plugins.presentations.lib.maclocontroller.ScreenList') | ||
2331 | 345 | @patch('openlp.plugins.presentations.lib.maclocontroller.Registry') | ||
2332 | 346 | def test_start_presentation(self, MockedRegistry, MockedScreenList): | ||
2333 | 347 | """ | ||
2334 | 348 | Test the start_presentation() method | ||
2335 | 349 | """ | ||
2336 | 350 | # GIVEN: a mocked client, and multiple screens | ||
2337 | 351 | mocked_screen_list = MagicMock() | ||
2338 | 352 | mocked_screen_list.__len__.return_value = 2 | ||
2339 | 353 | mocked_registry = MagicMock() | ||
2340 | 354 | mocked_main_window = MagicMock() | ||
2341 | 355 | MockedScreenList.return_value = mocked_screen_list | ||
2342 | 356 | MockedRegistry.return_value = mocked_registry | ||
2343 | 357 | mocked_screen_list.screen_list = [0, 1] | ||
2344 | 358 | mocked_registry.get.return_value = mocked_main_window | ||
2345 | 359 | |||
2346 | 360 | # WHEN: start_presentation() is called | ||
2347 | 361 | self.document.start_presentation() | ||
2348 | 362 | |||
2349 | 363 | # THEN: The presentation should be started | ||
2350 | 364 | self.mocked_client.start_presentation.assert_called_once_with() | ||
2351 | 365 | mocked_registry.get.assert_called_once_with('main_window') | ||
2352 | 366 | mocked_main_window.activateWindow.assert_called_once_with() | ||
2353 | 367 | |||
2354 | 368 | def test_get_slide_number(self): | ||
2355 | 369 | """ | ||
2356 | 370 | Test the get_slide_number() method | ||
2357 | 371 | """ | ||
2358 | 372 | # GIVEN: A mocked client | ||
2359 | 373 | self.mocked_client.get_slide_number.return_value = 5 | ||
2360 | 374 | |||
2361 | 375 | # WHEN: get_slide_number() is called | ||
2362 | 376 | result = self.document.get_slide_number() | ||
2363 | 377 | |||
2364 | 378 | # THEN: Then the result should be correct | ||
2365 | 379 | assert result == 5 | ||
2366 | 380 | |||
2367 | 381 | def test_get_slide_count(self): | ||
2368 | 382 | """ | ||
2369 | 383 | Test the get_slide_count() method | ||
2370 | 384 | """ | ||
2371 | 385 | # GIVEN: A mocked client | ||
2372 | 386 | self.mocked_client.get_slide_count.return_value = 8 | ||
2373 | 387 | |||
2374 | 388 | # WHEN: get_slide_count() is called | ||
2375 | 389 | result = self.document.get_slide_count() | ||
2376 | 390 | |||
2377 | 391 | # THEN: Then the result should be correct | ||
2378 | 392 | assert result == 8 | ||
2379 | 393 | |||
2380 | 394 | def test_goto_slide(self): | ||
2381 | 395 | """ | ||
2382 | 396 | Test the goto_slide() method | ||
2383 | 397 | """ | ||
2384 | 398 | # GIVEN: A mocked client | ||
2385 | 399 | # WHEN: goto_slide() is called | ||
2386 | 400 | self.document.goto_slide(3) | ||
2387 | 401 | |||
2388 | 402 | # THEN: Then the result should be correct | ||
2389 | 403 | self.mocked_client.goto_slide.assert_called_once_with(3) | ||
2390 | 404 | |||
2391 | 405 | def test_next_step(self): | ||
2392 | 406 | """ | ||
2393 | 407 | Test the next_step() method | ||
2394 | 408 | """ | ||
2395 | 409 | # GIVEN: A mocked client | ||
2396 | 410 | # WHEN: next_step() is called | ||
2397 | 411 | self.document.next_step() | ||
2398 | 412 | |||
2399 | 413 | # THEN: Then the result should be correct | ||
2400 | 414 | self.mocked_client.next_step.assert_called_once_with() | ||
2401 | 415 | |||
2402 | 416 | def test_previous_step(self): | ||
2403 | 417 | """ | ||
2404 | 418 | Test the previous_step() method | ||
2405 | 419 | """ | ||
2406 | 420 | # GIVEN: A mocked client | ||
2407 | 421 | # WHEN: previous_step() is called | ||
2408 | 422 | self.document.previous_step() | ||
2409 | 423 | |||
2410 | 424 | # THEN: Then the result should be correct | ||
2411 | 425 | self.mocked_client.previous_step.assert_called_once_with() | ||
2412 | 426 | |||
2413 | 427 | def test_get_slide_text(self): | ||
2414 | 428 | """ | ||
2415 | 429 | Test the get_slide_text() method | ||
2416 | 430 | """ | ||
2417 | 431 | # GIVEN: A mocked client | ||
2418 | 432 | self.mocked_client.get_slide_text.return_value = 'Some slide text' | ||
2419 | 433 | |||
2420 | 434 | # WHEN: get_slide_text() is called | ||
2421 | 435 | result = self.document.get_slide_text(1) | ||
2422 | 436 | |||
2423 | 437 | # THEN: Then the result should be correct | ||
2424 | 438 | self.mocked_client.get_slide_text.assert_called_once_with(1) | ||
2425 | 439 | assert result == 'Some slide text' | ||
2426 | 440 | |||
2427 | 441 | def test_get_slide_notes(self): | ||
2428 | 442 | """ | ||
2429 | 443 | Test the get_slide_notes() method | ||
2430 | 444 | """ | ||
2431 | 445 | # GIVEN: A mocked client | ||
2432 | 446 | self.mocked_client.get_slide_notes.return_value = 'This is a note' | ||
2433 | 447 | |||
2434 | 448 | # WHEN: get_slide_notes() is called | ||
2435 | 449 | result = self.document.get_slide_notes(2) | ||
2436 | 450 | |||
2437 | 451 | # THEN: Then the result should be correct | ||
2438 | 452 | self.mocked_client.get_slide_notes.assert_called_once_with(2) | ||
2439 | 453 | assert result == 'This is a note' |
Looks fine and does not show up on my machine (Linux).
Needs a respin as will have a conflict with previous merge.