diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 56a48319a..13ca5fb8f 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -252,7 +252,8 @@ class Settings(QtCore.QSettings): 'shortcuts/blankScreen': [QtGui.QKeySequence(QtCore.Qt.Key_Period)], 'shortcuts/collapse': [QtGui.QKeySequence(QtCore.Qt.Key_Minus)], 'shortcuts/desktopScreen': [QtGui.QKeySequence(QtCore.Qt.Key_D)], - 'shortcuts/delete': [QtGui.QKeySequence(QtGui.QKeySequence.Delete), QtGui.QKeySequence(QtCore.Qt.Key_Delete)], + 'shortcuts/delete': [QtGui.QKeySequence(QtGui.QKeySequence.Delete), + QtGui.QKeySequence(QtCore.Qt.Key_Delete)], 'shortcuts/down': [QtGui.QKeySequence(QtCore.Qt.Key_Down)], 'shortcuts/editSong': [], 'shortcuts/escapeItem': [QtGui.QKeySequence(QtCore.Qt.Key_Escape)], @@ -329,7 +330,8 @@ class Settings(QtCore.QSettings): 'shortcuts/moveBottom': [QtGui.QKeySequence(QtCore.Qt.Key_End)], 'shortcuts/moveDown': [QtGui.QKeySequence(QtCore.Qt.Key_PageDown)], 'shortcuts/nextTrackItem': [], - 'shortcuts/nextItem_live': [QtGui.QKeySequence(QtCore.Qt.Key_Down), QtGui.QKeySequence(QtCore.Qt.Key_PageDown)], + 'shortcuts/nextItem_live': [QtGui.QKeySequence(QtCore.Qt.Key_Down), + QtGui.QKeySequence(QtCore.Qt.Key_PageDown)], 'shortcuts/nextItem_preview': [QtGui.QKeySequence(QtCore.Qt.Key_Down), QtGui.QKeySequence(QtCore.Qt.Key_PageDown)], 'shortcuts/nextService': [QtGui.QKeySequence(QtCore.Qt.Key_Right)], @@ -339,7 +341,8 @@ class Settings(QtCore.QSettings): QtGui.QKeySequence(QtCore.Qt.ALT + QtCore.Qt.Key_F1)], 'shortcuts/openService': [], 'shortcuts/saveService': [], - 'shortcuts/previousItem_live': [QtGui.QKeySequence(QtCore.Qt.Key_Up), QtGui.QKeySequence(QtCore.Qt.Key_PageUp)], + 'shortcuts/previousItem_live': [QtGui.QKeySequence(QtCore.Qt.Key_Up), + QtGui.QKeySequence(QtCore.Qt.Key_PageUp)], 'shortcuts/playbackPause': [], 'shortcuts/playbackPlay': [], 'shortcuts/playbackStop': [], diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index a6a3c6efe..d2192f711 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -27,6 +27,7 @@ OpenLP work. from distutils.version import LooseVersion import logging import os +import re from PyQt5 import QtCore, QtGui, Qt, QtWidgets @@ -258,11 +259,12 @@ def check_item_selected(list_widget, message): return True -def clean_tags(text): +def clean_tags(text, chords=False): """ Remove Tags from text for display :param text: Text to be cleaned + :param chords: Clean ChordPro tags """ text = text.replace('
', '\n') text = text.replace('{br}', '\n') @@ -270,21 +272,47 @@ def clean_tags(text): for tag in FormattingTags.get_html_tags(): text = text.replace(tag['start tag'], '') text = text.replace(tag['end tag'], '') + # Remove ChordPro tags + if chords: + text = re.sub(r'\[.+?\]', r'', text) return text -def expand_tags(text): +def expand_tags(text, chords=False): """ Expand tags HTML for display :param text: The text to be expanded. + :param chords: Convert ChordPro tags to html """ + if chords: + text = expand_chords(text) for tag in FormattingTags.get_html_tags(): text = text.replace(tag['start tag'], tag['start html']) text = text.replace(tag['end tag'], tag['end html']) return text +def expand_chords(text): + """ + Expand ChordPro tags + + :param text: + """ + text_lines = text.split('{br}') + expanded_text_lines = [] + for line in text_lines: + # If a ChordPro is detected in the line, replace it with a html-span tag and wrap the line in a span tag. + if '[' in line and ']' in line: + new_line = '' + new_line += re.sub(r'(.*?)\[(.+?)\](.*?)', r'\1\3', line) + new_line += '' + expanded_text_lines.append(new_line) + else: + expanded_text_lines.append(line) + return '{br}'.join(expanded_text_lines) + + def create_separated_list(string_list): """ Returns a string that represents a join of a list of strings with a localized separator. This function corresponds diff --git a/openlp/core/lib/renderer.py b/openlp/core/lib/renderer.py index 74922b78d..1378b6742 100644 --- a/openlp/core/lib/renderer.py +++ b/openlp/core/lib/renderer.py @@ -254,11 +254,11 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties): # If there are (at least) two occurrences of [---] we use the first two slides (and neglect the last # for now). if len(slides) == 3: - html_text = expand_tags('\n'.join(slides[:2])) + html_text = expand_tags('\n'.join(slides[:2]), item.is_capable(ItemCapabilities.HasChords)) # We check both slides to determine if the optional split is needed (there is only one optional # split). else: - html_text = expand_tags('\n'.join(slides)) + html_text = expand_tags('\n'.join(slides), item.is_capable(ItemCapabilities.HasChords)) html_text = html_text.replace('\n', '
') if self._text_fits_on_slide(html_text): # The first two optional slides fit (as a whole) on one slide. Replace the first occurrence diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index c99229f7a..a4ea2b9c1 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -34,7 +34,7 @@ import ntpath from PyQt5 import QtGui from openlp.core.common import RegistryProperties, Settings, translate, AppLocation, md5_hash -from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, create_thumb +from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, expand_chords log = logging.getLogger(__name__) @@ -118,6 +118,8 @@ class ItemCapabilities(object): ``HasThumbnails`` The item has related thumbnails available + ``HasChords`` + The item has chords - only for songs """ CanPreview = 1 CanEdit = 2 @@ -140,6 +142,7 @@ class ItemCapabilities(object): HasDisplayTitle = 19 HasNotes = 20 HasThumbnails = 21 + HasChords = 22 class ServiceItem(RegistryProperties): @@ -260,13 +263,16 @@ class ServiceItem(RegistryProperties): previous_pages[verse_tag] = (slide['raw_slide'], pages) for page in pages: page = page.replace('
', '{br}') - html_data = expand_tags(html.escape(page.rstrip())) - self._display_frames.append({ + html_data = expand_tags(html.escape(page.rstrip()), self.is_capable(ItemCapabilities.HasChords)) + new_frame = { 'title': clean_tags(page), - 'text': clean_tags(page.rstrip()), + 'text': clean_tags(page.rstrip(), self.is_capable(ItemCapabilities.HasChords)), 'html': html_data.replace(' ', ' '), 'verseTag': verse_tag - }) + } + if self.is_capable(ItemCapabilities.HasChords): + new_frame['chords_text'] = expand_chords(clean_tags(page.rstrip())) + self._display_frames.append(new_frame) elif self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command: pass else: diff --git a/openlp/plugins/remotes/html/stage.css b/openlp/plugins/remotes/html/stage.css index 621722161..7444240d8 100644 --- a/openlp/plugins/remotes/html/stage.css +++ b/openlp/plugins/remotes/html/stage.css @@ -16,22 +16,22 @@ * with this program; if not, write to the Free Software Foundation, Inc., 59 * * Temple Place, Suite 330, Boston, MA 02111-1307 USA * ******************************************************************************/ - body { background-color: black; - font-family: sans-serif; + font-family: 'Arial Narrow', 'Avenir Next Condensed', sans-serif-condensed, Arial, sans-serif; + font-size: 4.4vw; overflow: hidden; } #currentslide { - font-size: 40pt; - color: white; + font-size: 100%; + color: lightgray; padding-bottom: 0px; } #nextslide { - font-size: 40pt; - color: grey; + font-size: 100%; + color: gray; padding-top: 0px; padding-bottom: 0px; } @@ -41,24 +41,90 @@ body { } #clock { - font-size: 30pt; + font-size: 75%; color: yellow; text-align: right; } #notes { - font-size: 36pt; + font-size: 90%; color: salmon; text-align: right; } +#controls { + display: none; +} + +#chords { + font-size: 50%; + color: gray; + background-color: gray; + color: white; + cursor: pointer; +} + +#header { + height: 1.4em; +} + +#transpose, +#transposevalue, +#capodisplay { + display: inline-block; + font-size: 75%; + color: gray; + vertical-align: middle; +} + +#header .button, +#plus, +#minus { + display: inline-block; + width: 3vw; + line-height: 3vw; + vertical-align: middle; + color: white; + background-color: gray; + font-size: 75%; + text-align: center; + cursor: pointer; +} + #verseorder { - font-size: 30pt; + font-size: 75%; color: green; text-align: left; + line-height: 1.5; + display: inline-block; + vertical-align: middle; } .currenttag { - color: lightgreen; - font-weight: bold; + color: lightgreen; + font-weight: bold; } + +.chordline { + line-height: 2.0; +} + +.chordline1 { + line-height: 1.0 +} + +.chordline span.chord span { + position: relative; +} + +.chordline span.chord span strong { + position: absolute; + top: -1em; + left: 0; + font: 500 75% 'Arial Narrow', 'Avenir Next Condensed', sans-serif-condensed, Arial, sans-serif; + color: yellow; +} + +.nextslide .chordline span.chord span strong { + color: gray; +} \ No newline at end of file diff --git a/openlp/plugins/remotes/html/stage.html b/openlp/plugins/remotes/html/stage.html index be25497bd..e3621f752 100644 --- a/openlp/plugins/remotes/html/stage.html +++ b/openlp/plugins/remotes/html/stage.html @@ -32,9 +32,15 @@ -
+
diff --git a/openlp/plugins/remotes/html/stage.js b/openlp/plugins/remotes/html/stage.js index f82aeecc7..24bf161f5 100644 --- a/openlp/plugins/remotes/html/stage.js +++ b/openlp/plugins/remotes/html/stage.js @@ -16,7 +16,69 @@ * with this program; if not, write to the Free Software Foundation, Inc., 59 * * Temple Place, Suite 330, Boston, MA 02111-1307 USA * ******************************************************************************/ + var lastChord; + +function getTransposeValue(songId) { + if (localStorage.getItem(songId + '_transposeValue')) {return localStorage.getItem(songId + '_transposeValue');} + else {return 0;} +} + +function storeTransposeValue(songId,transposeValueToSet) { + localStorage.setItem(songId + '_transposeValue', transposeValueToSet); +} + +function transposeChord(chord, transposeValue) { + var chordSplit = chord.replace('♭', 'b').split(/[\/\(\)]/), transposedChord = '', note, notenumber, rest, currentChord, + notesSharp = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','H'], + notesFlat = ['C','Db','D','Eb','Fb','F','Gb','G','Ab','A','B','H'], + notesPreferred = ['b','#','#','#','#','#','#','#','#','#','#','#']; + chordNotes = Array(); + for (i = 0; i <= chordSplit.length - 1; i++) { + if (i > 0) { + transposedChord += '/'; + } + currentchord = chordSplit[i]; + if (currentchord.charAt(0) === '(') { + transposedChord += '('; + if (currentchord.length > 1) { + currentchord = currentchord.substr(1); + } else { + currentchord = ""; + } + } + if (currentchord.length > 0) { + if (currentchord.length > 1) { + if ('#b'.indexOf(currentchord.charAt(1)) === -1) { + note = currentchord.substr(0, 1); + rest = currentchord.substr(1); + } else { + note = currentchord.substr(0, 2); + rest = currentchord.substr(2); + } + } else { + note = currentchord; + rest = ""; + } + notenumber = (notesSharp.indexOf(note) === -1?notesFlat.indexOf(note):notesSharp.indexOf(note)); + notenumber -= parseInt(transposeValue); + while (notenumber > 11) {notenumber -= 12;} + while (notenumber < 0) {notenumber += 12;} + if (i === 0) { + currentChord = notesPreferred[notenumber] === '#' ? notesSharp[notenumber] : notesFlat[notenumber]; + lastChord = currentChord; + }else { + currentChord = notesSharp.indexOf(lastChord) === -1 ? notesFlat[notenumber] : notesSharp[notenumber]; + } + if(!(notesFlat.indexOf(note)===-1 && notesSharp.indexOf(note)===-1)) transposedChord += currentChord + rest; else transposedChord += note + rest; //note+rest; + //transposedChord += currentChord + rest; + } + } + return transposedChord; +} + +var OpenLPChordOverflowFillCount = 0; window.OpenLP = { + showchords:true, loadService: function (event) { $.getJSON( "/api/service/list", @@ -27,6 +89,7 @@ window.OpenLP = { idx = parseInt(idx, 10); if (data.results.items[idx]["selected"]) { $("#notes").html(data.results.items[idx]["notes"].replace(/\n/g, "
")); + $("#songtitle").html(data.results.items[idx]["title"].replace(/\n/g, "
")); if (data.results.items.length > idx + 1) { OpenLP.nextSong = data.results.items[idx + 1]["title"]; } @@ -42,6 +105,7 @@ window.OpenLP = { "/api/controller/live/text", function (data, status) { OpenLP.currentSlides = data.results.slides; + $('#transposevalue').text(getTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0])); OpenLP.currentSlide = 0; OpenLP.currentTags = Array(); var div = $("#verseorder"); @@ -61,7 +125,7 @@ window.OpenLP = { } else { if ((slide["text"] == data.results.slides[lastChange]["text"]) && - (data.results.slides.length >= idx + (idx - lastChange))) { + (data.results.slides.length > idx + (idx - lastChange))) { // If the tag hasn't changed, check to see if the same verse // has been repeated consecutively. Note the verse may have been // split over several slides, so search through. If so, repeat the tag. @@ -92,6 +156,37 @@ window.OpenLP = { // Show the current slide on top. Any trailing slides for the same verse // are shown too underneath in grey. // Then leave a blank line between following verses + var transposeValue = getTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0]), + chordclass=/class="[a-z\s]*chord[a-z\s]*"\s*style="display:\s?none"/g, + chordclassshow='class="chord" style="display:inline"', + regchord=/([\(\w#b♭\+\*\d/\)-]+)<\/span>([\u0080-\uFFFF,\w]*)([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(
)?/g, + replaceChords=function(mstr,$1,$2,$3,$4) { +// regchord=/[\[{]([\(\w#b♭\+\*\d/\)-]+)[\]}]<\/span>([\u0080-\uFFFF,\w]*)([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(
)?/g, + var v='', w=''; + var $1len = 0, $2len = 0, slimchars='fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'; + $1 = transposeChord($1, transposeValue); + for (var i = 0; i < $1.length; i++) if (slimchars.indexOf($1.charAt(i)) === -1) {$1len += 2;} else {$1len += 1;} + for (var i = 0; i < $2.length; i++) if (slimchars.indexOf($2.charAt(i)) === -1) {$2len += 2;} else {$2len += 1;} + for (var i = 0; i < $3.length; i++) if (slimchars.indexOf($2.charAt(i)) === -1) {$2len += 2;} else {$2len += 1;} + if ($1len>=$2len && !$4) { + if ($2.length){ + if (!$3.length) { + for (c = 0; c < Math.ceil(($1len - $2len) / 2) + 1; c++) {w += '_';} + } else { + for (c = 0; c < $1len - $2len + 2; c++) {w += ' ';} + } + } else { + if (!$3.length) { + for (c = 0; c < Math.floor(($1len - $2len) / 2) + 1; c++) {w += '_';} + } else { + for (c = 0; c < $1len - $2len + 1; c++) {w += ' ';} + } + }; + } else { + if (!$2 && $3.charAt(0) == ' ') {for (c = 0; c < $1len; c++) {w += ' ';}} + } + return $.grep(['', $1, '', $2, w, $3, '', $4], Boolean).join(''); + }; $("#verseorder span").removeClass("currenttag"); $("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag"); var slide = OpenLP.currentSlides[OpenLP.currentSlide]; @@ -101,6 +196,10 @@ window.OpenLP = { text = slide["title"]; } else { text = slide["text"]; + if(OpenLP.showchords) { + text = text.replace(chordclass,chordclassshow); + text = text.replace(regchord, replaceChords); + } } // use thumbnail if available if (slide["img"]) { @@ -121,19 +220,24 @@ window.OpenLP = { text = text + OpenLP.currentSlides[idx]["title"]; } else { text = text + OpenLP.currentSlides[idx]["text"]; + if(OpenLP.showchords) { + text = text.replace(chordclass,chordclassshow); + text = text.replace(regchord, replaceChords); + } } if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1]) text = text + "

