~raoul-snyman/openlp/fix-macos-pdf-test

« back to all changes in this revision

Viewing changes to openlp/core/lib/serviceitem.py

  • Committer: Raoul Snyman
  • Date: 2019-02-14 07:04:30 UTC
  • mfrom: (2766.3.118 webengine-migrate)
  • Revision ID: raoul@snyman.info-20190214070430-nxe1vaeaapq3ult5
Migration from WebKit to Webengine. Also introduced reveal.js for slide rendering, new screen setup dialogs and many other changes.

Show diffs side-by-side

added added

removed removed

Lines of Context:
24
24
type and capability of an item.
25
25
"""
26
26
import datetime
27
 
import html
28
27
import logging
29
28
import ntpath
30
29
import os
31
30
import uuid
 
31
from copy import deepcopy
32
32
 
33
33
from PyQt5 import QtGui
34
34
 
36
36
from openlp.core.common import md5_hash
37
37
from openlp.core.common.applocation import AppLocation
38
38
from openlp.core.common.i18n import translate
39
 
from openlp.core.ui.icons import UiIcons
40
39
from openlp.core.common.mixins import RegistryProperties
41
40
from openlp.core.common.path import Path
42
41
from openlp.core.common.settings import Settings
43
 
from openlp.core.lib import ImageSource, clean_tags, expand_tags, expand_chords
 
42
from openlp.core.display.render import remove_tags, render_tags
 
43
from openlp.core.lib import ItemCapabilities
 
44
from openlp.core.ui.icons import UiIcons
 
45
 
44
46
 
45
47
log = logging.getLogger(__name__)
46
48
 
54
56
    Command = 3
55
57
 
56
58
 
57
 
class ItemCapabilities(object):
58
 
    """
59
 
    Provides an enumeration of a service item's capabilities
60
 
 
61
 
    ``CanPreview``
62
 
            The capability to allow the ServiceManager to add to the preview tab when making the previous item live.
63
 
 
64
 
    ``CanEdit``
65
 
            The capability to allow the ServiceManager to allow the item to be edited
66
 
 
67
 
    ``CanMaintain``
68
 
            The capability to allow the ServiceManager to allow the item to be reordered.
69
 
 
70
 
    ``RequiresMedia``
71
 
            Determines is the service_item needs a Media Player
72
 
 
73
 
    ``CanLoop``
74
 
            The capability to allow the SlideController to allow the loop processing.
75
 
 
76
 
    ``CanAppend``
77
 
            The capability to allow the ServiceManager to add leaves to the
78
 
            item
79
 
 
80
 
    ``NoLineBreaks``
81
 
            The capability to remove lines breaks in the renderer
82
 
 
83
 
    ``OnLoadUpdate``
84
 
            The capability to update MediaManager when a service Item is loaded.
85
 
 
86
 
    ``AddIfNewItem``
87
 
            Not Used
88
 
 
89
 
    ``ProvidesOwnDisplay``
90
 
            The capability to tell the SlideController the service Item has a different display.
91
 
 
92
 
    ``HasDetailedTitleDisplay``
93
 
            Being Removed and decommissioned.
94
 
 
95
 
    ``HasVariableStartTime``
96
 
            The capability to tell the ServiceManager that a change to start time is possible.
97
 
 
98
 
    ``CanSoftBreak``
99
 
            The capability to tell the renderer that Soft Break is allowed
100
 
 
101
 
    ``CanWordSplit``
102
 
            The capability to tell the renderer that it can split words is
103
 
            allowed
104
 
 
105
 
    ``HasBackgroundAudio``
106
 
            That a audio file is present with the text.
107
 
 
108
 
    ``CanAutoStartForLive``
109
 
            The capability to ignore the do not play if display blank flag.
110
 
 
111
 
    ``CanEditTitle``
112
 
            The capability to edit the title of the item
113
 
 
114
 
    ``IsOptical``
115
 
            Determines is the service_item is based on an optical device
116
 
 
117
 
    ``HasDisplayTitle``
118
 
            The item contains 'displaytitle' on every frame which should be
119
 
            preferred over 'title' when displaying the item
120
 
 
121
 
    ``HasNotes``
122
 
            The item contains 'notes'
123
 
 
124
 
    ``HasThumbnails``
125
 
            The item has related thumbnails available
126
 
 
127
 
    ``HasMetaData``
