diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 7bbd4349d..d4e114c74 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -129,7 +129,7 @@ class Settings(QtCore.QSettings): 'advanced/recent file count': 4, 'advanced/save current plugin': False, 'advanced/slide limits': SlideLimits.End, - 'advanced/slide max height': 0, + 'advanced/slide max height': -4, 'advanced/single click preview': False, 'advanced/single click service preview': False, 'advanced/x11 bypass wm': X11_BYPASS_DEFAULT, diff --git a/openlp/core/lib/htmlbuilder.py b/openlp/core/lib/htmlbuilder.py index f0d8ddef2..6f2fee68c 100644 --- a/openlp/core/lib/htmlbuilder.py +++ b/openlp/core/lib/htmlbuilder.py @@ -390,163 +390,207 @@ is the function which has to be called from outside. The generated and returned import logging from PyQt5 import QtWebKit +from string import Template from openlp.core.common import Settings from openlp.core.lib.theme import BackgroundType, BackgroundGradientType, VerticalType, HorizontalType log = logging.getLogger(__name__) -# TODO: Verify where this is used before converting to python3 -HTMLSRC = """ - - - -OpenLP Display - + + + + + + ${html_additions} +
+ +
+ + + """) + +LYRICS_SRC = Template(""" + .lyricstable { + z-index: 5; + position: absolute; + display: table; + ${stable} + } + .lyricscell { + display: table-cell; + word-wrap: break-word; + -webkit-transition: opacity 0.4s ease; + ${lyrics} + } + .lyricsmain { + ${main} + } + """) + +FOOTER_SRC = Template(""" + left: ${left}px; + bottom: ${bottom}px; + width: ${width}px; + font-family: ${family}; + font-size: ${size}pt; + color: ${color}; + text-align: left; + white-space: ${space}; + """) + +LYRICS_FORMAT_SRC = Template(""" + ${justify}word-wrap: break-word; + text-align: ${align}; + vertical-align: ${valign}; + font-family: ${font}; + font-size: ${size}pt; + color: ${color}; + line-height: ${line}%; margin: 0; padding: 0; - border: 0; - overflow: hidden; - -webkit-user-select: none; -} -body { - %s; -} -.size { - position: absolute; - left: 0px; - top: 0px; - width: 100%%; - height: 100%%; -} -#black { - z-index: 8; - background-color: black; - display: none; -} -#bgimage { - z-index: 1; -} -#image { - z-index: 2; -} -%s -#footer { - position: absolute; - z-index: 6; - %s -} -/* lyric css */ -%s -sup { - font-size: 0.6em; - vertical-align: top; - position: relative; - top: -0.3em; -} - - - - - - -%s -
- -
- - -""" + padding-bottom: ${bottom}; + padding-left: ${left}px; + width: ${width}px; + height: ${height}px;${font_style}${font_weight} + """) def build_html(item, screen, is_live, background, image=None, plugins=None): @@ -582,18 +626,17 @@ def build_html(item, screen, is_live, background, image=None, plugins=None): css_additions += plugin.get_display_css() js_additions += plugin.get_display_javascript() html_additions += plugin.get_display_html() - html = HTMLSRC % ( - build_background_css(item, width), - css_additions, - build_footer_css(item, height), - build_lyrics_css(item), - 'true' if theme_data and theme_data.display_slide_transition and is_live else 'false', - js_additions, - bgimage_src, - image_src, - html_additions - ) - return html + return HTML_SRC.substitute(bg_css=build_background_css(item, width), + css_additions=css_additions, + footer_css=build_footer_css(item, height), + lyrics_css=build_lyrics_css(item), + transitions='true' if (theme_data and + theme_data.display_slide_transition and + is_live) else 'false', + js_additions=js_additions, + bg_image=bgimage_src, + image=image_src, + html_additions=html_additions) def webkit_version(): @@ -650,24 +693,6 @@ def build_lyrics_css(item): :param item: Service Item containing theme and location information """ - # TODO: Verify this before converting to python3 - style = """ -.lyricstable { - z-index: 5; - position: absolute; - display: table; - %s -} -.lyricscell { - display: table-cell; - word-wrap: break-word; - -webkit-transition: opacity 0.4s ease; - %s -} -.lyricsmain { - %s -} -""" theme_data = item.theme_data lyricstable = '' lyrics = '' @@ -680,8 +705,7 @@ def build_lyrics_css(item): lyricsmain += ' text-shadow: {theme} {shadow}px ' \ '{shadow}px;'.format(theme=theme_data.font_main_shadow_color, shadow=theme_data.font_main_shadow_size) - lyrics_css = style % (lyricstable, lyrics, lyricsmain) - return lyrics_css + return LYRICS_SRC.substitute(stable=lyricstable, lyrics=lyrics, main=lyricsmain) def build_lyrics_outline_css(theme_data): @@ -710,38 +734,23 @@ def build_lyrics_format_css(theme_data, width, height): """ align = HorizontalType.Names[theme_data.display_horizontal_align] valign = VerticalType.Names[theme_data.display_vertical_align] - if theme_data.font_main_outline: - left_margin = int(theme_data.font_main_outline_size) * 2 - else: - left_margin = 0 - justify = 'white-space:pre-wrap;' + left_margin = (int(theme_data.font_main_outline_size) * 2) if theme_data.font_main_outline else 0 # fix tag incompatibilities - if theme_data.display_horizontal_align == HorizontalType.Justify: - justify = '' - if theme_data.display_vertical_align == VerticalType.Bottom: - padding_bottom = '0.5em' - else: - padding_bottom = '0' - lyrics = '{justify} word-wrap: break-word; ' \ - 'text-align: {align}; vertical-align: {valign}; font-family: {font}; ' \ - 'font-size: {size}pt; color: {color}; line-height: {line:d}%; margin: 0;' \ - 'padding: 0; padding-bottom: {bottom}; padding-left: {left}px; width: {width}px; ' \ - 'height: {height}px; '.format(justify=justify, - align=align, - valign=valign, - font=theme_data.font_main_name, - size=theme_data.font_main_size, - color=theme_data.font_main_color, - line=100 + int(theme_data.font_main_line_adjustment), - bottom=padding_bottom, - left=left_margin, - width=width, - height=height) - if theme_data.font_main_italics: - lyrics += 'font-style:italic; ' - if theme_data.font_main_bold: - lyrics += 'font-weight:bold; ' - return lyrics + justify = '' if (theme_data.display_horizontal_align == HorizontalType.Justify) else ' white-space: pre-wrap;\n' + padding_bottom = '0.5em' if (theme_data.display_vertical_align == VerticalType.Bottom) else '0' + return LYRICS_FORMAT_SRC.substitute(justify=justify, + align=align, + valign=valign, + font=theme_data.font_main_name, + size=theme_data.font_main_size, + color=theme_data.font_main_color, + line='{line:d}'.format(line=100 + int(theme_data.font_main_line_adjustment)), + bottom=padding_bottom, + left=left_margin, + width=width, + height=height, + font_style='\n font-style: italic;' if theme_data.font_main_italics else '', + font_weight='\n font-weight: bold;' if theme_data.font_main_bold else '') def build_footer_css(item, height): @@ -751,22 +760,11 @@ def build_footer_css(item, height): :param item: Service Item to be processed. :param height: """ - style = """ - left: {left}px; - bottom: {bottom}px; - width: {width}px; - font-family: {family}; - font-size: {size}pt; - color: {color}; - text-align: left; - white-space: {space}; - """ theme = item.theme_data if not theme or not item.footer: return '' bottom = height - int(item.footer.y()) - int(item.footer.height()) whitespace = 'normal' if Settings().value('themes/wrap footer') else 'nowrap' - lyrics_html = style.format(left=item.footer.x(), bottom=bottom, width=item.footer.width(), - family=theme.font_footer_name, size=theme.font_footer_size, - color=theme.font_footer_color, space=whitespace) - return lyrics_html + return FOOTER_SRC.substitute(left=item.footer.x(), bottom=bottom, width=item.footer.width(), + family=theme.font_footer_name, size=theme.font_footer_size, + color=theme.font_footer_color, space=whitespace) diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index e6f2dee49..9b6169a72 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -87,11 +87,14 @@ class AdvancedTab(SettingsTab): self.ui_layout.addRow(self.expand_service_item_check_box) self.slide_max_height_label = QtWidgets.QLabel(self.ui_group_box) self.slide_max_height_label.setObjectName('slide_max_height_label') - self.slide_max_height_spin_box = QtWidgets.QSpinBox(self.ui_group_box) - self.slide_max_height_spin_box.setObjectName('slide_max_height_spin_box') - self.slide_max_height_spin_box.setRange(0, 1000) - self.slide_max_height_spin_box.setSingleStep(20) - self.ui_layout.addRow(self.slide_max_height_label, self.slide_max_height_spin_box) + self.slide_max_height_combo_box = QtWidgets.QComboBox(self.ui_group_box) + self.slide_max_height_combo_box.addItem('', userData=0) + self.slide_max_height_combo_box.addItem('', userData=-4) + # Generate numeric values for combo box dynamically + for px in range(60, 801, 5): + self.slide_max_height_combo_box.addItem(str(px) + 'px', userData=px) + self.slide_max_height_combo_box.setObjectName('slide_max_height_combo_box') + self.ui_layout.addRow(self.slide_max_height_label, self.slide_max_height_combo_box) self.autoscroll_label = QtWidgets.QLabel(self.ui_group_box) self.autoscroll_label.setObjectName('autoscroll_label') self.autoscroll_combo_box = QtWidgets.QComboBox(self.ui_group_box) @@ -265,7 +268,8 @@ class AdvancedTab(SettingsTab): 'Expand new service items on creation')) self.slide_max_height_label.setText(translate('OpenLP.AdvancedTab', 'Max height for non-text slides\nin slide controller:')) - self.slide_max_height_spin_box.setSpecialValueText(translate('OpenLP.AdvancedTab', 'Disabled')) + self.slide_max_height_combo_box.setItemText(0, translate('OpenLP.AdvancedTab', 'Disabled')) + self.slide_max_height_combo_box.setItemText(1, translate('OpenLP.AdvancedTab', 'Automatic')) self.autoscroll_label.setText(translate('OpenLP.AdvancedTab', 'When changing slides:')) self.autoscroll_combo_box.setItemText(0, translate('OpenLP.AdvancedTab', 'Do not auto-scroll')) @@ -355,10 +359,13 @@ class AdvancedTab(SettingsTab): self.single_click_preview_check_box.setChecked(settings.value('single click preview')) self.single_click_service_preview_check_box.setChecked(settings.value('single click service preview')) self.expand_service_item_check_box.setChecked(settings.value('expand service item')) - self.slide_max_height_spin_box.setValue(settings.value('slide max height')) + slide_max_height_value = settings.value('slide max height') + for i in range(0, self.slide_max_height_combo_box.count()): + if self.slide_max_height_combo_box.itemData(i) == slide_max_height_value: + self.slide_max_height_combo_box.setCurrentIndex(i) autoscroll_value = settings.value('autoscrolling') for i in range(0, len(self.autoscroll_map)): - if self.autoscroll_map[i] == autoscroll_value: + if self.autoscroll_map[i] == autoscroll_value and i < self.autoscroll_combo_box.count(): self.autoscroll_combo_box.setCurrentIndex(i) self.enable_auto_close_check_box.setChecked(settings.value('enable exit confirmation')) self.hide_mouse_check_box.setChecked(settings.value('hide mouse')) @@ -439,7 +446,9 @@ class AdvancedTab(SettingsTab): settings.setValue('single click preview', self.single_click_preview_check_box.isChecked()) settings.setValue('single click service preview', self.single_click_service_preview_check_box.isChecked()) settings.setValue('expand service item', self.expand_service_item_check_box.isChecked()) - settings.setValue('slide max height', self.slide_max_height_spin_box.value()) + slide_max_height_index = self.slide_max_height_combo_box.currentIndex() + slide_max_height_value = self.slide_max_height_combo_box.itemData(slide_max_height_index) + settings.setValue('slide max height', slide_max_height_value) settings.setValue('autoscrolling', self.autoscroll_map[self.autoscroll_combo_box.currentIndex()]) settings.setValue('enable exit confirmation', self.enable_auto_close_check_box.isChecked()) settings.setValue('hide mouse', self.hide_mouse_check_box.isChecked()) diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index b9ba00f69..29bf0a3ff 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -564,14 +564,14 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self.progress_bar.setValue(self.progress_bar.maximum()) if self.has_run_wizard: text = translate('OpenLP.FirstTimeWizard', - 'Download complete. Click the {text} button to return to OpenLP.' - ).format(text=clean_button_text(self.buttonText(QtWidgets.QWizard.FinishButton))) + 'Download complete. Click the {button} button to return to OpenLP.' + ).format(button=clean_button_text(self.buttonText(QtWidgets.QWizard.FinishButton))) self.progress_label.setText(text) else: text = translate('OpenLP.FirstTimeWizard', 'Download complete. Click the {button} button to start OpenLP.' ).format(button=clean_button_text(self.buttonText(QtWidgets.QWizard.FinishButton))) - self.progress_label.setText() + self.progress_label.setText(text) else: if self.has_run_wizard: text = translate('OpenLP.FirstTimeWizard', @@ -582,7 +582,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): text = translate('OpenLP.FirstTimeWizard', 'Click the {button} button to start OpenLP.' ).format(button=clean_button_text(self.buttonText(QtWidgets.QWizard.FinishButton))) - self.progress_label.setText() + self.progress_label.setText(text) self.finish_button.setVisible(True) self.finish_button.setEnabled(True) self.cancel_button.setVisible(False) diff --git a/openlp/core/ui/lib/listpreviewwidget.py b/openlp/core/ui/lib/listpreviewwidget.py index 2383cc35f..2ea004160 100644 --- a/openlp/core/ui/lib/listpreviewwidget.py +++ b/openlp/core/ui/lib/listpreviewwidget.py @@ -63,6 +63,7 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): # Initialize variables. self.service_item = ServiceItem() self.screen_ratio = screen_ratio + self.auto_row_height = 100 # Connect signals self.verticalHeader().sectionResized.connect(self.row_resized) @@ -87,8 +88,14 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): height = self.viewport().width() // self.screen_ratio max_img_row_height = Settings().value('advanced/slide max height') # Adjust for row height cap if in use. - if isinstance(max_img_row_height, int) and max_img_row_height > 0 and height > max_img_row_height: - height = max_img_row_height + if isinstance(max_img_row_height, int): + if max_img_row_height > 0 and height > max_img_row_height: + height = max_img_row_height + elif max_img_row_height < 0: + # If auto setting, show that number of slides, or if the resulting slides too small, 100px. + # E.g. If setting is -4, 4 slides will be visible, unless those slides are < 100px high. + self.auto_row_height = max(self.viewport().height() / (-1 * max_img_row_height), 100) + height = min(height, self.auto_row_height) # Apply new height to slides for frame_number in range(len(self.service_item.get_frames())): self.setRowHeight(frame_number, height) @@ -99,7 +106,7 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): """ # Only for non-text slides when row height cap in use max_img_row_height = Settings().value('advanced/slide max height') - if self.service_item.is_text() or not isinstance(max_img_row_height, int) or max_img_row_height <= 0: + if self.service_item.is_text() or not isinstance(max_img_row_height, int) or max_img_row_height == 0: return # Get and validate label widget containing slide & adjust max width try: @@ -165,11 +172,15 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): slide_height = width // self.screen_ratio # Setup and validate row height cap if in use. max_img_row_height = Settings().value('advanced/slide max height') - if isinstance(max_img_row_height, int) and max_img_row_height > 0: - if slide_height > max_img_row_height: + if isinstance(max_img_row_height, int) and max_img_row_height != 0: + if max_img_row_height > 0 and slide_height > max_img_row_height: + # Manual Setting slide_height = max_img_row_height - label.setMaximumWidth(max_img_row_height * self.screen_ratio) - label.resize(max_img_row_height * self.screen_ratio, max_img_row_height) + elif max_img_row_height < 0 and slide_height > self.auto_row_height: + # Auto Setting + slide_height = self.auto_row_height + label.setMaximumWidth(slide_height * self.screen_ratio) + label.resize(slide_height * self.screen_ratio, slide_height) # Build widget with stretch padding container = QtWidgets.QWidget() hbox = QtWidgets.QHBoxLayout() diff --git a/openlp/plugins/bibles/forms/editbibledialog.py b/openlp/plugins/bibles/forms/editbibledialog.py index ca3bea4ed..f1e833637 100644 --- a/openlp/plugins/bibles/forms/editbibledialog.py +++ b/openlp/plugins/bibles/forms/editbibledialog.py @@ -104,7 +104,7 @@ class Ui_EditBibleDialog(object): for book in BiblesResourcesDB.get_books(): self.book_name_label[book['abbreviation']] = QtWidgets.QLabel(self.book_name_widget) self.book_name_label[book['abbreviation']].setObjectName( - 'book_name_label[{name}]'.format(book=book['abbreviation'])) + 'book_name_label[{book}]'.format(book=book['abbreviation'])) self.book_name_edit[book['abbreviation']] = QtWidgets.QLineEdit(self.book_name_widget) self.book_name_edit[book['abbreviation']].setObjectName( 'book_name_edit[{name}]'.format(name=book['abbreviation'])) diff --git a/openlp/plugins/songs/lib/importers/mediashout.py b/openlp/plugins/songs/lib/importers/mediashout.py index cb3a19640..a3bd7bbbc 100644 --- a/openlp/plugins/songs/lib/importers/mediashout.py +++ b/openlp/plugins/songs/lib/importers/mediashout.py @@ -24,15 +24,17 @@ The :mod:`mediashout` module provides the functionality for importing a MediaShout database into the OpenLP database. """ -# WARNING: See https://docs.python.org/2/library/sqlite3.html for value substitution +# WARNING: See https://docs.python.org/3/library/sqlite3.html for value substitution # in SQL statements import pyodbc +import logging from openlp.core.lib import translate from openlp.plugins.songs.lib.importers.songimport import SongImport VERSE_TAGS = ['V', 'C', 'B', 'O', 'P', 'I', 'E'] +log = logging.getLogger(__name__) class MediaShoutImport(SongImport): @@ -44,17 +46,18 @@ class MediaShoutImport(SongImport): """ Initialise the MediaShout importer. """ - SongImport.__init__(self, manager, **kwargs) + super(MediaShoutImport, self).__init__(manager, **kwargs) def do_import(self): """ Receive a single file to import. """ try: - conn = pyodbc.connect('DRIVER={Microsoft Access Driver (*.mdb)};DBQ={source};' - 'PWD=6NOZ4eHK7k'.format(sorce=self.import_source)) - except: + conn = pyodbc.connect('DRIVER={{Microsoft Access Driver (*.mdb)}};DBQ={source};' + 'PWD=6NOZ4eHK7k'.format(source=self.import_source)) + except Exception as e: # Unfortunately no specific exception type + log.exception(e) self.log_error(self.import_source, translate('SongsPlugin.MediaShoutImport', 'Unable to open the MediaShout database.')) return @@ -63,17 +66,21 @@ class MediaShoutImport(SongImport): songs = cursor.fetchall() self.import_wizard.progress_bar.setMaximum(len(songs)) for song in songs: + topics = [] if self.stop_import_flag: break - cursor.execute('SELECT Type, Number, Text FROM Verses WHERE Record = ? ORDER BY Type, Number', song.Record) + cursor.execute('SELECT Type, Number, Text FROM Verses WHERE Record = ? ORDER BY Type, Number', + float(song.Record)) verses = cursor.fetchall() - cursor.execute('SELECT Type, Number, POrder FROM PlayOrder WHERE Record = ? ORDER BY POrder', song.Record) + cursor.execute('SELECT Type, Number, POrder FROM PlayOrder WHERE Record = ? ORDER BY POrder', + float(song.Record)) verse_order = cursor.fetchall() - cursor.execute('SELECT Name FROM Themes INNER JOIN SongThemes ON SongThemes.ThemeId = Themes.ThemeId ' - 'WHERE SongThemes.Record = ?', song.Record) - topics = cursor.fetchall() + if cursor.tables(table='TableName', tableType='TABLE').fetchone(): + cursor.execute('SELECT Name FROM Themes INNER JOIN SongThemes ON SongThemes.ThemeId = Themes.ThemeId ' + 'WHERE SongThemes.Record = ?', float(song.Record)) + topics = cursor.fetchall() cursor.execute('SELECT Name FROM Groups INNER JOIN SongGroups ON SongGroups.GroupId = Groups.GroupId ' - 'WHERE SongGroups.Record = ?', song.Record) + 'WHERE SongGroups.Record = ?', float(song.Record)) topics += cursor.fetchall() self.process_song(song, verses, verse_order, topics) diff --git a/openlp/plugins/songs/lib/importers/opensong.py b/openlp/plugins/songs/lib/importers/opensong.py index a1bfacbcb..161f8cd68 100644 --- a/openlp/plugins/songs/lib/importers/opensong.py +++ b/openlp/plugins/songs/lib/importers/opensong.py @@ -156,8 +156,8 @@ class OpenSongImport(SongImport): ustring = str(root.__getattr__(attr)) if isinstance(fn_or_string, str): if attr in ['ccli']: + ustring = ''.join(re.findall('\d+', ustring)) if ustring: - ustring = ''.join(re.findall('\d+', ustring)) setattr(self, fn_or_string, int(ustring)) else: setattr(self, fn_or_string, None) diff --git a/openlp/plugins/songs/lib/importers/opspro.py b/openlp/plugins/songs/lib/importers/opspro.py index 8f9674deb..03c5001c6 100644 --- a/openlp/plugins/songs/lib/importers/opspro.py +++ b/openlp/plugins/songs/lib/importers/opspro.py @@ -55,7 +55,7 @@ class OPSProImport(SongImport): """ password = self.extract_mdb_password() try: - conn = pyodbc.connect('DRIVER={Microsoft Access Driver (*.mdb)};DBQ={source};' + conn = pyodbc.connect('DRIVER={{Microsoft Access Driver (*.mdb)}};DBQ={source};' 'PWD={password}'.format(source=self.import_source, password=password)) except (pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError) as e: log.warning('Unable to connect the OPS Pro database {source}. {error}'.format(source=self.import_source, @@ -74,11 +74,11 @@ class OPSProImport(SongImport): break # Type means: 0=Original, 1=Projection, 2=Own cursor.execute('SELECT Lyrics, Type, IsDualLanguage FROM Lyrics WHERE SongID = ? AND Type < 2 ' - 'ORDER BY Type DESC', song.ID) + 'ORDER BY Type DESC', float(song.ID)) lyrics = cursor.fetchone() cursor.execute('SELECT CategoryName FROM Category INNER JOIN SongCategory ' 'ON Category.ID = SongCategory.CategoryID WHERE SongCategory.SongID = ? ' - 'ORDER BY CategoryName', song.ID) + 'ORDER BY CategoryName', float(song.ID)) topics = cursor.fetchall() try: self.process_song(song, lyrics, topics) diff --git a/openlp/plugins/songs/lib/importers/presentationmanager.py b/openlp/plugins/songs/lib/importers/presentationmanager.py index 06ab3f60d..34a94dec2 100644 --- a/openlp/plugins/songs/lib/importers/presentationmanager.py +++ b/openlp/plugins/songs/lib/importers/presentationmanager.py @@ -56,7 +56,13 @@ class PresentationManagerImport(SongImport): # Open file with detected encoding and remove encoding declaration text = open(file_path, mode='r', encoding=encoding).read() text = re.sub('.+\?>\n', '', text) - tree = etree.fromstring(text, parser=etree.XMLParser(recover=True)) + try: + tree = etree.fromstring(text, parser=etree.XMLParser(recover=True)) + except ValueError: + self.log_error(file_path, + translate('SongsPlugin.PresentationManagerImport', + 'File is not in XML-format, which is the only format supported.')) + continue root = objectify.fromstring(etree.tostring(tree)) self.process_song(root) diff --git a/openlp/plugins/songs/lib/importers/songpro.py b/openlp/plugins/songs/lib/importers/songpro.py index b1c8e7fe8..90fe34492 100644 --- a/openlp/plugins/songs/lib/importers/songpro.py +++ b/openlp/plugins/songs/lib/importers/songpro.py @@ -72,7 +72,7 @@ class SongProImport(SongImport): Receive a single file or a list of files to import. """ self.encoding = None - with open(self.import_source, 'rt') as songs_file: + with open(self.import_source, 'rt', errors='ignore') as songs_file: self.import_wizard.progress_bar.setMaximum(0) tag = '' text = '' diff --git a/openlp/plugins/songs/lib/importers/songshowplus.py b/openlp/plugins/songs/lib/importers/songshowplus.py index 02b35fd9e..4d9096c93 100644 --- a/openlp/plugins/songs/lib/importers/songshowplus.py +++ b/openlp/plugins/songs/lib/importers/songshowplus.py @@ -106,6 +106,7 @@ class SongShowPlusImport(SongImport): song_data = open(file, 'rb') while True: block_key, = struct.unpack("I", song_data.read(4)) + log.debug('block_key: %d' % block_key) # The file ends with 4 NULL's if block_key == 0: break @@ -117,7 +118,13 @@ class SongShowPlusImport(SongImport): null, verse_name_length, = struct.unpack("BB", song_data.read(2)) verse_name = self.decode(song_data.read(verse_name_length)) length_descriptor_size, = struct.unpack("B", song_data.read(1)) - log.debug(length_descriptor_size) + log.debug('length_descriptor_size: %d' % length_descriptor_size) + # In the case of song_numbers the number is in the data from the + # current position to the next block starts + if block_key == SONG_NUMBER: + sn_bytes = song_data.read(length_descriptor_size - 1) + self.song_number = int.from_bytes(sn_bytes, byteorder='little') + continue # Detect if/how long the length descriptor is if length_descriptor_size == 12 or length_descriptor_size == 20: length_descriptor, = struct.unpack("I", song_data.read(4)) @@ -127,8 +134,9 @@ class SongShowPlusImport(SongImport): length_descriptor = 0 else: length_descriptor, = struct.unpack("B", song_data.read(1)) - log.debug(length_descriptor_size) + log.debug('length_descriptor: %d' % length_descriptor) data = song_data.read(length_descriptor) + log.debug(data) if block_key == TITLE: self.title = self.decode(data) elif block_key == AUTHOR: @@ -168,8 +176,6 @@ class SongShowPlusImport(SongImport): self.ssp_verse_order_list.append(verse_tag) elif block_key == SONG_BOOK: self.song_book_name = self.decode(data) - elif block_key == SONG_NUMBER: - self.song_number = ord(data) elif block_key == CUSTOM_VERSE: verse_tag = self.to_openlp_verse_tag(verse_name) self.add_verse(self.decode(data), verse_tag) diff --git a/openlp/plugins/songs/lib/importers/videopsalm.py b/openlp/plugins/songs/lib/importers/videopsalm.py index 60c8e27e4..25fd4d8eb 100644 --- a/openlp/plugins/songs/lib/importers/videopsalm.py +++ b/openlp/plugins/songs/lib/importers/videopsalm.py @@ -117,6 +117,4 @@ class VideoPsalmImport(SongImport): if not self.finish(): self.log_error('Could not import {title}'.format(title=self.title)) except Exception as e: - self.log_error(translate('SongsPlugin.VideoPsalmImport', 'File {name}').format(name=file.name), - translate('SongsPlugin.VideoPsalmImport', 'Error: {error}').format(error=e)) - song_file.close() + self.log_error(song_file.name, translate('SongsPlugin.VideoPsalmImport', 'Error: {error}').format(error=e)) diff --git a/openlp/plugins/songs/lib/importers/worshipcenterpro.py b/openlp/plugins/songs/lib/importers/worshipcenterpro.py index df04823e8..3d5cbe9ba 100644 --- a/openlp/plugins/songs/lib/importers/worshipcenterpro.py +++ b/openlp/plugins/songs/lib/importers/worshipcenterpro.py @@ -49,7 +49,7 @@ class WorshipCenterProImport(SongImport): Receive a single file to import. """ try: - conn = pyodbc.connect('DRIVER={Microsoft Access Driver (*.mdb)};' + conn = pyodbc.connect('DRIVER={{Microsoft Access Driver (*.mdb)}};' 'DBQ={source}'.format(source=self.import_source)) except (pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError) as e: log.warning('Unable to connect the WorshipCenter Pro ' diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index f52abb86f..b89858019 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -603,7 +603,7 @@ class SongMediaItem(MediaManagerItem): else: verse_index = VerseType.from_tag(verse[0]['type']) verse_tag = VerseType.translated_tags[verse_index] - verse_def = '{tag}{label}'.format(tzg=verse_tag, text=verse[0]['label']) + verse_def = '{tag}{text}'.format(tag=verse_tag, text=verse[0]['label']) service_item.add_from_text(verse[1], verse_def) service_item.title = song.title author_list = self.generate_footer(service_item, song) diff --git a/tests/functional/openlp_core_lib/test_htmlbuilder.py b/tests/functional/openlp_core_lib/test_htmlbuilder.py index 48c60b55f..31885c2e2 100644 --- a/tests/functional/openlp_core_lib/test_htmlbuilder.py +++ b/tests/functional/openlp_core_lib/test_htmlbuilder.py @@ -14,177 +14,190 @@ from tests.functional import MagicMock, patch from tests.helpers.testmixin import TestMixin HTML = """ - - - -OpenLP Display - - - - - - -plugin HTML -
- -
- - -""" + function show_text_completed(){ + return (timer == null); + } + + + + + + plugin HTML +
+ +
+ + + """ BACKGROUND_CSS_RADIAL = 'background: -webkit-gradient(radial, 5 50%, 100, 5 50%, 5, from(#000000), to(#FFFFFF)) fixed' LYRICS_CSS = """ -.lyricstable { - z-index: 5; - position: absolute; - display: table; - left: 10px; top: 20px; -} -.lyricscell { - display: table-cell; - word-wrap: break-word; - -webkit-transition: opacity 0.4s ease; - lyrics_format_css -} -.lyricsmain { - text-shadow: #000000 5px 5px; -} -""" + .lyricstable { + z-index: 5; + position: absolute; + display: table; + left: 10px; top: 20px; + } + .lyricscell { + display: table-cell; + word-wrap: break-word; + -webkit-transition: opacity 0.4s ease; + lyrics_format_css + } + .lyricsmain { + text-shadow: #000000 5px 5px; + } + """ LYRICS_OUTLINE_CSS = ' -webkit-text-stroke: 0.125em #000000; -webkit-text-fill-color: #FFFFFF; ' -LYRICS_FORMAT_CSS = ' word-wrap: break-word; text-align: justify; vertical-align: bottom; ' + \ - 'font-family: Arial; font-size: 40pt; color: #FFFFFF; line-height: 108%; margin: 0;padding: 0; ' + \ - 'padding-bottom: 0.5em; padding-left: 2px; width: 1580px; height: 810px; font-style:italic; font-weight:bold; ' +LYRICS_FORMAT_CSS = """ + word-wrap: break-word; + text-align: justify; + vertical-align: bottom; + font-family: Arial; + font-size: 40pt; + color: #FFFFFF; + line-height: 108%; + margin: 0; + padding: 0; + padding-bottom: 0.5em; + padding-left: 2px; + width: 1580px; + height: 810px; + font-style: italic; + font-weight: bold; + """ FOOTER_CSS_BASE = """ left: 10px; bottom: 0px; diff --git a/tests/functional/openlp_core_ui_lib/test_listpreviewwidget.py b/tests/functional/openlp_core_ui_lib/test_listpreviewwidget.py index 45c68219b..b0279cece 100644 --- a/tests/functional/openlp_core_ui_lib/test_listpreviewwidget.py +++ b/tests/functional/openlp_core_ui_lib/test_listpreviewwidget.py @@ -225,6 +225,44 @@ class TestListPreviewWidget(TestCase): calls = [call(0, 100), call(1, 100), call(0, 100), call(1, 100)] mocked_setRowHeight.assert_has_calls(calls) + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') + def test_replace_recalculate_layout_img_auto(self, mocked_setRowHeight, mocked_resizeRowsToContents): + """ + Test if "Max height for non-text slides..." auto, img slides resized in replace_service_item & __recalc... + """ + # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", + # an image ServiceItem and a ListPreviewWidget. + + # Mock Settings().value('advanced/slide max height') + self.mocked_Settings_obj.value.return_value = -4 + # Mock self.viewport().width() + self.mocked_viewport_obj.width.return_value = 200 + self.mocked_viewport_obj.height.return_value = 600 + # Mock image service item + service_item = MagicMock() + service_item.is_text.return_value = False + service_item.is_capable.return_value = False + service_item.get_frames.return_value = [{'title': None, 'path': None, 'image': None}, + {'title': None, 'path': None, 'image': None}] + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + list_preview_widget.replace_service_item(service_item, 200, 0) + # Change viewport width before forcing a resize + self.mocked_viewport_obj.width.return_value = 400 + + # WHEN: __recalculate_layout() is called (via screen_size_changed) + list_preview_widget.screen_size_changed(1) + self.mocked_viewport_obj.height.return_value = 200 + list_preview_widget.screen_size_changed(1) + + # THEN: resizeRowsToContents() should not be called, while setRowHeight() should be called + # twice for each slide. + self.assertEquals(mocked_resizeRowsToContents.call_count, 0, 'Should not be called') + self.assertEquals(mocked_setRowHeight.call_count, 6, 'Should be called 3 times for each slide') + calls = [call(0, 100), call(1, 100), call(0, 150), call(1, 150), call(0, 100), call(1, 100)] + mocked_setRowHeight.assert_has_calls(calls) + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.cellWidget') @@ -331,6 +369,41 @@ class TestListPreviewWidget(TestCase): # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should be called mocked_cellWidget_child.setMaximumWidth.assert_called_once_with(150) + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.cellWidget') + def test_row_resized_setting_changed(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents): + """ + Test if "Max height for non-text slides..." enabled while item live, program doesn't crash on row_resized. + """ + # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", + # an image ServiceItem and a ListPreviewWidget. + + # Mock Settings().value('advanced/slide max height') + self.mocked_Settings_obj.value.return_value = 0 + # Mock self.viewport().width() + self.mocked_viewport_obj.width.return_value = 200 + # Mock image service item + service_item = MagicMock() + service_item.is_text.return_value = False + service_item.is_capable.return_value = False + service_item.get_frames.return_value = [{'title': None, 'path': None, 'image': None}, + {'title': None, 'path': None, 'image': None}] + # Mock self.cellWidget().children() + mocked_cellWidget_obj = MagicMock() + mocked_cellWidget_obj.children.return_value = None + mocked_cellWidget.return_value = mocked_cellWidget_obj + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + list_preview_widget.replace_service_item(service_item, 200, 0) + self.mocked_Settings_obj.value.return_value = 100 + + # WHEN: row_resized() is called + list_preview_widget.row_resized(0, 100, 150) + + # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should fail + self.assertRaises(Exception) + @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.selectRow') @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.scrollToItem') @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.item') diff --git a/tests/functional/openlp_plugins/songs/test_opensongimport.py b/tests/functional/openlp_plugins/songs/test_opensongimport.py index 7d51527b8..d1386005f 100644 --- a/tests/functional/openlp_plugins/songs/test_opensongimport.py +++ b/tests/functional/openlp_plugins/songs/test_opensongimport.py @@ -22,13 +22,13 @@ """ This module contains tests for the OpenSong song importer. """ - import os from unittest import TestCase -from tests.helpers.songfileimport import SongImportTestHelper from openlp.plugins.songs.lib.importers.opensong import OpenSongImport from openlp.core.common import Registry + +from tests.helpers.songfileimport import SongImportTestHelper from tests.functional import patch, MagicMock TEST_PATH = os.path.abspath( @@ -54,6 +54,8 @@ class TestOpenSongFileImport(SongImportTestHelper): self.load_external_result_data(os.path.join(TEST_PATH, 'One, Two, Three, Four, Five.json'))) self.file_import([os.path.join(TEST_PATH, 'Amazing Grace2')], self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json'))) + self.file_import([os.path.join(TEST_PATH, 'Amazing Grace with bad CCLI')], + self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace without CCLI.json'))) class TestOpenSongImport(TestCase): diff --git a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py index 2dc28d541..e611089cc 100644 --- a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py +++ b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py @@ -52,6 +52,8 @@ class TestSongShowPlusFileImport(SongImportTestHelper): self.load_external_result_data(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.json'))) self.file_import([os.path.join(TEST_PATH, 'a mighty fortress is our god.sbsong')], self.load_external_result_data(os.path.join(TEST_PATH, 'a mighty fortress is our god.json'))) + self.file_import([os.path.join(TEST_PATH, 'cleanse-me.sbsong')], + self.load_external_result_data(os.path.join(TEST_PATH, 'cleanse-me.json'))) class TestSongShowPlusImport(TestCase): diff --git a/tests/helpers/songfileimport.py b/tests/helpers/songfileimport.py index bc4ebc9b5..9a9ab7d2b 100644 --- a/tests/helpers/songfileimport.py +++ b/tests/helpers/songfileimport.py @@ -29,6 +29,7 @@ from unittest import TestCase from openlp.plugins.songs.lib.importers.opensong import OpenSongImport from openlp.core.common import Registry + from tests.functional import patch, MagicMock, call log = logging.getLogger(__name__) @@ -36,7 +37,7 @@ log = logging.getLogger(__name__) class SongImportTestHelper(TestCase): """ - This class is designed to be a helper class to reduce repition when testing the import of song files. + This class is designed to be a helper class to reduce repetition when testing the import of song files. """ def __init__(self, *args, **kwargs): super(SongImportTestHelper, self).__init__(*args, **kwargs) diff --git a/tests/resources/opensongsongs/Amazing Grace b/tests/resources/opensongsongs/Amazing Grace index 97062dc21..6b2c172b5 100644 --- a/tests/resources/opensongsongs/Amazing Grace +++ b/tests/resources/opensongsongs/Amazing Grace @@ -53,4 +53,4 @@ - \ No newline at end of file + diff --git a/tests/resources/opensongsongs/Amazing Grace with bad CCLI b/tests/resources/opensongsongs/Amazing Grace with bad CCLI new file mode 100644 index 000000000..292d4a825 --- /dev/null +++ b/tests/resources/opensongsongs/Amazing Grace with bad CCLI @@ -0,0 +1,56 @@ + + + Amazing Grace (Demonstration) + John Newton, Edwin Excell & John P. Rees + Public Domain + V1 V2 V3 V4 V5 + + + GE + God: Assurance/Grace/Salvation + Worship: Praise + + + + [V] +;Test the chords format +;Chords beging with . +;Verses begin with their verse number +;Link words with _ +;Comments begin with ; +. D D7 G D +1A______ma________zing grace! How sweet the sound! +2'Twas grace that taught my heart to fear, +3The Lord has pro____mised good to me, +4Thro' ma________ny dan____gers, toils and snares +5When we've been there ten thou__sand years, + +. Bm E A A7 +1That saved a wretch like me! +2And grace my fears re___lieved. +3His Word my hope se___cures. +4I have al___rea____dy come. +5Bright shi___ning as the sun, + +. D D7 G D +1I once was lost, but now am found; +2How pre___cious did that grace ap____pear, +3He will my shield and por___tion be +4'Tis grace that brought me safe thus far, +5We've no less days to sing God's praise, + +. Bm A G D +1Was blind, but now I see. +2The hour I first be_lieved. +3As long as life en_dures. +4And grace will lead me home. +5Than when we first be_gun. + + + Demonstration Songs 0 + + + + + + diff --git a/tests/resources/opensongsongs/Amazing Grace without CCLI.json b/tests/resources/opensongsongs/Amazing Grace without CCLI.json new file mode 100644 index 000000000..799fd33d9 --- /dev/null +++ b/tests/resources/opensongsongs/Amazing Grace without CCLI.json @@ -0,0 +1,42 @@ +{ + "authors": [ + "John Newton", + "Edwin Excell", + "John P. Rees" + ], + "ccli_number": null, + "comments": "\n\n\n", + "copyright": "Public Domain ", + "song_book_name": "Demonstration Songs", + "song_number": 0, + "title": "Amazing Grace (Demonstration)", + "topics": [ + "Assurance", + "Grace", + "Praise", + "Salvation" + ], + "verse_order_list": [], + "verses": [ + [ + "Amazing grace! How sweet the sound!\nThat saved a wretch like me!\nI once was lost, but now am found;\nWas blind, but now I see.", + "v1" + ], + [ + "'Twas grace that taught my heart to fear,\nAnd grace my fears relieved.\nHow precious did that grace appear,\nThe hour I first believed.", + "v2" + ], + [ + "The Lord has promised good to me,\nHis Word my hope secures.\nHe will my shield and portion be\nAs long as life endures.", + "v3" + ], + [ + "Thro' many dangers, toils and snares\nI have already come.\n'Tis grace that brought me safe thus far,\nAnd grace will lead me home.", + "v4" + ], + [ + "When we've been there ten thousand years,\nBright shining as the sun,\nWe've no less days to sing God's praise,\nThan when we first begun.", + "v5" + ] + ] +} diff --git a/tests/resources/songshowplussongs/cleanse-me.json b/tests/resources/songshowplussongs/cleanse-me.json new file mode 100644 index 000000000..c88b434f9 --- /dev/null +++ b/tests/resources/songshowplussongs/cleanse-me.json @@ -0,0 +1,38 @@ +{ + "authors": [ + "J. Edwin Orr" + ], + "ccli_number": 56307, + "comments": "", + "copyright": "Public Domain ", + "song_book_name": "", + "song_number": 438, + "title": "Cleanse Me [438]", + "topics": [ + "Cleansing", + "Communion", + "Consecration", + "Holiness", + "Holy Spirit", + "Revival" + ], + "verse_order_list": [], + "verses": [ + [ + "Search me, O God,\r\nAnd know my heart today;\r\nTry me, O Savior,\r\nKnow my thoughts, I pray.\r\nSee if there be\r\nSome wicked way in me;\r\nCleanse me from every sin\r\nAnd set me free.", + "v1" + ], + [ + "I praise Thee, Lord,\r\nFor cleansing me from sin;\r\nFulfill Thy Word,\r\nAnd make me pure within.\r\nFill me with fire\r\nWhere once I burned with shame;\r\nGrant my desire\r\nTo magnify Thy name.", + "v2" + ], + [ + "Lord, take my life,\r\nAnd make it wholly Thine;\r\nFill my poor heart\r\nWith Thy great love divine.\r\nTake all my will,\r\nMy passion, self and pride;\r\nI now surrender, Lord\r\nIn me abide.", + "v3" + ], + [ + "O Holy Ghost,\r\nRevival comes from Thee;\r\nSend a revival,\r\nStart the work in me.\r\nThy Word declares\r\nThou wilt supply our need;\r\nFor blessings now,\r\nO Lord, I humbly plead.", + "v4" + ] + ] +} diff --git a/tests/resources/songshowplussongs/cleanse-me.sbsong b/tests/resources/songshowplussongs/cleanse-me.sbsong new file mode 100644 index 000000000..aa9915f8e Binary files /dev/null and b/tests/resources/songshowplussongs/cleanse-me.sbsong differ