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.unitWidth = kwargs.get('unitWidth', 10)
self.useIncludedFont = True self.useIncludedFont = True
self.numberPages = True
self.separatorSize = 5*self.unit 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.titleFontSize = 24
self.subtitleFontSize = 18 self.subtitleFontSize = 18
self.creditsFontSize = 12 self.creditsFontSize = 12
@ -190,9 +184,8 @@ class Document:
ET.SubElement(chordElement, "voicing", attrib={ ET.SubElement(chordElement, "voicing", attrib={
'instrument': 'guitar'}).text = ','.join(c.voicings['guitar']) 'instrument': 'guitar'}).text = ','.join(c.voicings['guitar'])
if inst == 'piano': if inst == 'piano':
# return first element of list as feature has not been implemented
ET.SubElement(chordElement, "voicing", attrib={ 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): for n, s in enumerate(self.sectionList):
sectionElement = ET.SubElement(root, "section", attrib={ 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 if len(fingering) == numStrings: # if the fingering is entered in concise format e.g. xx4455
output = list(fingering) output = list(fingering)
else: # if entered in long format e.g. x,x,10,10,11,11 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: if len(output) == numStrings:
return output return output
else: else:
raise Exception("Voicing <{}> is malformed.".format(fingering)) raise Exception("Voicing <{}> is malformed.".format(fingering))
elif instrument == 'piano':
return [parseName(note).upper() for note in fingering.split(",")]
else: else:
return [fingering] return [fingering]
@ -22,7 +24,6 @@ def parseFingering(fingering, instrument):
# dictionary holding text to be replaced in chord names # dictionary holding text to be replaced in chord names
nameReplacements = {"b": "", "#": ""} nameReplacements = {"b": "", "#": ""}
def parseName(chordName): def parseName(chordName):
""" """
Replaces symbols in chord names. Replaces symbols in chord names.
@ -30,4 +31,4 @@ def parseName(chordName):
parsedName = chordName parsedName = chordName
for i, j in nameReplacements.items(): for i, j in nameReplacements.items():
parsedName = parsedName.replace(i, j) 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.pdfgen import canvas
from reportlab.lib.units import mm from reportlab.lib.units import mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle 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 reportlab.platypus import BaseDocTemplate, Spacer, Paragraph, Flowable, Frame, PageTemplate, PageBreak
from chordsheet.document import Block from chordsheet.document import Block
@ -71,9 +72,12 @@ class GuitarChart(Flowable):
self.style = style self.style = style
self.guitarChordList = [ self.guitarChordList = [
c for c in chordList if 'guitar' in c.voicings.keys()] c for c in chordList if 'guitar' in c.voicings.keys()]
self.chartMargin = 15*mm
self.chartMargin = 13*mm
self.nStrings = 6 self.nStrings = 6
self.headingSize = 18
self.stringHzSp = 20*mm
self.stringHzGap = 2*mm
self.stringHeight = 5*mm
self.spaceAfter = self.style.separatorSize self.spaceAfter = self.style.separatorSize
@ -84,21 +88,21 @@ class GuitarChart(Flowable):
def wrap(self, availWidth, availHeight): def wrap(self, availWidth, availHeight):
self.nChords = trunc((availWidth - self.chartMargin - self.nChords = trunc((availWidth - self.chartMargin -
self.style.stringHzGap) / self.style.stringHzSp)
self.stringHzGap) / self.stringHzSp)
# the height of one layer of chart # 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 # only one line needed
if len(self.guitarChordList) <= self.nChords: 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 len(self.guitarChordList) # calculate the width
self.height = self.oneHeight # and its height self.height = self.oneHeight # and its height
# multiple lines needed # multiple lines needed
else: 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.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) return (self.width, self.height)
def draw(self): def draw(self):
@ -106,7 +110,8 @@ class GuitarChart(Flowable):
chartmargin = self.chartMargin chartmargin = self.chartMargin
for count, gcl in enumerate(self.splitChordList(self.guitarChordList, self.nChords)): 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 self.nStrings = 6
fontsize = 12 fontsize = 12
@ -117,41 +122,235 @@ class GuitarChart(Flowable):
for i in range(self.nStrings+1): # i is the string line currently being drawn 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-( 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) # j is which chord (0 is first chord, 1 is 2nd etc)
for j in range(len(stringList[-1])): for j in range(len(stringList[-1])):
currentWidth = canvas.stringWidth(stringList[i][j]) currentWidth = canvas.stringWidth(stringList[i][j])
if j == 0: 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) canvas.line(x, y, x+l, y)
else: 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) canvas.line(x, y, x+l, y)
if j == len(stringList[-1])-1: 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) canvas.line(x, y, x+l, y)
writeText(canvas, self.style, stringList[i][j], fontsize, v_origin-( 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 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): class ChordProgression(Flowable):
""" """
Flowable that draws a chord progression made up of blocks. 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.heading = heading # the title of the section
self.blockList = blockList self.blockList = blockList
self.timeSignature = timeSignature 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 self.spaceAfter = self.style.separatorSize
@ -182,11 +384,9 @@ class ChordProgression(Flowable):
while sum(lengthList) < blockList[i].length: while sum(lengthList) < blockList[i].length:
if blockList[i].length - sum(lengthList) >= maxWidth: if blockList[i].length - sum(lengthList) >= maxWidth:
lengthList.append(maxWidth) lengthList.append(maxWidth)
# print(lengthList)
else: else:
lengthList.append( lengthList.append(
blockList[i].length - sum(lengthList)) blockList[i].length - sum(lengthList))
# print(lengthList)
for l in lengthList: for l in lengthList:
# create a block with the given length # create a block with the given length
@ -218,7 +418,7 @@ class ChordProgression(Flowable):
trunc((availWidth/(self.style.unitWidth*self.style.unit)) / trunc((availWidth/(self.style.unitWidth*self.style.unit)) /
(2*self.timeSignature)) # width of each line, in beats (2*self.timeSignature)) # width of each line, in beats
self.width = self.widthInBeats * self.style.unitWidth * self.style.unit 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 sum([b.length for b in self.blockList]) / self.widthInBeats
return(self.width, self.height) return(self.width, self.height)
@ -227,7 +427,7 @@ class ChordProgression(Flowable):
return [self] return [self]
else: else:
vUnits = trunc( vUnits = trunc(
(availHeight - self.style.beatsHeight) / self.style.unitHeight)
(availHeight - self.beatsHeight) / self.unitHeight)
firstPart, secondPart = self.splitBlockList( firstPart, secondPart = self.splitBlockList(
self.blockList, vUnits * self.widthInBeats) self.blockList, vUnits * self.widthInBeats)
@ -239,7 +439,8 @@ class ChordProgression(Flowable):
canvas = self.canv canvas = self.canv
unitWidth = self.style.unitWidth*self.style.unit 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 h_loc = 0
v_loc = 0 v_loc = 0
@ -248,16 +449,16 @@ class ChordProgression(Flowable):
for u in range(maxWidth+1): for u in range(maxWidth+1):
y = v_origin y = v_origin
x = u*unitWidth
x = u*unitWidth + h_offset
if u % self.timeSignature == 0: if u % self.timeSignature == 0:
l = self.style.beatsHeight
l = self.beatsHeight
else: else:
l = self.style.beatsHeight/2
l = self.beatsHeight/2
canvas.line(x, y, x, y+l) canvas.line(x, y, x, y+l)
if u == maxWidth: # Avoid writing beat number after the final line if u == maxWidth: # Avoid writing beat number after the final line
break break
writeText(canvas, self.style, str((u % self.timeSignature)+1), self.style.beatsFontSize, 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) parsedBlockList = self.wrapBlocks(self.blockList, maxWidth)
@ -265,23 +466,26 @@ class ChordProgression(Flowable):
if h_loc == maxWidth: if h_loc == maxWidth:
v_loc += 1 v_loc += 1
h_loc = 0 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: 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: if b.chord is not None:
writeText(canvas, self.style, b.chord.name, self.style.chordNameFontSize, 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 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 chordsPresent = False
for c in cL: for c in cL:
if 'guitar' in c.voicings.keys():
if inst in c.voicings.keys():
chordsPresent = True chordsPresent = True
break break
return chordsPresent return chordsPresent
@ -294,8 +498,10 @@ class Renderer:
def savePDF(self, pathToPDF): def savePDF(self, pathToPDF):
template = PageTemplate(id='AllPages', frames=[Frame(self.style.leftMargin*mm, self.style.bottomMargin*mm, 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)]) leftPadding=0, bottomPadding=0, rightPadding=0, topPadding=0)])
rlDocList = [] 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: 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)) rlDocList.append(Spacer(0, self.style.separatorSize))
if guitarChartCheck(self.document.chordList):
if instChartCheck(self.document.chordList, 'guitar'):
rlDocList.extend([ rlDocList.extend([
Paragraph('Guitar chord voicings', styles['Heading']), Paragraph('Guitar chord voicings', styles['Heading']),
GuitarChart(self.style, self.document.chordList)]) 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: for s in self.document.sectionList:
rlDocList.append(Paragraph(s.name, styles['Heading'])) rlDocList.append(Paragraph(s.name, styles['Heading']))
# only draw the chord progression if there are blocks # only draw the chord progression if there are blocks

60
chordsheet/rlStylesheet.py

@ -47,64 +47,4 @@ def getStyleSheet(csStyle):
spaceAfter=2*mm) 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 return stylesheet

6
chordsheet/tableView.py

@ -64,7 +64,7 @@ class ChordTableView(MTableView):
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
self.model.setHorizontalHeaderLabels(['Chord', 'Voicing'])
self.model.setHorizontalHeaderLabels(['Chord', 'Guitar voicing', 'Piano voicing'])
def populate(self, cList): def populate(self, cList):
""" """
@ -73,7 +73,9 @@ class ChordTableView(MTableView):
self.model.removeRows(0, self.model.rowCount()) self.model.removeRows(0, self.model.rowCount())
for c in cList: for c in cList:
rowList = [QtGui.QStandardItem(c.name), QtGui.QStandardItem( 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: for item in rowList:
item.setEditable(False) item.setEditable(False)
item.setDropEnabled(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. Reimplement the built in closeEvent to allow asking the user to save.
""" """
self.saveWarning()
if self.saveWarning():
self.close()
def UIFileLoader(self, ui_file): def UIFileLoader(self, ui_file):
""" """
@ -250,6 +251,8 @@ class DocumentWindow(QMainWindow):
self.window.chordTableView.model.item(index.row(), 0).text()) self.window.chordTableView.model.item(index.row(), 0).text())
self.window.guitarVoicingLineEdit.setText( self.window.guitarVoicingLineEdit.setText(
self.window.chordTableView.model.item(index.row(), 1).text()) 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): def sectionClickedAction(self, index):
# set the controls to the values from the selected section # 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)) return settings.setValue(value, os.path.dirname(fullpath))
def menuFileNewAction(self): 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): 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): def openFile(self, filePath):
""" """
@ -410,7 +415,7 @@ class DocumentWindow(QMainWindow):
self.updateDocument() # update the document to catch all changes self.updateDocument() # update the document to catch all changes
if self.lastDoc == self.doc: if self.lastDoc == self.doc:
self.close()
return True
else: else:
wantToSave = UnsavedMessageBox().exec() wantToSave = UnsavedMessageBox().exec()
@ -420,11 +425,13 @@ class DocumentWindow(QMainWindow):
os.path.expanduser("~")), "Chordsheet ML files (*.xml *.cml)") os.path.expanduser("~")), "Chordsheet ML files (*.xml *.cml)")
self.currentFilePath = filePath[0] self.currentFilePath = filePath[0]
self.doc.saveXML(self.currentFilePath) self.doc.saveXML(self.currentFilePath)
self.close()
return True
elif wantToSave == QMessageBox.Discard: elif wantToSave == QMessageBox.Discard:
self.close()
# if cancel or anything else do nothing at all
return True
else:
return False
def guitarVoicingAction(self): def guitarVoicingAction(self):
gdialog = GuitarDialog() gdialog = GuitarDialog()
@ -436,9 +443,11 @@ class DocumentWindow(QMainWindow):
def clearChordLineEdits(self): def clearChordLineEdits(self):
self.window.chordNameLineEdit.clear() self.window.chordNameLineEdit.clear()
self.window.guitarVoicingLineEdit.clear() self.window.guitarVoicingLineEdit.clear()
self.window.pianoVoicingLineEdit.clear()
# necessary on Mojave with PyInstaller (or previous contents will be shown) # necessary on Mojave with PyInstaller (or previous contents will be shown)
self.window.chordNameLineEdit.repaint() self.window.chordNameLineEdit.repaint()
self.window.guitarVoicingLineEdit.repaint() self.window.guitarVoicingLineEdit.repaint()
self.window.pianoVoicingLineEdit.repaint()
def clearSectionLineEdits(self): def clearSectionLineEdits(self):
self.window.sectionNameLineEdit.clear() self.window.sectionNameLineEdit.clear()
@ -475,9 +484,17 @@ class DocumentWindow(QMainWindow):
self.updateChords() self.updateChords()
row = self.window.chordTableView.selectionModel().currentIndex().row() row = self.window.chordTableView.selectionModel().currentIndex().row()
oldName = self.window.chordTableView.model.item(row, 0).text()
self.doc.chordList.pop(row) self.doc.chordList.pop(row)
self.window.chordTableView.populate(self.doc.chordList) 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.clearChordLineEdits()
self.updateChordDict() self.updateChordDict()
@ -488,13 +505,21 @@ class DocumentWindow(QMainWindow):
cName = parseName(self.window.chordNameLineEdit.text()) cName = parseName(self.window.chordNameLineEdit.text())
if cName: if cName:
self.doc.chordList.append(Chord(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: else:
success = True #  chord successfully parsed success = True #  chord successfully parsed
else: else:
@ -514,15 +539,23 @@ class DocumentWindow(QMainWindow):
cName = parseName(self.window.chordNameLineEdit.text()) cName = parseName(self.window.chordNameLineEdit.text())
if cName: if cName:
self.doc.chordList[row].name = 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: else:
success = True
success = True #  chord successfully parsed
else: else:
ChordNameWarningMessageBox().exec() ChordNameWarningMessageBox().exec()
@ -665,6 +698,9 @@ class DocumentWindow(QMainWindow):
if self.window.chordTableView.model.item(i, 1).text(): if self.window.chordTableView.model.item(i, 1).text():
chordTableList[-1].voicings['guitar'] = parseFingering( chordTableList[-1].voicings['guitar'] = parseFingering(
self.window.chordTableView.model.item(i, 1).text(), 'guitar') 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 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'))) 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 = Renderer(doc, style)
ren.savePDF('test.pdf')
ren.savePDF('test.pdf')

50
ui/mainwindow.ui

@ -72,7 +72,7 @@
</size> </size>
</property> </property>
<property name="currentIndex"> <property name="currentIndex">
<number>0</number>
<number>2</number>
</property> </property>
<widget class="QWidget" name="tabWidgetOverview"> <widget class="QWidget" name="tabWidgetOverview">
<attribute name="title"> <attribute name="title">
@ -503,6 +503,30 @@
</item> </item>
<item> <item>
<layout class="QGridLayout" name="chordGridLayout"> <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"> <item row="0" column="1">
<widget class="QLineEdit" name="chordNameLineEdit"> <widget class="QLineEdit" name="chordNameLineEdit">
<property name="sizePolicy"> <property name="sizePolicy">
@ -532,27 +556,13 @@
</property> </property>
</widget> </widget>
</item> </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>
<item row="0" column="0">
<widget class="QLabel" name="chordNameLabel">
<item row="2" column="0">
<widget class="QLabel" name="pianoVoicingLabel">
<property name="text"> <property name="text">
<string>Chord name</string>
<string>Piano voicing</string>
</property> </property>
</widget> </widget>
</item> </item>

Loading…
Cancel
Save