Add option to add a split to a song

This will split the verse when added to the service but keep the verse together for editing.
Useful for the 10 line hymn verses which need 2 slides to display.

Fix some iffy spelling

lp:~trb143/openlp/splitter (revision 2738)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/2182/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/2085/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1972/
[SUCCESS] https://ci.openlp.io/job/Branch...

bzr-revno: 2777
This commit is contained in:
Tim Bentley 2017-10-03 17:19:06 +01:00
commit df39497cc1
10 changed files with 121 additions and 59 deletions

View File

@ -147,7 +147,7 @@ class UiStrings(object):
self.SaveService = translate('OpenLP.Ui', 'Save Service') self.SaveService = translate('OpenLP.Ui', 'Save Service')
self.Service = translate('OpenLP.Ui', 'Service') self.Service = translate('OpenLP.Ui', 'Service')
self.ShortResults = translate('OpenLP.Ui', 'Please type more text to use \'Search As You Type\'') self.ShortResults = translate('OpenLP.Ui', 'Please type more text to use \'Search As You Type\'')
self.Split = translate('OpenLP.Ui', 'Optional &Split') self.Split = translate('OpenLP.Ui', 'Overflow &Split')
self.SplitToolTip = translate('OpenLP.Ui', self.SplitToolTip = translate('OpenLP.Ui',
'Split a slide into two only if it does not fit on the screen as one slide.') 'Split a slide into two only if it does not fit on the screen as one slide.')
self.StartingImport = translate('OpenLP.Ui', 'Starting import...') self.StartingImport = translate('OpenLP.Ui', 'Starting import...')

View File

@ -243,6 +243,9 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
elif item.is_capable(ItemCapabilities.CanSoftBreak): elif item.is_capable(ItemCapabilities.CanSoftBreak):
pages = [] pages = []
if '[---]' in text: if '[---]' in text:
# Remove Overflow split if at start of the text
if text.startswith('[---]'):
text = text[5:]
# Remove two or more option slide breaks next to each other (causing infinite loop). # Remove two or more option slide breaks next to each other (causing infinite loop).
while '\n[---]\n[---]\n' in text: while '\n[---]\n[---]\n' in text:
text = text.replace('\n[---]\n[---]\n', '\n[---]\n') text = text.replace('\n[---]\n[---]\n', '\n[---]\n')

View File

@ -209,21 +209,21 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties):
Switches to the given row. Switches to the given row.
""" """
# Retrieve setting # Retrieve setting
autoscrolling = Settings().value('advanced/autoscrolling') auto_scrolling = Settings().value('advanced/autoscrolling')
# Check if auto-scroll disabled (None) and validate value as dict containing 'dist' and 'pos' # Check if auto-scroll disabled (None) and validate value as dict containing 'dist' and 'pos'
# 'dist' represents the slide to scroll to relative to the new slide (-1 = previous, 0 = current, 1 = next) # 'dist' represents the slide to scroll to relative to the new slide (-1 = previous, 0 = current, 1 = next)
# 'pos' represents the vert position of of the slide (0 = in view, 1 = top, 2 = middle, 3 = bottom) # 'pos' represents the vert position of of the slide (0 = in view, 1 = top, 2 = middle, 3 = bottom)
if not (isinstance(autoscrolling, dict) and 'dist' in autoscrolling and 'pos' in autoscrolling and if not (isinstance(auto_scrolling, dict) and 'dist' in auto_scrolling and 'pos' in auto_scrolling and
isinstance(autoscrolling['dist'], int) and isinstance(autoscrolling['pos'], int)): isinstance(auto_scrolling['dist'], int) and isinstance(auto_scrolling['pos'], int)):
return return
# prevent scrolling past list bounds # prevent scrolling past list bounds
scroll_to_slide = slide + autoscrolling['dist'] scroll_to_slide = slide + auto_scrolling['dist']
if scroll_to_slide < 0: if scroll_to_slide < 0:
scroll_to_slide = 0 scroll_to_slide = 0
if scroll_to_slide >= self.slide_count(): if scroll_to_slide >= self.slide_count():
scroll_to_slide = self.slide_count() - 1 scroll_to_slide = self.slide_count() - 1
# Scroll to item if possible. # Scroll to item if possible.
self.scrollToItem(self.item(scroll_to_slide, 0), autoscrolling['pos']) self.scrollToItem(self.item(scroll_to_slide, 0), auto_scrolling['pos'])
self.selectRow(slide) self.selectRow(slide)
def current_slide_number(self): def current_slide_number(self):

