From e38c6ebdabd18cbdb3563d8be2d4c73a3a0ff0a6 Mon Sep 17 00:00:00 2001 From: Ivan Holmes Date: Sat, 9 Nov 2019 14:23:03 +0000 Subject: [PATCH] rewrite to use reportlab flowables for rendering --- chordsheet/components/chordprogression.py | 61 --- chordsheet/components/guitarchart.py | 56 --- chordsheet/components/header.py | 4 - chordsheet/document.py | 6 +- chordsheet/parsers.py | 49 +-- chordsheet/primitives.py | 48 --- chordsheet/render.py | 439 ++++++++++++++-------- chordsheet/tableView.py | 34 +- examples/ah.xml | 2 + gui.py | 385 +++++++++++-------- test.py | 12 + 11 files changed, 582 insertions(+), 514 deletions(-) delete mode 100644 chordsheet/components/chordprogression.py delete mode 100644 chordsheet/components/guitarchart.py delete mode 100644 chordsheet/components/header.py delete mode 100644 chordsheet/primitives.py create mode 100644 test.py diff --git a/chordsheet/components/chordprogression.py b/chordsheet/components/chordprogression.py deleted file mode 100644 index 608e123..0000000 --- a/chordsheet/components/chordprogression.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- - -from reportlab.pdfgen import canvas -from reportlab.lib.units import mm -from reportlab.lib.pagesizes import A4 -from reportlab.pdfbase import pdfmetrics -from reportlab.pdfbase.ttfonts import TTFont -from reportlab.graphics.shapes import * - -from graphics.primitives import * - -class ChordProgression: - def __init__(self, currentCanvas, blockList, **kwargs): - self.currentCanvas = currentCanvas - - self.beatsHeight = kwargs.get('beatsHeight', 5*mm) - self.timeFontSize = kwargs.get('timeFontSize', 12) - self.chordNameFontSize = kwargs.get('chordNameFontSize', 18) - self.notesFontSize = kwargs.get('notesFontSize', 12) - self.unitWidth = kwargs.get('unitWidth', 10*mm) - self.unitHeight = kwargs.get('unitHeight', 20*mm) - - self.timeSignature = kwargs.get('timeSignature', 4) - def draw(self): - global cur_pos, margin, pagesize - writeText(self.currentCanvas, "Chord progression", size=18, align="left") - - v_origin = cur_pos + 2*mm + self.beatsHeight - h_origin = margin - - h_loc = 0 - v_loc = 0 - - maxWidth = int((((pagesize[0]-(2*margin))/self.unitWidth)//(self.timeSignature*2))*(self.timeSignature*2)) # use integer division to round maxWidth to nearest multiple of time signature - - for u in range(maxWidth+1): - s = 0 - x = u*self.unitWidth+margin - if u % self.timeSignature == 0: - e = -self.beatsHeight - else: - e = -self.beatsHeight/2 - drawVertLine(self.currentCanvas, s, e, x, h_origin, v_origin) - if u == maxWidth: # Avoid writing beat number after the final line - break - writeText(str((u % self.timeSignature)+1),size=self.timeFontSize, hpos=x+self.unitWidth/2, vpos=v_origin-self.beatsHeight) - - blockList = parseBlockList(self.blockList, maxWidth) - - for b in blockList: - if h_loc == maxWidth: - v_loc += 1 - h_loc = 0 - currentCanvas.rect(h_origin+(h_loc*self.unitWidth), v_origin+(v_loc*self.unitHeight), b[0]*self.unitWidth, self.unitHeight) - if b[2]: - writeText(currentCanvas, b[2], size=self.notesFontSize, hpos=h_origin+((h_loc+b[0]/2)*self.unitWidth), vpos=v_origin+((v_loc+1)*self.unitHeight)-(1.3*self.notesFontSize)) - v_offset = (v_loc*self.unitHeight+self.unitHeight/2)-self.chordNameFontSize/2 - writeText(currentCanvas, parseName(b[1]), size=self.chordNameFontSize, hpos=h_origin+((h_loc+b[0]/2)*self.unitWidth), vpos=v_origin+v_offset) - h_loc += b[0] - - cur_pos = v_origin+(v_loc+1)*self.unitHeight+self.beatsHeight \ No newline at end of file diff --git a/chordsheet/components/guitarchart.py b/chordsheet/components/guitarchart.py deleted file mode 100644 index 28d130a..0000000 --- a/chordsheet/components/guitarchart.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- - -from reportlab.pdfgen import canvas -from reportlab.lib.units import mm -from reportlab.lib.pagesizes import A4 -from reportlab.pdfbase import pdfmetrics -from reportlab.pdfbase.ttfonts import TTFont -from reportlab.graphics.shapes import * - -from graphics.primitives import * - -string_hz_sp = 20*mm -string_hz_gap = 2*mm -string_height = 5*mm - -def guitarChart(currentCanvas, string_hz_sp, string_hz_gap, string_height): - global cur_pos, margin, pagesize - - writeText("Guitar chord voicings", size=18, align="left") - - chartmargin = 15*mm - v_origin = cur_pos + 2*mm - h_origin = margin + chartmargin - nstrings = 6 - fontsize = 12 - - guitarChordList = [[chordList[q].guitar[r] for q in range(len(chordList)) if hasattr(chordList[q], 'guitar')] for r in range(6)] - guitarChordList.append([chordList[q].name for q in range(len(chordList))]) - - for i in range(nstrings+1): # i is the string currently being drawn - writeText(['e','B','G','D','A','E','Name'][i], size=fontsize, hpos=(h_origin), vpos=v_origin+(i*string_height), align='right') - - for j in range(len(ls.chords)): # j is which chord (0 is first chord, 1 is 2nd etc) - if j == 0: - charpos = string_hz_sp/2 - s = string_hz_gap - e = charpos-((c.stringWidth(chordList[i][j])/2)+string_hz_gap) - y = v_origin+(string_height*i)+string_height/2 - drawHorizLine(currentCanvas, s, e, y, h_origin, v_origin) - else: - charpos = string_hz_sp*(j+0.5) - s = charpos-string_hz_sp+(lastWidth/2+string_hz_gap) - e = charpos-((currentCanvas.stringWidth(chordList[i][j])/2)+string_hz_gap) - y = v_origin+(string_height*i)+string_height/2 - drawHorizLine(currentCanvas, s, e, y, h_origin, v_origin) - if j == len(ls.chords)-1: - s = charpos+(currentCanvas.stringWidth(chordList[i][j])/2+string_hz_gap) - e = charpos+string_hz_sp/2 - y = v_origin+(string_height*i)+string_height/2 - drawHorizLine(currentCanvas, s, e, y, h_origin, v_origin) - - writeText(chordList[i][j], size=fontsize, hpos=h_origin+charpos, vpos=v_origin+(i*string_height)) - - lastWidth = currentCanvas.stringWidth(chordList[i][j]) - - cur_pos += (string_height*(nstrings+2)) \ No newline at end of file diff --git a/chordsheet/components/header.py b/chordsheet/components/header.py deleted file mode 100644 index bf2c2fe..0000000 --- a/chordsheet/components/header.py +++ /dev/null @@ -1,4 +0,0 @@ -def header(): - writeText(ls.title.cdata, size=24) - writeText("Composer: {c}".format(c = ls.composer.cdata), size=12) - writeText("Arranger: {a}".format(a = ls.arranger.cdata), size=12) \ No newline at end of file diff --git a/chordsheet/document.py b/chordsheet/document.py index a47a329..5c63088 100644 --- a/chordsheet/document.py +++ b/chordsheet/document.py @@ -29,6 +29,10 @@ class Style: self.unitHeight = 20*self.unit self.beatsHeight = 5*self.unit + self.titleFontSize = 24 + self.subtitleFontSize = 18 + self.creditsFontSize = 12 + self.tempoFontSize = 12 self.notesFontSize = 12 self.chordNameFontSize = 18 self.beatsFontSize = 12 @@ -112,7 +116,7 @@ class Document: self.timeSignature = (int(root.find('timesignature').text) if root.find('timesignature') is not None else defaultTimeSignature) self.tempo = (root.find('tempo').text if root.find('tempo') is not None else None) - def newFromXML(filepath): + def newFromXML(self, filepath): """ Create a new Document object directly from an XML file. """ diff --git a/chordsheet/parsers.py b/chordsheet/parsers.py index 9585b08..7f4f757 100644 --- a/chordsheet/parsers.py +++ b/chordsheet/parsers.py @@ -1,30 +1,33 @@ # -*- coding: utf-8 -*- + def parseFingering(fingering, instrument): - """ - Converts fingerings into the list format that Chord objects understand. - """ - if instrument == 'guitar': - numStrings = 6 - 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(',')] - if len(output) == numStrings: - return output - else: - raise Exception("Voicing <{}> is malformed.".format(fingering)) - else: - return [fingering] + """ + Converts fingerings into the list format that Chord objects understand. + """ + if instrument == 'guitar': + numStrings = 6 + 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(',')] + if len(output) == numStrings: + return output + else: + raise Exception("Voicing <{}> is malformed.".format(fingering)) + else: + return [fingering] + # dictionary holding text to be replaced in chord names -nameReplacements = { "b":"♭", "#":"♯" } +nameReplacements = {"b": "♭", "#": "♯"} + def parseName(chordName): - """ - Replaces symbols in chord names. - """ - parsedName = chordName - for i, j in nameReplacements.items(): - parsedName = parsedName.replace(i, j) - return parsedName \ No newline at end of file + """ + Replaces symbols in chord names. + """ + parsedName = chordName + for i, j in nameReplacements.items(): + parsedName = parsedName.replace(i, j) + return parsedName diff --git a/chordsheet/primitives.py b/chordsheet/primitives.py deleted file mode 100644 index 9c6a934..0000000 --- a/chordsheet/primitives.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- - -from reportlab.pdfgen import canvas -from reportlab.lib.units import mm -from reportlab.graphics.shapes import * - -def writeText(currentCanvas, style, string, size, vpos, **kwargs): - """ - Wrapper function to conveniently write text and return how much vertical space it took up. - """ - margin = style.leftMargin*style.unit - - align = kwargs.get('align', 'centre') - if align == 'centre' or align == 'center': - hpos = kwargs.get('hpos', style.pageSize[0]/2) - elif align == 'left': - hpos = kwargs.get('hpos', margin) - elif align == 'right': - hpos = kwargs.get('hpos', style.pageSize[0]-margin) - spacing = kwargs.get('spacing', style.lineSpacing) - - currentCanvas.setFont(style.font, size) - - if align == 'centre' or align == 'center': - currentCanvas.drawCentredString(hpos, vpos+(0.75*size*spacing),string) - elif align == 'left': - currentCanvas.drawString(hpos, vpos+(0.75*size*spacing),string) - elif align == 'right': - currentCanvas.drawString(hpos-currentCanvas.stringWidth(string), vpos+(0.75*size*spacing),string) - - return size*style.lineSpacing - -def drawHorizLine(currentCanvas, startpoint, endpoint, v_pos, h_origin, v_origin): - """ - Draw a horizontal line on the canvas taking origin point into account. - """ - x1 = h_origin+startpoint - x2 = h_origin+endpoint - currentCanvas.line(x1, v_pos, x2, v_pos) - -def drawVertLine(currentCanvas, startpoint, endpoint, h_pos, h_origin, v_origin): - """ - Draw a vertical line on the canvas taking origin point into account. - """ - y1 = v_origin+startpoint - y2 = v_origin+endpoint - currentCanvas.line(h_pos, y1, h_pos, y2) - \ No newline at end of file diff --git a/chordsheet/render.py b/chordsheet/render.py index 2cec25c..3036d23 100644 --- a/chordsheet/render.py +++ b/chordsheet/render.py @@ -1,171 +1,290 @@ # -*- coding: utf-8 -*- +from math import trunc + from reportlab.pdfgen import canvas from reportlab.lib.units import mm -from reportlab.graphics.shapes import * -from chordsheet.primitives import writeText, drawVertLine, drawHorizLine +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.platypus import BaseDocTemplate, Spacer, Paragraph, Flowable, Frame, PageTemplate + from chordsheet.document import Block + +def writeText(canvas, style, string, size, vpos, width, **kwargs): + """ + Wrapper function to conveniently write text and return how much vertical space it took up. + """ + + align = kwargs.get('align', 'centre') + if align == 'centre' or align == 'center': + hpos = kwargs.get('hpos', width/2) + elif align == 'left': + hpos = kwargs.get('hpos', 0) + elif align == 'right': + hpos = kwargs.get('hpos', width) + spacing = kwargs.get('spacing', style.lineSpacing) + + canvas.setFont(style.font, size) + + if align == 'centre' or align == 'center': + canvas.drawCentredString(hpos, vpos-(0.75*size*spacing), string) + elif align == 'left': + canvas.drawString(hpos, vpos-(0.75*size*spacing), string) + elif align == 'right': + canvas.drawString(hpos-canvas.stringWidth(string), + vpos-(0.75*size*spacing), string) + + return size*style.lineSpacing + + def splitBlocks(blockList, maxWidth): - h_loc = 0 - splitBlockList = [] - for i in range(len(blockList)): - c_orig = blockList[i].chord - n_orig = blockList[i].notes - if h_loc == maxWidth: - h_loc = 0 - if h_loc+blockList[i].length > maxWidth: - lengthList = [maxWidth - h_loc] - while sum(lengthList) < blockList[i].length: - if blockList[i].length - sum(lengthList) >= maxWidth: - lengthList.append(maxWidth) - else: - lengthList.append(blockList[i].length - sum(lengthList)) - - for l in lengthList: - splitBlockList.append(Block(l, chord=c_orig, notes=n_orig)) # create a block with the given length - - h_loc = lengthList[-1] - else: - splitBlockList.append(blockList[i]) - h_loc += blockList[i].length - return splitBlockList - -def guitarChart(currentCanvas, style, chordList, cur_pos): - title_height = writeText(currentCanvas, style, "Guitar chord voicings", 18, cur_pos, align="left") - cur_pos += title_height - - string_hz_sp = style.stringHzSp - string_hz_gap = style.stringHzGap - string_height = style.stringHeight - - margin = style.leftMargin*style.unit - pagesize = style.pageSize - - chartmargin = 15*mm - v_origin = cur_pos + 2*mm - h_origin = margin + chartmargin - nstrings = 6 - fontsize = 12 - - guitarChordList = [[chordList[q].voicings['guitar'][-(r+1)] for q in range(len(chordList)) if 'guitar' in chordList[q].voicings.keys()] for r in range(nstrings)] - guitarChordList.append([chordList[q].name for q in range(len(chordList)) if 'guitar' in chordList[q].voicings.keys()]) - - for i in range(nstrings+1): # i is the string currently being drawn - writeText(currentCanvas, style, ['e','B','G','D','A','E','Name'][i], fontsize, v_origin+(i*string_height), hpos=h_origin, align='right') - - for j in range(len(guitarChordList[-1])): # j is which chord (0 is first chord, 1 is 2nd etc) - if j == 0: - charpos = string_hz_sp/2 - s = string_hz_gap - e = charpos-((currentCanvas.stringWidth(guitarChordList[i][j])/2)+string_hz_gap) - y = v_origin+(string_height*i)+string_height/2 - drawHorizLine(currentCanvas, s, e, y, h_origin, v_origin) - else: - charpos = string_hz_sp*(j+0.5) - s = charpos-string_hz_sp+(lastWidth/2+string_hz_gap) - e = charpos-((currentCanvas.stringWidth(guitarChordList[i][j])/2)+string_hz_gap) - y = v_origin+(string_height*i)+string_height/2 - drawHorizLine(currentCanvas, s, e, y, h_origin, v_origin) - - if j == len(guitarChordList[-1])-1: - s = charpos+(currentCanvas.stringWidth(guitarChordList[i][j])/2+string_hz_gap) - e = charpos+string_hz_sp/2 - y = v_origin+(string_height*i)+string_height/2 - drawHorizLine(currentCanvas, s, e, y, h_origin, v_origin) - - writeText(currentCanvas, style, guitarChordList[i][j], fontsize, v_origin+(i*string_height), hpos=h_origin+charpos) - - lastWidth = currentCanvas.stringWidth(guitarChordList[i][j]) - - return (string_height*(nstrings+1)) + title_height # calculate the height of the block - -def chordProgression(currentCanvas, style, document, cur_pos): - margin = style.leftMargin*style.unit - unitWidth = style.unitWidth*style.unit - pagesize = style.pageSize - - title_height = writeText(currentCanvas, style, "Chord progression", 18, cur_pos, align="left") - cur_pos += title_height - - v_origin = cur_pos + 2*mm + style.beatsHeight - h_origin = margin - - h_loc = 0 - v_loc = 0 - - if (unitWidth * document.timeSignature * 2) >= ((pagesize[0]-(2*margin) + 1)): # adding 1 to allow for rounding errors - raise Exception("Beat width (unitWidth) is too high. It is {current} pt and can be a maximum of {max} pt".format(current = unitWidth, max = ((pagesize[0]-(2*margin)/(document.timeSignature * 2))))) - - maxWidth = int((((pagesize[0]-(2*margin))/unitWidth)//(document.timeSignature*2))*(document.timeSignature*2)) # use integer division to round maxWidth to nearest two bars - - for u in range(maxWidth+1): - s = 0 - x = u*unitWidth+margin - if u % document.timeSignature == 0: - e = -style.beatsHeight - else: - e = -style.beatsHeight/2 - drawVertLine(currentCanvas, s, e, x, h_origin, v_origin) - if u == maxWidth: # Avoid writing beat number after the final line - break - writeText(currentCanvas, style, str((u % document.timeSignature)+1), style.beatsFontSize, v_origin-style.beatsHeight, hpos=x+unitWidth/2) - - parsedBlockList = splitBlocks(document.blockList, maxWidth) - - for b in parsedBlockList: - if h_loc == maxWidth: - v_loc += 1 - h_loc = 0 - currentCanvas.rect(h_origin+(h_loc*unitWidth), v_origin+(v_loc*style.unitHeight), b.length*unitWidth, style.unitHeight) - if b.notes is not None: - writeText(currentCanvas, style, b.notes, style.notesFontSize, v_origin+((v_loc+1)*style.unitHeight)-(1.3*style.notesFontSize), hpos=h_origin+((h_loc+b.length/2)*unitWidth)) - v_offset = ((v_loc*style.unitHeight)+style.unitHeight/2)-style.chordNameFontSize/2 - if b.chord is not None: - writeText(currentCanvas, style, b.chord.name, style.chordNameFontSize, v_origin+v_offset, hpos=h_origin+((h_loc+b.length/2)*unitWidth)) - h_loc += b.length - - return v_origin + (v_loc+1)*style.unitHeight + style.beatsHeight + title_height # calculate the height of the generated chart + h_loc = 0 + splitBlockList = [] + for i in range(len(blockList)): + c_orig = blockList[i].chord + n_orig = blockList[i].notes + if h_loc == maxWidth: + h_loc = 0 + if h_loc+blockList[i].length > maxWidth: + lengthList = [maxWidth - h_loc] + while sum(lengthList) < blockList[i].length: + if blockList[i].length - sum(lengthList) >= maxWidth: + lengthList.append(maxWidth) + else: + lengthList.append(blockList[i].length - sum(lengthList)) + + for l in lengthList: + # create a block with the given length + splitBlockList.append(Block(l, chord=c_orig, notes=n_orig)) + + h_loc = lengthList[-1] + else: + splitBlockList.append(blockList[i]) + h_loc += blockList[i].length + return splitBlockList + + +class GuitarChart(Flowable): + """ + Flowable that draws a guitar chord voicing chart. + """ + + def __init__(self, style, chordList): + self.style = style + self.guitarChordList = [ + c for c in chordList if 'guitar' in c.voicings.keys()] + self.chartMargin = 15*mm + self.nStrings = 6 + self.headingSize = 18 + + self.spaceAfter = self.style.separatorSize * mm + + def wrap(self, availWidth, availHeight): + self.width = self.chartMargin + self.style.stringHzGap + self.style.stringHzSp * \ + (len(self.guitarChordList)) # calculate the width of the flowable + self.height = self.style.stringHeight * \ + (self.nStrings+1) + self.headingSize * \ + self.style.lineSpacing + 2*mm # and its height + return (self.width, self.height) + + def draw(self): + canvas = self.canv + title_height = writeText(canvas, self.style, "Guitar chord voicings", + self.headingSize, self.height, self.width, align="left") + + chartmargin = self.chartMargin + v_origin = self.height - title_height - 2*mm + h_origin = chartmargin + self.nStrings = 6 + fontsize = 12 + + stringList = [ + [c.voicings['guitar'][-(r+1)] for c in self.guitarChordList] for r in range(self.nStrings)] + stringList.append([c.name for c in self.guitarChordList]) + + 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=h_origin, 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 + 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 + 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 + 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)) + + lastWidth = currentWidth + + +class ChordProgression(Flowable): + """ + Flowable that draws a chord progression made up of blocks. + """ + + def __init__(self, style, blockList, timeSignature): + self.style = style + self.blockList = blockList + self.timeSignature = timeSignature + self.headingSize = 18 + + self.spaceAfter = self.style.separatorSize * mm + + def wrap(self, availWidth, availHeight): + self.widthInBeats = 2 * self.timeSignature * \ + 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.headingSize * self.style.lineSpacing + 2 * mm + self.style.beatsHeight + \ + self.style.unitHeight * \ + sum([b.length for b in self.blockList]) / self.widthInBeats + return(self.width, self.height) + + def draw(self): + canvas = self.canv + unitWidth = self.style.unitWidth*self.style.unit + + title_height = writeText(canvas, self.style, "Chord progression", + self.headingSize, self.height, self.width, align="left") + + v_origin = self.height - self.style.beatsHeight - title_height - 2*mm + h_origin = 0 + + h_loc = 0 + v_loc = 0 + + maxWidth = self.widthInBeats + + for u in range(maxWidth+1): + y = v_origin + x = u*unitWidth + if u % self.timeSignature == 0: + l = self.style.beatsHeight + else: + l = self.style.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) + + parsedBlockList = splitBlocks(self.blockList, maxWidth) + + for b in parsedBlockList: + 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) + 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_origin+((h_loc+b.length/2)*unitWidth)) + v_offset = ((v_loc*self.style.unitHeight) + + self.style.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_origin+((h_loc+b.length/2)*unitWidth)) + h_loc += b.length + def guitarChartCheck(cL): - chordsPresent = False - for c in cL: - if 'guitar' in c.voicings.keys(): - chordsPresent = True - break - return chordsPresent + chordsPresent = False + for c in cL: + if 'guitar' in c.voicings.keys(): + chordsPresent = True + break + return chordsPresent + + +class TitleBlock(Flowable): + """ + Flowable that draws the title and other text at the top of the document. + """ + + def __init__(self, style, document): + self.style = style + self.lS = style.lineSpacing + + self.title = document.title + self.subtitle = document.subtitle + self.composer = document.composer + self.arranger = document.arranger + self.tempo = document.tempo + + self.spaceAfter = self.style.separatorSize * mm + + def wrap(self, availWidth, availHeight): + self.width = availWidth + self.height = sum([self.style.titleFontSize * self.lS if self.title else 0, + self.style.subtitleFontSize * self.lS if self.subtitle else 0, + self.style.creditsFontSize * self.lS if self.composer else 0, + self.style.titleFontSize * self.lS if self.arranger else 0, + self.style.tempoFontSize * self.lS if self.tempo else 0]) + return(self.width, self.height) + + def draw(self): + canvas = self.canv + curPos = self.height + + if self.title: + curPos -= writeText(canvas, self.style, + self.title, 24, curPos, self.width) + + if self.subtitle: + curPos -= writeText(canvas, self.style, + self.subtitle, 18, curPos, self.width) + + if self.composer: + curPos -= writeText(canvas, self.style, + "Composer: {c}".format(c=self.composer), 12, curPos, self.width) + + if self.arranger: + curPos -= writeText(canvas, self.style, + "Arranger: {a}".format(a=self.arranger), 12, curPos, self.width) + + if self.tempo: + curPos -= writeText(canvas, self.style, "♩ = {t} bpm".format( + t=self.tempo), 12, curPos, self.width, align="left") + def savePDF(document, style, pathToPDF): - - c = canvas.Canvas(pathToPDF, pagesize=style.pageSize, bottomup=0) - - curPos = style.topMargin*style.unit - - if document.title is not None: - curPos += writeText(c, style, document.title, 24, curPos) - - if document.subtitle is not None: - curPos += writeText(c, style, document.subtitle, 18, curPos) - - if document.composer is not None: - curPos += writeText(c, style, "Composer: {c}".format(c = document.composer), 12, curPos) - - if document.arranger is not None: - curPos += writeText(c, style, "Arranger: {a}".format(a = document.arranger), 12, curPos) - - if document.tempo is not None: - curPos += writeText(c, style, "♩ = {t} bpm".format(t = document.tempo), 12, curPos, align = "left") - - curPos += style.separatorSize*style.unit - - if guitarChartCheck(document.chordList): - curPos += guitarChart(c, style, document.chordList, curPos) - - curPos += style.separatorSize*style.unit - - if document.blockList: - curPos += chordProgression(c, style, document, curPos) - - curPos += style.separatorSize*style.unit - - c.save() \ No newline at end of file + template = PageTemplate(id='AllPages', frames=[Frame(style.leftMargin*mm, style.topMargin*mm, style.pageSize[0] - style.leftMargin*mm*2, style.pageSize[1] - style.topMargin*mm*2, + leftPadding=0, bottomPadding=0, rightPadding=0, topPadding=0)]) + + rlDocList = [] + rlDoc = BaseDocTemplate( + pathToPDF, pagesize=style.pageSize, pageTemplates=[template]) + + if document.title: + rlDocList.append(TitleBlock(style, document)) + + if guitarChartCheck(document.chordList): + rlDocList.append(GuitarChart(style, document.chordList)) + + if document.blockList: + rlDocList.append(ChordProgression( + style, document.blockList, document.timeSignature)) + + rlDoc.build(rlDocList) diff --git a/chordsheet/tableView.py b/chordsheet/tableView.py index 4dfb827..ab6170e 100644 --- a/chordsheet/tableView.py +++ b/chordsheet/tableView.py @@ -1,15 +1,22 @@ -import sys -from PyQt5 import QtWidgets, QtGui, QtCore +from PyQt5 import QtWidgets, QtGui + class MItemModel(QtGui.QStandardItemModel): + """ + Special item model to ensure whole row is moved. + """ def dropMimeData(self, data, action, row, col, parent): """ Always move the entire row, and don't allow column "shifting" """ return super().dropMimeData(data, action, row, 0, parent) - + + class MProxyStyle(QtWidgets.QProxyStyle): + """ + Proxy style to change the appearance of the TableView. + """ def drawPrimitive(self, element, option, painter, widget=None): """ @@ -24,32 +31,36 @@ class MProxyStyle(QtWidgets.QProxyStyle): option = option_new super().drawPrimitive(element, option, painter, widget) + class MTableView(QtWidgets.QTableView): """ Subclass the built in TableView to customise it. """ + def __init__(self, parent): super().__init__(parent) - self.model = MItemModel() + self.model = MItemModel() self.setModel(self.model) self.verticalHeader().hide() self.horizontalHeader().show() self.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Interactive) self.horizontalHeader().setStretchLastSection(True) - + self.setShowGrid(False) - #self.setDragDropMode(self.InternalMove) - #self.setDragDropOverwriteMode(False) + # self.setDragDropMode(self.InternalMove) + # self.setDragDropOverwriteMode(False) # Set our custom style - this draws the drop indicator across the whole row self.setStyle(MProxyStyle()) + class ChordTableView(MTableView): """ Subclass MTableView to add properties just for the chord table. """ + def __init__(self, parent): super().__init__(parent) @@ -61,7 +72,8 @@ 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 ""))] + rowList = [QtGui.QStandardItem(c.name), QtGui.QStandardItem( + ",".join(c.voicings['guitar'] if 'guitar' in c.voicings.keys() else ""))] for item in rowList: item.setEditable(False) item.setDropEnabled(False) @@ -73,6 +85,7 @@ class BlockTableView(MTableView): """ Subclass MTableView to add properties just for the block table. """ + def __init__(self, parent): super().__init__(parent) @@ -84,9 +97,10 @@ class BlockTableView(MTableView): """ self.model.removeRows(0, self.model.rowCount()) for b in bList: - rowList = [QtGui.QStandardItem((b.chord.name if b.chord else "")), QtGui.QStandardItem(str(b.length)), QtGui.QStandardItem(b.notes)] + rowList = [QtGui.QStandardItem((b.chord.name if b.chord else "")), QtGui.QStandardItem( + str(b.length)), QtGui.QStandardItem(b.notes)] for item in rowList: item.setEditable(False) item.setDropEnabled(False) - self.model.appendRow(rowList) \ No newline at end of file + self.model.appendRow(rowList) diff --git a/examples/ah.xml b/examples/ah.xml index 255932c..3415169 100644 --- a/examples/ah.xml +++ b/examples/ah.xml @@ -1,7 +1,9 @@ "African Heritage" + A corroboration Ivan Holmes Ivan Holmes and Joe Buckley + 120 6 diff --git a/gui.py b/gui.py index b84532b..f8ab90e 100755 --- a/gui.py +++ b/gui.py @@ -6,15 +6,19 @@ Created on Wed May 29 00:02:24 2019 @author: ivan """ -import sys, fitz, io, subprocess, os +import sys +import fitz +import io +import subprocess +import os from copy import copy from PyQt5.QtWidgets import QApplication, QAction, QLabel, QDialogButtonBox, QDialog, QFileDialog, QMessageBox, QPushButton, QLineEdit, QCheckBox, QSpinBox, QDoubleSpinBox, QTableWidgetItem, QTabWidget, QComboBox, QWidget, QScrollArea, QMainWindow, QShortcut from PyQt5.QtCore import QFile, QObject, Qt, pyqtSlot, QSettings from PyQt5.QtGui import QPixmap, QImage, QKeySequence from PyQt5 import uic -from chordsheet.tableView import ChordTableView, BlockTableView , MItemModel, MProxyStyle - +from chordsheet.tableView import ChordTableView, BlockTableView + from reportlab.lib.units import mm, cm, inch, pica from reportlab.lib.pagesizes import A4, A5, LETTER, LEGAL from reportlab.pdfbase import pdfmetrics @@ -32,24 +36,30 @@ if getattr(sys, 'frozen', False): else: scriptDir = os.path.abspath(os.path.dirname(os.path.abspath(__file__))) -QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) # enable automatic high DPI scaling on Windows +# enable automatic high DPI scaling on Windows +QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) QApplication.setOrganizationName("Ivan Holmes") QApplication.setOrganizationDomain("ivanholmes.co.uk") QApplication.setApplicationName("Chordsheet") settings = QSettings() -pdfmetrics.registerFont(TTFont('FreeSans', os.path.join(scriptDir, 'fonts', 'FreeSans.ttf'))) +pdfmetrics.registerFont( + TTFont('FreeSans', os.path.join(scriptDir, 'fonts', 'FreeSans.ttf'))) if sys.platform == "darwin": - pdfmetrics.registerFont(TTFont('HelveticaNeue', 'HelveticaNeue.ttc', subfontIndex=0)) + pdfmetrics.registerFont( + TTFont('HelveticaNeue', 'HelveticaNeue.ttc', subfontIndex=0)) # dictionaries for combo boxes -pageSizeDict = {'A4':A4, 'A5':A5, 'Letter':LETTER, 'Legal':LEGAL} -unitDict = {'mm':mm, 'cm':cm, 'inch':inch, 'point':1, 'pica':pica} # point is 1 because reportlab's native unit is points. +pageSizeDict = {'A4': A4, 'A5': A5, 'Letter': LETTER, 'Legal': LEGAL} +# point is 1 because reportlab's native unit is points. +unitDict = {'mm': mm, 'cm': cm, 'inch': inch, 'point': 1, 'pica': pica} + class DocumentWindow(QMainWindow): """ Class for the main window of the application. """ + def __init__(self, doc, style, filename=None): """ Initialisation function for the main window of the application. @@ -59,14 +69,14 @@ class DocumentWindow(QMainWindow): style -- the Style object for the window to use """ super().__init__() - + self.doc = doc self.style = style - + self.lastDoc = copy(self.doc) self.currentFilePath = filename - self.UIFileLoader(str(os.path.join(scriptDir, 'ui','mainwindow.ui'))) + self.UIFileLoader(str(os.path.join(scriptDir, 'ui', 'mainwindow.ui'))) self.UIInitStyle() self.updateChordDict() @@ -77,7 +87,7 @@ class DocumentWindow(QMainWindow): if filename: try: self.openFile(filename) - except: + except Exception: UnreadableMessageBox().exec() def closeEvent(self, event): @@ -85,17 +95,17 @@ class DocumentWindow(QMainWindow): Reimplement the built in closeEvent to allow asking the user to save. """ self.saveWarning() - + def UIFileLoader(self, ui_file): """ Loads the .ui file for this window and connects the UI elements to their actions. """ ui_file = QFile(ui_file) ui_file.open(QFile.ReadOnly) - + self.window = uic.loadUi(ui_file) ui_file.close() - + # link all the UI elements self.window.actionAbout.triggered.connect(self.menuFileAboutAction) @@ -103,7 +113,8 @@ class DocumentWindow(QMainWindow): self.window.actionOpen.triggered.connect(self.menuFileOpenAction) self.window.actionSave.triggered.connect(self.menuFileSaveAction) self.window.actionSave_as.triggered.connect(self.menuFileSaveAsAction) - self.window.actionSave_PDF.triggered.connect(self.menuFileSavePDFAction) + self.window.actionSave_PDF.triggered.connect( + self.menuFileSavePDFAction) self.window.actionPrint.triggered.connect(self.menuFilePrintAction) self.window.actionClose.triggered.connect(self.menuFileCloseAction) self.window.actionUndo.triggered.connect(self.menuEditUndoAction) @@ -125,25 +136,29 @@ class DocumentWindow(QMainWindow): self.window.actionCopy.setShortcut(QKeySequence.Copy) self.window.actionPaste.setShortcut(QKeySequence.Paste) - self.window.pageSizeComboBox.currentIndexChanged.connect(self.pageSizeAction) - self.window.documentUnitsComboBox.currentIndexChanged.connect(self.unitAction) + self.window.pageSizeComboBox.currentIndexChanged.connect( + self.pageSizeAction) + self.window.documentUnitsComboBox.currentIndexChanged.connect( + self.unitAction) + + self.window.includedFontCheckBox.stateChanged.connect( + self.includedFontAction) - self.window.includedFontCheckBox.stateChanged.connect(self.includedFontAction) - self.window.generateButton.clicked.connect(self.generateAction) - self.window.guitarVoicingButton.clicked.connect(self.guitarVoicingAction) + self.window.guitarVoicingButton.clicked.connect( + self.guitarVoicingAction) self.window.addChordButton.clicked.connect(self.addChordAction) self.window.removeChordButton.clicked.connect(self.removeChordAction) self.window.updateChordButton.clicked.connect(self.updateChordAction) - + self.window.addBlockButton.clicked.connect(self.addBlockAction) self.window.removeBlockButton.clicked.connect(self.removeBlockAction) self.window.updateBlockButton.clicked.connect(self.updateBlockAction) self.window.chordTableView.clicked.connect(self.chordClickedAction) self.window.blockTableView.clicked.connect(self.blockClickedAction) - + def UIInitDocument(self): """ Fills the window's fields with the values from its document. @@ -156,56 +171,63 @@ class DocumentWindow(QMainWindow): self.window.arrangerLineEdit.setText(self.doc.arranger) self.window.timeSignatureSpinBox.setValue(self.doc.timeSignature) self.window.tempoLineEdit.setText(self.doc.tempo) - + self.window.chordTableView.populate(self.doc.chordList) self.window.blockTableView.populate(self.doc.blockList) self.updateChordDict() - + def UIInitStyle(self): """ Fills the window's fields with the values from its style. """ self.window.pageSizeComboBox.addItems(list(pageSizeDict.keys())) - self.window.pageSizeComboBox.setCurrentText(list(pageSizeDict.keys())[0]) - + self.window.pageSizeComboBox.setCurrentText( + list(pageSizeDict.keys())[0]) + self.window.documentUnitsComboBox.addItems(list(unitDict.keys())) - self.window.documentUnitsComboBox.setCurrentText(list(unitDict.keys())[0]) - + self.window.documentUnitsComboBox.setCurrentText( + list(unitDict.keys())[0]) + self.window.lineSpacingDoubleSpinBox.setValue(self.style.lineSpacing) - + self.window.leftMarginLineEdit.setText(str(self.style.leftMargin)) self.window.topMarginLineEdit.setText(str(self.style.topMargin)) - + self.window.fontComboBox.setDisabled(True) self.window.includedFontCheckBox.setChecked(True) self.window.beatWidthLineEdit.setText(str(self.style.unitWidth)) - + def pageSizeAction(self, index): self.pageSizeSelected = self.window.pageSizeComboBox.itemText(index) - + def unitAction(self, index): self.unitSelected = self.window.documentUnitsComboBox.itemText(index) - + def includedFontAction(self): if self.window.includedFontCheckBox.isChecked(): self.style.useIncludedFont = True else: self.style.useIncludedFont = False - + def chordClickedAction(self, index): # set the controls to the values from the selected chord - self.window.chordNameLineEdit.setText(self.window.chordTableView.model.item(index.row(), 0).text()) - self.window.guitarVoicingLineEdit.setText(self.window.chordTableView.model.item(index.row(), 1).text()) + self.window.chordNameLineEdit.setText( + self.window.chordTableView.model.item(index.row(), 0).text()) + self.window.guitarVoicingLineEdit.setText( + self.window.chordTableView.model.item(index.row(), 1).text()) def blockClickedAction(self, index): # set the controls to the values from the selected block bChord = self.window.blockTableView.model.item(index.row(), 0).text() - self.window.blockChordComboBox.setCurrentText(bChord if bChord else "None") - self.window.blockLengthLineEdit.setText(self.window.blockTableView.model.item(index.row(), 1).text()) - self.window.blockNotesLineEdit.setText(self.window.blockTableView.model.item(index.row(), 2).text()) + self.window.blockChordComboBox.setCurrentText( + bChord if bChord else "None") + self.window.blockLengthLineEdit.setText( + self.window.blockTableView.model.item(index.row(), 1).text()) + self.window.blockNotesLineEdit.setText( + self.window.blockTableView.model.item(index.row(), 2).text()) - def getPath(self, value): + def getPath(self, value): """ Wrapper for Qt settings to return home directory if no setting exists. """ @@ -218,14 +240,17 @@ class DocumentWindow(QMainWindow): return settings.setValue(value, os.path.dirname(fullpath)) def menuFileNewAction(self): - self.doc = Document() # new document object - self.lastDoc = copy(self.doc) # copy this object as reference to check against on quitting - self.currentFilePath = None # reset file path (this document hasn't been saved yet) + 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 self.UIInitDocument() self.updatePreview() - + def menuFileOpenAction(self): - filePath = QFileDialog.getOpenFileName(self.window.tabWidget, 'Open file', self.getPath("workingPath"), "Chordsheet ML files (*.xml *.cml)")[0] + filePath = QFileDialog.getOpenFileName(self.window.tabWidget, 'Open file', self.getPath( + "workingPath"), "Chordsheet ML files (*.xml *.cml)")[0] if filePath: self.openFile(filePath) @@ -239,18 +264,20 @@ class DocumentWindow(QMainWindow): self.setPath("workingPath", self.currentFilePath) self.UIInitDocument() self.updatePreview() - + def menuFileSaveAction(self): self.updateDocument() if not self.currentFilePath: - filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath("workingPath"), "Chordsheet ML files (*.xml *.cml)")[0] + filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath( + "workingPath"), "Chordsheet ML files (*.xml *.cml)")[0] else: filePath = self.currentFilePath self.saveFile(filePath) - + def menuFileSaveAsAction(self): self.updateDocument() - filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath("workingPath"), "Chordsheet ML files (*.xml *.cml)")[0] + filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath( + "workingPath"), "Chordsheet ML files (*.xml *.cml)")[0] if filePath: self.saveFile(filePath) @@ -262,16 +289,17 @@ class DocumentWindow(QMainWindow): self.doc.saveXML(self.currentFilePath) self.lastDoc = copy(self.doc) self.setPath("workingPath", self.currentFilePath) - self.updateTitleBar() # as we may have a new filename - + self.updateTitleBar() # as we may have a new filename + def menuFileSavePDFAction(self): self.updateDocument() self.updatePreview() - filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath("lastExportPath"), "PDF files (*.pdf)")[0] + filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath( + "lastExportPath"), "PDF files (*.pdf)")[0] if filePath: savePDF(d, s, filePath) self.setPath("lastExportPath", filePath) - + def menuFilePrintAction(self): if sys.platform == "darwin": pass @@ -284,63 +312,64 @@ class DocumentWindow(QMainWindow): self.saveWarning() def menuFileAboutAction(self): - aDialog = AboutDialog() + AboutDialog() def menuEditUndoAction(self): try: - QApplication.focusWidget().undo() # see if the built in widget supports it - except: - pass # if not just fail silently + QApplication.focusWidget().undo() # see if the built in widget supports it + except Exception: + pass #  if not just fail silently def menuEditRedoAction(self): try: QApplication.focusWidget().redo() - except: + except Exception: pass def menuEditCutAction(self): try: QApplication.focusWidget().cut() - except: + except Exception: pass def menuEditCopyAction(self): try: QApplication.focusWidget().copy() - except: + except Exception: pass def menuEditPasteAction(self): try: QApplication.focusWidget().paste() - except: + except Exception: pass def saveWarning(self): """ Function to check if the document has unsaved data in it and offer to save it. """ - 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() else: wantToSave = UnsavedMessageBox().exec() - + if wantToSave == QMessageBox.Save: - if not (self.currentFilePath): - filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', str(os.path.expanduser("~")), "Chordsheet ML files (*.xml *.cml)") + if not self.currentFilePath: + filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', str( + os.path.expanduser("~")), "Chordsheet ML files (*.xml *.cml)") self.currentFilePath = filePath[0] self.doc.saveXML(self.currentFilePath) self.close() - + elif wantToSave == QMessageBox.Discard: self.close() # if cancel or anything else do nothing at all - + def guitarVoicingAction(self): gdialog = GuitarDialog() - + voicing = gdialog.getVoicing() if voicing: self.window.guitarVoicingLineEdit.setText(voicing) @@ -348,31 +377,32 @@ class DocumentWindow(QMainWindow): def clearChordLineEdits(self): self.window.chordNameLineEdit.clear() self.window.guitarVoicingLineEdit.clear() - self.window.chordNameLineEdit.repaint() # 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.guitarVoicingLineEdit.repaint() def updateChordDict(self): """ Updates the dictionary used to generate the Chord menu (on the block tab) """ - self.chordDict = {'None':None} - self.chordDict.update({c.name:c for c in self.doc.chordList}) + self.chordDict = {'None': None} + self.chordDict.update({c.name: c for c in self.doc.chordList}) self.window.blockChordComboBox.clear() self.window.blockChordComboBox.addItems(list(self.chordDict.keys())) def removeChordAction(self): - if self.window.chordTableView.selectionModel().hasSelection(): # check for selection + if self.window.chordTableView.selectionModel().hasSelection(): #  check for selection self.updateChords() - + row = self.window.chordTableView.selectionModel().currentIndex().row() self.doc.chordList.pop(row) self.window.chordTableView.populate(self.doc.chordList) self.clearChordLineEdits() self.updateChordDict() - + def addChordAction(self): - success = False # initialise + success = False # initialise self.updateChords() cName = parseName(self.window.chordNameLineEdit.text()) @@ -380,23 +410,24 @@ class DocumentWindow(QMainWindow): 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: - VoicingWarningMessageBox().exec() # Voicing is malformed, warn user + 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 else: - success = True # chord successfully parsed + success = True #  chord successfully parsed else: - NameWarningMessageBox().exec() # Chord has no name, warn user + NameWarningMessageBox().exec() # Chord has no name, warn user - if success == True: # if chord was parsed properly + if success == True: # if chord was parsed properly self.window.chordTableView.populate(self.doc.chordList) self.clearChordLineEdits() self.updateChordDict() def updateChordAction(self): - success = False # see comments above - if self.window.chordTableView.selectionModel().hasSelection(): # check for selection + success = False # see comments above + if self.window.chordTableView.selectionModel().hasSelection(): #  check for selection self.updateChords() row = self.window.chordTableView.selectionModel().currentIndex().row() cName = parseName(self.window.chordNameLineEdit.text()) @@ -404,71 +435,77 @@ class DocumentWindow(QMainWindow): self.doc.chordList[row] = Chord(cName) if self.window.guitarVoicingLineEdit.text(): try: - self.doc.chordList[row].voicings['guitar'] = parseFingering(self.window.guitarVoicingLineEdit.text(), 'guitar') + self.doc.chordList[row].voicings['guitar'] = parseFingering( + self.window.guitarVoicingLineEdit.text(), 'guitar') success = True - except: + except Exception: VoicingWarningMessageBox().exec() else: success = True else: NameWarningMessageBox().exec() - + if success == True: self.window.chordTableView.populate(self.doc.chordList) self.clearChordLineEdits() self.updateChordDict() - + def clearBlockLineEdits(self): self.window.blockLengthLineEdit.clear() self.window.blockNotesLineEdit.clear() - self.window.blockLengthLineEdit.repaint() # necessary on Mojave with PyInstaller (or previous contents will be shown) + # necessary on Mojave with PyInstaller (or previous contents will be shown) + self.window.blockLengthLineEdit.repaint() self.window.blockNotesLineEdit.repaint() def removeBlockAction(self): - if self.window.blockTableView.selectionModel().hasSelection(): # check for selection + if self.window.blockTableView.selectionModel().hasSelection(): #  check for selection self.updateBlocks() row = self.window.blockTableView.selectionModel().currentIndex().row() self.doc.blockList.pop(row) self.window.blockTableView.populate(self.doc.blockList) - + def addBlockAction(self): self.updateBlocks() try: - bLength = int(self.window.blockLengthLineEdit.text()) # can the value entered for block length be cast as an integer - except: + #  can the value entered for block length be cast as an integer + bLength = int(self.window.blockLengthLineEdit.text()) + except Exception: bLength = False - if bLength: # create the block + if bLength: # create the block self.doc.blockList.append(Block(bLength, - chord = self.chordDict[self.window.blockChordComboBox.currentText()], - notes = (self.window.blockNotesLineEdit.text() if not "" else None))) + chord=self.chordDict[self.window.blockChordComboBox.currentText( + )], + notes=(self.window.blockNotesLineEdit.text() if not "" else None))) self.window.blockTableView.populate(self.doc.blockList) self.clearBlockLineEdits() else: - LengthWarningMessageBox().exec() # show warning that length was not entered or in wrong format + # show warning that length was not entered or in wrong format + LengthWarningMessageBox().exec() def updateBlockAction(self): - if self.window.blockTableView.selectionModel().hasSelection(): # check for selection + if self.window.blockTableView.selectionModel().hasSelection(): #  check for selection self.updateBlocks() try: bLength = int(self.window.blockLengthLineEdit.text()) - except: + except Exception: bLength = False row = self.window.blockTableView.selectionModel().currentIndex().row() if bLength: self.doc.blockList[row] = (Block(bLength, - chord = self.chordDict[self.window.blockChordComboBox.currentText()], - notes = (self.window.blockNotesLineEdit.text() if not "" else None))) + chord=self.chordDict[self.window.blockChordComboBox.currentText( + )], + notes=(self.window.blockNotesLineEdit.text() if not "" else None))) self.window.blockTableView.populate(self.doc.blockList) self.clearBlockLineEdits() else: LengthWarningMessageBox().exec() - + def generateAction(self): self.updateDocument() self.updatePreview() @@ -482,23 +519,28 @@ class DocumentWindow(QMainWindow): savePDF(self.doc, self.style, self.currentPreview) pdfView = fitz.Document(stream=self.currentPreview, filetype='pdf') - pix = pdfView[0].getPixmap(matrix = fitz.Matrix(4, 4), alpha = False) # render at 4x resolution and scale + # render at 4x resolution and scale + pix = pdfView[0].getPixmap(matrix=fitz.Matrix(4, 4), alpha=False) fmt = QImage.Format_RGB888 qtimg = QImage(pix.samples, pix.width, pix.height, pix.stride, fmt) - self.window.imageLabel.setPixmap(QPixmap.fromImage(qtimg).scaled(self.window.scrollArea.width()-30, self.window.scrollArea.height()-30, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)) + self.window.imageLabel.setPixmap(QPixmap.fromImage(qtimg).scaled(self.window.scrollArea.width( + )-30, self.window.scrollArea.height()-30, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)) # -30 because the scrollarea has a margin of 12 each side (extra for safety) - self.window.imageLabel.repaint() # necessary on Mojave with PyInstaller (or previous contents will be shown) - except: - warning = QMessageBox.warning(self, "Preview failed", "Could not update the preview.", buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok) - + # necessary on Mojave with PyInstaller (or previous contents will be shown) + self.window.imageLabel.repaint() + except Exception: + QMessageBox.warning(self, "Preview failed", "Could not update the preview.", + buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok) + def updateTitleBar(self): """ Update the application's title bar to reflect the current document. """ if self.currentFilePath: - self.setWindowTitle(_version.appName + " – " + os.path.basename(self.currentFilePath)) + self.setWindowTitle(_version.appName + " – " + + os.path.basename(self.currentFilePath)) else: self.setWindowTitle(_version.appName) @@ -508,69 +550,91 @@ class DocumentWindow(QMainWindow): """ chordTableList = [] for i in range(self.window.chordTableView.model.rowCount()): - chordTableList.append(Chord(parseName(self.window.chordTableView.model.item(i, 0).text()))), + chordTableList.append( + Chord(parseName(self.window.chordTableView.model.item(i, 0).text()))), if self.window.chordTableView.model.item(i, 1).text(): - chordTableList[-1].voicings['guitar'] = parseFingering(self.window.chordTableView.model.item(i, 1).text(), 'guitar') - + chordTableList[-1].voicings['guitar'] = parseFingering( + self.window.chordTableView.model.item(i, 1).text(), 'guitar') + self.doc.chordList = chordTableList - + def updateBlocks(self): """ Update the block list by reading the table. """ blockTableList = [] for i in range(self.window.blockTableView.model.rowCount()): - blockLength = int(self.window.blockTableView.model.item(i, 1).text()) - blockChord = self.chordDict[(self.window.blockTableView.model.item(i, 0).text() if self.window.blockTableView.model.item(i, 0).text() else "None")] - blockNotes = self.window.blockTableView.model.item(i, 2).text() if self.window.blockTableView.model.item(i, 2).text() else None - blockTableList.append(Block(blockLength, chord=blockChord, notes=blockNotes)) - + blockLength = int( + self.window.blockTableView.model.item(i, 1).text()) + blockChord = self.chordDict[(self.window.blockTableView.model.item( + i, 0).text() if self.window.blockTableView.model.item(i, 0).text() else "None")] + blockNotes = self.window.blockTableView.model.item(i, 2).text( + ) if self.window.blockTableView.model.item(i, 2).text() else None + blockTableList.append( + Block(blockLength, chord=blockChord, notes=blockNotes)) + self.doc.blockList = blockTableList def updateDocument(self): """ Update the Document object by reading values from the UI. """ - self.doc.title = self.window.titleLineEdit.text() # Title can be empty string but not None - self.doc.subtitle = (self.window.subtitleLineEdit.text() if self.window.subtitleLineEdit.text() else None) - self.doc.composer = (self.window.composerLineEdit.text() if self.window.composerLineEdit.text() else None) - self.doc.arranger = (self.window.arrangerLineEdit.text() if self.window.arrangerLineEdit.text() else None) - self.doc.tempo = (self.window.tempoLineEdit.text() if self.window.tempoLineEdit.text() else None) - self.doc.timeSignature = int(self.window.timeSignatureSpinBox.value()) if self.window.timeSignatureSpinBox.value() else self.doc.timeSignature + self.doc.title = self.window.titleLineEdit.text( + ) # Title can be empty string but not None + self.doc.subtitle = (self.window.subtitleLineEdit.text( + ) if self.window.subtitleLineEdit.text() else None) + self.doc.composer = (self.window.composerLineEdit.text( + ) if self.window.composerLineEdit.text() else None) + self.doc.arranger = (self.window.arrangerLineEdit.text( + ) if self.window.arrangerLineEdit.text() else None) + self.doc.tempo = (self.window.tempoLineEdit.text() + if self.window.tempoLineEdit.text() else None) + self.doc.timeSignature = int(self.window.timeSignatureSpinBox.value( + )) if self.window.timeSignatureSpinBox.value() else self.doc.timeSignature self.style.pageSize = pageSizeDict[self.pageSizeSelected] self.style.unit = unitDict[self.unitSelected] - self.style.leftMargin = float(self.window.leftMarginLineEdit.text()) if self.window.leftMarginLineEdit.text() else self.style.leftMargin - self.style.topMargin = float(self.window.topMarginLineEdit.text()) if self.window.topMarginLineEdit.text() else self.style.topMargin - self.style.lineSpacing = float(self.window.lineSpacingDoubleSpinBox.value()) if self.window.lineSpacingDoubleSpinBox.value() else self.style.lineSpacing + self.style.leftMargin = float(self.window.leftMarginLineEdit.text( + )) if self.window.leftMarginLineEdit.text() else self.style.leftMargin + self.style.topMargin = float(self.window.topMarginLineEdit.text( + )) if self.window.topMarginLineEdit.text() else self.style.topMargin + self.style.lineSpacing = float(self.window.lineSpacingDoubleSpinBox.value( + )) if self.window.lineSpacingDoubleSpinBox.value() else self.style.lineSpacing # make sure the unit width isn't too wide to draw! if self.window.beatWidthLineEdit.text(): if (self.style.pageSize[0] - 2 * self.style.leftMargin * mm) >= (float(self.window.beatWidthLineEdit.text()) * 2 * self.doc.timeSignature * mm): - self.style.unitWidth = float(self.window.beatWidthLineEdit.text()) + self.style.unitWidth = float( + self.window.beatWidthLineEdit.text()) else: - maxBeatWidth = (self.style.pageSize[0] - 2 * self.style.leftMargin * mm) / (2 * self.doc.timeSignature * mm) - warning = QMessageBox.warning(self, "Out of range", "Beat width is out of range. It can be a maximum of {}.".format(maxBeatWidth), buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok) + maxBeatWidth = ( + self.style.pageSize[0] - 2 * self.style.leftMargin * mm) / (2 * self.doc.timeSignature * mm) + QMessageBox.warning(self, "Out of range", "Beat width is out of range. It can be a maximum of {}.".format( + maxBeatWidth), buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok) self.updateChords() self.updateBlocks() - - self.style.font = ('FreeSans' if self.style.useIncludedFont else 'HelveticaNeue') + + self.style.font = ( + 'FreeSans' if self.style.useIncludedFont else 'HelveticaNeue') # something for the font box here - + + class GuitarDialog(QDialog): """ Dialogue to allow the user to enter a guitar chord voicing. Not particularly advanced at present! May be extended in future. """ + def __init__(self): super().__init__() - self.UIFileLoader(str(os.path.join(scriptDir, 'ui','guitardialog.ui'))) - + self.UIFileLoader( + str(os.path.join(scriptDir, 'ui', 'guitardialog.ui'))) + def UIFileLoader(self, ui_file): ui_file = QFile(ui_file) ui_file.open(QFile.ReadOnly) - + self.dialog = uic.loadUi(ui_file) ui_file.close() @@ -590,32 +654,37 @@ class GuitarDialog(QDialog): else: return None + class AboutDialog(QDialog): """ Dialogue showing information about the program. """ + def __init__(self): super().__init__() - self.UIFileLoader(str(os.path.join(scriptDir, 'ui','aboutdialog.ui'))) - - icon = QImage(str(os.path.join(scriptDir, 'ui','icon.png'))) - self.dialog.iconLabel.setPixmap(QPixmap.fromImage(icon).scaled(self.dialog.iconLabel.width(), self.dialog.iconLabel.height(), Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)) + self.UIFileLoader(str(os.path.join(scriptDir, 'ui', 'aboutdialog.ui'))) + + icon = QImage(str(os.path.join(scriptDir, 'ui', 'icon.png'))) + self.dialog.iconLabel.setPixmap(QPixmap.fromImage(icon).scaled(self.dialog.iconLabel.width( + ), self.dialog.iconLabel.height(), Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)) self.dialog.versionLabel.setText("Version " + _version.version) self.dialog.exec() - + def UIFileLoader(self, ui_file): ui_file = QFile(ui_file) ui_file.open(QFile.ReadOnly) - + self.dialog = uic.loadUi(ui_file) ui_file.close() + class UnsavedMessageBox(QMessageBox): """ Message box to alert the user of unsaved changes and allow them to choose how to act. """ + def __init__(self): super().__init__() @@ -623,13 +692,16 @@ class UnsavedMessageBox(QMessageBox): self.setWindowTitle("Unsaved changes") self.setText("The document has been modified.") self.setInformativeText("Do you want to save your changes?") - self.setStandardButtons(QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) + self.setStandardButtons( + QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) self.setDefaultButton(QMessageBox.Save) + class UnreadableMessageBox(QMessageBox): """ Message box to warn the user that the chosen file cannot be opened. """ + def __init__(self): super().__init__() @@ -640,10 +712,12 @@ class UnreadableMessageBox(QMessageBox): self.setStandardButtons(QMessageBox.Ok) self.setDefaultButton(QMessageBox.Ok) + class NameWarningMessageBox(QMessageBox): """ Message box to warn the user that a chord must have a name """ + def __init__(self): super().__init__() @@ -654,42 +728,51 @@ class NameWarningMessageBox(QMessageBox): self.setStandardButtons(QMessageBox.Ok) self.setDefaultButton(QMessageBox.Ok) + class VoicingWarningMessageBox(QMessageBox): """ Message box to warn the user that the voicing entered could not be parsed """ + def __init__(self): super().__init__() self.setIcon(QMessageBox.Warning) self.setWindowTitle("Malformed voicing") - self.setText("The voicing you entered was not understood and has not been applied.") - self.setInformativeText("Please try re-entering it in the correct format.") + self.setText( + "The voicing you entered was not understood and has not been applied.") + self.setInformativeText( + "Please try re-entering it in the correct format.") self.setStandardButtons(QMessageBox.Ok) self.setDefaultButton(QMessageBox.Ok) + class LengthWarningMessageBox(QMessageBox): """ Message box to warn the user that a block must have a length """ + def __init__(self): super().__init__() self.setIcon(QMessageBox.Warning) self.setWindowTitle("Block without valid length") self.setText("Blocks must have a whole number length.") - self.setInformativeText("Please enter a valid length for your block and try again.") + self.setInformativeText( + "Please enter a valid length for your block and try again.") self.setStandardButtons(QMessageBox.Ok) self.setDefaultButton(QMessageBox.Ok) if __name__ == '__main__': app = QApplication(sys.argv) - + d = Document() s = Style() - - w = DocumentWindow(d, s, filename=(sys.argv[1] if len(sys.argv) > 1 else None)) # pass first argument as filename + + # pass first argument as filename + w = DocumentWindow(d, s, filename=( + sys.argv[1] if len(sys.argv) > 1 else None)) w.show() sys.exit(app.exec_()) diff --git a/test.py b/test.py new file mode 100644 index 0000000..746669b --- /dev/null +++ b/test.py @@ -0,0 +1,12 @@ +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont + +from chordsheet.document import Document, Style +from chordsheet.render import savePDF + +pdfmetrics.registerFont(TTFont('FreeSans', os.path.join('fonts', 'FreeSans.ttf'))) + +doc = Document.newFromXML('examples/example.xml') +style = Style() + +savePDF(doc, style, 'test.pdf') \ No newline at end of file