← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~tomasgroth/openlp/chords into lp:openlp

 

Tomas Groth has proposed merging lp:~tomasgroth/openlp/chords into lp:openlp.

Requested reviews:
  OpenLP Core (openlp-core)
Related bugs:
  Bug #1530597 in OpenLP: "Importing Songbeamer songs using latin1 encoding doesn't get decoded correctly"
  https://bugs.launchpad.net/openlp/+bug/1530597

For more details, see:
https://code.launchpad.net/~tomasgroth/openlp/chords/+merge/311066

Added support for chords in Chord Pro format (using brackets), with support for chord transposing and 3 different notations.
Added support for import of song in ChordPro file format
Added support for importing chords and verseorder from songbeamer.
Add support for export and import of chords in openlyrics
Added support for importing chords from opensong.
-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~tomasgroth/openlp/chords into lp:openlp.
=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	2016-10-30 08:29:22 +0000
+++ openlp/core/lib/__init__.py	2016-11-16 19:47:28 +0000
@@ -26,6 +26,8 @@
 
 import logging
 import os
+import re
+import math
 from distutils.version import LooseVersion
 
 from PyQt5 import QtCore, QtGui, Qt, QtWidgets
@@ -281,11 +283,12 @@
     return True
 
 
-def clean_tags(text):
+def clean_tags(text, remove_chords=False):
     """
     Remove Tags from text for display
 
     :param text: Text to be cleaned
+    :param remove_chords: Clean ChordPro tags
     """
     text = text.replace('<br>', '\n')
     text = text.replace('{br}', '\n')
@@ -293,6 +296,9 @@
     for tag in FormattingTags.get_html_tags():
         text = text.replace(tag['start tag'], '')
         text = text.replace(tag['end tag'], '')
+    # Remove ChordPro tags
+    if remove_chords:
+        text = re.sub(r'\[.+?\]', r'', text)
     return text
 
 
@@ -302,12 +308,107 @@
 
     :param text: The text to be expanded.
     """
+    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_and_align_chords_in_line(match):
+    """
+    Expand the chords in the line and align them using whitespaces.
+    NOTE: There is equivalent javascript code in chords.js, in the updateSlide function. Make sure to update both!
+
+    :param match:
+    :return: The line with expanded html-chords
+    """
+    slimchars = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
+    whitespaces = ''
+    chordlen = 0
+    taillen = 0
+    # The match could be "[G]sweet the " from a line like "A[D]mazing [D7]grace! How [G]sweet the [D]sound!"
+    # The actual chord, would be "G" in match "[G]sweet the "
+    chord = match.group(1)
+    # The tailing word of the chord, would be "sweet" in match "[G]sweet the "
+    tail = match.group(2)
+    # The remainder of the line, until line end or next chord. Would be " the " in match "[G]sweet the "
+    remainder = match.group(3)
+    # Line end if found, else None
+    end = match.group(4)
+    # Based on char width calculate width of chord
+    for chord_char in chord:
+        if chord_char not in slimchars:
+            chordlen += 2
+        else:
+            chordlen += 1
+    # Based on char width calculate width of tail
+    for tail_char in tail:
+        if tail_char not in slimchars:
+            taillen += 2
+        else:
+            taillen += 1
+    # Based on char width calculate width of remainder
+    for remainder_char in remainder:
+        if remainder_char not in slimchars:
+            taillen += 2
+        else:
+            taillen += 1
+    # If the chord is wider than the tail+remainder and the line goes on, some padding is needed
+    if chordlen >= taillen and end is None:
+        # Decide if the padding should be "_" for drawing out words or spaces
+        if tail:
+            if not remainder:
+                for c in range(math.ceil((chordlen - taillen) / 2) + 1):
+                    whitespaces += '_'
+            else:
+                for c in range(chordlen - taillen + 2):
+                    whitespaces += '&nbsp;'
+        else:
+            if not remainder:
+                for c in range(math.floor((chordlen - taillen) / 2)):
+                    whitespaces += '_'
+            else:
+                for c in range(chordlen - taillen + 1):
+                    whitespaces += '&nbsp;'
+    else:
+        if not tail and remainder and remainder[0] == ' ':
+            for c in range(chordlen):
+                whitespaces += '&nbsp;'
+    if whitespaces:
+        whitespaces = '<span class="ws">' + whitespaces + '</span>'
+    return '<span class="chord"><span><strong>' + chord + '</strong></span></span>' + tail + whitespaces + remainder
+
+
+def expand_chords(text):
+    """
+    Expand ChordPro tags
+
+    :param text:
+    """
+    text_lines = text.split('{br}')
+    expanded_text_lines = []
+    chords_on_prev_line = False
+    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:
+            if chords_on_prev_line:
+                new_line = '<span class="chordline">'
+            else:
+                new_line = '<span class="chordline firstchordline">'
+                chords_on_prev_line = True
+            # Matches a chord, a tail, a remainder and a line end. See expand_and_align_chords_in_line() for more info.
+            new_line += re.sub(r'\[(\w.*?)\]([\u0080-\uFFFF,\w]*)'
+                               '([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(\Z)?',
+                               expand_and_align_chords_in_line, line)
+            new_line += '</span>'
+            expanded_text_lines.append(new_line)
+        else:
+            chords_on_prev_line = False
+            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
@@ -344,7 +445,7 @@
 from .pluginmanager import PluginManager
 from .settingstab import SettingsTab
 from .serviceitem import ServiceItem, ServiceItemType, ItemCapabilities
-from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css
+from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css, build_chords_css
 from .imagemanager import ImageManager
 from .renderer import Renderer
 from .mediamanageritem import MediaManagerItem

=== modified file 'openlp/core/lib/htmlbuilder.py'
--- openlp/core/lib/htmlbuilder.py	2016-08-05 19:41:22 +0000
+++ openlp/core/lib/htmlbuilder.py	2016-11-16 19:47:28 +0000
@@ -124,6 +124,25 @@
             position: relative;
             top: -0.3em;
         }
+        /* Chords css */
+        .chordline {
+          line-height: 1.0em;
+        }
+        .chordline span.chord span {
+          position: relative;
+        }
+        .chordline span.chord span strong {
+          position: absolute;
+          top: -0.8em;
+          left: 0;
+          font-size: 75%;
+          font-weight: normal;
+          line-height: normal;
+          display: none;
+        }
+        .firstchordline {
+            line-height: 1.0em;
+        }
         </style>
         <script>
             var timer = null;
@@ -444,6 +463,7 @@
         position: relative;
         top: -0.3em;
     }
+    /* Chords css */${chords_css}
     </style>
     <script>
         var timer = null;
@@ -592,6 +612,26 @@
     height: ${height}px;${font_style}${font_weight}
     """)
 
+CHORDS_FORMAT = Template("""
+    .chordline {
+      line-height: ${chord_line_height};
+    }
+    .chordline span.chord span {
+      position: relative;
+    }
+    .chordline span.chord span strong {
+      position: absolute;
+      top: -0.8em;
+      left: 0;
+      font-size: 75%;
+      font-weight: normal;
+      line-height: normal;
+      display: ${chords_display};
+    }
+    .firstchordline {
+        line-height: ${first_chord_line_height};
+    }""")
+
 
 def build_html(item, screen, is_live, background, image=None, plugins=None):
     """
@@ -636,7 +676,8 @@
                                js_additions=js_additions,
                                bg_image=bgimage_src,
                                image=image_src,
-                               html_additions=html_additions)
+                               html_additions=html_additions,
+                               chords_css=build_chords_css())
 
 
 def webkit_version():
@@ -768,3 +809,16 @@
     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)
+
+
+def build_chords_css():
+    if Settings().value('songs/mainview chords'):
+        chord_line_height = '2.0em'
+        chords_display = 'inline'
+        first_chord_line_height = '2.1em'
+    else:
+        chord_line_height = '1.0em'
+        chords_display = 'none'
+        first_chord_line_height = '1.0em'
+    return CHORDS_FORMAT.substitute(chord_line_height=chord_line_height, chords_display=chords_display,
+                                    first_chord_line_height=first_chord_line_height)

=== modified file 'openlp/core/lib/renderer.py'
--- openlp/core/lib/renderer.py	2016-08-05 19:41:22 +0000
+++ openlp/core/lib/renderer.py	2016-11-16 19:47:28 +0000
@@ -27,7 +27,7 @@
 
 from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings
 from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \
-    build_lyrics_format_css, build_lyrics_outline_css
+    build_lyrics_format_css, build_lyrics_outline_css, build_chords_css
 from openlp.core.common import ThemeLevel
 from openlp.core.ui import MainDisplay
 
@@ -383,13 +383,14 @@
             </script>
             <style>
                 *{margin: 0; padding: 0; border: 0;}
-                #main {position: absolute; top: 0px; ${format_css} ${outline_css}}
+                #main {position: absolute; top: 0px; ${format_css} ${outline_css}} ${chords_css}
             </style></head>
             <body><div id="main"></div></body></html>""")
         self.web.setHtml(html.substitute(format_css=build_lyrics_format_css(theme_data,
                                                                             self.page_width,
                                                                             self.page_height),
-                                         outline_css=build_lyrics_outline_css(theme_data)))
+                                         outline_css=build_lyrics_outline_css(theme_data),
+                                         chords_css=build_chords_css()))
         self.empty_height = self.web_frame.contentsSize().height()
 
     def _paginate_slide(self, lines, line_end):

=== modified file 'openlp/core/lib/serviceitem.py'
--- openlp/core/lib/serviceitem.py	2016-07-01 21:17:20 +0000
+++ openlp/core/lib/serviceitem.py	2016-11-16 19:47:28 +0000
@@ -34,7 +34,7 @@
 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
+from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, expand_chords, create_thumb
 
 log = logging.getLogger(__name__)
 
@@ -117,7 +117,6 @@
 
     ``HasThumbnails``
             The item has related thumbnails available
-
     """
     CanPreview = 1
     CanEdit = 2
@@ -261,12 +260,14 @@
                 for page in pages:
                     page = page.replace('<br>', '{br}')
                     html_data = expand_tags(html.escape(page.rstrip()))
-                    self._display_frames.append({
+                    new_frame = {
                         'title': clean_tags(page),
-                        'text': clean_tags(page.rstrip()),
+                        'text': clean_tags(page.rstrip(), True),
+                        'chords_text': clean_tags(expand_chords(page.rstrip()), False),
                         'html': html_data.replace('&amp;nbsp;', '&nbsp;'),
-                        'verseTag': verse_tag
-                    })
+                        'verseTag': verse_tag,
+                    }
+                    self._display_frames.append(new_frame)
         elif self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command:
             pass
         else:

=== modified file 'openlp/core/ui/media/__init__.py'
--- openlp/core/ui/media/__init__.py	2016-07-01 21:17:20 +0000
+++ openlp/core/ui/media/__init__.py	2016-11-16 19:47:28 +0000
@@ -143,6 +143,7 @@
                                                                          seconds=seconds,
                                                                          millis=millis)
 
+
 from .mediacontroller import MediaController
 from .playertab import PlayerTab
 

=== modified file 'openlp/core/ui/printserviceform.py'
--- openlp/core/ui/printserviceform.py	2016-01-09 16:26:14 +0000
+++ openlp/core/ui/printserviceform.py	2016-11-16 19:47:28 +0000
@@ -37,7 +37,7 @@
 DEFAULT_CSS = """/*
 Edit this file to customize the service order print. Note, that not all CSS
 properties are supported. See:
-http://doc.trolltech.com/4.7/richtext-html-subset.html#css-properties
+https://doc.qt.io/qt-5/richtext-html-subset.html#css-properties
 */
 
 .serviceTitle {
@@ -172,6 +172,11 @@
         self._add_element('h1', html.escape(self.title_line_edit.text()), html_data.body, classId='serviceTitle')
         for index, item in enumerate(self.service_manager.service_items):
             self._add_preview_item(html_data.body, item['service_item'], index)
+        # Remove chord span and ws span elements since printing them are not yet support
+        for chord_span in html_data.find_class('chord'):
+            chord_span.drop_tree()
+        for ws_span in html_data.find_class('ws'):
+            ws_span.drop_tree()
         # Add the custom service notes:
         if self.footer_text_edit.toPlainText():
             div = self._add_element('div', parent=html_data.body, classId='customNotes')

=== modified file 'openlp/plugins/bibles/lib/manager.py'
--- openlp/plugins/bibles/lib/manager.py	2016-11-01 19:17:57 +0000
+++ openlp/plugins/bibles/lib/manager.py	2016-11-16 19:47:28 +0000
@@ -423,4 +423,5 @@
         for bible in self.db_cache:
             self.db_cache[bible].finalise()
 
+
 __all__ = ['BibleFormat']

=== modified file 'openlp/plugins/presentations/lib/pptviewlib/ppttest.py'
--- openlp/plugins/presentations/lib/pptviewlib/ppttest.py	2015-12-31 22:46:06 +0000
+++ openlp/plugins/presentations/lib/pptviewlib/ppttest.py	2016-11-16 19:47:28 +0000
@@ -197,6 +197,7 @@
     def openDialog(self):
         self.pptEdit.setText(QtWidgets.QFileDialog.getOpenFileName(self, 'Open file')[0])
 
+
 if __name__ == '__main__':
     pptdll = cdll.LoadLibrary(r'pptviewlib.dll')
     pptdll.SetDebug(1)

=== added file 'openlp/plugins/remotes/html/chords.html'
--- openlp/plugins/remotes/html/chords.html	1970-01-01 00:00:00 +0000
+++ openlp/plugins/remotes/html/chords.html	2016-11-16 19:47:28 +0000
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<!--
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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; version 2 of the License.                              #
+#                                                                             #
+# 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, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+-->
+<head>
+  <meta charset="utf-8" />
+  <title>${chords_title}</title>
+  <link rel="stylesheet" href="/css/stage.css" />
+  <link rel="stylesheet" href="/css/chords.css" />
+  <link rel="shortcut icon" type="image/x-icon" href="/images/favicon.ico">
+  <script type="text/javascript" src="/assets/jquery.min.js"></script>
+  <script type="text/javascript" src="/js/chords.js"></script>
+</head>
+<body>
+<input type="hidden" id="next-text" value="${next}" />
+<div id="right">
+  <div id="clock"></div>
+  <div id="notes"></div>
+</div>
+<div id="header">
+  <div id="verseorder"></div>
+  <div id="transpose">Transpose:</div> <div class="button" id="transposedown">-</div> <div id="transposevalue">0</div> <div class="button" id="transposeup">+</div> <div id="capodisplay">(Capo)</div>
+</div>
+<div id="currentslide"></div>
+<div id="nextslide"></div>
+</body>
+</html>

=== added file 'openlp/plugins/remotes/html/css/chords.css'
--- openlp/plugins/remotes/html/css/chords.css	1970-01-01 00:00:00 +0000
+++ openlp/plugins/remotes/html/css/chords.css	2016-11-16 19:47:28 +0000
@@ -0,0 +1,91 @@
+/******************************************************************************
+* OpenLP - Open Source Lyrics Projection                                      *
+* --------------------------------------------------------------------------- *
+* Copyright (c) 2008-2016 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; version 2 of the License.                              *
+*                                                                             *
+* 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, write to the Free Software Foundation, Inc., 59  *
+* Temple Place, Suite 330, Boston, MA 02111-1307 USA                          *
+******************************************************************************/
+
+#chords {
+  font-size: 20pt;
+  color: gray;
+  background-color: gray;
+  color: white;
+  cursor: pointer;
+}
+
+#header {
+  padding-bottom: 1em;
+
+}
+
+#transpose,
+#transposevalue,
+#capodisplay {
+  display: inline-block;
+  font-size: 30pt;
+  color: gray;
+  vertical-align: middle;
+}
+
+#header .button,
+#plus,
+#minus {
+  display: inline-block;
+  width: 1.2em;
+  line-height: 1.2em;
+  vertical-align: middle;
+  color: white;
+  background-color: gray;
+  font-size: 30pt;
+  text-align: center;
+  cursor: pointer;
+}
+
+/* Extending exiting definition in stage.css */
+#verseorder {
+  line-height: 1.5;
+  display: inline-block;
+  vertical-align: middle;
+}
+
+.chordline {
+  line-height: 2.0;
+}
+
+.chordline1 {
+  line-height: 1.0
+}
+
+.firstchordline {
+  line-height: 2.1em;
+}
+
+.chordline span.chord span {
+  position: relative;
+}
+
+.chordline span.chord span strong {
+  position: absolute;
+  top: -0.8em;
+  left: 0;
+  font-size: 30pt;
+  font-weight: normal;
+  line-height: normal;
+  color: yellow;
+}
+
+#nextslide .chordline span.chord span strong {
+  color: gray;
+}

