Browse Source

add piano chord chart and piano voicings

master
Ivan Holmes 5 years ago
parent
commit
3230dc71b7
  1. 11
      chordsheet/document.py
  2. 7
      chordsheet/parsers.py
  3. 311
      chordsheet/render.py
  4. 60
      chordsheet/rlStylesheet.py
  5. 6
      chordsheet/tableView.py
  6. 94
      examples/example.xml
  7. 143
      examples/examplelong.xml
  8. 102
      gui.py
  9. 6
      test.py
  10. 50
      ui/mainwindow.ui

11
chordsheet/document.py

@ -22,16 +22,10 @@ class Style:
self.unitWidth = kwargs.get('unitWidth', 10)
self.useIncludedFont = True
self.numberPages = True
self.separatorSize = 5*self.unit
self.stringHzSp = 20*self.unit
self.stringHzGap = 2*self.unit
self.stringHeight = 5*self.unit
self.unitHeight = 20*self.unit
self.beatsHeight = 5*self.unit
self.titleFontSize = 24
self.subtitleFontSize = 18
self.creditsFontSize = 12
@ -190,9 +184,8 @@ class Document:
ET.SubElement(chordElement, "voicing", attrib={
'instrument': 'guitar'}).text = ','.join(c.voicings['guitar'])
if inst == 'piano':
# return first element of list as feature has not been implemented
ET.SubElement(chordElement, "voicing", attrib={
'instrument': 'piano'}).text = c.voicings['piano'][0]
'instrument': 'piano'}).text = ','.join(c.voicings['piano'])
for n, s in enumerate(self.sectionList):
sectionElement = ET.SubElement(root, "section", attrib={

7
chordsheet/parsers.py

@ -10,11 +10,13 @@ def parseFingering(fingering, instrument):
if len(fingering) == numStrings: # if the fingering is entered in concise format e.g. xx4455
output = list(fingering)
else: # if entered in long format e.g. x,x,10,10,11,11
output = [x for x in fingering.split(',')]
output = fingering.split(",")
if len(output) == numStrings:
return output
else:
raise Exception("Voicing <{}> is malformed.".format(fingering))
elif instrument == 'piano':
return [parseName(note).upper() for note in fingering.split(",")]
else:
return [fingering]
@ -22,7 +24,6 @@ def parseFingering(fingering, instrument):
# dictionary holding text to be replaced in chord names
nameReplacements = {"b": "", "#": ""}
def parseName(chordName):
"""
Replaces symbols in chord names.
@ -30,4 +31,4 @@ def parseName(chordName):
parsedName = chordName
for i, j in nameReplacements.items():
parsedName = parsedName.replace(i, j)
return parsedName
return parsedName

311
chordsheet/render.py

@ -6,6 +6,7 @@ from io import BytesIO
from reportlab.pdfgen import canvas
from reportlab.lib.units import mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.colors import black, white
from reportlab.platypus import BaseDocTemplate, Spacer, Paragraph, Flowable, Frame, PageTemplate, PageBreak
from chordsheet.document import Block
@ -71,9 +72,12 @@ class GuitarChart(Flowable):
self.style = style
self.guitarChordList = [
c for c in chordList if 'guitar' in c.voicings.keys()]
self.chartMargin = 15*mm
self.chartMargin = 13*mm
self.nStrings = 6
self.headingSize = 18
self.stringHzSp = 20*mm
self.stringHzGap = 2*mm
self.stringHeight = 5*mm
self.spaceAfter = self.style.separatorSize
@ -84,21 +88,21 @@ class GuitarChart(Flowable):
def wrap(self, availWidth, availHeight):
self.nChords = trunc((availWidth - self.chartMargin -
self.style.stringHzGap) / self.style.stringHzSp)
self.stringHzGap) / self.stringHzSp)
# the height of one layer of chart
self.oneHeight = self.style.stringHeight * (self.nStrings+1)
self.oneHeight = self.stringHeight * (self.nStrings+1)
# only one line needed
if len(self.guitarChordList) <= self.nChords:
self.width = self.chartMargin + self.style.stringHzGap + self.style.stringHzSp * \
self.width = self.chartMargin + self.stringHzGap + self.stringHzSp * \
len(self.guitarChordList) # calculate the width
self.height = self.oneHeight # and its height
# multiple lines needed
else:
self.width = self.chartMargin + self.style.stringHzGap + \
self.style.stringHzSp * self.nChords
self.width = self.chartMargin + self.stringHzGap + \
self.stringHzSp * self.nChords
self.height = self.oneHeight * ceil(len(self.guitarChordList) / self.nChords) + \
(self.style.stringHeight *
trunc(len(self.guitarChordList) / self.nChords))
(self.stringHeight *
trunc(len(self.guitarChordList) / self.nChords))
return (self.width, self.height)
def draw(self):
@ -106,7 +110,8 @@ class GuitarChart(Flowable):
chartmargin = self.chartMargin
for count, gcl in enumerate(self.splitChordList(self.guitarChordList, self.nChords)):
v_origin = self.height - count * (self.oneHeight + self.style.stringHeight)
v_origin = self.height - count * \
(self.oneHeight + self.stringHeight)
self.nStrings = 6
fontsize = 12
@ -117,41 +122,235 @@ class GuitarChart(Flowable):
for i in range(self.nStrings+1): # i is the string line currently being drawn
writeText(canvas, self.style, ['e', 'B', 'G', 'D', 'A', 'E', 'Name'][i], fontsize, v_origin-(
i*self.style.stringHeight), self.width, hpos=chartmargin, align='right')
i*self.stringHeight), self.width, hpos=chartmargin, align='right')
# j is which chord (0 is first chord, 1 is 2nd etc)
for j in range(len(stringList[-1])):
currentWidth = canvas.stringWidth(stringList[i][j])
if j == 0:
x = self.style.stringHzGap + chartmargin
l = self.style.stringHzSp/2 - self.style.stringHzGap - \
((currentWidth/2)) - self.style.stringHzGap
y = v_origin-(self.style.stringHeight*i) - \
self.style.stringHeight/2
x = self.stringHzGap + chartmargin
l = self.stringHzSp/2 - self.stringHzGap - \
((currentWidth/2)) - self.stringHzGap
y = v_origin-(self.stringHeight*i) - \
self.stringHeight/2
canvas.line(x, y, x+l, y)
else:
x = chartmargin + self.style.stringHzSp * \
(j-0.5)+(lastWidth/2+self.style.stringHzGap)
l = self.style.stringHzSp - currentWidth / \
2 - lastWidth/2 - self.style.stringHzGap*2
y = v_origin-(self.style.stringHeight*i) - \
self.style.stringHeight/2
x = chartmargin + self.stringHzSp * \
(j-0.5)+(lastWidth/2+self.stringHzGap)
l = self.stringHzSp - currentWidth / \
2 - lastWidth/2 - self.stringHzGap*2
y = v_origin-(self.stringHeight*i) - \
self.stringHeight/2
canvas.line(x, y, x+l, y)
if j == len(stringList[-1])-1:
x = chartmargin + self.style.stringHzSp * \
(j+0.5) + currentWidth/2 + self.style.stringHzGap
l = self.style.stringHzSp/2 - currentWidth/2 - self.style.stringHzGap
y = v_origin-(self.style.stringHeight*i) - \
self.style.stringHeight/2
x = chartmargin + self.stringHzSp * \
(j+0.5) + currentWidth/2 + self.stringHzGap
l = self.stringHzSp/2 - currentWidth/2 - self.stringHzGap
y = v_origin-(self.stringHeight*i) - \
self.stringHeight/2
canvas.line(x, y, x+l, y)
writeText(canvas, self.style, stringList[i][j], fontsize, v_origin-(
i*self.style.stringHeight), self.width, hpos=chartmargin+self.style.stringHzSp*(j+0.5))
i*self.stringHeight), self.width, hpos=chartmargin+self.stringHzSp*(j+0.5))
lastWidth = currentWidth
class PianoChart(Flowable):
"""
Flowable that draws a series of piano chord charts.
"""
def __init__(self, style, chordList):
self.style = style
self.pianoChordList = [
c for c in chordList if 'piano' in c.voicings.keys()]
self.whiteKeyWidth = 2.5 * mm
self.blackKeyWidth = 1.5 * mm
self.whiteKeyHeight = 10 * mm
self.blackKeyHeight = 5.5 * mm
self.dotRadius = 0.5 * mm
self.chartMargin = 0.7 * mm
self.iconHzSpacing = 5 * mm
self.vSpacing = 2*mm
self.indicatorFontSize = 8
self.chordNameFontSize = 12
self.lineSpacing = 1.15
self.keyDict = {'A': 'white', 'A♯': 'black', 'B': 'white', 'C': 'white', 'C♯': 'black', 'D': 'white',
'D♯': 'black', 'E': 'white', 'F': 'white', 'F♯': 'black', 'G': 'white', 'G♯': 'black'}
self.keyList = list(self.keyDict.keys())
self.spaceAfter = self.style.separatorSize
def wrap(self, availWidth, availHeight):
self.availWidth = availWidth
vUnits = 1
currentWidth = self.chartMargin
widest = 0
for index, c in enumerate(self.pianoChordList):
cKL, vL, fKN, iconWidth = self.calculate(c)
if currentWidth + iconWidth >= availWidth + self.iconHzSpacing:
vUnits += 1
currentWidth = self.chartMargin
else:
currentWidth += self.iconHzSpacing
currentWidth += iconWidth
if currentWidth > widest:
widest = currentWidth
if vUnits == 1:
widest -= self.iconHzSpacing #  chop off the trailing space
self.oneHeight = self.chordNameFontSize * self.lineSpacing + \
self.whiteKeyHeight + self.indicatorFontSize * self.lineSpacing
self.width = widest
self.height = self.oneHeight * vUnits + self.vSpacing * (vUnits - 1)
return (self.width, self.height)
def replaceFlats(self, fingering):
# note name replacements
noteReplacements = {"B♭": "A♯", "D♭": "C♯",
"E♭": "D♯", "G♭": "F♯", "A♭": "G♯"}
parsedFingering = []
for key in fingering:
parsedFingering.append(noteReplacements.get(key, key))
return parsedFingering
def splitChordList(self, chordList, width):
bigList = []
currentList = []
currentWidth = self.chartMargin
for c in self.pianoChordList:
cKL, vL, fKN, iconWidth = self.calculate(c)
if currentWidth + iconWidth >= width + self.iconHzSpacing:
bigList.append(currentList)
currentList = [c]
currentWidth = self.chartMargin
else:
currentList.append(c)
currentWidth += self.iconHzSpacing
currentWidth += iconWidth
bigList.append(currentList)
return bigList
def calculate(self, c):
voicingList = self.replaceFlats(c.voicings['piano'])
# save this as we convert all the flats to sharps, but the user would probably like to see the name they entered...
firstKeyName = c.voicings['piano'][0]
chartKeyList = []
# get the list of keys to be drawn for each chord
for count, note in enumerate(voicingList):
if count == 0:
curIndex = self.keyList.index(note)
if self.keyDict[self.keyList[curIndex-1]] == 'black':
chartKeyList.append(
self.keyList[curIndex-2]) # don't start on a black key
chartKeyList.append(self.keyList[curIndex-1]) # the key before
chartKeyList.append(note)
else:
lastIndex = self.keyList.index(lastNote)
curIndex = self.keyList.index(note)
if curIndex > lastIndex:
chartKeyList.extend(
self.keyList[lastIndex+1:((curIndex+1) % len(self.keyList))])
elif curIndex < lastIndex:
chartKeyList.extend(self.keyList[lastIndex+1:])
chartKeyList.extend(
self.keyList[0:((curIndex+1) % len(self.keyList))])
else:
chartKeyList.append(note)
if count == len(voicingList) - 1:
curIndex = self.keyList.index(note)
chartKeyList.append(
self.keyList[((curIndex+1) % len(self.keyList))])
# don't finish on a black key
if self.keyDict[self.keyList[((curIndex+1) % len(self.keyList))]] == 'black':
chartKeyList.append(
self.keyList[((curIndex+2) % len(self.keyList))])
lastNote = note
iconWidth = sum([self.whiteKeyWidth if self.keyDict[k]
== 'white' else 0 for k in chartKeyList])
return chartKeyList, voicingList, firstKeyName, iconWidth
def draw(self):
canvas = self.canv
for index, cL in enumerate(self.splitChordList(self.pianoChordList, self.width)):
h_offset = self.chartMargin
v_offset = self.height - self.oneHeight * index - self.vSpacing * \
index - self.chordNameFontSize * self.lineSpacing
for c in cL:
chartKeyList, voicingList, firstKeyName, iconWidth = self.calculate(
c)
# draw chord names
canvas.setFont(self.style.font, self.chordNameFontSize)
canvas.drawCentredString(h_offset + iconWidth/2, v_offset+(
0.3*self.chordNameFontSize*self.lineSpacing), c.name)
# draw the keys
count = 0
for key in chartKeyList:
if self.keyDict[key] == 'white':
canvas.rect(h_offset + (count*self.whiteKeyWidth), v_offset -
self.whiteKeyHeight, self.whiteKeyWidth, self.whiteKeyHeight)
count += 1
elif self.keyDict[key] == 'black':
canvas.rect(h_offset + (count*self.whiteKeyWidth) - (self.blackKeyWidth/2),
v_offset-self.blackKeyHeight, self.blackKeyWidth, self.blackKeyHeight, fill=1)
# draw the indicator dots
count = 0
dotCount = 0
for key in chartKeyList:
if self.keyDict[key] == 'white':
count += 1
if len(voicingList) > dotCount and key == voicingList[dotCount]:
hpos = h_offset + \
(count*self.whiteKeyWidth) - \
(self.whiteKeyWidth/2)
if dotCount == 0:
canvas.setFont(self.style.font,
self.indicatorFontSize)
canvas.drawCentredString(
hpos, v_offset - self.whiteKeyHeight*1.3, firstKeyName)
dotCount += 1
canvas.circle(hpos, v_offset - self.whiteKeyHeight + (self.whiteKeyWidth/2),
self.dotRadius, stroke=0, fill=1)
elif self.keyDict[key] == 'black':
if len(voicingList) > dotCount and key == voicingList[dotCount]:
hpos = h_offset + \
(count*self.whiteKeyWidth)
if dotCount == 0:
canvas.setFont(self.style.font,
self.indicatorFontSize)
canvas.drawCentredString(
hpos, v_offset - self.whiteKeyHeight*1.3, firstKeyName)
dotCount += 1
canvas.setFillColor(white)
canvas.circle(hpos, v_offset - self.blackKeyHeight + (self.blackKeyWidth/2),
self.dotRadius, stroke=0, fill=1)
canvas.setFillColor(black)
h_offset += iconWidth + self.iconHzSpacing
class ChordProgression(Flowable):
"""
Flowable that draws a chord progression made up of blocks.
@ -162,7 +361,10 @@ class ChordProgression(Flowable):
self.heading = heading # the title of the section
self.blockList = blockList
self.timeSignature = timeSignature
self.headingSize = 18
self.chartMargin = 0.7*mm # kludge factor to account for line width
self.unitHeight = 20*mm
self.beatsHeight = 5*mm
self.spaceAfter = self.style.separatorSize
@ -182,11 +384,9 @@ class ChordProgression(Flowable):
while sum(lengthList) < blockList[i].length:
if blockList[i].length - sum(lengthList) >= maxWidth:
lengthList.append(maxWidth)
# print(lengthList)
else:
lengthList.append(
blockList[i].length - sum(lengthList))
# print(lengthList)
for l in lengthList:
# create a block with the given length
@ -218,7 +418,7 @@ class ChordProgression(Flowable):
trunc((availWidth/(self.style.unitWidth*self.style.unit)) /
(2*self.timeSignature)) # width of each line, in beats
self.width = self.widthInBeats * self.style.unitWidth * self.style.unit
self.height = self.style.beatsHeight + self.style.unitHeight * \
self.height = self.beatsHeight + self.unitHeight * \
sum([b.length for b in self.blockList]) / self.widthInBeats
return(self.width, self.height)
@ -227,7 +427,7 @@ class ChordProgression(Flowable):
return [self]
else:
vUnits = trunc(
(availHeight - self.style.beatsHeight) / self.style.unitHeight)
(availHeight - self.beatsHeight) / self.unitHeight)
firstPart, secondPart = self.splitBlockList(
self.blockList, vUnits * self.widthInBeats)
@ -239,7 +439,8 @@ class ChordProgression(Flowable):
canvas = self.canv
unitWidth = self.style.unitWidth*self.style.unit
v_origin = self.height - self.style.beatsHeight
v_origin = self.height - self.beatsHeight
h_offset = self.chartMargin
h_loc = 0
v_loc = 0
@ -248,16 +449,16 @@ class ChordProgression(Flowable):
for u in range(maxWidth+1):
y = v_origin
x = u*unitWidth
x = u*unitWidth + h_offset
if u % self.timeSignature == 0:
l = self.style.beatsHeight
l = self.beatsHeight
else:
l = self.style.beatsHeight/2
l = self.beatsHeight/2
canvas.line(x, y, x, y+l)
if u == maxWidth: # Avoid writing beat number after the final line
break
writeText(canvas, self.style, str((u % self.timeSignature)+1), self.style.beatsFontSize,
v_origin+self.style.beatsHeight, self.width, hpos=x+unitWidth/2)
v_origin+self.beatsHeight, self.width, hpos=x+unitWidth/2)
parsedBlockList = self.wrapBlocks(self.blockList, maxWidth)
@ -265,23 +466,26 @@ class ChordProgression(Flowable):
if h_loc == maxWidth:
v_loc += 1
h_loc = 0
canvas.rect(h_loc*unitWidth, v_origin-((v_loc+1)*self.style.unitHeight),
b.length*unitWidth, self.style.unitHeight)
canvas.rect(h_offset+h_loc*unitWidth, v_origin-((v_loc+1)*self.unitHeight),
b.length*unitWidth, self.unitHeight)
if b.notes is not None:
writeText(canvas, self.style, b.notes, self.style.notesFontSize, v_origin-((v_loc+1)*self.style.unitHeight)+(
1.3*self.style.notesFontSize), self.width, hpos=((h_loc+b.length/2)*unitWidth))
v_offset = ((v_loc*self.style.unitHeight) +
self.style.unitHeight/2)-self.style.chordNameFontSize/2
writeText(canvas, self.style, b.notes, self.style.notesFontSize, v_origin-((v_loc+1)*self.unitHeight)+(
1.3*self.style.notesFontSize), self.width, hpos=h_offset+((h_loc+b.length/2)*unitWidth))
v_offset = ((v_loc*self.unitHeight) +
self.unitHeight/2)-self.style.chordNameFontSize/2
if b.chord is not None:
writeText(canvas, self.style, b.chord.name, self.style.chordNameFontSize,
v_origin-v_offset, self.width, hpos=((h_loc+b.length/2)*unitWidth))
v_origin-v_offset, self.width, hpos=h_offset+((h_loc+b.length/2)*unitWidth))
h_loc += b.length
def guitarChartCheck(cL):
def instChartCheck(cL, inst):
"""
Check if a file contains a chord chart for a certain instrument.
"""
chordsPresent = False
for c in cL:
if 'guitar' in c.voicings.keys():
if inst in c.voicings.keys():
chordsPresent = True
break
return chordsPresent
@ -294,8 +498,10 @@ class Renderer:
def savePDF(self, pathToPDF):
template = PageTemplate(id='AllPages', frames=[Frame(self.style.leftMargin*mm, self.style.bottomMargin*mm,
self.style.pageSize[0] - self.style.leftMargin*mm - self.style.rightMargin*mm,
self.style.pageSize[1] - self.style.topMargin*mm - self.style.bottomMargin*mm,
self.style.pageSize[0] - self.style.leftMargin *
mm - self.style.rightMargin*mm,
self.style.pageSize[1] - self.style.topMargin *
mm - self.style.bottomMargin*mm,
leftPadding=0, bottomPadding=0, rightPadding=0, topPadding=0)])
rlDocList = []
@ -325,11 +531,16 @@ class Renderer:
if self.document.title or self.document.subtitle or self.document.composer or self.document.arranger or self.document.tempo:
rlDocList.append(Spacer(0, self.style.separatorSize))
if guitarChartCheck(self.document.chordList):
if instChartCheck(self.document.chordList, 'guitar'):
rlDocList.extend([
Paragraph('Guitar chord voicings', styles['Heading']),
GuitarChart(self.style, self.document.chordList)])
if instChartCheck(self.document.chordList, 'piano'):
rlDocList.extend([
Paragraph('Piano chord voicings', styles['Heading']),
PianoChart(self.style, self.document.chordList)])
for s in self.document.sectionList:
rlDocList.append(Paragraph(s.name, styles['Heading']))
# only draw the chord progression if there are blocks