View File

@ -25,14 +25,13 @@ The duplicate song removal logic for OpenLP.
import logging import logging
import multiprocessing import multiprocessing
import os
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.common import Registry, RegistryProperties, translate from openlp.core.common import Registry, RegistryProperties, translate
from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings
from openlp.plugins.songs.lib import delete_song from openlp.plugins.songs.lib import delete_song
from openlp.plugins.songs.lib.db import Song, MediaFile from openlp.plugins.songs.lib.db import Song
from openlp.plugins.songs.forms.songreviewwidget import SongReviewWidget from openlp.plugins.songs.forms.songreviewwidget import SongReviewWidget
from openlp.plugins.songs.lib.songcompare import songs_probably_equal from openlp.plugins.songs.lib.songcompare import songs_probably_equal

View File

@ -42,10 +42,14 @@ class Ui_EditVerseDialog(object):
self.dialog_layout.addWidget(self.verse_text_edit) self.dialog_layout.addWidget(self.verse_text_edit)
self.verse_type_layout = QtWidgets.QHBoxLayout() self.verse_type_layout = QtWidgets.QHBoxLayout()
self.verse_type_layout.setObjectName('verse_type_layout') self.verse_type_layout.setObjectName('verse_type_layout')
self.split_button = QtWidgets.QPushButton(edit_verse_dialog) self.forced_split_button = QtWidgets.QPushButton(edit_verse_dialog)
self.split_button.setIcon(build_icon(':/general/general_add.png')) self.forced_split_button.setIcon(build_icon(':/general/general_add.png'))
self.split_button.setObjectName('split_button') self.forced_split_button.setObjectName('forced_split_button')
self.verse_type_layout.addWidget(self.split_button) self.verse_type_layout.addWidget(self.forced_split_button)
self.overflow_split_button = QtWidgets.QPushButton(edit_verse_dialog)
self.overflow_split_button.setIcon(build_icon(':/general/general_add.png'))
self.overflow_split_button.setObjectName('overflow_split_button')
self.verse_type_layout.addWidget(self.overflow_split_button)
self.verse_type_label = QtWidgets.QLabel(edit_verse_dialog) self.verse_type_label = QtWidgets.QLabel(edit_verse_dialog)
self.verse_type_label.setObjectName('verse_type_label') self.verse_type_label.setObjectName('verse_type_label')
self.verse_type_layout.addWidget(self.verse_type_label) self.verse_type_layout.addWidget(self.verse_type_label)
@ -93,8 +97,11 @@ class Ui_EditVerseDialog(object):
self.verse_type_combo_box.setItemText(VerseType.Intro, VerseType.translated_names[VerseType.Intro]) self.verse_type_combo_box.setItemText(VerseType.Intro, VerseType.translated_names[VerseType.Intro])
self.verse_type_combo_box.setItemText(VerseType.Ending, VerseType.translated_names[VerseType.Ending]) self.verse_type_combo_box.setItemText(VerseType.Ending, VerseType.translated_names[VerseType.Ending])
self.verse_type_combo_box.setItemText(VerseType.Other, VerseType.translated_names[VerseType.Other]) self.verse_type_combo_box.setItemText(VerseType.Other, VerseType.translated_names[VerseType.Other])
self.split_button.setText(UiStrings().Split) self.overflow_split_button.setText(UiStrings().Split)
self.split_button.setToolTip(UiStrings().SplitToolTip) self.overflow_split_button.setToolTip(UiStrings().SplitToolTip)
self.forced_split_button.setText(translate('SongsPlugin.EditVerseForm', '&Forced Split'))
self.forced_split_button.setToolTip(translate('SongsPlugin.EditVerseForm', 'Split the verse when displayed '
'regardless of the screen size.'))
self.insert_button.setText(translate('SongsPlugin.EditVerseForm', '&Insert')) self.insert_button.setText(translate('SongsPlugin.EditVerseForm', '&Insert'))
self.insert_button.setToolTip(translate('SongsPlugin.EditVerseForm', self.insert_button.setToolTip(translate('SongsPlugin.EditVerseForm',
'Split a slide into two by inserting a verse splitter.')) 'Split a slide into two by inserting a verse splitter.'))