=== added file 'openlp/plugins/remotes/html/js/chords.js'
--- openlp/plugins/remotes/html/js/chords.js	1970-01-01 00:00:00 +0000
+++ openlp/plugins/remotes/html/js/chords.js	2016-11-16 19:47:28 +0000
@@ -0,0 +1,315 @@
+/******************************************************************************
+ * OpenLP - Open Source Lyrics Projection                                      *
+ * --------------------------------------------------------------------------- *
+ * Copyright (c) 2008-2016 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; version 2 of the License.                              *
+ *                                                                             *
+ * 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, write to the Free Software Foundation, Inc., 59  *
+ * Temple Place, Suite 330, Boston, MA 02111-1307 USA                          *
+ ******************************************************************************/
+var lastChord;
+
+var notesSharpNotation = {}
+var notesFlatNotation = {}
+
+// See https://en.wikipedia.org/wiki/Musical_note#12-tone_chromatic_scale
+notesSharpNotation['german'] = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','H'];
+notesFlatNotation['german'] = ['C','Db','D','Eb','Fb','F','Gb','G','Ab','A','B','H'];
+notesSharpNotation['english'] = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
+notesFlatNotation['english'] = ['C','Db','D','Eb','Fb','F','Gb','G','Ab','A','Bb','B'];
+notesSharpNotation['neo-latin'] = ['Do','Do#','Re','Re#','Mi','Fa','Fa#','Sol','Sol#','La','La#','Si'];
+notesFlatNotation['neo-latin'] = ['Do','Reb','Re','Mib','Fab','Fa','Solb','Sol','Lab','La','Sib','Si'];
+
+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);
+}
+
+// NOTE: This function has a python equivalent in openlp/plugins/songs/lib/__init__.py - make sure to update both!
+function transposeChord(chord, transposeValue, notation) {
+  var chordSplit = chord.replace('♭', 'b').split(/[\/\(\)]/);
+  var transposedChord = '', note, notenumber, rest, currentChord;
+  var notesSharp = notesSharpNotation[notation];
+  var notesFlat = notesFlatNotation[notation];
+  var notesPreferred = ['b','#','#','#','#','#','#','#','#','#','#','#'];
+  var 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;
+      }
+    }
+  }
+  return transposedChord;
+}
+
+var OpenLPChordOverflowFillCount = 0;
+window.OpenLP = {
+  showchords:true,
+  loadService: function (event) {
+    $.getJSON(
+      "/api/service/list",
+      function (data, status) {
+        OpenLP.nextSong = "";
+        $("#notes").html("");
+        for (idx in data.results.items) {
+          idx = parseInt(idx, 10);
+          if (data.results.items[idx]["selected"]) {
+            $("#notes").html(data.results.items[idx]["notes"].replace(/\n/g, "<br />"));
+            if (data.results.items.length > idx + 1) {
+              OpenLP.nextSong = data.results.items[idx + 1]["title"];
+            }
+            break;
+          }
+        }
+        OpenLP.updateSlide();
+      }
+    );
+  },
+  loadSlides: function (event) {
+    $.getJSON(
+      "/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");
+        div.html("");
+        var tag = "";
+        var tags = 0;
+        var lastChange = 0;
+        $.each(data.results.slides, function(idx, slide) {
+          var prevtag = tag;
+          tag = slide["tag"];
+          if (tag != prevtag) {
+            // If the tag has changed, add new one to the list
+            lastChange = idx;
+            tags = tags + 1;
+            div.append("&nbsp;<span>");
+            $("#verseorder span").last().attr("id", "tag" + tags).text(tag);
+          }
+          else {
+            if ((slide["chords_text"] == data.results.slides[lastChange]["chords_text"]) &&
+              (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.
+              var match = true;
+              for (var idx2 = 0; idx2 < idx - lastChange; idx2++) {
+                if(data.results.slides[lastChange + idx2]["chords_text"] != data.results.slides[idx + idx2]["chords_text"]) {
+                    match = false;
+                    break;
+                }
+              }
+              if (match) {
+                lastChange = idx;
+                tags = tags + 1;
+                div.append("&nbsp;<span>");
+                $("#verseorder span").last().attr("id", "tag" + tags).text(tag);
+              }
+            }
+          }
+          OpenLP.currentTags[idx] = tags;
+          if (slide["selected"])
+            OpenLP.currentSlide = idx;
+        })
+        OpenLP.loadService();
+      }
+    );
+  },
+  updateSlide: function() {
+    // 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]);
+    var chordclass=/class="[a-z\s]*chord[a-z\s]*"\s*style="display:\s?none"/g;
+    var chordclassshow='class="chord"';
+    var regchord=/<span class="chord"><span><strong>([\(\w#b♭\+\*\d/\)-]+)<\/strong><\/span><\/span>([\u0080-\uFFFF,\w]*)(<span class="ws">.+?<\/span>)?([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(<br>)?/g;
+    // NOTE: There is equivalent python code in openlp/core/lib/__init__.py, in the expand_and_align_chords_in_line function. Make sure to update both!
+    var replaceChords=function(mstr,$chord,$tail,$skips,$remainder,$end) {
+      var w='';
+      var $chordlen = 0;
+      var $taillen = 0;
+      var slimchars='fiíIÍjlĺľrtť.,;/ ()|"\'!:\\';
+      // Transpose chord as dictated by the transpose value in local storage
+      $chord = transposeChord($chord, transposeValue, OpenLP.chordNotation);
+      // Replace any padding '_' added to tail
+      $tail = $tail.replace(/_+$/, '')
+      for (var i = 0; i < $chord.length; i++) if (slimchars.indexOf($chord.charAt(i)) === -1) {$chordlen += 2;} else {$chordlen += 1;}
+      for (var i = 0; i < $tail.length; i++) if (slimchars.indexOf($tail.charAt(i)) === -1) {$taillen += 2;} else {$taillen += 1;}
+      for (var i = 0; i < $remainder.length; i++) if (slimchars.indexOf($tail.charAt(i)) === -1) {$taillen += 2;} else {$taillen += 1;}
+      if ($chordlen >= $taillen && !$end) {
+        if ($tail.length){
+          if (!$remainder.length) {
+            for (c = 0; c < Math.ceil(($chordlen - $taillen) / 2) + 1; c++) {w += '_';}
+          } else {
+            for (c = 0; c < $chordlen - $taillen + 2; c++) {w += '&nbsp;';}
+          }
+        } else {
+          if (!$remainder.length) {
+            for (c = 0; c < Math.floor(($chordlen - $taillen) / 2) + 1; c++) {w += '_';}
+          } else {
+            for (c = 0; c < $chordlen - $taillen + 1; c++) {w += '&nbsp;';}
+          }
+        };
+      } else {
+        if (!$tail && $remainder.charAt(0) == ' ') {for (c = 0; c < $chordlen; c++) {w += '&nbsp;';}}
+      }
+      if (w!='') {
+        w='<span class="ws">' + w + '</span>';
+      }
+      return $.grep(['<span class="chord"><span><strong>', $chord, '</strong></span></span>', $tail, w, $remainder, $end], Boolean).join('');
+    };
+    $("#verseorder span").removeClass("currenttag");
+    $("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag");
+    var slide = OpenLP.currentSlides[OpenLP.currentSlide];
+    var text = "";
+    // use title if available
+    if (slide["title"]) {
+        text = slide["title"];
+    } else {
+        text = slide["chords_text"];
+        if(OpenLP.showchords) {
+            text = text.replace(chordclass,chordclassshow);
+            text = text.replace(regchord, replaceChords);
+        }
+    }
+    // use thumbnail if available
+    if (slide["img"]) {
+        text += "<br /><img src='" + slide["img"].replace("/thumbnails/", "/thumbnails320x240/") + "'><br />";
+    }
+    // use notes if available
+    if (slide["slide_notes"]) {
+        text += '<br />' + slide["slide_notes"];
+    }
+    text = text.replace(/\n/g, "<br />");
+    $("#currentslide").html(text);
+    text = "";
+    if (OpenLP.currentSlide < OpenLP.currentSlides.length - 1) {
+      for (var idx = OpenLP.currentSlide + 1; idx < OpenLP.currentSlides.length; idx++) {
+        if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
+            text = text + "<p class=\"nextslide\">";
+        if (OpenLP.currentSlides[idx]["title"]) {
+            text = text + OpenLP.currentSlides[idx]["title"];
+        } else {
+            text = text + OpenLP.currentSlides[idx]["chords_text"];
+            if(OpenLP.showchords) {
+              text = text.replace(chordclass,chordclassshow);
+              text = text.replace(regchord, replaceChords);
+            }
+        }
+        if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
+            text = text + "</p>";
+        else
+            text = text + "<br />";
+      }
+      text = text.replace(/\n/g, "<br />");
+      $("#nextslide").html(text);
+      $("#nextslide").class("nextslide");
+    }
+    else {
+      text = "<p class=\"nextslide\">" + $("#next-text").val() + ": " + OpenLP.nextSong + "</p>";
+      $("#nextslide").html(text);
+    }
+  if(!OpenLP.showchords) $(".chordline").toggleClass('chordline1');
+  },
+  updateClock: function(data) {
+    var div = $("#clock");
+    var t = new Date();
+    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 + '';
+    div.html(h + ":" + m);
+  },
+  pollServer: function () {
+    $.getJSON(
+      "/api/poll",
+      function (data, status) {
+        OpenLP.updateClock(data);
+        OpenLP.chordNotation = data.results.chordNotation;
+        if (OpenLP.currentItem != data.results.item || OpenLP.currentService != data.results.service) {
+          OpenLP.currentItem = data.results.item;
+          OpenLP.currentService = data.results.service;
+          OpenLP.loadSlides();
+        }
+        else if (OpenLP.currentSlide != data.results.slide) {
+          OpenLP.currentSlide = parseInt(data.results.slide, 10);
+          OpenLP.updateSlide();
+        }
+      }
+    );
+  }
+}
+$.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());
+    OpenLP.loadSlides();
+  });
+  $('#transposedown').click(function(e) {  
+    $('#transposevalue').text(parseInt($('#transposevalue').text()) - 1);
+    storeTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0], $('#transposevalue').text());
+    OpenLP.loadSlides();
+  });
+});

=== modified file 'openlp/plugins/remotes/lib/httprouter.py'
--- openlp/plugins/remotes/lib/httprouter.py	2016-08-03 21:19:14 +0000
+++ openlp/plugins/remotes/lib/httprouter.py	2016-11-16 19:47:28 +0000
@@ -152,6 +152,7 @@
             ('^/$', {'function': self.serve_file, 'secure': False}),
             ('^/(stage)$', {'function': self.serve_file, 'secure': False}),
             ('^/(stage)/(.*)$', {'function': self.stages, 'secure': False}),