60
chordsheet/rlStylesheet.py

@ -47,64 +47,4 @@ def getStyleSheet(csStyle):
spaceAfter=2*mm)
)
# stylesheet.add(ParagraphStyle(name='Heading3',
# parent=stylesheet['Normal'],
# fontName = csStyle.font,
# fontSize=12,
# leading=14,
# spaceBefore=12,
# spaceAfter=6),
# alias='h3')
# stylesheet.add(ParagraphStyle(name='Heading4',
# parent=stylesheet['Normal'],
# fontName = csStyle.font,
# fontSize=10,
# leading=12,
# spaceBefore=10,
# spaceAfter=4),
# alias='h4')
# stylesheet.add(ParagraphStyle(name='Heading5',
# parent=stylesheet['Normal'],
# fontName = csStyle.font,
# fontSize=9,
# leading=10.8,
# spaceBefore=8,
# spaceAfter=4),
# alias='h5')
# stylesheet.add(ParagraphStyle(name='Heading6',
# parent=stylesheet['Normal'],
# fontName = csStyle.font,
# fontSize=7,
# leading=8.4,
# spaceBefore=6,
# spaceAfter=2),
# alias='h6')
# stylesheet.add(ParagraphStyle(name='Bullet',
# parent=stylesheet['Normal'],
# firstLineIndent=0,
# spaceBefore=3),
# alias='bu')
# stylesheet.add(ParagraphStyle(name='Definition',
# parent=stylesheet['Normal'],
# firstLineIndent=0,
# leftIndent=36,
# bulletIndent=0,
# spaceBefore=6,
# bulletFontName=csStyle.font),
# alias='df')
# stylesheet.add(ParagraphStyle(name='Code',
# parent=stylesheet['Normal'],
# fontName='Courier',
# fontSize=8,
# leading=8.8,
# firstLineIndent=0,
# leftIndent=36,
# hyphenationLang=''))
return stylesheet