View File

@ -48,12 +48,13 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
self.setupUi(self) self.setupUi(self)
self.has_single_verse = False self.has_single_verse = False
self.insert_button.clicked.connect(self.on_insert_button_clicked) self.insert_button.clicked.connect(self.on_insert_button_clicked)
self.split_button.clicked.connect(self.on_split_button_clicked) self.overflow_split_button.clicked.connect(self.on_overflow_split_button_clicked)
self.verse_text_edit.cursorPositionChanged.connect(self.on_cursor_position_changed) self.verse_text_edit.cursorPositionChanged.connect(self.on_cursor_position_changed)
self.verse_type_combo_box.currentIndexChanged.connect(self.on_verse_type_combo_box_changed) self.verse_type_combo_box.currentIndexChanged.connect(self.on_verse_type_combo_box_changed)
self.forced_split_button.clicked.connect(self.on_forced_split_button_clicked)
if Settings().value('songs/enable chords'): if Settings().value('songs/enable chords'):
self.transpose_down_button.clicked.connect(self.on_transepose_down_button_clicked) self.transpose_down_button.clicked.connect(self.on_transpose_down_button_clicked)
self.transpose_up_button.clicked.connect(self.on_transepose_up_button_clicked) self.transpose_up_button.clicked.connect(self.on_transpose_up_button_clicked)
def insert_verse(self, verse_tag, verse_num=1): def insert_verse(self, verse_tag, verse_num=1):
""" """
@ -68,13 +69,27 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
self.verse_text_edit.insertPlainText('---[{tag}:{number}]---\n'.format(tag=verse_tag, number=verse_num)) self.verse_text_edit.insertPlainText('---[{tag}:{number}]---\n'.format(tag=verse_tag, number=verse_num))
self.verse_text_edit.setFocus() self.verse_text_edit.setFocus()
def on_split_button_clicked(self): def on_overflow_split_button_clicked(self):
""" """
The split button has been pressed The optional split button has been pressed so we need add the split
"""
self._add_splitter_to_text('[---]')
def on_forced_split_button_clicked(self):
"""
The force split button has been pressed so we need add the split
"""
self._add_splitter_to_text('[--}{--]')
def _add_splitter_to_text(self, insert_string):
"""
Add a custom splitter to the song text
:param insert_string: The string to insert
:return:
""" """
text = self.verse_text_edit.toPlainText() text = self.verse_text_edit.toPlainText()
position = self.verse_text_edit.textCursor().position() position = self.verse_text_edit.textCursor().position()
insert_string = '[---]'
if position and text[position - 1] != '\n': if position and text[position - 1] != '\n':
insert_string = '\n' + insert_string insert_string = '\n' + insert_string
if position == len(text) or text[position] != '\n': if position == len(text) or text[position] != '\n':
@ -101,7 +116,7 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
""" """
self.update_suggested_verse_number() self.update_suggested_verse_number()
def on_transepose_up_button_clicked(self): def on_transpose_up_button_clicked(self):
""" """
The transpose up button clicked The transpose up button clicked
""" """
@ -118,7 +133,7 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
self.verse_text_edit.setFocus() self.verse_text_edit.setFocus()
self.verse_text_edit.moveCursor(QtGui.QTextCursor.End) self.verse_text_edit.moveCursor(QtGui.QTextCursor.End)
def on_transepose_down_button_clicked(self): def on_transpose_down_button_clicked(self):
""" """
The transpose down button clicked The transpose down button clicked
""" """

