From 3230dc71b73c4b5de727daa7d85c3a6e1c4413e9 Mon Sep 17 00:00:00 2001 From: Ivan Holmes Date: Mon, 11 Nov 2019 19:23:30 +0000 Subject: [PATCH] add piano chord chart and piano voicings --- chordsheet/document.py | 11 +- chordsheet/parsers.py | 7 +- chordsheet/render.py | 311 +++++++++++++++++++++++++++++++------ chordsheet/rlStylesheet.py | 60 ------- chordsheet/tableView.py | 6 +- examples/example.xml | 94 ++++++++++- examples/examplelong.xml | 143 +++++++++++++++++ gui.py | 102 ++++++++---- test.py | 6 +- ui/mainwindow.ui | 50 +++--- 10 files changed, 609 insertions(+), 181 deletions(-) create mode 100644 examples/examplelong.xml diff --git a/chordsheet/document.py b/chordsheet/document.py index c636c92..41c2b89 100644 --- a/chordsheet/document.py +++ b/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={ diff --git a/chordsheet/parsers.py b/chordsheet/parsers.py index 7f4f757..6cdc462 100644 --- a/chordsheet/parsers.py +++ b/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 \ No newline at end of file diff --git a/chordsheet/render.py b/chordsheet/render.py index 35ddcd3..733f3ec 100644 --- a/chordsheet/render.py +++ b/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 diff --git a/chordsheet/rlStylesheet.py b/chordsheet/rlStylesheet.py index f561216..44cc1b7 100644 --- a/chordsheet/rlStylesheet.py +++ b/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 \ No newline at end of file diff --git a/chordsheet/tableView.py b/chordsheet/tableView.py index 4cd5e17..09e34f7 100644 --- a/chordsheet/tableView.py +++ b/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) diff --git a/examples/example.xml b/examples/example.xml index 70c62a9..3a6c738 100644 --- a/examples/example.xml +++ b/examples/example.xml @@ -1 +1,93 @@ -Example SongIvan Holmes4Cx,3,2,0,1,0F1,3,3,2,1,1G3,2,0,0,0,3C/G3,3,2,0,1,0Dmx,x,0,2,3,1
16CIntro, strum lightly4C4F8G4C4F8G4C4Dm4C/G4G4C4Contemplation time8CCrescendo until end
\ No newline at end of file + + + Example Song + Ivan Holmes + 4 + + + C7 + x,3,2,0,1,0 + Bb,E,C,G + + + F + 1,3,3,2,1,1 + A,C,F + + + G + 3,2,0,0,0,3 + B,D,G + + + C/G + 3,3,2,0,1,0 + G,C,E + + + Dm + x,x,0,2,3,1 + A,D,F + + +
+ + 16 + C7 + Intro, strum lightly + + + 4 + C7 + + + 4 + F + + + 8 + G + + + 4 + C7 + + + 4 + F + + + 8 + G + + + 4 + C7 + + + 4 + Dm + + + 4 + C/G + + + 4 + G + + + 4 + C7 + + + 4 + Contemplation time + + + 8 + C7 + Crescendo until end + +
+
\ No newline at end of file diff --git a/examples/examplelong.xml b/examples/examplelong.xml new file mode 100644 index 0000000..100838f --- /dev/null +++ b/examples/examplelong.xml @@ -0,0 +1,143 @@ + + + Example Song + Ivan Holmes + 4 + + + C7 + x,3,2,0,1,0 + Bb,E,C,G + + + F + 1,3,3,2,1,1 + A,C,F + + + G + 3,2,0,0,0,3 + B,D,G + + + C/G + 3,3,2,0,1,0 + G,C,E + + + Dm + x,x,0,2,3,1 + A,D,F + + + q7 + x,3,2,0,1,0 + Bb,E,C,G + + + q3 + 1,3,3,2,1,1 + A,C,F + + + q5 + 3,2,0,0,0,3 + B,D,G + + + q/G + 3,3,2,0,1,0 + G,C,E + + + q + x,x,0,2,3,1 + A,D,F + + + yeeq + x,3,2,0,1,0 + Bb,E,C,G + + + quu + 1,3,3,2,1,1 + A,C,F + + + b3 + 3,2,0,0,0,3 + B,D + + + aa + 3,3,2,0,1,0 + G,C,E + + + z + x,x,0,2,3,1 + A,D,F,G,E,B,C + + +
+ + 16 + C7 + Intro, strum lightly + + + 4 + C7 + + + 4 + F + + + 8 + G + + + 4 + C7 + + + 4 + F + + + 8 + G + + + 4 + C7 + + + 4 + Dm + + + 4 + C/G + + + 4 + G + + + 4 + C7 + + + 4 + Contemplation time + + + 8 + C7 + Crescendo until end + +
+
\ No newline at end of file diff --git a/gui.py b/gui.py index c3d83d0..83f8907 100755 --- a/gui.py +++ b/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 diff --git a/test.py b/test.py index f91d85c..f35a214 100644 --- a/test.py +++ b/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') \ No newline at end of file +ren.savePDF('test.pdf') \ No newline at end of file diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui index 6f12a3d..8327c84 100644 --- a/ui/mainwindow.ui +++ b/ui/mainwindow.ui @@ -72,7 +72,7 @@ - 0 + 2 @@ -503,6 +503,30 @@ + + + + Chord name + + + + + + + + 0 + 0 + + + + + + + + Guitar voicing + + + @@ -532,27 +556,13 @@ - - - - - 0 - 0 - - - - - - - - Guitar voicing - - + + - - + + - Chord name + Piano voicing