6
chordsheet/tableView.py

@ -64,7 +64,7 @@ class ChordTableView(MTableView):
def __init__(self, parent):
super().__init__(parent)
self.model.setHorizontalHeaderLabels(['Chord', 'Voicing'])
self.model.setHorizontalHeaderLabels(['Chord', 'Guitar voicing', 'Piano voicing'])
def populate(self, cList):
"""
@ -73,7 +73,9 @@ class ChordTableView(MTableView):
self.model.removeRows(0, self.model.rowCount())
for c in cList:
rowList = [QtGui.QStandardItem(c.name), QtGui.QStandardItem(
",".join(c.voicings['guitar'] if 'guitar' in c.voicings.keys() else ""))]
",".join(c.voicings['guitar'] if 'guitar' in c.voicings.keys() else "")),
QtGui.QStandardItem(
",".join(c.voicings['piano'] if 'piano' in c.voicings.keys() else ""))]
for item in rowList:
item.setEditable(False)
item.setDropEnabled(False)

94
examples/example.xml

@ -1 +1,93 @@
<chordsheet><title>Example Song</title><composer>Ivan Holmes</composer><timesignature>4</timesignature><chords><chord><name>C</name><voicing instrument="guitar">x,3,2,0,1,0</voicing></chord><chord><name>F</name><voicing instrument="guitar">1,3,3,2,1,1</voicing></chord><chord><name>G</name><voicing instrument="guitar">3,2,0,0,0,3</voicing></chord><chord><name>C/G</name><voicing instrument="guitar">3,3,2,0,1,0</voicing></chord><chord><name>Dm</name><voicing instrument="guitar">x,x,0,2,3,1</voicing></chord></chords><section name="Example section"><block><length>16</length><chord>C</chord><notes>Intro, strum lightly</notes></block><block><length>4</length><chord>C</chord></block><block><length>4</length><chord>F</chord></block><block><length>8</length><chord>G</chord></block><block><length>4</length><chord>C</chord></block><block><length>4</length><chord>F</chord></block><block><length>8</length><chord>G</chord></block><block><length>4</length><chord>C</chord></block><block><length>4</length><chord>Dm</chord></block><block><length>4</length><chord>C/G</chord></block><block><length>4</length><chord>G</chord></block><block><length>4</length><chord>C</chord></block><block><length>4</length><notes>Contemplation time</notes></block><block><length>8</length><chord>C</chord><notes>Crescendo until end</notes></block></section></chordsheet>
<?xml version="1.0" encoding="UTF-8"?>
<chordsheet>
<title>Example Song</title>
<composer>Ivan Holmes</composer>
<timesignature>4</timesignature>
<chords>
<chord>
<name>C7</name>
<voicing instrument="guitar">x,3,2,0,1,0</voicing>
<voicing instrument="piano">Bb,E,C,G</voicing>
</chord>
<chord>
<name>F</name>
<voicing instrument="guitar">1,3,3,2,1,1</voicing>
<voicing instrument="piano">A,C,F</voicing>
</chord>
<chord>
<name>G</name>
<voicing instrument="guitar">3,2,0,0,0,3</voicing>
<voicing instrument="piano">B,D,G</voicing>
</chord>
<chord>
<name>C/G</name>
<voicing instrument="guitar">3,3,2,0,1,0</voicing>
<voicing instrument="piano">G,C,E</voicing>
</chord>
<chord>
<name>Dm</name>
<voicing instrument="guitar">x,x,0,2,3,1</voicing>
<voicing instrument="piano">A,D,F</voicing>
</chord>
</chords>
<section name="Example section">
<block>
<length>16</length>
<chord>C7</chord>
<notes>Intro, strum lightly</notes>
</block>
<block>
<length>4</length>
<chord>C7</chord>
</block>
<block>
<length>4</length>
<chord>F</chord>
</block>
<block>
<length>8</length>
<chord>G</chord>
</block>
<block>
<length>4</length>
<chord>C7</chord>
</block>
<block>
<length>4</length>
<chord>F</chord>
</block>
<block>
<length>8</length>
<chord>G</chord>
</block>
<block>
<length>4</length>
<chord>C7</chord>
</block>
<block>
<length>4</length>
<chord>Dm</chord>
</block>
<block>
<length>4</length>
<chord>C/G</chord>
</block>
<block>
<length>4</length>
<chord>G</chord>
</block>
<block>
<length>4</length>
<chord>C7</chord>
</block>
<block>
<length>4</length>
<notes>Contemplation time</notes>
</block>
<block>
<length>8</length>
<chord>C7</chord>
<notes>Crescendo until end</notes>
</block>
</section>
</chordsheet>

143
examples/examplelong.xml

@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8"?>
<chordsheet>
<title>Example Song</title>
<composer>Ivan Holmes</composer>
<timesignature>4</timesignature>
<chords>
<chord>
<name>C7</name>
<voicing instrument="guitar">x,3,2,0,1,0</voicing>
<voicing instrument="piano">Bb,E,C,G</voicing>
</chord>
<chord>
<name>F</name>
<voicing instrument="guitar">1,3,3,2,1,1</voicing>
<voicing instrument="piano">A,C,F</voicing>
</chord>
<chord>
<name>G</name>
<voicing instrument="guitar">3,2,0,0,0,3</voicing>
<voicing instrument="piano">B,D,G</voicing>
</chord>
<chord>
<name>C/G</name>
<voicing instrument="guitar">3,3,2,0,1,0</voicing>
<voicing instrument="piano">G,C,E</voicing>
</chord>
<chord>
<name>Dm</name>
<voicing instrument="guitar">x,x,0,2,3,1</voicing>
<voicing instrument="piano">A,D,F</voicing>
</chord>
<chord>
<name>q7</name>
<voicing instrument="guitar">x,3,2,0,1,0</voicing>
<voicing instrument="piano">Bb,E,C,G</voicing>
</chord>
<chord>
<name>q3</name>
<voicing instrument="guitar">1,3,3,2,1,1</voicing>
<voicing instrument="piano">A,C,F</voicing>
</chord>
<chord>
<name>q5</name>
<voicing instrument="guitar">3,2,0,0,0,3</voicing>
<voicing instrument="piano">B,D,G</voicing>
</chord>
<chord>
<name>q/G</name>
<voicing instrument="guitar">3,3,2,0,1,0</voicing>
<voicing instrument="piano">G,C,E</voicing>
</chord>
<chord>
<name>q</name>
<voicing instrument="guitar">x,x,0,2,3,1</voicing>
<voicing instrument="piano">A,D,F</voicing>
</chord>
<chord>
<name>yeeq</name>
<voicing instrument="guitar">x,3,2,0,1,0</voicing>
<voicing instrument="piano">Bb,E,C,G</voicing>
</chord>
<chord>
<name>quu</name>
<voicing instrument="guitar">1,3,3,2,1,1</voicing>
<voicing instrument="piano">A,C,F</voicing>
</chord>
<chord>
<name>b3</name>
<voicing instrument="guitar">3,2,0,0,0,3</voicing>
<voicing instrument="piano">B,D</voicing>
</chord>
<chord>
<name>aa</name>
<voicing instrument="guitar">3,3,2,0,1,0</voicing>
<voicing instrument="piano">G,C,E</voicing>
</chord>
<chord>
<name>z</name>
<voicing instrument="guitar">x,x,0,2,3,1</voicing>
<voicing instrument="piano">A,D,F,G,E,B,C</voicing>
</chord>
</chords>
<section name="Example section">
<block>
<length>16</length>
<chord>C7</chord>
<notes>Intro, strum lightly</notes>
</block>
<block>
<length>4</length>
<chord>C7</chord>
</block>
<block>
<length>4</length>
<chord>F</chord>
</block>
<block>
<length>8</length>
<chord>G</chord>
</block>
<block>
<length>4</length>
<chord>C7</chord>
</block>
<block>
<length>4</length>
<chord>F</chord>
</block>
<block>
<length>8</length>
<chord>G</chord>
</block>
<block>
<length>4</length>
<chord>C7</chord>
</block>
<block>
<length>4</length>
<chord>Dm</chord>
</block>
<block>
<length>4</length>
<chord>C/G</chord>
</block>
<block>
<length>4</length>
<chord>G</chord>
</block>
<block>
<length>4</length>
<chord>C7</chord>
</block>
<block>
<length>4</length>
<notes>Contemplation time</notes>
</block>
<block>
<length>8</length>
<chord>C7</chord>
<notes>Crescendo until end</notes>
</block>
</section>
</chordsheet>

102
gui.py

@ -100,7 +100,8 @@ class DocumentWindow(QMainWindow):
"""
Reimplement the built in closeEvent to allow asking the user to save.
"""
self.saveWarning()
if self.saveWarning():
self.close()
def UIFileLoader(self, ui_file):
"""
@ -250,6 +251,8 @@ class DocumentWindow(QMainWindow):
self.window.chordTableView.model.item(index.row(), 0).text())
self.window.guitarVoicingLineEdit.setText(
self.window.chordTableView.model.item(index.row(), 1).text())
self.window.pianoVoicingLineEdit.setText(
self.window.chordTableView.model.item(index.row(), 2).text())
def sectionClickedAction(self, index):
# set the controls to the values from the selected section
@ -297,21 +300,23 @@ class DocumentWindow(QMainWindow):
return settings.setValue(value, os.path.dirname(fullpath))
def menuFileNewAction(self):
self.doc = Document() #  new document object
# copy this object as reference to check against on quitting
self.lastDoc = copy(self.doc)
#  reset file path (this document hasn't been saved yet)
self.currentFilePath = None
# new renderer
self.renderer = Renderer(self.doc, self.style)
self.UIInitDocument()
self.updatePreview()
if self.saveWarning(): # ask the user if they want to save
self.doc = Document() #  new document object
# copy this object as reference to check against on quitting
self.lastDoc = copy(self.doc)
#  reset file path (this document hasn't been saved yet)
self.currentFilePath = None
# new renderer
self.renderer = Renderer(self.doc, self.style)
self.UIInitDocument()
self.updatePreview()
def menuFileOpenAction(self):
filePath = QFileDialog.getOpenFileName(self.window.tabWidget, 'Open file', self.getPath(
"workingPath"), "Chordsheet ML files (*.xml *.cml)")[0]
if filePath:
self.openFile(filePath)
if self.saveWarning(): # ask the user if they want to save
filePath = QFileDialog.getOpenFileName(self.window.tabWidget, 'Open file', self.getPath(
"workingPath"), "Chordsheet ML files (*.xml *.cml)")[0]
if filePath:
self.openFile(filePath)
def openFile(self, filePath):
"""
@ -410,7 +415,7 @@ class DocumentWindow(QMainWindow):
self.updateDocument() # update the document to catch all changes
if self.lastDoc == self.doc:
self.close()
return True
else:
wantToSave = UnsavedMessageBox().exec()
@ -420,11 +425,13 @@ class DocumentWindow(QMainWindow):
os.path.expanduser("~")), "Chordsheet ML files (*.xml *.cml)")
self.currentFilePath = filePath[0]
self.doc.saveXML(self.currentFilePath)
self.close()
return True
elif wantToSave == QMessageBox.Discard:
self.close()
# if cancel or anything else do nothing at all
return True
else:
return False
def guitarVoicingAction(self):
gdialog = GuitarDialog()
@ -436,9 +443,11 @@ class DocumentWindow(QMainWindow):
def clearChordLineEdits(self):
self.window.chordNameLineEdit.clear()
self.window.guitarVoicingLineEdit.clear()
self.window.pianoVoicingLineEdit.clear()
# necessary on Mojave with PyInstaller (or previous contents will be shown)
self.window.chordNameLineEdit.repaint()
self.window.guitarVoicingLineEdit.repaint()
self.window.pianoVoicingLineEdit.repaint()
def clearSectionLineEdits(self):
self.window.sectionNameLineEdit.clear()
@ -475,9 +484,17 @@ class DocumentWindow(QMainWindow):
self.updateChords()
row = self.window.chordTableView.selectionModel().currentIndex().row()
oldName = self.window.chordTableView.model.item(row, 0).text()
self.doc.chordList.pop(row)
self.window.chordTableView.populate(self.doc.chordList)
# remove the chord if any of the blocks have it attached
for s in self.doc.sectionList:
for b in s.blockList:
if b.chord:
if b.chord.name == oldName:
b.chord = None
self.window.blockTableView.populate(self.currentSection.blockList)
self.clearChordLineEdits()
self.updateChordDict()
@ -488,13 +505,21 @@ class DocumentWindow(QMainWindow):
cName = parseName(self.window.chordNameLineEdit.text())
if cName:
self.doc.chordList.append(Chord(cName))
if self.window.guitarVoicingLineEdit.text():
try:
self.doc.chordList[-1].voicings['guitar'] = parseFingering(
self.window.guitarVoicingLineEdit.text(), 'guitar')
success = True #  chord successfully parsed
except Exception:
VoicingWarningMessageBox().exec() # Voicing is malformed, warn user
if self.window.guitarVoicingLineEdit.text() or self.window.pianoVoicingLineEdit.text():
if self.window.guitarVoicingLineEdit.text():
try:
self.doc.chordList[-1].voicings['guitar'] = parseFingering(
self.window.guitarVoicingLineEdit.text(), 'guitar')
success = True #  chord successfully parsed
except Exception:
VoicingWarningMessageBox().exec() # Voicing is malformed, warn user
if self.window.pianoVoicingLineEdit.text():
try:
self.doc.chordList[-1].voicings['piano'] = parseFingering(
self.window.pianoVoicingLineEdit.text(), 'piano')
success = True #  chord successfully parsed
except Exception:
VoicingWarningMessageBox().exec() # Voicing is malformed, warn user
else:
success = True #  chord successfully parsed
else:
@ -514,15 +539,23 @@ class DocumentWindow(QMainWindow):
cName = parseName(self.window.chordNameLineEdit.text())
if cName:
self.doc.chordList[row].name = cName
if self.window.guitarVoicingLineEdit.text():
try:
self.doc.chordList[row].voicings['guitar'] = parseFingering(
self.window.guitarVoicingLineEdit.text(), 'guitar')
success = True
except Exception:
VoicingWarningMessageBox().exec()
if self.window.guitarVoicingLineEdit.text() or self.window.pianoVoicingLineEdit.text():
if self.window.guitarVoicingLineEdit.text():
try:
self.doc.chordList[-1].voicings['guitar'] = parseFingering(
self.window.guitarVoicingLineEdit.text(), 'guitar')
success = True #  chord successfully parsed
except Exception:
VoicingWarningMessageBox().exec() # Voicing is malformed, warn user
if self.window.pianoVoicingLineEdit.text():
try:
self.doc.chordList[-1].voicings['piano'] = parseFingering(
self.window.pianoVoicingLineEdit.text(), 'piano')
success = True #  chord successfully parsed
except Exception:
VoicingWarningMessageBox().exec() # Voicing is malformed, warn user
else:
success = True
success = True #  chord successfully parsed
else:
ChordNameWarningMessageBox().exec()
@ -665,6 +698,9 @@ class DocumentWindow(QMainWindow):
if self.window.chordTableView.model.item(i, 1).text():
chordTableList[-1].voicings['guitar'] = parseFingering(
self.window.chordTableView.model.item(i, 1).text(), 'guitar')
if self.window.chordTableView.model.item(i, 2).text():
chordTableList[-1].voicings['piano'] = parseFingering(
self.window.chordTableView.model.item(i, 2).text(), 'piano')
self.doc.chordList = chordTableList

6
test.py

@ -8,8 +8,8 @@ from chordsheet.render import Renderer
pdfmetrics.registerFont(TTFont('FreeSans', os.path.join('fonts', 'FreeSans.ttf')))
doc = Document.newFromXML('examples/angela.xml')
style = Style(unitWidth=20)
doc = Document.newFromXML('examples/test.xml')
style = Style(unitWidth=10)
ren = Renderer(doc, style)
ren.savePDF('test.pdf')
ren.savePDF('test.pdf')

50
ui/mainwindow.ui

@ -72,7 +72,7 @@
</size>
</property>
<property name="currentIndex">
<number>0</number>
<number>2</number>
</property>
<widget class="QWidget" name="tabWidgetOverview">
<attribute name="title">
@ -503,6 +503,30 @@
</item>
<item>
<layout class="QGridLayout" name="chordGridLayout">
<item row="0" column="0">
<widget class="QLabel" name="chordNameLabel">
<property name="text">
<string>Chord name</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="guitarVoicingLineEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="guitarVoicingLabel">
<property name="text">
<string>Guitar voicing</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="chordNameLineEdit">
<property name="sizePolicy">
@ -532,27 +556,13 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="guitarVoicingLineEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="guitarVoicingLabel">
<property name="text">
<string>Guitar voicing</string>
</property>
</widget>
<item row="2" column="1">
<widget class="QLineEdit" name="pianoVoicingLineEdit"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="chordNameLabel">
<item row="2" column="0">
<widget class="QLabel" name="pianoVoicingLabel">
<property name="text">
<string>Chord name</string>
<string>Piano voicing</string>
</property>
</widget>
</item>

Loading…
Cancel
Save