# -*- coding: utf-8 -*- ########################################################################## # OpenLP - Open Source Lyrics Projection # # ---------------------------------------------------------------------- # # Copyright (c) 2008-2022 OpenLP Developers # # ---------------------------------------------------------------------- # # This program is free software: you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # # the Free Software Foundation, either version 3 of the License, or # # (at your option) any later version. # # # # This program is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program. If not, see . # ########################################################################## """ The actual print service dialog """ import datetime import html import lxml.html from PyQt5 import QtCore, QtGui, QtPrintSupport, QtWidgets from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.mixins import RegistryProperties from openlp.core.common.registry import Registry from openlp.core.lib import get_text_file_string from openlp.core.ui.printservicedialog import Ui_PrintServiceDialog, ZoomSize DEFAULT_CSS = """/* Edit this file to customize the service order print. Note, that not all CSS properties are supported. See: https://doc.qt.io/qt-5/richtext-html-subset.html#css-properties */ .serviceTitle { font-weight: 600; font-size: x-large; color: black; } .item { color: black; } .itemTitle { font-weight: 600; font-size: large; } .itemText { margin-top: 10px; } .itemFooter { font-size: 8px; } .itemNotes {} .itemNotesTitle { font-weight: bold; font-size: 12px; } .itemNotesText { font-size: 11px; } .media {} .mediaTitle { font-weight: bold; font-size: 11px; } .mediaText {} .imageList {} .customNotes { margin-top: 10px; } .customNotesTitle { font-weight: bold; font-size: 11px; } .customNotesText { font-size: 11px; } .newPage { page-break-before: always; } table.line {} table.segment { float: left; } td.chord { font-size: 80%; } td.lyrics { } """ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryProperties): """ The :class:`~openlp.core.ui.printserviceform.PrintServiceForm` class displays a dialog for printing the service. """ def __init__(self): """ Constructor """ super(PrintServiceForm, self).__init__(Registry().get('main_window'), QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint) self.printer = QtPrintSupport.QPrinter() self.print_dialog = QtPrintSupport.QPrintDialog(self.printer, self) self.document = QtGui.QTextDocument() self.zoom = 0 self.setup_ui(self) # Load the settings for the dialog. self.settings.beginGroup('advanced') self.slide_text_check_box.setChecked(self.settings.value('print slide text')) self.page_break_after_text.setChecked(self.settings.value('add page break')) if not self.slide_text_check_box.isChecked(): self.page_break_after_text.setDisabled(True) self.meta_data_check_box.setChecked(self.settings.value('print file meta data')) self.notes_check_box.setChecked(self.settings.value('print notes')) self.zoom_combo_box.setCurrentIndex(self.settings.value('display size')) self.settings.endGroup() # Signals self.print_button.triggered.connect(self.print_service_order) self.zoom_out_button.clicked.connect(self.zoom_out) self.zoom_in_button.clicked.connect(self.zoom_in) self.zoom_original_button.clicked.connect(self.zoom_original) self.preview_widget.paintRequested.connect(self.paint_requested) self.zoom_combo_box.currentIndexChanged.connect(self.display_size_changed) self.plain_copy.triggered.connect(self.copy_text) self.html_copy.triggered.connect(self.copy_html_text) self.slide_text_check_box.stateChanged.connect(self.on_slide_text_check_box_changed) self.update_preview_text() def toggle_options(self, checked): """ Toggle various options """ self.options_widget.setVisible(checked) if checked: left = self.options_button.pos().x() top = self.toolbar.height() self.options_widget.move(left, top) self.title_line_edit.setFocus() else: self.save_options() self.update_preview_text() def update_preview_text(self): """ Creates the html text and updates the html of *self.document*. """ html_data = self._add_element('html') self._add_element('head', parent=html_data) self._add_element('title', self.title_line_edit.text(), html_data.head) css_path = AppLocation.get_data_path() / 'serviceprint' / 'service_print.css' custom_css = get_text_file_string(css_path) if not custom_css: custom_css = DEFAULT_CSS self._add_element('style', custom_css, html_data.head, attribute=('type', 'text/css')) self._add_element('body', parent=html_data) self._add_element('h1', html.escape(self.title_line_edit.text()), html_data.body, class_id='serviceTitle') for index, item in enumerate(self.service_manager.service_items): self._add_preview_item(html_data.body, item['service_item'], index) if not self.show_chords_check_box.isChecked(): # Remove chord row and spacing span elements when not printing chords for chord_row in html_data.find_class('chordrow'): chord_row.drop_tree() for spacing_span in html_data.find_class('chordspacing'): spacing_span.drop_tree() # Add the custom service notes: if self.footer_text_edit.toPlainText(): div = self._add_element('div', parent=html_data.body, class_id='customNotes') self._add_element( 'span', translate('OpenLP.ServiceManager', 'Service Notes: '), div, class_id='customNotesTitle') self._add_element('span', html.escape(self.footer_text_edit.toPlainText()), div, class_id='customNotesText') self.document.setHtml(lxml.html.tostring(html_data).decode()) self.preview_widget.updatePreview() def _add_preview_item(self, body, item, index): """ Add a preview item """ div = self._add_element('div', class_id='item', parent=body) # Add the title of the service item. item_title = self._add_element('h2', parent=div, class_id='itemTitle') self._add_element('span', ' ' + html.escape(item.get_display_title()), item_title) if self.slide_text_check_box.isChecked(): # Add the text of the service item. if item.is_text(): verse_def = None verse_html = None for slide in item.print_slides: if not verse_def or verse_def != slide['verse'] or verse_html == slide['text']: text_div = self._add_element('div', parent=div, class_id='itemText') elif 'chordspacing' not in slide['text']: self._add_element('br', parent=text_div) self._add_element('span', slide['text'], text_div) verse_def = slide['verse'] verse_html = slide['text'] # Break the page before the div element. if index != 0 and self.page_break_after_text.isChecked(): div.set('class', 'item newPage') # Add the image names of the service item. elif item.is_image(): ol = self._add_element('ol', parent=div, class_id='imageList') for slide in range(len(item.get_frames())): self._add_element('li', item.get_frame_title(slide), ol) # add footer footer_html = item.footer_html footer_html = footer_html.partition('
')[2] if footer_html: footer_html = html.escape(footer_html.replace('
', '\n')) self._add_element('div', footer_html.replace('\n', '
'), parent=div, class_id='itemFooter') # Add service items' notes. if self.notes_check_box.isChecked(): if item.notes: p = self._add_element('div', class_id='itemNotes', parent=div) self._add_element('span', translate('OpenLP.ServiceManager', 'Notes: '), p, class_id='itemNotesTitle') self._add_element('span', html.escape(item.notes).replace('\n', '
'), p, class_id='itemNotesText') # Add play length of media files. if item.is_media() and self.meta_data_check_box.isChecked(): tme = item.media_length if item.end_time > 0: tme = item.end_time - item.start_time title = self._add_element('div', class_id='media', parent=div) self._add_element( 'span', translate('OpenLP.ServiceManager', 'Playing time: '), title, class_id='mediaTitle') self._add_element('span', str(datetime.timedelta(seconds=tme)), title, class_id='mediaText') def _add_element(self, tag, text=None, parent=None, class_id=None, attribute=None): """ Creates a html element. If ``text`` is given, the element's text will set and if a ``parent`` is given, the element is appended. :param tag: The html tag, e. g. ``'span'``. Defaults to ``None``. :param text: The text for the tag. Defaults to ``None``. :param parent: The parent element. Defaults to ``None``. :param class_id: Value for the class attribute :param attribute: Tuple name/value pair to add as an optional attribute """ if text is not None: element = lxml.html.fragment_fromstring(str(text), create_parent=tag) else: element = lxml.html.Element(tag) if parent is not None: parent.append(element) if class_id is not None: element.set('class', class_id) if attribute is not None: element.set(attribute[0], attribute[1]) return element def paint_requested(self, printer): """ Paint the preview of the *self.document*. ``printer`` A *QPrinter* object. """ self.document.print_(printer) def display_size_changed(self, display): """ The Zoom Combo box has changed so set up the size. """ if display == ZoomSize.Page: self.preview_widget.fitInView() elif display == ZoomSize.Width: self.preview_widget.fitToWidth() elif display == ZoomSize.OneHundred: self.preview_widget.fitToWidth() self.preview_widget.zoomIn(1) elif display == ZoomSize.SeventyFive: self.preview_widget.fitToWidth() self.preview_widget.zoomIn(0.75) elif display == ZoomSize.Fifty: self.preview_widget.fitToWidth() self.preview_widget.zoomIn(0.5) elif display == ZoomSize.TwentyFive: self.preview_widget.fitToWidth() self.preview_widget.zoomIn(0.25) self.settings.beginGroup('advanced') self.settings.setValue('display size', display) self.settings.endGroup() def copy_text(self): """ Copies the display text to the clipboard as plain text """ self.update_song_usage() cursor = QtGui.QTextCursor(self.document) cursor.select(QtGui.QTextCursor.Document) clipboard_text = cursor.selectedText() # We now have the unprocessed unicode service text in the cursor # So we replace u2028 with \n and u2029 with \n\n and a few others clipboard_text = clipboard_text.replace('\u2028', '\n') clipboard_text = clipboard_text.replace('\u2029', '\n\n') clipboard_text = clipboard_text.replace('\u2018', '\'') clipboard_text = clipboard_text.replace('\u2019', '\'') clipboard_text = clipboard_text.replace('\u201c', '"') clipboard_text = clipboard_text.replace('\u201d', '"') clipboard_text = clipboard_text.replace('\u2026', '...') clipboard_text = clipboard_text.replace('\u2013', '-') clipboard_text = clipboard_text.replace('\u2014', '-') # remove the icon from the text clipboard_text = clipboard_text.replace('\ufffc\xa0', '') # and put it all on the clipboard self.main_window.clipboard.setText(clipboard_text) def copy_html_text(self): """ Copies the display text to the clipboard as Html """ self.update_song_usage() self.main_window.clipboard.setText(self.document.toHtml()) def print_service_order(self): """ Called, when the *print_button* is clicked. Opens the *print_dialog*. """ if not self.print_dialog.exec(): return self.update_song_usage() # Print the document. self.document.print_(self.printer) def zoom_in(self): """ Called when *zoom_in_button* is clicked. """ self.preview_widget.zoomIn() self.zoom -= 0.1 def zoom_out(self): """ Called when *zoom_out_button* is clicked. """ self.preview_widget.zoomOut() self.zoom += 0.1 def zoom_original(self): """ Called when *zoom_out_button* is clicked. """ self.preview_widget.zoomIn(1 + self.zoom) self.zoom = 0 def update_text_format(self, value): """ Called when html copy check box is selected. """ if value == QtCore.Qt.Checked: self.copyTextButton.setText(UiStrings().CopyToHtml) else: self.copyTextButton.setText(UiStrings().CopyToText) def on_slide_text_check_box_changed(self, state): """ Disable or enable the ``page_break_after_text`` checkbox as it should only be enabled, when the ``slide_text_check_box`` is enabled. """ self.page_break_after_text.setDisabled(state == QtCore.Qt.Unchecked) def save_options(self): """ Save the settings and close the dialog. """ # Save the settings for this dialog. self.settings.beginGroup('advanced') self.settings.setValue('print slide text', self.slide_text_check_box.isChecked()) self.settings.setValue('add page break', self.page_break_after_text.isChecked()) self.settings.setValue('print file meta data', self.meta_data_check_box.isChecked()) self.settings.setValue('print notes', self.notes_check_box.isChecked()) self.settings.endGroup() def update_song_usage(self): """ Update the song usage """ # Only continue when we include the song's text. if not self.slide_text_check_box.isChecked(): return for item in self.service_manager.service_items: # Trigger Audit requests Registry().register_function('print_service_started', [item['service_item']])