+            ('^/(chords)$', {'function': self.serve_file, 'secure': False}),
             ('^/(main)$', {'function': self.serve_file, 'secure': False}),
             (r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
             (r'^/api/poll$', {'function': self.poll, 'secure': False}),
@@ -318,10 +319,12 @@
         """
         remote = translate('RemotePlugin.Mobile', 'Remote')
         stage = translate('RemotePlugin.Mobile', 'Stage View')
+        chords = translate('RemotePlugin.Mobile', 'Chords View')
         live = translate('RemotePlugin.Mobile', 'Live View')
         self.template_vars = {
             'app_title': "{main} {remote}".format(main=UiStrings().OLPV2x, remote=remote),
             'stage_title': "{main} {stage}".format(main=UiStrings().OLPV2x, stage=stage),
+            'chords_title': "{main} {chords}".format(main=UiStrings().OLPV2x, chords=chords),
             'live_title': "{main} {live}".format(main=UiStrings().OLPV2x, live=live),
             'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
             'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
@@ -482,7 +485,8 @@
             'display': self.live_controller.desktop_screen.isChecked(),
             'version': 2,
             'isSecure': Settings().value(self.settings_section + '/authentication enabled'),
-            'isAuthorised': self.authorised
+            'isAuthorised': self.authorised,
+            'chordNotation': Settings().value('songs/chord notation'),
         }
         self.do_json_header()
         return json.dumps({'results': result}).encode()
@@ -554,6 +558,7 @@
                         item['tag'] = str(frame['verseTag'])
                     else:
                         item['tag'] = str(index + 1)
+                    item['chords_text'] = str(frame['chords_text'])
                     item['text'] = str(frame['text'])
                     item['html'] = str(frame['html'])
                 # Handle images, unless a custom thumbnail is given or if thumbnails is disabled

=== modified file 'openlp/plugins/remotes/lib/remotetab.py'
--- openlp/plugins/remotes/lib/remotetab.py	2016-05-27 08:13:14 +0000
+++ openlp/plugins/remotes/lib/remotetab.py	2016-11-16 19:47:28 +0000
@@ -81,6 +81,12 @@
         self.stage_url.setObjectName('stage_url')
         self.stage_url.setOpenExternalLinks(True)
         self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
+        self.chords_url_label = QtWidgets.QLabel(self.http_settings_group_box)
+        self.chords_url_label.setObjectName('chords_url_label')
+        self.chords_url = QtWidgets.QLabel(self.http_settings_group_box)
+        self.chords_url.setObjectName('chords_url')
+        self.chords_url.setOpenExternalLinks(True)
+        self.http_setting_layout.addRow(self.chords_url_label, self.chords_url)
         self.live_url_label = QtWidgets.QLabel(self.http_settings_group_box)
         self.live_url_label.setObjectName('live_url_label')
         self.live_url = QtWidgets.QLabel(self.http_settings_group_box)
@@ -117,6 +123,12 @@
         self.stage_https_url.setObjectName('stage_https_url')
         self.stage_https_url.setOpenExternalLinks(True)
         self.https_settings_layout.addRow(self.stage_https_url_label, self.stage_https_url)
+        self.chords_https_url_label = QtWidgets.QLabel(self.http_settings_group_box)
+        self.chords_https_url_label.setObjectName('chords_https_url_label')
+        self.chords_https_url = QtWidgets.QLabel(self.https_settings_group_box)
+        self.chords_https_url.setObjectName('chords_https_url')
+        self.chords_https_url.setOpenExternalLinks(True)
+        self.https_settings_layout.addRow(self.chords_https_url_label, self.chords_https_url)
         self.live_https_url_label = QtWidgets.QLabel(self.https_settings_group_box)
         self.live_https_url_label.setObjectName('live_url_label')
         self.live_https_url = QtWidgets.QLabel(self.https_settings_group_box)
@@ -186,6 +198,7 @@
         self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
         self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
         self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
+        self.chords_url_label.setText(translate('RemotePlugin.RemoteTab', 'Chords view URL:'))
         self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
         self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
         self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab',
@@ -207,6 +220,7 @@
         self.https_port_label.setText(self.port_label.text())
         self.remote_https_url_label.setText(self.remote_url_label.text())
         self.stage_https_url_label.setText(self.stage_url_label.text())
+        self.chords_https_url_label.setText(self.chords_url_label.text())
         self.live_https_url_label.setText(self.live_url_label.text())
         self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication'))
         self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:'))
@@ -225,6 +239,10 @@
         https_url_temp = https_url + 'stage'
         self.stage_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
         self.stage_https_url.setText('<a href="{url}">{url}</a>'.format(url=https_url_temp))
+        http_url_temp = http_url + 'chords'
+        https_url_temp = https_url + 'chords'
+        self.chords_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
+        self.chords_https_url.setText('<a href="{url}">{url}</a>'.format(url=https_url_temp))
         http_url_temp = http_url + 'main'
         https_url_temp = https_url + 'main'
         self.live_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))

=== modified file 'openlp/plugins/songs/forms/editversedialog.py'
--- openlp/plugins/songs/forms/editversedialog.py	2016-10-27 17:45:50 +0000
+++ openlp/plugins/songs/forms/editversedialog.py	2016-11-16 19:47:28 +0000
@@ -63,6 +63,20 @@
         self.verse_type_layout.addWidget(self.insert_button)
         self.verse_type_layout.addStretch()
         self.dialog_layout.addLayout(self.verse_type_layout)
+        self.transpose_layout = QtWidgets.QHBoxLayout()
+        self.transpose_layout.setObjectName('transpose_layout')
+        self.transpose_label = QtWidgets.QLabel(edit_verse_dialog)
+        self.transpose_label.setObjectName('transpose_label')
+        self.transpose_layout.addWidget(self.transpose_label)
+        self.transpose_up_button = QtWidgets.QPushButton(edit_verse_dialog)
+        self.transpose_up_button.setIcon(build_icon(':/services/service_up.png'))
+        self.transpose_up_button.setObjectName('transpose_up')
+        self.transpose_layout.addWidget(self.transpose_up_button)
+        self.transpose_down_button = QtWidgets.QPushButton(edit_verse_dialog)
+        self.transpose_down_button.setIcon(build_icon(':/services/service_down.png'))
+        self.transpose_down_button.setObjectName('transpose_down')
+        self.transpose_layout.addWidget(self.transpose_down_button)
+        self.dialog_layout.addLayout(self.transpose_layout)
         self.button_box = create_button_box(edit_verse_dialog, 'button_box', ['cancel', 'ok'])
         self.dialog_layout.addWidget(self.button_box)
         self.retranslateUi(edit_verse_dialog)
@@ -82,3 +96,6 @@
         self.insert_button.setText(translate('SongsPlugin.EditVerseForm', '&Insert'))
         self.insert_button.setToolTip(translate('SongsPlugin.EditVerseForm',
                                       'Split a slide into two by inserting a verse splitter.'))
+        self.transpose_label.setText(translate('SongsPlugin.EditVerseForm', 'Transpose:'))
+        self.transpose_up_button.setText(translate('SongsPlugin.EditVerseForm', 'Up'))
+        self.transpose_down_button.setText(translate('SongsPlugin.EditVerseForm', 'Down'))

=== modified file 'openlp/plugins/songs/forms/editverseform.py'
--- openlp/plugins/songs/forms/editverseform.py	2016-05-27 08:13:14 +0000
+++ openlp/plugins/songs/forms/editverseform.py	2016-11-16 19:47:28 +0000
@@ -25,7 +25,7 @@
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
-from openlp.plugins.songs.lib import VerseType
+from openlp.plugins.songs.lib import VerseType, transpose_lyrics
 from .editversedialog import Ui_EditVerseDialog
 
 log = logging.getLogger(__name__)
@@ -48,6 +48,8 @@
         self.split_button.clicked.connect(self.on_split_button_clicked)
         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.transpose_down_button.clicked.connect(self.on_transepose_down_button_clicked)
+        self.transpose_up_button.clicked.connect(self.on_transepose_up_button_clicked)
 
     def insert_verse(self, verse_tag, verse_num=1):
         """
@@ -95,6 +97,24 @@
         """
         self.update_suggested_verse_number()
 
+    def on_transepose_up_button_clicked(self):
+        """
+        The transpose up button clicked
+        """
+        transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), 1)
+        self.verse_text_edit.setPlainText(transposed_lyrics)
+        self.verse_text_edit.setFocus()
+        self.verse_text_edit.moveCursor(QtGui.QTextCursor.End)
+
+    def on_transepose_down_button_clicked(self):
+        """
+        The transpose down button clicked
+        """
+        transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), -1)
+        self.verse_text_edit.setPlainText(transposed_lyrics)
+        self.verse_text_edit.setFocus()
+        self.verse_text_edit.moveCursor(QtGui.QTextCursor.End)
+
     def update_suggested_verse_number(self):
         """
         Adjusts the verse number SpinBox in regard to the selected verse type and the cursor's position.

=== modified file 'openlp/plugins/songs/lib/__init__.py'
--- openlp/plugins/songs/lib/__init__.py	2016-08-13 14:29:12 +0000
+++ openlp/plugins/songs/lib/__init__.py	2016-11-16 19:47:28 +0000
@@ -29,7 +29,7 @@
 
 from PyQt5 import QtWidgets
 
-from openlp.core.common import AppLocation, CONTROL_CHARS
+from openlp.core.common import AppLocation, CONTROL_CHARS, Settings
 from openlp.core.lib import translate
 from openlp.plugins.songs.lib.db import Author, MediaFile, Song, Topic
 from openlp.plugins.songs.lib.ui import SongStrings
@@ -541,3 +541,123 @@
     except OSError:
         log.exception('Could not remove directory: {path}'.format(path=save_path))
     song_plugin.manager.delete_object(Song, song_id)
+
+
+def transpose_lyrics(lyrics, transepose_value):
+    """
+    Transepose lyrics
+
+    :param lyrcs: The lyrics to be transposed
+    :param transepose_value: The value to transpose the lyrics with
+    :return: The transposed lyrics
+    """
+    # Split text by verse delimiter - both normal and optional
+    verse_list = re.split('(---\[.+?:.+?\]---|\[---\])', lyrics)
+    transposed_lyrics = ''
+    notation = Settings().value('songs/chord notation')
+    for verse in verse_list:
+        if verse.startswith('---[') or verse == '[---]':
+            transposed_lyrics += verse
+        else:
+            transposed_lyrics += transpose_verse(verse, transepose_value, notation)
+    return transposed_lyrics
+
+
+def transpose_verse(verse_text, transepose_value, notation):
+    """
+    Transepose lyrics
+
+    :param lyrcs: The lyrics to be transposed
+    :param transepose_value: The value to transpose the lyrics with
+    :return: The transposed lyrics
+    """
+    if '[' not in verse_text:
+        return verse_text
+    # Split the lyrics based on chord tags
+    lyric_list = re.split('(\[|\]|/)', verse_text)
+    transposed_lyrics = ''
+    in_tag = False
+    for word in lyric_list:
+        if not in_tag:
+            transposed_lyrics += word
+            if word == '[':
+                in_tag = True
+        else:
+            if word == ']':
+                in_tag = False
+                transposed_lyrics += word
+            elif word == '/':
+                transposed_lyrics += word
+            else:
+                # This MUST be a chord
+                transposed_lyrics += transpose_chord(word, transepose_value, notation)
+    # If still inside a chord tag something is wrong!
+    if in_tag:
+        return verse_text
+    else:
+        return transposed_lyrics
+
+
+def transpose_chord(chord, transpose_value, notation):
+    """
+    Transpose chord according to the notation used.
+    NOTE: This function has a javascript equivalent in chords.js - make sure to update both!
+
+    :param chord: The chord to transpose.
+    :param transpose_value: The value the chord should be transposed.
+    :param notation: The notation to use when transposing.
+    :return: The transposed chord.
+    """
+    # See https://en.wikipedia.org/wiki/Musical_note#12-tone_chromatic_scale
+    notes_sharp_notation = {}
+    notes_flat_notation = {}
+    notes_sharp_notation['german'] = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'H']
+    notes_flat_notation['german'] = ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'H']
+    notes_sharp_notation['english'] = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
+    notes_flat_notation['english'] = ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']
+    notes_sharp_notation['neo-latin'] = ['Do', 'Do#', 'Re', 'Re#', 'Mi', 'Fa', 'Fa#', 'Sol', 'Sol#', 'La', 'La#', 'Si']
+    notes_flat_notation['neo-latin'] = ['Do', 'Reb', 'Re', 'Mib', 'Fab', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']
+    chord_split = chord.replace('♭', 'b').split('/[\/\(\)]/')
+    transposed_chord = ''
+    last_chord = ''
+    notes_sharp = notes_sharp_notation[notation]
+    notes_flat = notes_flat_notation[notation]
+    notes_preferred = ['b', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#']
+    for i in range(0, len(chord_split)):
+        if i > 0:
+            transposed_chord += '/'
+        currentchord = chord_split[i]
+        if currentchord[0] == '(':
+            transposed_chord += '('
+            if len(currentchord) > 1:
+                currentchord = currentchord[1:]
+            else:
+                currentchord = ""
+        if len(currentchord) > 0:
+            if len(currentchord) > 1:
+                if '#b'.find(currentchord[1]) == -1:
+                    note = currentchord[0:1]
+                    rest = currentchord[1:]
+                else:
+                    note = currentchord[0:2]
+                    rest = currentchord[2:]
+            else:
+                note = currentchord
+                rest = ''
+            notenumber = notes_flat.index(note) if note not in notes_sharp else notes_sharp.index(note)
+            notenumber += transpose_value
+            while notenumber > 11:
+                notenumber -= 12
+            while notenumber < 0:
+                notenumber += 12
+            if i == 0:
+                current_chord = notes_sharp[notenumber] if notes_preferred[notenumber] == '#' else notes_flat[
+                    notenumber]
+                last_chord = current_chord
+            else:
+                current_chord = notes_flat[notenumber] if last_chord not in notes_sharp else notes_sharp[notenumber]
+            if not (note not in notes_flat and note not in notes_sharp):
+                transposed_chord += current_chord + rest
+            else:
+                transposed_chord += note + rest
+    return transposed_chord

=== modified file 'openlp/plugins/songs/lib/importer.py'
--- openlp/plugins/songs/lib/importer.py	2016-05-27 08:13:14 +0000
+++ openlp/plugins/songs/lib/importer.py	2016-11-16 19:47:28 +0000
@@ -48,6 +48,7 @@
 from .importers.presentationmanager import PresentationManagerImport
 from .importers.lyrix import LyrixImport
 from .importers.videopsalm import VideoPsalmImport
+from .importers.chordpro import ChordProImport
 
 log = logging.getLogger(__name__)
 
@@ -155,29 +156,30 @@
     OpenLP2 = 1
     Generic = 2
     CCLI = 3
-    DreamBeam = 4
-    EasySlides = 5
-    EasyWorshipDB = 6
-    EasyWorshipService = 7
-    FoilPresenter = 8
-    Lyrix = 9
-    MediaShout = 10
-    OpenSong = 11
-    OPSPro = 12
-    PowerPraise = 13
-    PowerSong = 14
-    PresentationManager = 15
-    ProPresenter = 16
-    SongBeamer = 17
-    SongPro = 18
-    SongShowPlus = 19
-    SongsOfFellowship = 20
-    SundayPlus = 21
-    VideoPsalm = 22
-    WordsOfWorship = 23
-    WorshipAssistant = 24
-    WorshipCenterPro = 25
-    ZionWorx = 26
+    ChordPro = 4
+    DreamBeam = 5
+    EasySlides = 6
+    EasyWorshipDB = 7
+    EasyWorshipService = 8
+    FoilPresenter = 9
+    Lyrix = 10
+    MediaShout = 11
+    OpenSong = 12
+    OPSPro = 13
+    PowerPraise = 14
+    PowerSong = 15
+    PresentationManager = 16
+    ProPresenter = 17
+    SongBeamer = 18
+    SongPro = 19
+    SongShowPlus = 20
+    SongsOfFellowship = 21
+    SundayPlus = 22
+    VideoPsalm = 23
+    WordsOfWorship = 24
+    WorshipAssistant = 25
+    WorshipCenterPro = 26
+    ZionWorx = 27
 
     # Set optional attribute defaults
     __defaults__ = {
@@ -224,6 +226,13 @@
             'filter': '{text} (*.usr *.txt *.bin)'.format(text=translate('SongsPlugin.ImportWizardForm',
                                                                          'CCLI SongSelect Files'))
         },
+        ChordPro: {
+            'class': ChordProImport,
+            'name': 'ChordPro',
+            'prefix': 'chordPro',
+            'filter': '{text} (*.cho  *.crd *.chordpro *.chopro *.txt)'.format(
+                text=translate('SongsPlugin.ImportWizardForm', 'ChordPro Files'))
+        },
         DreamBeam: {
             'class': DreamBeamImport,
             'name': 'DreamBeam',
@@ -427,6 +436,7 @@
             SongFormat.OpenLP2,
             SongFormat.Generic,
             SongFormat.CCLI,
+            SongFormat.ChordPro,
             SongFormat.DreamBeam,
             SongFormat.EasySlides,
             SongFormat.EasyWorshipDB,

=== added file 'openlp/plugins/songs/lib/importers/chordpro.py'
--- openlp/plugins/songs/lib/importers/chordpro.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/songs/lib/importers/chordpro.py	2016-11-16 19:47:28 +0000
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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; version 2 of the License.                              #
+#                                                                             #
+# 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, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`chordpro` module provides the functionality for importing
+ChordPro files into the current database.
+"""
+
+import logging
+
+from .songimport import SongImport
+
+
+log = logging.getLogger(__name__)
+
+
+class ChordProImport(SongImport):
+    """
+    The :class:`ChordProImport` class provides OpenLP with the
+    ability to import ChordPro files.
+    This importer is based on the information available on these webpages:
+    http://webchord.sourceforge.net/tech.html
+    http://www.vromans.org/johan/projects/Chordii/chordpro/
+    http://www.tenbyten.com/software/songsgen/help/HtmlHelp/files_reference.htm
+    http://linkesoft.com/songbook/chordproformat.html
+    """
+    def do_import(self):
+        self.import_wizard.progress_bar.setMaximum(len(self.import_source))
+        for filename in self.import_source:
+            if self.stop_import_flag:
+                return
+            song_file = open(filename, 'rt')
+            self.do_import_file(song_file)
+            song_file.close()
+
+    def do_import_file(self, song_file):
+        """
+        Imports the songs in the given file
+        :param song_file: The file object to be imported from.
+        """
+        self.set_defaults()
+        # Loop over the lines of the file
+        file_content = song_file.read()
+        current_verse = ''
+        current_verse_type = 'v'
+        skip_block = False
+        for line in file_content.splitlines():
+            line = line.rstrip()
+            # Detect tags
+            if line.startswith('{'):
+                tag_name, tag_value = self.parse_tag(line)
+                # Detect which tag
+                if tag_name in ['title', 't']:
+                    self.title = tag_value
+                elif tag_name in ['subtitle', 'su', 'st']:
+                    self.alternate_title = tag_value
+                elif tag_name in ['comment', 'c', 'comment_italic', 'ci', 'comment_box', 'cb']:
+                    # Detect if the comment is used as a chorus repeat marker
+                    if tag_value.lower().startswith('chorus'):
+                        if current_verse.strip():
+                            # Add collected verse to the lyrics
+                            self.add_verse(current_verse.rstrip(), current_verse_type)
+                            current_verse_type = 'v'
+                            current_verse = ''
+                        self.repeat_verse('c1')
+                    else:
+                        self.add_comment(tag_value)
+                elif tag_name in ['start_of_chorus', 'soc']:
+                    current_verse_type = 'c'
+                elif tag_name in ['end_of_chorus', 'eoc']:
+                    # Add collected chorus to the lyrics
+                    self.add_verse(current_verse.rstrip(), current_verse_type)
+                    current_verse_type = 'v'
+                    current_verse = ''
+                elif tag_name in ['start_of_tab', 'sot']:
+                    if current_verse.strip():
+                        # Add collected verse to the lyrics
+                        self.add_verse(current_verse.rstrip(), current_verse_type)
+                        current_verse_type = 'v'
+                        current_verse = ''
+                    skip_block = True
+                elif tag_name in ['end_of_tab', 'eot']:
+                    skip_block = False
+                elif tag_name in ['new_song', 'ns']:
+                    # A new song starts below this tag
+                    if self.verses and self.title:
+                        if current_verse.strip():
+                            self.add_verse(current_verse.rstrip(), current_verse_type)
+                        if not self.finish():
+                            self.log_error(song_file.name)
+                    self.set_defaults()
+                    current_verse_type = 'v'
+                    current_verse = ''
+                else:
+                    # Unsupported tag
+                    log.debug('unsupported tag: %s' % line)
+            elif line.startswith('#'):
+                # Found a comment line, which is ignored...
+                continue
+            elif line == "['|]":
+                # Found a vertical bar
+                continue
+            else:
+                if skip_block:
+                    continue
+                elif line == '' and current_verse.strip() and current_verse_type != 'c':
+                    # Add collected verse to the lyrics
+                    self.add_verse(current_verse.rstrip(), current_verse_type)
+                    current_verse_type = 'v'
+                    current_verse = ''
+                else:
+                    if current_verse.strip() == '':
+                        current_verse = line + '\n'
+                    else:
+                        current_verse += line + '\n'
+        if current_verse.strip():
+            self.add_verse(current_verse.rstrip(), current_verse_type)
+        if not self.finish():
+            self.log_error(song_file.name)
+
+    def parse_tag(self, line):
+        """
+        :param line: Line with the tag to be parsed
+        :return: A tuple with tag name and tag value (if any)
+        """
+        # Strip the first '}'
+        line = line[1:].strip()
+        colon_idx = line.find(':')
+        # check if this is a tag without value
+        if colon_idx < 0:
+            # strip the final '}' and return the tag name
+            return line[:-1], None
+        tag_name = line[:colon_idx]
+        tag_value = line[colon_idx + 1:-1].strip()
+        return tag_name, tag_value

=== modified file 'openlp/plugins/songs/lib/importers/opensong.py'
--- openlp/plugins/songs/lib/importers/opensong.py	2016-06-07 07:35:06 +0000
+++ openlp/plugins/songs/lib/importers/opensong.py	2016-11-16 19:47:28 +0000
@@ -26,7 +26,7 @@
 from lxml import objectify
 from lxml.etree import Error, LxmlError
 
-from openlp.core.common import translate
+from openlp.core.common import translate, Settings
 from openlp.plugins.songs.lib import VerseType
 from openlp.plugins.songs.lib.importers.songimport import SongImport
 from openlp.plugins.songs.lib.ui import SongStrings
@@ -87,7 +87,7 @@
     All verses are imported and tagged appropriately.
 
     Guitar chords can be provided "above" the lyrics (the line is preceded by a period "."), and one or more "_" can
-    be used to signify long-drawn-out words. Chords and "_" are removed by this importer. For example::
+    be used to signify long-drawn-out words. For example::
 
         . A7        Bm
         1 Some____ Words
@@ -195,14 +195,34 @@
             lyrics = str(root.lyrics)
         else:
             lyrics = ''
+        chords = []
         for this_line in lyrics.split('\n'):
             if not this_line.strip():
                 continue
             # skip this line if it is a comment
             if this_line.startswith(';'):
                 continue
-            # skip guitar chords and page and column breaks
-            if this_line.startswith('.') or this_line.startswith('---') or this_line.startswith('-!!'):
+            # skip page and column breaks
+            if this_line.startswith('---') or this_line.startswith('-!!'):
+                continue
+            # guitar chords marker
+            if this_line.startswith('.'):
+                # Find the position of the chords so they can be inserted in the lyrics
+                chords = []
+                this_line = this_line[1:]
+                chord = ''
+                i = 0
+                while i < len(this_line):
+                    if this_line[i] != ' ':
+                        chord_pos = i
+                        chord += this_line[i]
+                        i += 1
+                        while i < len(this_line) and this_line[i] != ' ':
+                            chord += this_line[i]
+                            i += 1
+                        chords.append((chord_pos, chord))
+                        chord = ''
+                    i += 1
                 continue
             # verse/chorus/etc. marker
             if this_line.startswith('['):
@@ -228,12 +248,19 @@
             # number at start of line.. it's verse number
             if this_line[0].isdigit():
                 verse_num = this_line[0]
-                this_line = this_line[1:].strip()
+                this_line = this_line[1:]
             verses.setdefault(verse_tag, {})
             verses[verse_tag].setdefault(verse_num, {})
             if inst not in verses[verse_tag][verse_num]:
                 verses[verse_tag][verse_num][inst] = []
                 our_verse_order.append([verse_tag, verse_num, inst])
+            # If chords exists insert them
+            if chords and not Settings().value('songs/disable chords import'):
+                offset = 0
+                for (column, chord) in chords:
+                    this_line = '{pre}[{chord}]{post}'.format(pre=this_line[:offset + column], chord=chord,
+                                                              post=this_line[offset + column:])
+                    offset += len(chord) + 2
             # Tidy text and remove the ____s from extended words
             this_line = self.tidy_text(this_line)
             this_line = this_line.replace('_', '')

=== modified file 'openlp/plugins/songs/lib/importers/songbeamer.py'
--- openlp/plugins/songs/lib/importers/songbeamer.py	2016-01-10 00:22:31 +0000
+++ openlp/plugins/songs/lib/importers/songbeamer.py	2016-11-16 19:47:28 +0000
@@ -23,13 +23,15 @@
 The :mod:`songbeamer` module provides the functionality for importing SongBeamer songs into the OpenLP database.
 """
 import chardet
-import codecs
 import logging
 import os
 import re
+import base64
+import math
 
 from openlp.plugins.songs.lib import VerseType
 from openlp.plugins.songs.lib.importers.songimport import SongImport
+from openlp.core.common import Settings, is_win, is_macosx, get_file_encoding
 
 log = logging.getLogger(__name__)
 
@@ -110,27 +112,32 @@
             self.set_defaults()
             self.current_verse = ''
             self.current_verse_type = VerseType.tags[VerseType.Verse]
-            read_verses = False
+            self.chord_table = None
             file_name = os.path.split(import_file)[1]
             if os.path.isfile(import_file):
-                # First open in binary mode to detect the encoding
-                detect_file = open(import_file, 'rb')
-                details = chardet.detect(detect_file.read())
-                detect_file.close()
-                infile = codecs.open(import_file, 'r', details['encoding'])
+                # Detect the encoding
+                self.input_file_encoding = get_file_encoding(import_file)['encoding']
+                # The encoding should only be ANSI (cp1251), UTF-8, Unicode, Big-Endian-Unicode.
+                # So if it doesn't start with 'u' we default to cp1252. See:
+                # https://forum.songbeamer.com/viewtopic.php?p=419&sid=ca4814924e37c11e4438b7272a98b6f2
+                if not self.input_file_encoding.lower().startswith('u'):
+                    self.input_file_encoding = 'cp1252'
+                infile = open(import_file, 'rt', encoding=self.input_file_encoding)
                 song_data = infile.readlines()
                 infile.close()
             else:
                 continue
             self.title = file_name.split('.sng')[0]
             read_verses = False
+            # The first verse separator doesn't count, but the others does, so line count starts at -1
+            line_number = -1
             for line in song_data:
-                # Just make sure that the line is of the type 'Unicode'.
-                line = str(line).strip()
+                line = line.rstrip()
+                stripped_line = line.strip()
                 if line.startswith('#') and not read_verses:
-                    self.parseTags(line)
-                elif line.startswith('--'):
-                    # --- and -- allowed for page-breaks (difference in Songbeamer only in printout)
+                    self.parse_tags(line)
+                elif stripped_line.startswith('---'):
+                    # '---' is a verse breaker
                     if self.current_verse:
                         self.replace_html_tags()
                         self.add_verse(self.current_verse, self.current_verse_type)
@@ -138,19 +145,51 @@
                         self.current_verse_type = VerseType.tags[VerseType.Verse]
                     read_verses = True
                     verse_start = True
+                    # Songbeamer allows chord on line "-1"
+                    if line_number == -1:
+                        first_line = self.insert_chords(line_number, '')
+                        if first_line:
+                            self.current_verse = first_line.strip() + '\n'
+                    line_number += 1
+                elif stripped_line.startswith('--'):
+                    # '--' is a page breaker, we convert to optional page break
+                    self.current_verse += '[---]\n'
+                    line_number += 1
                 elif read_verses:
                     if verse_start:
                         verse_start = False
                         if not self.check_verse_marks(line):
-                            self.current_verse = line + '\n'
+                            self.current_verse += line.strip() + '\n'
                     else:
-                        self.current_verse += line + '\n'
+                        line = self.insert_chords(line_number, line)
+                        self.current_verse += line.strip() + '\n'
+                        line_number += 1
             if self.current_verse:
                 self.replace_html_tags()
                 self.add_verse(self.current_verse, self.current_verse_type)
             if not self.finish():
                 self.log_error(import_file)
 
+    def insert_chords(self, line_number, line):
+        """
+        Insert chords into text if any exists and chords import is enabled
+
+        :param linenumber: Number of the current line
+        :param line: The line of lyrics to insert chords
+        """
+        if self.chord_table and not Settings().value('songs/disable chords import') and line_number in self.chord_table:
+            line_idx = sorted(self.chord_table[line_number].keys(), reverse=True)
+            for idx in line_idx:
+                # In SongBeamer the column position of the chord can be a decimal, we just round it up.
+                int_idx = int(math.ceil(idx))
+                if int_idx < 0:
+                    int_idx = 0
+                elif int_idx > len(line):
+                    # If a chord is placed beyond the current end of the line, extend the line with spaces.
+                    line += ' ' * (int_idx - len(line))
+                line = line[:int_idx] + '[' + self.chord_table[line_number][idx] + ']' + line[int_idx:]
+        return line
+
     def replace_html_tags(self):
         """
         This can be called to replace SongBeamer's specific (html) tags with OpenLP's specific (html) tags.
@@ -158,7 +197,7 @@
         for pair in SongBeamerImport.HTML_TAG_PAIRS:
             self.current_verse = pair[0].sub(pair[1], self.current_verse)
 
-    def parseTags(self, line):
+    def parse_tags(self, line):
         """
         Parses a meta data line.
 
@@ -175,6 +214,8 @@
             self.add_copyright(tag_val[1])
         elif tag_val[0] == '#AddCopyrightInfo':
             pass
+        elif tag_val[0] == '#AudioFile':
+            self.parse_audio_file(tag_val[1])
         elif tag_val[0] == '#Author':
             self.parse_author(tag_val[1])
         elif tag_val[0] == '#BackgroundImage':
@@ -186,13 +227,16 @@
         elif tag_val[0] == '#CCLI':
             self.ccli_number = tag_val[1]
         elif tag_val[0] == '#Chords':
-            pass
+            self.chord_table = self.parse_chords(tag_val[1])
         elif tag_val[0] == '#ChurchSongID':
             pass
         elif tag_val[0] == '#ColorChords':
             pass
         elif tag_val[0] == '#Comments':
-            self.comments = tag_val[1]
+            try:
+                self.comments = base64.b64decode(tag_val[1]).decode(self.input_file_encoding)
+            except ValueError:
+                self.comments = tag_val[1]
         elif tag_val[0] == '#Editor':
             pass
         elif tag_val[0] == '#Font':
@@ -242,7 +286,7 @@
         elif tag_val[0] == '#TextAlign':
             pass
         elif tag_val[0] == '#Title':
-            self.title = str(tag_val[1]).strip()
+            self.title = tag_val[1].strip()
         elif tag_val[0] == '#TitleAlign':
             pass
         elif tag_val[0] == '#TitleFontSize':
@@ -262,25 +306,80 @@
         elif tag_val[0] == '#Version':
             pass
         elif tag_val[0] == '#VerseOrder':
-            # TODO: add the verse order.
-            pass
+            verse_order = tag_val[1].strip()
+            for verse_mark in verse_order.split(','):
+                new_verse_mark = self.convert_verse_marks(verse_mark)
+                if new_verse_mark:
+                    self.verse_order_list.append(new_verse_mark)
 
     def check_verse_marks(self, line):
         """
         Check and add the verse's MarkType. Returns ``True`` if the given line contains a correct verse mark otherwise
         ``False``.
 
-        :param line: The line to check for marks (unicode).
-        """
+        :param line: The line to check for marks.
+        """
+        new_verse_mark = self.convert_verse_marks(line)
+        if new_verse_mark:
+            self.current_verse_type = new_verse_mark
+            return True
+        return False
+
+    def convert_verse_marks(self, line):
+        """
+        Convert the verse's MarkType. Returns the OpenLP versemark if the given line contains a correct SongBeamer verse
+        mark otherwise ``None``.
+
+        :param line: The line to check for marks.
+        """
+        new_verse_mark = None
         marks = line.split(' ')
         if len(marks) <= 2 and marks[0].lower() in SongBeamerTypes.MarkTypes:
-            self.current_verse_type = SongBeamerTypes.MarkTypes[marks[0].lower()]
+            new_verse_mark = SongBeamerTypes.MarkTypes[marks[0].lower()]
             if len(marks) == 2:
-                # If we have a digit, we append it to current_verse_type.
+                # If we have a digit, we append it to the converted verse mark
                 if marks[1].isdigit():
-                    self.current_verse_type += marks[1]
-            return True
+                    new_verse_mark += marks[1]
         elif marks[0].lower().startswith('$$m='):  # this verse-mark cannot be numbered
-            self.current_verse_type = SongBeamerTypes.MarkTypes['$$m=']
-            return True
-        return False
+            new_verse_mark = SongBeamerTypes.MarkTypes['$$m=']
+        return new_verse_mark
+
+    def parse_chords(self, chords):
+        """
+        Parse chords. The chords are in a base64 encode string. The decoded string is an index of chord placement
+        separated by "\r", like this: "<linecolumn>,<linenumber>,<chord>\r"
+
+        :param chords: Chords in a base64 encoded string
+        """
+        chord_list = base64.b64decode(chords).decode(self.input_file_encoding).split('\r')
+        chord_table = {}
+        for chord_index in chord_list:
+            if not chord_index:
+                continue
+            [col_str, line_str, chord] = chord_index.split(',')
+            col = float(col_str)
+            line = int(line_str)
+            if line not in chord_table:
+                chord_table[line] = {}
+            chord_table[line][col] = chord
+        return chord_table
+
+    def parse_audio_file(self, audio_file_path):
+        """
+        Parse audio file. The path is relative to the SongsBeamer Songs folder.
+
+        :param audio_file_path: Path to the audio file
+        """
+        # The path is relative to SongBeamers Song folder
+        if is_win():
+            user_doc_folder = os.path.expandvars('$DOCUMENTS')
+        elif is_macosx():
+            user_doc_folder = os.path.join(os.path.expanduser('~'), 'Documents')
+        else:
+            # SongBeamer only runs on mac and win...
+            return
+        audio_file_path = os.path.normpath(os.path.join(user_doc_folder, 'SongBeamer', 'Songs', audio_file_path))
+        if os.path.isfile(audio_file_path):
+            self.add_media_file(audio_file_path)
+        else:
+            log.debug('Could not import mediafile "%s" since it does not exists!' % audio_file_path)

=== modified file 'openlp/plugins/songs/lib/importers/songimport.py'
--- openlp/plugins/songs/lib/importers/songimport.py	2016-09-23 21:46:43 +0000
+++ openlp/plugins/songs/lib/importers/songimport.py	2016-11-16 19:47:28 +0000
@@ -304,12 +304,23 @@
         if verse_def not in self.verse_order_list_generated:
             self.verse_order_list_generated.append(verse_def)
 
-    def repeat_verse(self):
+    def repeat_verse(self, verse_def=None):
         """
-        Repeat the previous verse in the verse order
+        Repeat the verse with the given verse_def or default to repeating the previous verse in the verse order
+
+        :param verse_def: verse_def of the verse to be repeated
         """
         if self.verse_order_list_generated:
-            self.verse_order_list_generated.append(self.verse_order_list_generated[-1])
+            if verse_def:
+                # If the given verse_def is only one char (like 'v' or 'c'), postfix it with '1'
+                if len(verse_def) == 1:
+                    verse_def += '1'
+                if verse_def in self.verse_order_list_generated:
+                    self.verse_order_list_generated.append(verse_def)
+                else:
+                    log.warning('Trying to add unknown verse_def "%s"' % verse_def)
+            else:
+                self.verse_order_list_generated.append(self.verse_order_list_generated[-1])
             self.verse_order_list_generated_useful = True
 
     def check_complete(self):

=== modified file 'openlp/plugins/songs/lib/importers/videopsalm.py'
--- openlp/plugins/songs/lib/importers/videopsalm.py	2016-07-07 20:47:57 +0000
+++ openlp/plugins/songs/lib/importers/videopsalm.py	2016-11-16 19:47:28 +0000
@@ -26,8 +26,9 @@
 import logging
 import json
 import os
+import re
 
-from openlp.core.common import translate
+from openlp.core.common import translate, Settings
 from openlp.plugins.songs.lib.importers.songimport import SongImport
 from openlp.plugins.songs.lib.db import AuthorType
 
@@ -121,7 +122,11 @@
                 if 'Memo3' in song:
                     self.add_comment(song['Memo3'])
                 for verse in song['Verses']:
-                    self.add_verse(verse['Text'], 'v')
+                    verse_text = verse['Text']
+                    # Strip out chords if set up to
+                    if Settings().value('songs/disable chords import'):
+                        verse_text = re.sub(r'\[\w.*?\]', '', verse_text)
+                    self.add_verse(verse_text, 'v')
                 if not self.finish():
                     self.log_error('Could not import {title}'.format(title=self.title))
         except Exception as e:

=== modified file 'openlp/plugins/songs/lib/openlyricsxml.py'
--- openlp/plugins/songs/lib/openlyricsxml.py	2016-10-25 20:34:48 +0000
+++ openlp/plugins/songs/lib/openlyricsxml.py	2016-11-16 19:47:28 +0000
@@ -61,7 +61,7 @@
 
 from lxml import etree, objectify
 
-from openlp.core.common import translate
+from openlp.core.common import translate, Settings
 from openlp.core.common.versionchecker import get_application_version
 from openlp.core.lib import FormattingTags
 from openlp.plugins.songs.lib import VerseType, clean_song
@@ -154,7 +154,7 @@
         OpenLP does not support the attribute *lang*.
 
     ``<chord>``
-        This property is not supported.
+        This property is fully supported.
 
     ``<comments>``
         The ``<comments>`` property is fully supported. But comments in lyrics are not supported.
@@ -323,7 +323,19 @@
                 # Do not add the break attribute to the last lines element.
                 if index < len(optional_verses) - 1:
                     lines_element.set('break', 'optional')
-        return self._extract_xml(song_xml).decode()
+        xml_text = self._extract_xml(song_xml).decode()
+        return self._chordpro_to_openlyrics(xml_text)
+
+    def _chordpro_to_openlyrics(self, text):
+        """
+        Convert chords from Chord Pro format to Open Lyrics format
+
+        :param text: the lyric with chords
+        :return: the lyrics with the converted chords
+        """
+        # Process chords.
+        new_text = re.sub(r'\[(\w.*?)\]', r'<chord name="\1"/>', text)
+        return new_text
 
     def _get_missing_tags(self, text):
         """
@@ -595,8 +607,7 @@
 
     def _process_lines_mixed_content(self, element, newlines=True):
         """
-        Converts the xml text with mixed content to OpenLP representation. Chords are skipped and formatting tags are
-        converted.
+        Converts the xml text with mixed content to OpenLP representation. Chords and formatting tags are converted.
 
         :param element: The property object (lxml.etree.Element).
         :param newlines: The switch to enable/disable processing of line breaks <br/>. The <br/> is used since
@@ -608,12 +619,14 @@
         # TODO: Verify format() with template variables
         if element.tag == NSMAP % 'comment':
             if element.tail:
-                # Append tail text at chord element.
+                # Append tail text at comment element.
                 text += element.tail
             return text
-        # Skip <chord> element - not yet supported.
+        # Convert chords to ChordPro format which OpenLP uses internally
         # TODO: Verify format() with template variables
         elif element.tag == NSMAP % 'chord':
+            if not Settings().value('songs/disable chords import'):
+                text += '[{chord}]'.format(chord=element.get('name'))
             if element.tail:
                 # Append tail text at chord element.
                 text += element.tail
@@ -666,7 +679,7 @@
             text = self._process_lines_mixed_content(element)
         # OpenLyrics version <= 0.7 contains <line> elements to represent lines. First child element is tested.
         else:
-            # Loop over the "line" elements removing comments and chords.
+            # Loop over the "line" elements removing comments
             for line in element:
                 # Skip comment lines.
                 # TODO: Verify format() with template variables

=== modified file 'openlp/plugins/songs/lib/songstab.py'
--- openlp/plugins/songs/lib/songstab.py	2016-07-24 20:20:25 +0000
+++ openlp/plugins/songs/lib/songstab.py	2016-11-16 19:47:28 +0000
@@ -57,6 +57,34 @@
         self.display_copyright_check_box.setObjectName('copyright_check_box')
         self.mode_layout.addWidget(self.display_copyright_check_box)
         self.left_layout.addWidget(self.mode_group_box)
+
+        self.chords_group_box = QtWidgets.QGroupBox(self.left_column)
+        self.chords_group_box.setObjectName('chords_group_box')
+        self.chords_layout = QtWidgets.QVBoxLayout(self.chords_group_box)
+        self.chords_layout.setObjectName('chords_layout')
+        self.mainview_chords_check_box = QtWidgets.QCheckBox(self.mode_group_box)
+        self.mainview_chords_check_box.setObjectName('tool_bar_active_check_box')
+        self.chords_layout.addWidget(self.mainview_chords_check_box)
+        self.disable_chords_import_check_box = QtWidgets.QCheckBox(self.mode_group_box)
+        self.disable_chords_import_check_box.setObjectName('tool_bar_active_check_box')
+        self.chords_layout.addWidget(self.disable_chords_import_check_box)
+
+        # Chords notation
+        self.chord_notation_label = QtWidgets.QLabel(self.chords_group_box)
+        self.chord_notation_label.setWordWrap(True)
+        self.chords_layout.addWidget(self.chord_notation_label)
+        self.english_notation_radio_button = QtWidgets.QRadioButton(self.chords_group_box)
+        self.english_notation_radio_button.setObjectName('english_notation_radio_button')
+        self.chords_layout.addWidget(self.english_notation_radio_button)
+        self.german_notation_radio_button = QtWidgets.QRadioButton(self.chords_group_box)
+        self.german_notation_radio_button.setObjectName('german_notation_radio_button')
+        self.chords_layout.addWidget(self.german_notation_radio_button)
+        self.neolatin_notation_radio_button = QtWidgets.QRadioButton(self.chords_group_box)
+        self.neolatin_notation_radio_button.setObjectName('neolatin_notation_radio_button')
+        self.chords_layout.addWidget(self.neolatin_notation_radio_button)
+
+        self.left_layout.addWidget(self.chords_group_box)
+
         self.left_layout.addStretch()
         self.right_layout.addStretch()
         self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed)
@@ -64,6 +92,11 @@
         self.add_from_service_check_box.stateChanged.connect(self.on_add_from_service_check_box_changed)
         self.display_songbook_check_box.stateChanged.connect(self.on_songbook_check_box_changed)
         self.display_copyright_check_box.stateChanged.connect(self.on_copyright_check_box_changed)
+        self.mainview_chords_check_box.stateChanged.connect(self.on_mainview_chords_check_box_changed)
+        self.disable_chords_import_check_box.stateChanged.connect(self.on_disable_chords_import_check_box_changed)
+        self.english_notation_radio_button.clicked.connect(self.on_english_notation_button_clicked)
+        self.german_notation_radio_button.clicked.connect(self.on_german_notation_button_clicked)
+        self.neolatin_notation_radio_button.clicked.connect(self.on_neolatin_notation_button_clicked)
 
     def retranslateUi(self):
         self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Songs Mode'))
@@ -76,6 +109,15 @@
         self.display_copyright_check_box.setText(translate('SongsPlugin.SongsTab',
                                                            'Display "{symbol}" symbol before copyright '
                                                            'info').format(symbol=SongStrings.CopyrightSymbol))
+        self.chords_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Chords'))
+        self.mainview_chords_check_box.setText(translate('SongsPlugin.SongsTab', 'Display chords in the main view'))
+        self.disable_chords_import_check_box.setText(translate('SongsPlugin.SongsTab',
+                                                               'Ignore chords when importing songs'))
+        self.chord_notation_label.setText(translate('SongsPlugin.SongsTab', 'Chord notation to use:'))
+        self.english_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'English') + ' (C-D-E-F-G-A-B)')
+        self.german_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'German') + ' (C-D-E-F-G-A-H)')
+        self.neolatin_notation_radio_button.setText(
+            translate('SongsPlugin.SongsTab', 'Neo-Latin') + ' (Do-Re-Mi-Fa-Sol-La-Si)')
 
     def on_search_as_type_check_box_changed(self, check_state):
         self.song_search = (check_state == QtCore.Qt.Checked)
@@ -95,6 +137,21 @@
     def on_copyright_check_box_changed(self, check_state):
         self.display_copyright_symbol = (check_state == QtCore.Qt.Checked)
 
+    def on_mainview_chords_check_box_changed(self, check_state):
+        self.mainview_chords = (check_state == QtCore.Qt.Checked)
+
+    def on_disable_chords_import_check_box_changed(self, check_state):
+        self.disable_chords_import = (check_state == QtCore.Qt.Checked)
+
+    def on_english_notation_button_clicked(self):
+        self.chord_notation = 'english'
+
+    def on_german_notation_button_clicked(self):
+        self.chord_notation = 'german'
+
+    def on_neolatin_notation_button_clicked(self):
+        self.chord_notation = 'neo-latin'
+
     def load(self):
         settings = Settings()
         settings.beginGroup(self.settings_section)
@@ -103,11 +160,22 @@
         self.update_load = settings.value('add song from service')
         self.display_songbook = settings.value('display songbook')
         self.display_copyright_symbol = settings.value('display copyright symbol')
+        self.chord_notation = settings.value('chord notation')
+        self.mainview_chords = settings.value('mainview chords')
+        self.disable_chords_import = settings.value('disable chords import')
         self.tool_bar_active_check_box.setChecked(self.tool_bar)
         self.update_on_edit_check_box.setChecked(self.update_edit)
         self.add_from_service_check_box.setChecked(self.update_load)
         self.display_songbook_check_box.setChecked(self.display_songbook)
         self.display_copyright_check_box.setChecked(self.display_copyright_symbol)
+        self.mainview_chords_check_box.setChecked(self.mainview_chords)
+        self.disable_chords_import_check_box.setChecked(self.disable_chords_import)
+        if self.chord_notation == 'german':
+            self.german_notation_radio_button.setChecked(True)
+        elif self.chord_notation == 'neo-latin':
+            self.neolatin_notation_radio_button.setChecked(True)
+        else:
+            self.english_notation_radio_button.setChecked(True)
         settings.endGroup()
 
     def save(self):
@@ -118,6 +186,9 @@
         settings.setValue('add song from service', self.update_load)
         settings.setValue('display songbook', self.display_songbook)
         settings.setValue('display copyright symbol', self.display_copyright_symbol)
+        settings.setValue('mainview chords', self.mainview_chords)
+        settings.setValue('disable chords import', self.disable_chords_import)
+        settings.setValue('chord notation', self.chord_notation)
         settings.endGroup()
         if self.tab_visited:
             self.settings_form.register_post_process('songs_config_updated')

=== modified file 'openlp/plugins/songs/songsplugin.py'
--- openlp/plugins/songs/songsplugin.py	2016-09-19 18:51:48 +0000
+++ openlp/plugins/songs/songsplugin.py	2016-11-16 19:47:28 +0000
@@ -65,7 +65,10 @@
     'songs/last directory export': '',
     'songs/songselect username': '',
     'songs/songselect password': '',
-    'songs/songselect searches': ''
+    'songs/songselect searches': '',
+    'songs/chord notation': 'english',  # Can be english, german or neo-latin
+    'songs/mainview chords': False,
+    'songs/disable chords import': False,
 }
 
 

=== modified file 'scripts/check_dependencies.py'
--- scripts/check_dependencies.py	2016-05-17 21:28:27 +0000
+++ scripts/check_dependencies.py	2016-11-16 19:47:28 +0000
@@ -250,5 +250,6 @@
     print_qt_image_formats()
     print_enchant_backends_and_languages()
 
+
 if __name__ == '__main__':
     main()

=== modified file 'scripts/jenkins_script.py'
--- scripts/jenkins_script.py	2016-08-02 18:57:10 +0000
+++ scripts/jenkins_script.py	2016-11-16 19:47:28 +0000
@@ -218,5 +218,6 @@
     else:
         parser.print_help()
 
+
 if __name__ == '__main__':
     main()

=== modified file 'tests/functional/openlp_core_common/test_actions.py'
--- tests/functional/openlp_core_common/test_actions.py	2016-06-02 10:27:30 +0000
+++ tests/functional/openlp_core_common/test_actions.py	2016-11-16 19:47:28 +0000
@@ -120,11 +120,11 @@
         self.list.add(self.action2)
 
         # WHEN: Iterating over the list
-        l = [a for a in self.list]
+        list = [a for a in self.list]
         # THEN: Make sure they are returned in correct order
         self.assertEquals(len(self.list), 2)
-        self.assertIs(l[0], self.action1)
-        self.assertIs(l[1], self.action2)
+        self.assertIs(list[0], self.action1)
+        self.assertIs(list[1], self.action2)
 
     def test_remove(self):
         """

=== modified file 'tests/functional/openlp_core_lib/test_htmlbuilder.py'
--- tests/functional/openlp_core_lib/test_htmlbuilder.py	2016-06-07 13:21:07 +0000
+++ tests/functional/openlp_core_lib/test_htmlbuilder.py	2016-11-16 19:47:28 +0000
@@ -8,7 +8,7 @@
 
 from openlp.core.common import Settings
 from openlp.core.lib.htmlbuilder import build_html, build_background_css, build_lyrics_css, build_lyrics_outline_css, \
-    build_lyrics_format_css, build_footer_css, webkit_version
+    build_lyrics_format_css, build_footer_css, webkit_version, build_chords_css
 from openlp.core.lib.theme import HorizontalType, VerticalType
 from tests.functional import MagicMock, patch
 from tests.helpers.testmixin import TestMixin
@@ -60,6 +60,25 @@
         position: relative;
         top: -0.3em;
     }
+    /* Chords css */
+    .chordline {
+      line-height: 1.0em;
+    }
+    .chordline span.chord span {
+      position: relative;
+    }
+    .chordline span.chord span strong {
+      position: absolute;
+      top: -0.8em;
+      left: 0;
+      font-size: 75%;
+      font-weight: normal;
+      line-height: normal;
+      display: none;
+    }
+    .firstchordline {
+        line-height: 1.0em;
+    }
     </style>
     <script>
         var timer = null;
@@ -211,6 +230,29 @@
 FOOTER_CSS = FOOTER_CSS_BASE % ('nowrap')
 FOOTER_CSS_WRAP = FOOTER_CSS_BASE % ('normal')
 FOOTER_CSS_INVALID = ''
+CHORD_CSS_ENABLED = """
+    .chordline {
+      line-height: 2.0em;
+    }
+    .chordline span.chord span {
+      position: relative;
+    }
+    .chordline span.chord span strong {
+      position: absolute;
+      top: -0.8em;
+      left: 0;
+      font-size: 75%;
+      font-weight: normal;
+      line-height: normal;
+      display: inline;
+    }
+    .firstchordline {
+        line-height: 2.1em;
+    }"""
+
+__default_settings__ = {
+    'songs/mainview chords': False,
+}
 
 
 class Htmbuilder(TestCase, TestMixin):
@@ -222,6 +264,7 @@
         Create the UI
         """
         self.build_settings()
+        Settings().extend_default_settings(__default_settings__)
 
     def tearDown(self):
         """
@@ -403,3 +446,16 @@
         # WHEN: Retrieving the webkit version
         # THEN: Webkit versions should match
         self.assertEquals(webkit_version(), webkit_ver, "The returned webkit version doesn't match the installed one")
+
+    def test_build_chords_css(self):
+        """
+        Test the build_chords_css() function
+        """
+        # GIVEN: A setting that activates chords on the mainview
+        Settings().setValue('songs/mainview chords', True)
+
+        # WHEN: Building the chord CSS
+        chord_css = build_chords_css()
+
+        # THEN: The build css should look as expected
+        self.assertEqual(CHORD_CSS_ENABLED, chord_css, 'The chord CSS should look as expected')

=== added file 'tests/functional/openlp_plugins/songs/test_chordproimport.py'
--- tests/functional/openlp_plugins/songs/test_chordproimport.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/songs/test_chordproimport.py	2016-11-16 19:47:28 +0000
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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; version 2 of the License.                              #
+#                                                                             #
+# 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, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+This module contains tests for the OpenSong song importer.
+"""
+import os
+
+from tests.helpers.songfileimport import SongImportTestHelper
+from tests.functional import patch, MagicMock
+
+TEST_PATH = os.path.abspath(
+    os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'chordprosongs'))
+
+
+class TestChordProFileImport(SongImportTestHelper):
+
+    def __init__(self, *args, **kwargs):
+        self.importer_class_name = 'ChordProImport'
+        self.importer_module_name = 'chordpro'
+        super(TestChordProFileImport, self).__init__(*args, **kwargs)
+
+    @patch('openlp.plugins.songs.lib.importers.opensong.Settings')
+    def test_song_import(self, mocked_settings):
+        """
+        Test that loading an ChordPro file works correctly on various files
+        """
+        # Mock out the settings - always return False
+        mocked_returned_settings = MagicMock()
+        mocked_returned_settings.value.return_value = False
+        mocked_settings.return_value = mocked_returned_settings
+        # Do the test import
+        self.file_import([os.path.join(TEST_PATH, 'swing-low.chordpro')],
+                         self.load_external_result_data(os.path.join(TEST_PATH, 'swing-low.json')))

=== modified file 'tests/functional/openlp_plugins/songs/test_ewimport.py'
--- tests/functional/openlp_plugins/songs/test_ewimport.py	2016-05-31 21:40:13 +0000
+++ tests/functional/openlp_plugins/songs/test_ewimport.py	2016-11-16 19:47:28 +0000
@@ -116,6 +116,7 @@
         self.field_type = field_type
         self.size = size
 
+
 TEST_DATA_ENCODING = 'cp1252'
 CODE_PAGE_MAPPINGS = [
     (852, 'cp1250'), (737, 'cp1253'), (775, 'cp1257'), (855, 'cp1251'), (857, 'cp1254'),

=== modified file 'tests/functional/openlp_plugins/songs/test_lib.py'
--- tests/functional/openlp_plugins/songs/test_lib.py	2016-05-31 21:40:13 +0000
+++ tests/functional/openlp_plugins/songs/test_lib.py	2016-11-16 19:47:28 +0000
@@ -24,7 +24,7 @@
 """
 from unittest import TestCase
 
-from openlp.plugins.songs.lib import VerseType, clean_string, clean_title, strip_rtf
+from openlp.plugins.songs.lib import VerseType, clean_string, clean_title, strip_rtf, transpose_chord
 from openlp.plugins.songs.lib.songcompare import songs_probably_equal, _remove_typos, _op_length
 from tests.functional import patch, MagicMock, PropertyMock
 
@@ -264,6 +264,32 @@
             # THEN: The stripped text matches thed expected result
             assert result == exp_result, 'The result should be %s' % exp_result
 
+    def transpose_chord_up_test(self):
+        """
+        Test that the transpose_chord() method works when transposing up
+        """
+        # GIVEN: A Chord
+        chord = 'C'
+
+        # WHEN: Transposing it 1 up
+        new_chord = transpose_chord(chord, 1, 'english')
+
+        # THEN: The chord should be transposed up one note
+        self.assertEqual(new_chord, 'C#', 'The chord should be transposed up.')
+
+    def transpose_chord_down_test(self):
+        """
+        Test that the transpose_chord() method works when transposing down
+        """
+        # GIVEN: A Chord
+        chord = 'C'
+
+        # WHEN: Transposing it 1 down
+        new_chord = transpose_chord(chord, -1, 'english')
+
+        # THEN: The chord should be transposed down one note
+        self.assertEqual(new_chord, 'B', 'The chord should be transposed down.')
+
 
 class TestVerseType(TestCase):
     """

=== modified file 'tests/functional/openlp_plugins/songs/test_opensongimport.py'
--- tests/functional/openlp_plugins/songs/test_opensongimport.py	2016-06-07 07:35:06 +0000
+++ tests/functional/openlp_plugins/songs/test_opensongimport.py	2016-11-16 19:47:28 +0000
@@ -42,10 +42,16 @@
         self.importer_module_name = 'opensong'
         super(TestOpenSongFileImport, self).__init__(*args, **kwargs)
 
-    def test_song_import(self):
+    @patch('openlp.plugins.songs.lib.importers.opensong.Settings')
+    def test_song_import(self, mocked_settings):
         """
         Test that loading an OpenSong file works correctly on various files
         """
+        # Mock out the settings - always return False
+        mocked_returned_settings = MagicMock()
+        mocked_returned_settings.value.return_value = False
+        mocked_settings.return_value = mocked_returned_settings
+        # Do the test import
         self.file_import([os.path.join(TEST_PATH, 'Amazing Grace')],
                          self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
         self.file_import([os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer')],

=== modified file 'tests/functional/openlp_plugins/songs/test_songbeamerimport.py'
--- tests/functional/openlp_plugins/songs/test_songbeamerimport.py	2016-05-31 21:40:13 +0000
+++ tests/functional/openlp_plugins/songs/test_songbeamerimport.py	2016-11-16 19:47:28 +0000
@@ -42,10 +42,17 @@
         self.importer_module_name = 'songbeamer'
         super(TestSongBeamerFileImport, self).__init__(*args, **kwargs)
 
-    def test_song_import(self):
+    @patch('openlp.plugins.songs.lib.importers.songbeamer.Settings')
+    def test_song_import(self, mocked_settings):
         """
         Test that loading an OpenSong file works correctly on various files
         """
+        # Mock out the settings - always return False
+        mocked_returned_settings = MagicMock()
+        mocked_returned_settings.value.return_value = False
+        mocked_settings.return_value = mocked_returned_settings
+        self.file_import([os.path.join(TEST_PATH, 'Amazing Grace.sng')],
+                         self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
         self.file_import([os.path.join(TEST_PATH, 'Lobsinget dem Herrn.sng')],
                          self.load_external_result_data(os.path.join(TEST_PATH, 'Lobsinget dem Herrn.json')))
 
@@ -59,6 +66,16 @@
         Create the registry
         """
         Registry.create()
+        self.song_import_patcher = patch('openlp.plugins.songs.lib.importers.songbeamer.SongImport')
+        self.song_import_patcher.start()
+        mocked_manager = MagicMock()
+        self.importer = SongBeamerImport(mocked_manager, filenames=[])
+
+    def tearDown(self):
+        """
+        Clean up
+        """
+        self.song_import_patcher.stop()
 
     def test_create_importer(self):
         """
@@ -78,43 +95,38 @@
         """
         Test SongBeamerImport.do_import handles different invalid import_source values
         """
-        # GIVEN: A mocked out SongImport class, and a mocked out "manager"
-        with patch('openlp.plugins.songs.lib.importers.songbeamer.SongImport'):
-            mocked_manager = MagicMock()
-            mocked_import_wizard = MagicMock()
-            importer = SongBeamerImport(mocked_manager, filenames=[])
-            importer.import_wizard = mocked_import_wizard
-            importer.stop_import_flag = True
-
-            # WHEN: Import source is not a list
-            for source in ['not a list', 0]:
-                importer.import_source = source
-
-                # THEN: do_import should return none and the progress bar maximum should not be set.
-                self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is not a list')
-                self.assertEqual(mocked_import_wizard.progress_bar.setMaximum.called, False,
-                                 'setMaxium on import_wizard.progress_bar should not have been called')
+        # GIVEN: A mocked out import wizard
+        mocked_import_wizard = MagicMock()
+        self.importer.import_wizard = mocked_import_wizard
+        self.importer.stop_import_flag = True
+
+        # WHEN: Import source is not a list
+        for source in ['not a list', 0]:
+            self.importer.import_source = source
+
+            # THEN: do_import should return none and the progress bar maximum should not be set.
+            self.assertIsNone(self.importer.do_import(),
+                              'do_import should return None when import_source is not a list')
+            self.assertEqual(mocked_import_wizard.progress_bar.setMaximum.called, False,
+                             'setMaxium on import_wizard.progress_bar should not have been called')
 
     def test_valid_import_source(self):
         """
         Test SongBeamerImport.do_import handles different invalid import_source values
         """
-        # GIVEN: A mocked out SongImport class, and a mocked out "manager"
-        with patch('openlp.plugins.songs.lib.importers.songbeamer.SongImport'):
-            mocked_manager = MagicMock()
-            mocked_import_wizard = MagicMock()
-            importer = SongBeamerImport(mocked_manager, filenames=[])
-            importer.import_wizard = mocked_import_wizard
-            importer.stop_import_flag = True
-
-            # WHEN: Import source is a list
-            importer.import_source = ['List', 'of', 'files']
-
-            # THEN: do_import should return none and the progress bar setMaximum should be called with the length of
-            #       import_source.
-            self.assertIsNone(importer.do_import(),
-                              'do_import should return None when import_source is a list and stop_import_flag is True')
-            mocked_import_wizard.progress_bar.setMaximum.assert_called_with(len(importer.import_source))
+        # GIVEN: A mocked out import wizard
+        mocked_import_wizard = MagicMock()
+        self.importer.import_wizard = mocked_import_wizard
+        self.importer.stop_import_flag = True
+
+        # WHEN: Import source is a list
+        self.importer.import_source = ['List', 'of', 'files']
+
+        # THEN: do_import should return none and the progress bar setMaximum should be called with the length of
+        #       import_source.
+        self.assertIsNone(self.importer.do_import(),
+                          'do_import should return None when import_source is a list and stop_import_flag is True')
+        mocked_import_wizard.progress_bar.setMaximum.assert_called_with(len(self.importer.import_source))
 
     def test_check_verse_marks(self):
         """
@@ -123,75 +135,76 @@
 
         # GIVEN: line with unnumbered verse-type
         line = 'Refrain'
-        self.current_verse_type = None
+        self.importer.current_verse_type = None
         # WHEN: line is being checked for verse marks
-        result = SongBeamerImport.check_verse_marks(self, line)
-        # THEN: we should get back true and c as self.current_verse_type
+        result = self.importer.check_verse_marks(line)
+        # THEN: we should get back true and c as self.importer.current_verse_type
         self.assertTrue(result, 'Versemark for <Refrain> should be found, value true')
-        self.assertEqual(self.current_verse_type, 'c', '<Refrain> should be interpreted as <c>')
+        self.assertEqual(self.importer.current_verse_type, 'c', '<Refrain> should be interpreted as <c>')
 
         # GIVEN: line with unnumbered verse-type and trailing space
         line = 'ReFrain '
-        self.current_verse_type = None
+        self.importer.current_verse_type = None
         # WHEN: line is being checked for verse marks
-        result = SongBeamerImport.check_verse_marks(self, line)
-        # THEN: we should get back true and c as self.current_verse_type
+        result = self.importer.check_verse_marks(line)
+        # THEN: we should get back true and c as self.importer.current_verse_type
         self.assertTrue(result, 'Versemark for <ReFrain > should be found, value true')
-        self.assertEqual(self.current_verse_type, 'c', '<ReFrain > should be interpreted as <c>')
+        self.assertEqual(self.importer.current_verse_type, 'c', '<ReFrain > should be interpreted as <c>')
 
         # GIVEN: line with numbered verse-type
         line = 'VersE 1'
-        self.current_verse_type = None
+        self.importer.current_verse_type = None
         # WHEN: line is being checked for verse marks
-        result = SongBeamerImport.check_verse_marks(self, line)
-        # THEN: we should get back true and v1 as self.current_verse_type
+        result = self.importer.check_verse_marks(line)
+        # THEN: we should get back true and v1 as self.importer.current_verse_type
         self.assertTrue(result, 'Versemark for <VersE 1> should be found, value true')
-        self.assertEqual(self.current_verse_type, 'v1', u'<VersE 1> should be interpreted as <v1>')
+        self.assertEqual(self.importer.current_verse_type, 'v1', u'<VersE 1> should be interpreted as <v1>')
 
         # GIVEN: line with special unnumbered verse-mark (used in Songbeamer to allow usage of non-supported tags)
         line = '$$M=special'
-        self.current_verse_type = None
+        self.importer.current_verse_type = None
         # WHEN: line is being checked for verse marks
-        result = SongBeamerImport.check_verse_marks(self, line)
-        # THEN: we should get back true and o as self.current_verse_type
+        result = self.importer.check_verse_marks(line)
+        # THEN: we should get back true and o as self.importer.current_verse_type
         self.assertTrue(result, 'Versemark for <$$M=special> should be found, value true')
-        self.assertEqual(self.current_verse_type, 'o', u'<$$M=special> should be interpreted as <o>')
+        self.assertEqual(self.importer.current_verse_type, 'o', u'<$$M=special> should be interpreted as <o>')
 
         # GIVEN: line with song-text with 3 words
         line = 'Jesus my saviour'
-        self.current_verse_type = None
+        self.importer.current_verse_type = None
         # WHEN: line is being checked for verse marks
-        result = SongBeamerImport.check_verse_marks(self, line)
-        # THEN: we should get back false and none as self.current_verse_type
+        result = self.importer.check_verse_marks(line)
+        # THEN: we should get back false and none as self.importer.current_verse_type
         self.assertFalse(result, 'No versemark for <Jesus my saviour> should be found, value false')
-        self.assertIsNone(self.current_verse_type, '<Jesus my saviour> should be interpreted as none versemark')
+        self.assertIsNone(self.importer.current_verse_type,
+                          '<Jesus my saviour> should be interpreted as none versemark')
 
         # GIVEN: line with song-text with 2 words
         line = 'Praise him'
-        self.current_verse_type = None
+        self.importer.current_verse_type = None
         # WHEN: line is being checked for verse marks
-        result = SongBeamerImport.check_verse_marks(self, line)
-        # THEN: we should get back false and none as self.current_verse_type
+        result = self.importer.check_verse_marks(line)
+        # THEN: we should get back false and none as self.importer.current_verse_type
         self.assertFalse(result, 'No versemark for <Praise him> should be found, value false')
-        self.assertIsNone(self.current_verse_type, '<Praise him> should be interpreted as none versemark')
+        self.assertIsNone(self.importer.current_verse_type, '<Praise him> should be interpreted as none versemark')
 
         # GIVEN: line with only a space (could occur, nothing regular)
         line = ' '
-        self.current_verse_type = None
+        self.importer.current_verse_type = None
         # WHEN: line is being checked for verse marks
-        result = SongBeamerImport.check_verse_marks(self, line)
-        # THEN: we should get back false and none as self.current_verse_type
+        result = self.importer.check_verse_marks(line)
+        # THEN: we should get back false and none as self.importer.current_verse_type
         self.assertFalse(result, 'No versemark for < > should be found, value false')
-        self.assertIsNone(self.current_verse_type, '< > should be interpreted as none versemark')
+        self.assertIsNone(self.importer.current_verse_type, '< > should be interpreted as none versemark')
 
         # GIVEN: blank line (could occur, nothing regular)
         line = ''
-        self.current_verse_type = None
+        self.importer.current_verse_type = None
         # WHEN: line is being checked for verse marks
-        result = SongBeamerImport.check_verse_marks(self, line)
-        # THEN: we should get back false and none as self.current_verse_type
+        result = self.importer.check_verse_marks(line)
+        # THEN: we should get back false and none as self.importer.current_verse_type
         self.assertFalse(result, 'No versemark for <> should be found, value false')
-        self.assertIsNone(self.current_verse_type, '<> should be interpreted as none versemark')
+        self.assertIsNone(self.importer.current_verse_type, '<> should be interpreted as none versemark')
 
     def test_verse_marks_defined_in_lowercase(self):
         """

=== modified file 'tests/functional/openlp_plugins/songs/test_videopsalm.py'
--- tests/functional/openlp_plugins/songs/test_videopsalm.py	2016-07-07 20:47:57 +0000
+++ tests/functional/openlp_plugins/songs/test_videopsalm.py	2016-11-16 19:47:28 +0000
@@ -25,6 +25,7 @@
 import os
 
 from tests.helpers.songfileimport import SongImportTestHelper
+from tests.functional import patch, MagicMock
 
 TEST_PATH = os.path.abspath(
     os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'videopsalmsongs'))
@@ -37,10 +38,16 @@
         self.importer_module_name = 'videopsalm'
         super(TestVideoPsalmFileImport, self).__init__(*args, **kwargs)
 
-    def test_song_import(self):
+    @patch('openlp.plugins.songs.lib.importers.videopsalm.Settings')
+    def test_song_import(self, mocked_settings):
         """
         Test that loading an VideoPsalm file works correctly on various files
         """
+        # Mock out the settings - always return False
+        mocked_returned_settings = MagicMock()
+        mocked_returned_settings.value.return_value = False
+        mocked_settings.return_value = mocked_returned_settings
+        # Do the test import
         self.file_import(os.path.join(TEST_PATH, 'videopsalm-as-safe-a-stronghold.json'),
                          self.load_external_result_data(os.path.join(TEST_PATH, 'as-safe-a-stronghold.json')))
         self.file_import(os.path.join(TEST_PATH, 'videopsalm-as-safe-a-stronghold2.json'),

=== added directory 'tests/resources/chordprosongs'
=== added file 'tests/resources/chordprosongs/swing-low.chordpro'
--- tests/resources/chordprosongs/swing-low.chordpro	1970-01-01 00:00:00 +0000
+++ tests/resources/chordprosongs/swing-low.chordpro	2016-11-16 19:47:28 +0000
@@ -0,0 +1,29 @@
+{title:Swing Low Sweet Chariot}
+{st:Traditional}
+
+{start_of_chorus}
+Swing [D]low, sweet [G]chari[D]ot,
+Comin' for to carry me [A7]home.
+Swing [D7]low, sweet [G]chari[D]ot,
+Comin' for to [A7]carry me [D]home.
+{end_of_chorus}
+
+I looked over Jordan, and what did I see,
+     Comin' for to carry me home.
+A band of angels comin' after me,
+     Comin' for to carry me home.
+
+{c:Chorus}
+
+If you get there before I do,
+     Comin' for to carry me home.
+Just tell my friends that I'm a comin' too.
+     Comin' for to carry me home.
+
+{c:Chorus}
+
+I'm sometimes up and sometimes down,
+     Comin' for to carry me home.
+But still my soul feels heavenly bound.
+     Comin' for to carry me home.
+{c:Chorus}

=== added file 'tests/resources/chordprosongs/swing-low.json'
--- tests/resources/chordprosongs/swing-low.json	1970-01-01 00:00:00 +0000
+++ tests/resources/chordprosongs/swing-low.json	2016-11-16 19:47:28 +0000
@@ -0,0 +1,22 @@
+{
+    "title": "Swing Low Sweet Chariot",
+    "alternative_title": "Traditional",
+    "verses": [
+        [
+            "Swing [D]low, sweet [G]chari[D]ot,\nComin' for to carry me [A7]home.\nSwing [D7]low, sweet [G]chari[D]ot,\nComin' for to [A7]carry me [D]home.",
+            "c"
+        ],
+        [
+            "I looked over Jordan, and what did I see,\n     Comin' for to carry me home.\nA band of angels comin' after me,\n     Comin' for to carry me home.",
+            "v"
+        ],
+        [
+            "If you get there before I do,\n     Comin' for to carry me home.\nJust tell my friends that I'm a comin' too.\n     Comin' for to carry me home.",
+            "v"
+        ],
+        [
+            "I'm sometimes up and sometimes down,\n     Comin' for to carry me home.\nBut still my soul feels heavenly bound.\n     Comin' for to carry me home.",
+            "v"
+        ]
+    ]
+}

=== modified file 'tests/resources/opensongsongs/Amazing Grace without CCLI.json'
--- tests/resources/opensongsongs/Amazing Grace without CCLI.json	2016-06-07 07:35:06 +0000
+++ tests/resources/opensongsongs/Amazing Grace without CCLI.json	2016-11-16 19:47:28 +0000
@@ -19,23 +19,23 @@
     "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.",
+            "A[D]mazing [D7]grace! How [G]sweet the [D]sound!\nThat [Bm]saved a [E]wretch like [A]me![A7]\nI [D]once was [D7]lost, but [G]now am [D]found;\nWas [Bm]blind, but [A]now I [G]see.[D]",
             "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.",
+            "'Twas [D]grace that [D7]taught my [G]heart to [D]fear,\nAnd [Bm]grace my [E]fears re[A]lieved.[A7]\nHow [D]precious [D7]did that [G]grace ap[D]pear,\nThe [Bm]hour I [A]first be[G]lieved.[D]",
             "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.",
+            "The [D]Lord has [D7]promised [G]good to [D]me,\nHis [Bm]Word my [E]hope se[A]cures.[A7]\nHe [D]will my [D7]shield and [G]portion [D]be\nAs [Bm]long as [A]life en[G]dures.[D]",
             "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.",
+            "Thro' [D]many [D7]dangers, [G]toils and [D]snares\nI [Bm]have al[E]ready [A]come.[A7]\n'Tis [D]grace that [D7]brought me [G]safe thus [D]far,\nAnd [Bm]grace will [A]lead me [G]home.[D]",
             "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.",
+            "When [D]we've been [D7]there ten [G]thousand [D]years,\nBright [Bm]shining [E]as the [A]sun,[A7]\nWe've [D]no less [D7]days to [G]sing God's [D]praise,\nThan [Bm]when we [A]first be[G]gun.[D]",
             "v5"
         ]
     ]

=== modified file 'tests/resources/opensongsongs/Amazing Grace.json'
--- tests/resources/opensongsongs/Amazing Grace.json	2014-04-21 15:49:41 +0000
+++ tests/resources/opensongsongs/Amazing Grace.json	2016-11-16 19:47:28 +0000
@@ -19,24 +19,24 @@
     "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.",
+            "A[D]mazing [D7]grace! How [G]sweet the [D]sound!\nThat [Bm]saved a [E]wretch like [A]me![A7]\nI [D]once was [D7]lost, but [G]now am [D]found;\nWas [Bm]blind, but [A]now I [G]see.[D]",
             "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.",
+            "'Twas [D]grace that [D7]taught my [G]heart to [D]fear,\nAnd [Bm]grace my [E]fears re[A]lieved.[A7]\nHow [D]precious [D7]did that [G]grace ap[D]pear,\nThe [Bm]hour I [A]first be[G]lieved.[D]",
             "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.",
+            "The [D]Lord has [D7]promised [G]good to [D]me,\nHis [Bm]Word my [E]hope se[A]cures.[A7]\nHe [D]will my [D7]shield and [G]portion [D]be\nAs [Bm]long as [A]life en[G]dures.[D]",
             "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.",
+            "Thro' [D]many [D7]dangers, [G]toils and [D]snares\nI [Bm]have al[E]ready [A]come.[A7]\n'Tis [D]grace that [D7]brought me [G]safe thus [D]far,\nAnd [Bm]grace will [A]lead me [G]home.[D]",
             "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.",
+            "When [D]we've been [D7]there ten [G]thousand [D]years,\nBright [Bm]shining [E]as the [A]sun,[A7]\nWe've [D]no less [D7]days to [G]sing God's [D]praise,\nThan [Bm]when we [A]first be[G]gun.[D]",
             "v5"
         ]
     ]
-}
\ No newline at end of file
+}

=== added file 'tests/resources/songbeamersongs/Amazing Grace.json'
--- tests/resources/songbeamersongs/Amazing Grace.json	1970-01-01 00:00:00 +0000
+++ tests/resources/songbeamersongs/Amazing Grace.json	2016-11-16 19:47:28 +0000
@@ -0,0 +1,29 @@
+{
+    "authors": [
+        "John Newton"
+    ],
+    "title": "Amazing grace",
+    "verse_order_list": ["v1", "v2", "v3", "v4", "v5"],
+    "verses": [
+        [
+            "[D]Amazing gr[D7]ace how [G]sweet the [D]sound\nThat saved a [E7]wretch like [A7]me\nI [D]once was [D7]lost but [G]now im [D]found\nWas b[E7]lind but no[A7]w i [D]see [A7]\n",
+            "v1"
+        ],
+        [
+            "T'was [D]grace that [D7]taught my [G]heart to [D]fear\nAnd grace my [E7]fears [A7]relieved\nHow [D]precious [D7]did that [G]Grace [D]appear\nThe [E7]hour I [A7]first bel[D]ieved.[A7]\n",
+            "v2"
+        ],
+        [
+            "Through [D]many [D7]dangers, [G]toils and [D]snares\nI have [E7]already [A7]come;\n'Tis [D]Grace that [D7]brought me [G]safe thus [D]far\nand [E7]Grace will [A7]lead me [D]home. [A7]\n",
+            "v3"
+        ],
+        [
+            "When we[D]'ve been here[D7] ten thous[G]and y[D]ears\nBright shining [E7]as the [A7]sun.\nWe've [D]no less [D7]days to s[G]ing God's p[D]raise\nThan w[E7]hen we've [A7]first [D]begun.[A7]\n",
+            "v4"
+        ],
+        [
+            "[D]Amazing gr[D7]ace how [G]sweet the [D]sound\nThat saved a [E7]wretch like [A7]me\nI [D]once was [D7]lost but [G]now im [D]found\nWas b[E7]lind but no[A7]w i [D]see [A]\n",
+            "v5"
+        ]
+    ]
+}

=== added file 'tests/resources/songbeamersongs/Amazing Grace.sng'
--- tests/resources/songbeamersongs/Amazing Grace.sng	1970-01-01 00:00:00 +0000
+++ tests/resources/songbeamersongs/Amazing Grace.sng	2016-11-16 19:47:28 +0000
@@ -0,0 +1,37 @@
+#LangCount=1
+#Title=Amazing grace
+#Chords=MCwwLEQNMTAsMCxENw0xOCwwLEcNMjgsMCxEDTEzLDEsRTcNMjUsMSxBNw0yLDIsRA0xMSwyLEQ3DTIwLDIsRw0yNywyLEQNNSwzLEU3DTE2LDMsQTcNMjAsMyxEDTI0LDMsQTcNNiw1LEQNMTcsNSxENw0yNyw1LEcNMzYsNSxEDTEzLDYsRTcNMTksNixBNw00LDcsRA0xMyw3LEQ3DTIyLDcsRw0yOCw3LEQNNCw4LEU3DTExLDgsQTcNMjAsOCxEDTI2LDgsQTcNOCwxMCxEDTEzLDEwLEQ3DTIyLDEwLEcNMzIsMTAsRA03LDExLEU3DTE1LDExLEE3DTUsMTIsRA0xNiwxMixENw0yNywxMixHDTM3LDEyLEQNNCwxMyxFNw0xNSwxMyxBNw0yMywxMyxEDTI5LDEzLEE3DTcsMTUsRA0yMCwxNSxENw0zMCwxNSxHDTM1LDE1LEQNMTUsMTYsRTcNMjIsMTYsQTcNNiwxNyxEDTE0LDE3LEQ3DTIzLDE3LEcNMzQsMTcsRA02LDE4LEU3DTE2LDE4LEE3DTIyLDE4LEQNMjgsMTgsQTcNMCwyMCxEDTEwLDIwLEQ3DTE4LDIwLEcNMjgsMjAsRA0xMywyMSxFNw0yNSwyMSxBNw0yLDIyLEQNMTEsMjIsRDcNMjAsMjIsRw0yNywyMixEDTUsMjMsRTcNMTYsMjMsQTcNMjAsMjMsRA0yNCwyMyxBDQ==
+#Author=John Newton
+#Editor=SongBeamer 4.37a
+#Version=3
+#VerseOrder=Verse 1,Verse 2,Verse 3,Verse 4,Verse 5
+---
+Verse 1
+Amazing grace how sweet the sound
+That saved a wretch like me
+I once was lost but now im found
+Was blind but now i see
+---
+Verse 2
+T'was grace that taught my heart to fear
+And grace my fears relieved
+How precious did that Grace appear
+The hour I first believed.
+---
+Verse 3
+Through many dangers, toils and snares
+I have already come;
+'Tis Grace that brought me safe thus far
+and Grace will lead me home.
+---
+Verse 4
+When we've been here ten thousand years
+Bright shining as the sun.
+We've no less days to sing God's praise
+Than when we've first begun.
+---
+Verse 5
+Amazing grace how sweet the sound
+That saved a wretch like me
+I once was lost but now im found
+Was blind but now i see


References