128
 
            The item has Meta Data about item
129
 
    """
130
 
    CanPreview = 1
131
 
    CanEdit = 2
132
 
    CanMaintain = 3
133
 
    RequiresMedia = 4
134
 
    CanLoop = 5
135
 
    CanAppend = 6
136
 
    NoLineBreaks = 7
137
 
    OnLoadUpdate = 8
138
 
    AddIfNewItem = 9
139
 
    ProvidesOwnDisplay = 10
140
 
    # HasDetailedTitleDisplay = 11
141
 
    HasVariableStartTime = 12
142
 
    CanSoftBreak = 13
143
 
    CanWordSplit = 14
144
 
    HasBackgroundAudio = 15
145
 
    CanAutoStartForLive = 16
146
 
    CanEditTitle = 17
147
 
    IsOptical = 18
148
 
    HasDisplayTitle = 19
149
 
    HasNotes = 20
150
 
    HasThumbnails = 21
151
 
    HasMetaData = 22
152
 
 
153
 
 
154
59
class ServiceItem(RegistryProperties):
155
60
    """
156
61
    The service item is a base class for the plugins to use to interact with
167
72
        """
168
73
        if plugin:
169
74
            self.name = plugin.name
 
75
        self._rendered_slides = None
 
76
        self._display_slides = None
170
77
        self.title = ''
 
78
        self.slides = []
171
79
        self.processor = None
172
80
        self.audit = ''
173
81
        self.items = []
176
84
        self.foot_text = ''
177
85
        self.theme = None
178
86
        self.service_item_type = None
179
 
        self._raw_frames = []
180
 
        self._display_frames = []
181
87
        self.unique_identifier = 0
182
88
        self.notes = ''
183
89
        self.from_plugin = False
248
154
        else:
249
155
            self.icon = UiIcons().clone
250
156
 
251
 
    def render(self, provides_own_theme_data=False):
252
 
        """
253
 
        The render method is what generates the frames for the screen and obtains the display information from the
254
 
        renderer. At this point all slides are built for the given display size.
 
157
    def _create_slides(self):
 
158
        """
 
159
        Create frames for rendering and display
 
160
        """
 
161
        self._rendered_slides = []
 
162
        self._display_slides = []
255
163
 
256
 
        :param provides_own_theme_data: This switch disables the usage of the item's theme. However, this is
257
 
            disabled by default. If this is used, it has to be taken care, that
258
 
            the renderer knows the correct theme data. However, this is needed
259
 
            for the theme manager.