View File

@ -546,12 +546,12 @@ def delete_song(song_id, song_plugin):
song_plugin.manager.delete_object(Song, song_id) song_plugin.manager.delete_object(Song, song_id)
def transpose_lyrics(lyrics, transepose_value): def transpose_lyrics(lyrics, transpose_value):
""" """
Transepose lyrics Transpose lyrics
:param lyrcs: The lyrics to be transposed :param lyrics: The lyrics to be transposed
:param transepose_value: The value to transpose the lyrics with :param transpose_value: The value to transpose the lyrics with
:return: The transposed lyrics :return: The transposed lyrics
""" """
# Split text by verse delimiter - both normal and optional # Split text by verse delimiter - both normal and optional
@ -562,16 +562,17 @@ def transpose_lyrics(lyrics, transepose_value):
if verse.startswith('---[') or verse == '[---]': if verse.startswith('---[') or verse == '[---]':
transposed_lyrics += verse transposed_lyrics += verse
else: else:
transposed_lyrics += transpose_verse(verse, transepose_value, notation) transposed_lyrics += transpose_verse(verse, transpose_value, notation)
return transposed_lyrics return transposed_lyrics
def transpose_verse(verse_text, transepose_value, notation): def transpose_verse(verse_text, transpose_value, notation):
""" """
Transepose lyrics Transpose Verse
:param lyrcs: The lyrics to be transposed :param verse_text: The lyrics to be transposed
:param transepose_value: The value to transpose the lyrics with :param transpose_value: The value to transpose the lyrics with
:param notation: which notation to use
:return: The transposed lyrics :return: The transposed lyrics
""" """
if '[' not in verse_text: if '[' not in verse_text:
@ -589,11 +590,11 @@ def transpose_verse(verse_text, transepose_value, notation):
if word == ']': if word == ']':
in_tag = False in_tag = False
transposed_lyrics += word transposed_lyrics += word
elif word == '/': elif word == '/' or word == '--}{--':
transposed_lyrics += word transposed_lyrics += word
else: else:
# This MUST be a chord # This MUST be a chord
transposed_lyrics += transpose_chord(word, transepose_value, notation) transposed_lyrics += transpose_chord(word, transpose_value, notation)
# If still inside a chord tag something is wrong! # If still inside a chord tag something is wrong!
if in_tag: if in_tag:
return verse_text return verse_text
@ -629,36 +630,36 @@ def transpose_chord(chord, transpose_value, notation):
for i in range(0, len(chord_split)): for i in range(0, len(chord_split)):
if i > 0: if i > 0:
transposed_chord += '/' transposed_chord += '/'
currentchord = chord_split[i] current_chord = chord_split[i]
if currentchord and currentchord[0] == '(': if current_chord and current_chord[0] == '(':
transposed_chord += '(' transposed_chord += '('
if len(currentchord) > 1: if len(current_chord) > 1:
currentchord = currentchord[1:] current_chord = current_chord[1:]
else: else:
currentchord = '' current_chord = ''
if len(currentchord) > 0: if len(current_chord) > 0:
if len(currentchord) > 1: if len(current_chord) > 1:
if '#b'.find(currentchord[1]) == -1: if '#b'.find(current_chord[1]) == -1:
note = currentchord[0:1] note = current_chord[0:1]
rest = currentchord[1:] rest = current_chord[1:]
else: else:
note = currentchord[0:2] note = current_chord[0:2]
rest = currentchord[2:] rest = current_chord[2:]
else: else:
note = currentchord note = current_chord
rest = '' rest = ''
notenumber = notes_flat.index(note) if note not in notes_sharp else notes_sharp.index(note) note_number = notes_flat.index(note) if note not in notes_sharp else notes_sharp.index(note)
notenumber += transpose_value note_number += transpose_value
while notenumber > 11: while note_number > 11:
notenumber -= 12 note_number -= 12
while notenumber < 0: while note_number < 0:
notenumber += 12 note_number += 12
if i == 0: if i == 0:
current_chord = notes_sharp[notenumber] if notes_preferred[notenumber] == '#' else notes_flat[ current_chord = notes_sharp[note_number] if notes_preferred[note_number] == '#' else notes_flat[
notenumber] note_number]
last_chord = current_chord last_chord = current_chord
else: else:
current_chord = notes_flat[notenumber] if last_chord not in notes_sharp else notes_sharp[notenumber] current_chord = notes_flat[note_number] if last_chord not in notes_sharp else notes_sharp[note_number]
if not (note not in notes_flat and note not in notes_sharp): if not (note not in notes_flat and note not in notes_sharp):
transposed_chord += current_chord + rest transposed_chord += current_chord + rest
else: else:

View File

@ -577,7 +577,7 @@ class SongMediaItem(MediaManagerItem):
if not song.verse_order.strip(): if not song.verse_order.strip():
for verse in verse_list: for verse in verse_list:
# We cannot use from_loose_input() here, because database is supposed to contain English lowercase # We cannot use from_loose_input() here, because database is supposed to contain English lowercase
# singlechar tags. # single char tags.
verse_tag = verse[0]['type'] verse_tag = verse[0]['type']
verse_index = None verse_index = None
if len(verse_tag) > 1: if len(verse_tag) > 1:
@ -588,7 +588,9 @@ class SongMediaItem(MediaManagerItem):
verse_index = VerseType.from_tag(verse_tag) verse_index = VerseType.from_tag(verse_tag)
verse_tag = VerseType.translated_tags[verse_index].upper() verse_tag = VerseType.translated_tags[verse_index].upper()
verse_def = '{tag}{label}'.format(tag=verse_tag, label=verse[0]['label']) verse_def = '{tag}{label}'.format(tag=verse_tag, label=verse[0]['label'])
service_item.add_from_text(str(verse[1]), verse_def) force_verse = verse[1].split('[--}{--]\n')
for split_verse in force_verse:
service_item.add_from_text(split_verse, verse_def)
else: else:
# Loop through the verse list and expand the song accordingly. # Loop through the verse list and expand the song accordingly.
for order in song.verse_order.lower().split(): for order in song.verse_order.lower().split():
@ -603,7 +605,9 @@ class SongMediaItem(MediaManagerItem):
verse_index = VerseType.from_tag(verse[0]['type']) verse_index = VerseType.from_tag(verse[0]['type'])
verse_tag = VerseType.translated_tags[verse_index] verse_tag = VerseType.translated_tags[verse_index]
verse_def = '{tag}{label}'.format(tag=verse_tag, label=verse[0]['label']) verse_def = '{tag}{label}'.format(tag=verse_tag, label=verse[0]['label'])
service_item.add_from_text(verse[1], verse_def) force_verse = verse[1].split('[--}{--]\n')
for split_verse in force_verse:
service_item.add_from_text(split_verse, verse_def)
service_item.title = song.title service_item.title = song.title
author_list = self.generate_footer(service_item, song) author_list = self.generate_footer(service_item, song)
service_item.data_string = {'title': song.search_title, 'authors': ', '.join(author_list)} service_item.data_string = {'title': song.search_title, 'authors': ', '.join(author_list)}

View File