"; else text = text + "
"; } - text = text.replace(/\n/g, "
"); + text = text.replace(/\n/g, "
"); $("#nextslide").html(text); } else { text = "

" + $("#next-text").val() + ": " + OpenLP.nextSong + "

"; $("#nextslide").html(text); } + if(!OpenLP.showchords) $(".chordline").toggleClass('chordline1'); }, updateClock: function(data) { var div = $("#clock"); @@ -141,6 +245,7 @@ window.OpenLP = { var h = t.getHours(); if (data.results.twelve && h > 12) h = h - 12; + if (h < 10) h = '0' + h + ''; var m = t.getMinutes(); if (m < 10) m = '0' + m + ''; @@ -151,8 +256,7 @@ window.OpenLP = { "/api/poll", function (data, status) { OpenLP.updateClock(data); - if (OpenLP.currentItem != data.results.item || - OpenLP.currentService != data.results.service) { + if (OpenLP.currentItem != data.results.item || OpenLP.currentService != data.results.service) { OpenLP.currentItem = data.results.item; OpenLP.currentService = data.results.service; OpenLP.loadSlides(); @@ -163,8 +267,27 @@ window.OpenLP = { } } ); +// $('span.chord').each(function(){this.style.display="inline"}); } } $.ajaxSetup({ cache: false }); setInterval("OpenLP.pollServer();", 500); OpenLP.pollServer(); +$(document).ready(function() { + $('#transposeup').click(function(e) { + $('#transposevalue').text(parseInt($('#transposevalue').text()) + 1); + storeTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0], $('#transposevalue').text()); + //alert(getTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0])); + //$('body').get(0).style.'font-size' = (parseFloat($('body').css('font-size')) + 0.1) + 'vw'); + OpenLP.loadSlides(); + }); + $('#transposedown').click(function(e) { + $('#transposevalue').text(parseInt($('#transposevalue').text()) - 1); + storeTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0], $('#transposevalue').text()); + OpenLP.loadSlides(); + }); + $("#chords").click(function(){ OpenLP.showchords=OpenLP.showchords?false:true; OpenLP.updateSlide(); }); + $('#plus').click(function() { var fs=$('#currentslide').css('font-size').match(/\d+/); $('#currentslide').css("font-size",+fs+10+"px");$('#nextslide').css("font-size",+fs+10+"px"); } ); + $("#minus").click(function() {var fs=$('#currentslide').css('font-size').match(/\d+/); $('#currentslide').css("font-size",+fs-10+"px");$('#nextslide').css("font-size",+fs-10+"px"); } ); + $('body').hover(function(){ $('#controls').fadeIn(500);},function(){ $('#controls').fadeOut(500);}); +}); diff --git a/openlp/plugins/remotes/lib/httprouter.py b/openlp/plugins/remotes/lib/httprouter.py index 9f6e68506..1ab688351 100644 --- a/openlp/plugins/remotes/lib/httprouter.py +++ b/openlp/plugins/remotes/lib/httprouter.py @@ -546,8 +546,14 @@ class HttpRouter(RegistryProperties): item['tag'] = str(frame['verseTag']) else: item['tag'] = str(index + 1) - item['text'] = str(frame['text']) + # Use chords if available and enabled + if current_item.is_capable(ItemCapabilities.HasChords): + item['text'] = str(frame['chords_text']) + else: + item['text'] = str(frame['text']) item['html'] = str(frame['html']) + print('text: %s' % item['text']) + print('html: %s' % item['html']) # Handle images, unless a custom thumbnail is given or if thumbnails is disabled elif current_item.is_image() and not frame.get('image', '') and Settings().value('remotes/thumbnails'): item['tag'] = str(index + 1) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 687aac9ac..d0c6c0619 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -471,6 +471,9 @@ class SongMediaItem(MediaManagerItem): if song.media_files: service_item.add_capability(ItemCapabilities.HasBackgroundAudio) service_item.background_audio = [m.file_name for m in song.media_files] + # If chords are enabled and detected, mark the item as having chords + if Settings().value(self.settings_section + '/chords') and '[' in song.lyrics: + service_item.add_capability(ItemCapabilities.HasChords) return True def generate_footer(self, item, song): diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 670d0d602..2ae4d92b8 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -65,7 +65,10 @@ __default_settings__ = { 'songs/last directory export': '', 'songs/songselect username': '', 'songs/songselect password': '', - 'songs/songselect searches': '' + 'songs/songselect searches': '', + 'songs/chords': True, + 'songs/stageview chords': False, + 'songs/mainview chords': False }