260
 
        """
261
 
        log.debug('Render called')
262
 
        self._display_frames = []
263
 
        self.bg_image_bytes = None
264
 
        if not provides_own_theme_data:
265
 
            self.renderer.set_item_theme(self.theme)
266
 
            self.theme_data, self.main, self.footer = self.renderer.pre_render()
267
 
        if self.service_item_type == ServiceItemType.Text:
268
 
            expand_chord_tags = hasattr(self, 'name') and self.name == 'songs' and Settings().value(
269
 
                'songs/enable chords')
270
 
            log.debug('Formatting slides: {title}'.format(title=self.title))
271
 
            # Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
272
 
            # the dict instead of rendering them again.
273
 
            previous_pages = {}
274
 
            for slide in self._raw_frames:
275
 
                verse_tag = slide['verseTag']
276
 
                if verse_tag in previous_pages and previous_pages[verse_tag][0] == slide['raw_slide']:
277
 
                    pages = previous_pages[verse_tag][1]
278
 
                else:
279
 
                    pages = self.renderer.format_slide(slide['raw_slide'], self)
280
 
                    previous_pages[verse_tag] = (slide['raw_slide'], pages)
281
 
                for page in pages:
282
 
                    page = page.replace('<br>', '{br}')
283
 
                    html_data = expand_tags(page.rstrip(), expand_chord_tags)
284
 
                    new_frame = {
285
 
                        'title': clean_tags(page),
286
 
                        'text': clean_tags(page.rstrip(), expand_chord_tags),
287
 
                        'chords_text': expand_chords(clean_tags(page.rstrip(), False)),
288
 
                        'html': html_data.replace('&amp;nbsp;', '&nbsp;'),
289
 
                        'printing_html': expand_tags(html.escape(page.rstrip()), expand_chord_tags, True),
290
 
                        'verseTag': verse_tag,
291
 
                    }
292
 
                    self._display_frames.append(new_frame)
293
 
        elif self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command:
294
 
            pass
295
 
        else:
296
 
            log.error('Invalid value renderer: {item}'.format(item=self.service_item_type))
297
 
        self.title = clean_tags(self.title)
298
 
        # The footer should never be None, but to be compatible with a few
299
 
        # nightly builds between 1.9.4 and 1.9.5, we have to correct this to
300
 
        # avoid tracebacks.
301
 
        if self.raw_footer is None:
302
 
            self.raw_footer = []
 
164
        # Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
 
165
        # the dict instead of rendering them again.
 
166
        previous_pages = {}
 
167
        index = 0
303
168
        self.foot_text = '<br>'.join([_f for _f in self.raw_footer if _f])
 
169
        for raw_slide in self.slides:
 
170
            verse_tag = raw_slide['verse']
 
171
            if verse_tag in previous_pages and previous_pages[verse_tag][0] == raw_slide:
 
172
                pages = previous_pages[verse_tag][1]
 
173
            else:
 
174
                pages = self.renderer.format_slide(raw_slide['text'], self)
 
175
                previous_pages[verse_tag] = (raw_slide, pages)
 
176
            for page in pages:
 
177
                rendered_slide = {
 
178
                    'title': raw_slide['title'],
 
179
                    'text': render_tags(page),
 
180
                    'verse': index,
 
181
                    'footer': self.foot_text,
 
182
                }
 
183
                self._rendered_slides.append(rendered_slide)
 
184
                display_slide = {
 
185
                    'title': raw_slide['title'],
 
186
                    'text': remove_tags(page),
 
187
                    'verse': verse_tag,
 
188
                }
 
189
                self._display_slides.append(display_slide)
 
190
                index += 1
 
191
 
 
192
    @property
 
193
    def rendered_slides(self):
 
194
        """
 
195
        Render the frames and return them
 
196
        """
 
197
        if not self._rendered_slides:
 
198
            self._create_slides()
 
199
        return self._rendered_slides
 
200
 
 
201
    @property
 
202
    def display_slides(self):
 
203
        """
 
204
        Render the frames and return them
 
205
        """
 
206
        if not self._display_slides:
 
207
            self._create_slides()
 
208
        return self._display_slides
304
209
 
305
210
    def add_from_image(self, path, title, background=None, thumbnail=None):
306
211
        """
308
213
 
309
214
        :param path: The directory in which the image file is located.
310
215
        :param title: A title for the slide in the service item.
311
 
        :param background:
 
216
        :param background: The background colour
312
217
        :param thumbnail: Optional alternative thumbnail, used for remote thumbnails.
313
218
        """
314
219
        if background:
315
220
            self.image_border = background
316
221
        self.service_item_type = ServiceItemType.Image
317
 
        if not thumbnail:
318
 
            self._raw_frames.append({'title': title, 'path': path})
319
 
        else:
320
 
            self._raw_frames.append({'title': title, 'path': path, 'image': thumbnail})
321
 
        self.image_manager.add_image(path, ImageSource.ImagePlugin, self.image_border)
 
222
        slide = {'title': title, 'path': path}
 
223
        if thumbnail:
 
224
            slide['thumbnail'] = thumbnail
 
225
        self.slides.append(slide)
 
226
        # self.image_manager.add_image(path, ImageSource.ImagePlugin, self.image_border)
322
227
        self._new_item()
323
228
 
324
 
    def add_from_text(self, raw_slide, verse_tag=None):
 
229
    def add_from_text(self, text, verse_tag=None):
325
230
        """
326
231
        Add a text slide to the service item.
327
232
 
328
 
        :param raw_slide: The raw text of the slide.
 
233
        :param text: The raw text of the slide.
329
234
        :param verse_tag:
330
235
        """
331
236
        if verse_tag:
332
237
            verse_tag = verse_tag.upper()
 
238
        else:
 
239
            # For items that don't have a verse tag, autoincrement the slide numbers
 
240
            verse_tag = str(len(self.slides))
333
241
        self.service_item_type = ServiceItemType.Text
334
 
        title = raw_slide[:30].split('\n')[0]
335
 
        self._raw_frames.append({'title': title, 'raw_slide': raw_slide, 'verseTag': verse_tag})
 
242
        title = text[:30].split('\n')[0]
 
243
        self.slides.append({'title': title, 'text': text, 'verse': verse_tag})
336
244
        self._new_item()
337
245
 
338
246
    def add_from_command(self, path, file_name, image, display_title=None, notes=None):
349
257
        # If the item should have a display title but this frame doesn't have one, we make one up
350
258
        if self.is_capable(ItemCapabilities.HasDisplayTitle) and not display_title:
351
259
            display_title = translate('OpenLP.ServiceItem',
352
 
                                      '[slide {frame:d}]').format(frame=len(self._raw_frames) + 1)
 
260
                                      '[slide {frame:d}]').format(frame=len(self.slides) + 1)
353
261
        # Update image path to match servicemanager location if file was loaded from service
354
262
        if image and not self.has_original_files and self.name == 'presentations':
355
263
            file_location = os.path.join(path, file_name)
356
264
            file_location_hash = md5_hash(file_location.encode('utf-8'))
357
265
            image = os.path.join(str(AppLocation.get_section_data_path(self.name)), 'thumbnails',
358
266
                                 file_location_hash, ntpath.basename(image))
359
 
        self._raw_frames.append({'title': file_name, 'image': image, 'path': path,
360
 
                                 'display_title': display_title, 'notes': notes})
361
 
        if self.is_capable(ItemCapabilities.HasThumbnails):
362
 
            self.image_manager.add_image(image, ImageSource.CommandPlugins, '#000000')
 
267
        self.slides.append({'title': file_name, 'image': image, 'path': path, 'display_title': display_title,
 
268
                            'notes': notes, 'thumbnail': image})
 
269
        # if self.is_capable(ItemCapabilities.HasThumbnails):
 
270
        #     self.image_manager.add_image(image, ImageSource.CommandPlugins, '#000000')
363
271
        self._new_item()
364
272
 
365
273
    def get_service_repr(self, lite_save):
394
302
        }
395
303
        service_data = []
396
304
        if self.service_item_type == ServiceItemType.Text:
397
 
            service_data = [slide for slide in self._raw_frames]
 
305
            for slide in self.slides:
 
306
                data_slide = deepcopy(slide)
 
307
                data_slide['raw_slide'] = data_slide.pop('text')
 
308
                data_slide['verseTag'] = data_slide.pop('verse')
 
309
                service_data.append(data_slide)
398
310
        elif self.service_item_type == ServiceItemType.Image:
399
311
            if lite_save:
400
 
                for slide in self._raw_frames:
 
312
                for slide in self.slides:
401
313
                    service_data.append({'title': slide['title'], 'path': slide['path']})
402
314
            else:
403
 
                service_data = [slide['title'] for slide in self._raw_frames]
 
315
                service_data = [slide['title'] for slide in self.slides]
404
316
        elif self.service_item_type == ServiceItemType.Command:
405
 
            for slide in self._raw_frames:
 
317
            for slide in self.slides:
406
318
                service_data.append({'title': slide['title'], 'image': slide['image'], 'path': slide['path'],
407
319
                                     'display_title': slide['display_title'], 'notes': slide['notes']})
408
320
        return {'header': service_header, 'data': service_data}
454
366
        self.theme_overwritten = header.get('theme_overwritten', False)
455
367
        if self.service_item_type == ServiceItemType.Text:
456
368
            for slide in service_item['serviceitem']['data']:
457
 
                self._raw_frames.append(slide)
 
369
                self.add_from_text(slide['raw_slide'], slide['verseTag'])
 
370
            self._create_slides()
458
371
        elif self.service_item_type == ServiceItemType.Image:
459
372
            settings_section = service_item['serviceitem']['header']['name']
460
373
            background = QtGui.QColor(Settings().value(settings_section + '/background color'))
478
391
                    self.add_from_command(path, text_image['title'], text_image['image'],
479
392
                                          text_image.get('display_title', ''), text_image.get('notes', ''))
480
393
                else:
481
 
                    self.add_from_command(text_image['path'], text_image['title'], text_image['image'])
 
394
                    self.add_from_command(Path(text_image['path']), text_image['title'], text_image['image'])
482
395
        self._new_item()
483
396
 
484
397
    def get_display_title(self):
489
402
                or self.is_capable(ItemCapabilities.CanEditTitle):
490
403
            return self.title
491
404
        else:
492
 
            if len(self._raw_frames) > 1:
 
405
            if len(self.slides) > 1:
493
406
                return self.title
494
407
            else:
495
 
                return self._raw_frames[0]['title']
 
408
                return self.slides[0]['title']
496
409
 
497
410
    def merge(self, other):
498
411
        """