@ -71,6 +71,7 @@ log = logging.getLogger(__name__)
NAMESPACE = 'http://openlyrics.info/namespace/2009/song' NAMESPACE = 'http://openlyrics.info/namespace/2009/song'
NSMAP = '{{' + NAMESPACE + '}}{tag}' NSMAP = '{{' + NAMESPACE + '}}{tag}'
NEWPAGETAG = '<p style="page-break-after: always;"/>'
class SongXML(object): class SongXML(object):
@ -281,7 +282,7 @@ class OpenLyrics(object):
tags_element = None tags_element = None
match = re.search('\{/?\w+\}', song.lyrics, re.UNICODE) match = re.search('\{/?\w+\}', song.lyrics, re.UNICODE)
if match: if match:
# Named 'format_' - 'format' is built-in fuction in Python. # Named 'format_' - 'format' is built-in function in Python.
format_ = etree.SubElement(song_xml, 'format') format_ = etree.SubElement(song_xml, 'format')
tags_element = etree.SubElement(format_, 'tags') tags_element = etree.SubElement(format_, 'tags')
tags_element.set('application', 'OpenLP') tags_element.set('application', 'OpenLP')
@ -472,6 +473,7 @@ class OpenLyrics(object):
text = text.replace('{{/{tag}}}'.format(tag=tag), '</tag>') text = text.replace('{{/{tag}}}'.format(tag=tag), '</tag>')
# Replace \n with <br/>. # Replace \n with <br/>.
text = text.replace('\n', '<br/>') text = text.replace('\n', '<br/>')
text = text.replace('[--}{--]', NEWPAGETAG)
element = etree.XML('<lines>{text}</lines>'.format(text=text)) element = etree.XML('<lines>{text}</lines>'.format(text=text))
verse_element.append(element) verse_element.append(element)
return element return element
@ -634,6 +636,9 @@ class OpenLyrics(object):
if element.tail: if element.tail:
text += element.tail text += element.tail
return text return text
elif newlines and element.tag == NSMAP.format(tag='p') and 'page-break-after' in str(element.attrib):
text += '[--}{--]'
return text
# Start formatting tag. # Start formatting tag.
if element.tag == NSMAP.format(tag='tag'): if element.tag == NSMAP.format(tag='tag'):
text += '{{{name}}}'.format(name=element.get('name')) text += '{{{name}}}'.format(name=element.get('name'))

View File

@ -72,3 +72,31 @@ class TestEditVerseForm(TestCase, TestMixin):
# THEN the verse number must not be changed # THEN the verse number must not be changed
self.assertEqual(3, self.edit_verse_form.verse_number_box.value(), 'The verse number should be 3') self.assertEqual(3, self.edit_verse_form.verse_number_box.value(), 'The verse number should be 3')
def test_on_divide_split_button_clicked(self):
"""
Test that divide adds text at the correct position
"""
# GIVEN some input values
self.edit_verse_form.verse_type_combo_box.currentIndex = MagicMock(return_value=4)
self.edit_verse_form.verse_text_edit.setPlainText('Text\n')
# WHEN the method is called
self.edit_verse_form.on_forced_split_button_clicked()
# THEN the verse number must not be changed
self.assertEqual('[--}{--]\nText\n', self.edit_verse_form.verse_text_edit.toPlainText(),
'The verse number should be [--}{--]\nText\n')
def test_on_split_button_clicked(self):
"""
Test that divide adds text at the correct position
"""
# GIVEN some input values
self.edit_verse_form.verse_type_combo_box.currentIndex = MagicMock(return_value=4)
self.edit_verse_form.verse_text_edit.setPlainText('Text\n')
# WHEN the method is called
self.edit_verse_form.on_overflow_split_button_clicked()
# THEN the verse number must not be changed
self.assertEqual('[---]\nText\n', self.edit_verse_form.verse_text_edit.toPlainText(),
'The verse number should be [---]\nText\n')