508
421
        if other.theme is not None:
509
422
            self.theme = other.theme
510
423
            self._new_item()
511
 
        self.render()
512
424
        if self.is_capable(ItemCapabilities.HasBackgroundAudio):
513
425
            log.debug(self.background_audio)
514
426
 
578
490
        Returns the frames for the ServiceItem
579
491
        """
580
492
        if self.service_item_type == ServiceItemType.Text:
581
 
            return self._display_frames
 
493
            return self.display_slides
582
494
        else:
583
 
            return self._raw_frames
 
495
            return self.slides
584
496
 
585
497
    def get_rendered_frame(self, row):
586
498
        """
589
501
        :param row: The service item slide to be returned
590
502
        """
591
503
        if self.service_item_type == ServiceItemType.Text:
592
 
            return self._display_frames[row]['html'].split('\n')[0]
 
504
            # return self.display_frames[row]['html'].split('\n')[0]
 
505
            return self.rendered_slides[row]['text']
593
506
        elif self.service_item_type == ServiceItemType.Image:
594
 
            return self._raw_frames[row]['path']
 
507
            return self.slides[row]['path']
595
508
        else:
596
 
            return self._raw_frames[row]['image']
 
509
            return self.slides[row]['image']
597
510
 
598
511
    def get_frame_title(self, row=0):
599
512
        """
600
513
        Returns the title of the raw frame
601
514
        """
602
515
        try:
603
 
            return self._raw_frames[row]['title']
 
516
            return self.get_frames()[row]['title']
604
517
        except IndexError:
605
518
            return ''
606
519
 
610
523
        """
611
524
        if not frame:
612
525
            try:
613
 
                frame = self._raw_frames[row]
 
526
                frame = self.slides[row]
614
527
            except IndexError:
615
528
                return ''
616
529
        if self.is_image() or self.is_capable(ItemCapabilities.IsOptical):
627
540
        """
628
541
        Remove the specified frame from the item
629
542
        """
630
 
        if frame in self._raw_frames:
631
 
            self._raw_frames.remove(frame)
 
543
        if frame in self.slides:
 
544
            self.slides.remove(frame)
632
545
 
633
546
    def get_media_time(self):
634
547
        """
662
575
        self.theme_overwritten = (theme is None)
663
576
        self.theme = theme
664
577
        self._new_item()
665
 
        self.render()
666
578
 
667
579
    def remove_invalid_frames(self, invalid_paths=None):
668
580
        """
677
589
        """
678
590
        Returns if there are any frames in the service item
679
591
        """
680
 
        return not bool(self._raw_frames)
 
592
        return not bool(self.slides)
681
593
 
682
594
    def validate_item(self, suffix_list=None):
683
595
        """
684
596
        Validates a service item to make sure it is valid
685
597
        """
686
598
        self.is_valid = True
687
 
        for frame in self._raw_frames:
688
 
            if self.is_image() and not os.path.exists(frame['path']):
 
599
        for slide in self.slides:
 
600
            if self.is_image() and not os.path.exists(slide['path']):
689
601
                self.is_valid = False
690
602
                break
691
603
            elif self.is_command():
692
604
                if self.is_capable(ItemCapabilities.IsOptical) and State().check_preconditions('media'):
693
 
                    if not os.path.exists(frame['title']):
 
605
                    if not os.path.exists(slide['title']):
694
606
                        self.is_valid = False
695
607
                        break
696
608
                else:
697
 
                    file_name = os.path.join(frame['path'], frame['title'])
 
609
                    file_name = os.path.join(slide['path'], slide['title'])
698
610
                    if not os.path.exists(file_name):
699
611
                        self.is_valid = False
700
612
                        break
701
613
                    if suffix_list and not self.is_text():
702
 
                        file_suffix = frame['title'].split('.')[-1]
 
614
                        file_suffix = slide['title'].split('.')[-1]
703
615
                        if file_suffix.lower() not in suffix_list:
704
616
                            self.is_valid = False
705
617
                            break