Browse Source

rewrite to use reportlab flowables for rendering

master
Ivan Holmes 5 years ago
parent
commit
e38c6ebdab
  1. 61
      chordsheet/components/chordprogression.py
  2. 56
      chordsheet/components/guitarchart.py
  3. 4
      chordsheet/components/header.py
  4. 6
      chordsheet/document.py
  5. 5
      chordsheet/parsers.py
  6. 48
      chordsheet/primitives.py
  7. 305
      chordsheet/render.py
  8. 26
      chordsheet/tableView.py
  9. 2
      examples/ah.xml
  10. 271
      gui.py
  11. 12
      test.py

61
chordsheet/components/chordprogression.py

@ -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

56
chordsheet/components/guitarchart.py

@ -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))

4
chordsheet/components/header.py

@ -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)

6
chordsheet/document.py

@ -29,6 +29,10 @@ class Style:
self.unitHeight = 20*self.unit self.unitHeight = 20*self.unit
self.beatsHeight = 5*self.unit self.beatsHeight = 5*self.unit
self.titleFontSize = 24
self.subtitleFontSize = 18
self.creditsFontSize = 12
self.tempoFontSize = 12
self.notesFontSize = 12 self.notesFontSize = 12
self.chordNameFontSize = 18 self.chordNameFontSize = 18
self.beatsFontSize = 12 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.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) 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. Create a new Document object directly from an XML file.
""" """

5
chordsheet/parsers.py

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
def parseFingering(fingering, instrument): def parseFingering(fingering, instrument):
""" """
Converts fingerings into the list format that Chord objects understand. Converts fingerings into the list format that Chord objects understand.
@ -17,8 +18,10 @@ def parseFingering(fingering, instrument):
else: else:
return [fingering] return [fingering]
# dictionary holding text to be replaced in chord names # dictionary holding text to be replaced in chord names
nameReplacements = { "b":"", "#":"" }
nameReplacements = {"b": "", "#": ""}
def parseName(chordName): def parseName(chordName):
""" """

48
chordsheet/primitives.py

@ -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)

305
chordsheet/render.py

@ -1,11 +1,42 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from math import trunc
from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas
from reportlab.lib.units import mm 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 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): def splitBlocks(blockList, maxWidth):
h_loc = 0 h_loc = 0
splitBlockList = [] splitBlockList = []
@ -23,7 +54,8 @@ def splitBlocks(blockList, maxWidth):
lengthList.append(blockList[i].length - sum(lengthList)) lengthList.append(blockList[i].length - sum(lengthList))
for l in lengthList: for l in lengthList:
splitBlockList.append(Block(l, chord=c_orig, notes=n_orig)) # create a block with the given length
# create a block with the given length
splitBlockList.append(Block(l, chord=c_orig, notes=n_orig))
h_loc = lengthList[-1] h_loc = lengthList[-1]
else: else:
@ -31,101 +63,151 @@ def splitBlocks(blockList, maxWidth):
h_loc += blockList[i].length h_loc += blockList[i].length
return splitBlockList 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
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 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()])
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(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 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')
for j in range(len(guitarChordList[-1])): # j is which chord (0 is first chord, 1 is 2nd etc)
# j is which chord (0 is first chord, 1 is 2nd etc)
for j in range(len(stringList[-1])):
currentWidth = canvas.stringWidth(stringList[i][j])
if j == 0: 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)
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: 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
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 h_loc = 0
v_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
maxWidth = self.widthInBeats
for u in range(maxWidth+1): for u in range(maxWidth+1):
s = 0
x = u*unitWidth+margin
if u % document.timeSignature == 0:
e = -style.beatsHeight
y = v_origin
x = u*unitWidth
if u % self.timeSignature == 0:
l = self.style.beatsHeight
else: else:
e = -style.beatsHeight/2
drawVertLine(currentCanvas, s, e, x, h_origin, v_origin)
l = self.style.beatsHeight/2
canvas.line(x, y, x, y+l)
if u == maxWidth: # Avoid writing beat number after the final line if u == maxWidth: # Avoid writing beat number after the final line
break break
writeText(currentCanvas, style, str((u % document.timeSignature)+1), style.beatsFontSize, v_origin-style.beatsHeight, hpos=x+unitWidth/2)
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(document.blockList, maxWidth)
parsedBlockList = splitBlocks(self.blockList, maxWidth)
for b in parsedBlockList: for b in parsedBlockList:
if h_loc == maxWidth: if h_loc == maxWidth:
v_loc += 1 v_loc += 1
h_loc = 0 h_loc = 0
currentCanvas.rect(h_origin+(h_loc*unitWidth), v_origin+(v_loc*style.unitHeight), b.length*unitWidth, style.unitHeight)
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: 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
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: 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))
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 h_loc += b.length
return v_origin + (v_loc+1)*style.unitHeight + style.beatsHeight + title_height # calculate the height of the generated chart
def guitarChartCheck(cL): def guitarChartCheck(cL):
chordsPresent = False chordsPresent = False
@ -135,37 +217,74 @@ def guitarChartCheck(cL):
break break
return chordsPresent return chordsPresent
def savePDF(document, style, pathToPDF):
c = canvas.Canvas(pathToPDF, pagesize=style.pageSize, bottomup=0)
class TitleBlock(Flowable):
"""
Flowable that draws the title and other text at the top of the document.
"""
curPos = style.topMargin*style.unit
def __init__(self, style, document):
self.style = style
self.lS = style.lineSpacing
if document.title is not None:
curPos += writeText(c, style, document.title, 24, curPos)
self.title = document.title
self.subtitle = document.subtitle
self.composer = document.composer
self.arranger = document.arranger
self.tempo = document.tempo
if document.subtitle is not None:
curPos += writeText(c, style, document.subtitle, 18, curPos)
self.spaceAfter = self.style.separatorSize * mm
if document.composer is not None:
curPos += writeText(c, style, "Composer: {c}".format(c = document.composer), 12, curPos)
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)
if document.arranger is not None:
curPos += writeText(c, style, "Arranger: {a}".format(a = document.arranger), 12, curPos)
def draw(self):
canvas = self.canv
curPos = self.height
if document.tempo is not None:
curPos += writeText(c, style, "♩ = {t} bpm".format(t = document.tempo), 12, curPos, align = "left")
if self.title:
curPos -= writeText(canvas, self.style,
self.title, 24, curPos, self.width)
curPos += style.separatorSize*style.unit
if self.subtitle:
curPos -= writeText(canvas, self.style,
self.subtitle, 18, curPos, self.width)
if guitarChartCheck(document.chordList):
curPos += guitarChart(c, style, document.chordList, curPos)
if self.composer:
curPos -= writeText(canvas, self.style,
"Composer: {c}".format(c=self.composer), 12, curPos, self.width)
curPos += style.separatorSize*style.unit
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")
if document.blockList:
curPos += chordProgression(c, style, document, curPos)
curPos += style.separatorSize*style.unit
def savePDF(document, style, pathToPDF):
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))
c.save()
rlDoc.build(rlDocList)

26
chordsheet/tableView.py

@ -1,7 +1,10 @@
import sys
from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5 import QtWidgets, QtGui
class MItemModel(QtGui.QStandardItemModel): class MItemModel(QtGui.QStandardItemModel):
"""
Special item model to ensure whole row is moved.
"""
def dropMimeData(self, data, action, row, col, parent): def dropMimeData(self, data, action, row, col, parent):
""" """
@ -9,7 +12,11 @@ class MItemModel(QtGui.QStandardItemModel):
""" """
return super().dropMimeData(data, action, row, 0, parent) return super().dropMimeData(data, action, row, 0, parent)
class MProxyStyle(QtWidgets.QProxyStyle): class MProxyStyle(QtWidgets.QProxyStyle):
"""
Proxy style to change the appearance of the TableView.
"""
def drawPrimitive(self, element, option, painter, widget=None): def drawPrimitive(self, element, option, painter, widget=None):
""" """
@ -24,10 +31,12 @@ class MProxyStyle(QtWidgets.QProxyStyle):
option = option_new option = option_new
super().drawPrimitive(element, option, painter, widget) super().drawPrimitive(element, option, painter, widget)
class MTableView(QtWidgets.QTableView): class MTableView(QtWidgets.QTableView):
""" """
Subclass the built in TableView to customise it. Subclass the built in TableView to customise it.
""" """
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
@ -40,16 +49,18 @@ class MTableView(QtWidgets.QTableView):
self.horizontalHeader().setStretchLastSection(True) self.horizontalHeader().setStretchLastSection(True)
self.setShowGrid(False) 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 # Set our custom style - this draws the drop indicator across the whole row
self.setStyle(MProxyStyle()) self.setStyle(MProxyStyle())
class ChordTableView(MTableView): class ChordTableView(MTableView):
""" """
Subclass MTableView to add properties just for the chord table. Subclass MTableView to add properties just for the chord table.
""" """
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
@ -61,7 +72,8 @@ class ChordTableView(MTableView):
""" """
self.model.removeRows(0, self.model.rowCount()) self.model.removeRows(0, self.model.rowCount())
for c in cList: for c in cList:
rowList = [QtGui.QStandardItem(c.name), QtGui.QStandardItem(",".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: for item in rowList:
item.setEditable(False) item.setEditable(False)
item.setDropEnabled(False) item.setDropEnabled(False)
@ -73,6 +85,7 @@ class BlockTableView(MTableView):
""" """
Subclass MTableView to add properties just for the block table. Subclass MTableView to add properties just for the block table.
""" """
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
@ -84,7 +97,8 @@ class BlockTableView(MTableView):
""" """
self.model.removeRows(0, self.model.rowCount()) self.model.removeRows(0, self.model.rowCount())
for b in bList: 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: for item in rowList:
item.setEditable(False) item.setEditable(False)
item.setDropEnabled(False) item.setDropEnabled(False)

2
examples/ah.xml

@ -1,7 +1,9 @@
<chordsheet> <chordsheet>
<title>"African Heritage"</title> <title>"African Heritage"</title>
<subtitle>A corroboration</subtitle>
<composer>Ivan Holmes</composer> <composer>Ivan Holmes</composer>
<arranger>Ivan Holmes and Joe Buckley</arranger> <arranger>Ivan Holmes and Joe Buckley</arranger>
<tempo>120</tempo>
<timesignature>6</timesignature> <timesignature>6</timesignature>
<chords> <chords>
<chord> <chord>

271
gui.py

@ -6,14 +6,18 @@ Created on Wed May 29 00:02:24 2019
@author: ivan @author: ivan
""" """
import sys, fitz, io, subprocess, os
import sys
import fitz
import io
import subprocess
import os
from copy import copy 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.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.QtCore import QFile, QObject, Qt, pyqtSlot, QSettings
from PyQt5.QtGui import QPixmap, QImage, QKeySequence from PyQt5.QtGui import QPixmap, QImage, QKeySequence
from PyQt5 import uic 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.units import mm, cm, inch, pica
from reportlab.lib.pagesizes import A4, A5, LETTER, LEGAL from reportlab.lib.pagesizes import A4, A5, LETTER, LEGAL
@ -32,24 +36,30 @@ if getattr(sys, 'frozen', False):
else: else:
scriptDir = os.path.abspath(os.path.dirname(os.path.abspath(__file__))) 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.setOrganizationName("Ivan Holmes")
QApplication.setOrganizationDomain("ivanholmes.co.uk") QApplication.setOrganizationDomain("ivanholmes.co.uk")
QApplication.setApplicationName("Chordsheet") QApplication.setApplicationName("Chordsheet")
settings = QSettings() 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": if sys.platform == "darwin":
pdfmetrics.registerFont(TTFont('HelveticaNeue', 'HelveticaNeue.ttc', subfontIndex=0))
pdfmetrics.registerFont(
TTFont('HelveticaNeue', 'HelveticaNeue.ttc', subfontIndex=0))
# dictionaries for combo boxes # 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 DocumentWindow(QMainWindow):
""" """
Class for the main window of the application. Class for the main window of the application.
""" """
def __init__(self, doc, style, filename=None): def __init__(self, doc, style, filename=None):
""" """
Initialisation function for the main window of the application. Initialisation function for the main window of the application.
@ -66,7 +76,7 @@ class DocumentWindow(QMainWindow):
self.lastDoc = copy(self.doc) self.lastDoc = copy(self.doc)
self.currentFilePath = filename 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.UIInitStyle()
self.updateChordDict() self.updateChordDict()
@ -77,7 +87,7 @@ class DocumentWindow(QMainWindow):
if filename: if filename:
try: try:
self.openFile(filename) self.openFile(filename)
except:
except Exception:
UnreadableMessageBox().exec() UnreadableMessageBox().exec()
def closeEvent(self, event): def closeEvent(self, event):
@ -103,7 +113,8 @@ class DocumentWindow(QMainWindow):
self.window.actionOpen.triggered.connect(self.menuFileOpenAction) self.window.actionOpen.triggered.connect(self.menuFileOpenAction)
self.window.actionSave.triggered.connect(self.menuFileSaveAction) self.window.actionSave.triggered.connect(self.menuFileSaveAction)
self.window.actionSave_as.triggered.connect(self.menuFileSaveAsAction) 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.actionPrint.triggered.connect(self.menuFilePrintAction)
self.window.actionClose.triggered.connect(self.menuFileCloseAction) self.window.actionClose.triggered.connect(self.menuFileCloseAction)
self.window.actionUndo.triggered.connect(self.menuEditUndoAction) self.window.actionUndo.triggered.connect(self.menuEditUndoAction)
@ -125,14 +136,18 @@ class DocumentWindow(QMainWindow):
self.window.actionCopy.setShortcut(QKeySequence.Copy) self.window.actionCopy.setShortcut(QKeySequence.Copy)
self.window.actionPaste.setShortcut(QKeySequence.Paste) 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.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.addChordButton.clicked.connect(self.addChordAction)
self.window.removeChordButton.clicked.connect(self.removeChordAction) self.window.removeChordButton.clicked.connect(self.removeChordAction)
self.window.updateChordButton.clicked.connect(self.updateChordAction) self.window.updateChordButton.clicked.connect(self.updateChordAction)
@ -166,10 +181,12 @@ class DocumentWindow(QMainWindow):
Fills the window's fields with the values from its style. Fills the window's fields with the values from its style.
""" """
self.window.pageSizeComboBox.addItems(list(pageSizeDict.keys())) 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.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.lineSpacingDoubleSpinBox.setValue(self.style.lineSpacing)
@ -195,15 +212,20 @@ class DocumentWindow(QMainWindow):
def chordClickedAction(self, index): def chordClickedAction(self, index):
# set the controls to the values from the selected chord # 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): def blockClickedAction(self, index):
# set the controls to the values from the selected block # set the controls to the values from the selected block
bChord = self.window.blockTableView.model.item(index.row(), 0).text() 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):
""" """
@ -218,14 +240,17 @@ class DocumentWindow(QMainWindow):
return settings.setValue(value, os.path.dirname(fullpath)) return settings.setValue(value, os.path.dirname(fullpath))
def menuFileNewAction(self): def menuFileNewAction(self):
self.doc = Document() # new document object
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.UIInitDocument()
self.updatePreview() self.updatePreview()
def menuFileOpenAction(self): def menuFileOpenAction(self):
filePath = QFileDialog.getOpenFileName(self.window.tabWidget, 'Open file', self.getPath("workingPath"), "Chordsheet ML files (*.xml *.cml)")[0]
filePath = QFileDialog.getOpenFileName(self.window.tabWidget, 'Open file', self.getPath(
"workingPath"), "Chordsheet ML files (*.xml *.cml)")[0]
if filePath: if filePath:
self.openFile(filePath) self.openFile(filePath)
@ -243,14 +268,16 @@ class DocumentWindow(QMainWindow):
def menuFileSaveAction(self): def menuFileSaveAction(self):
self.updateDocument() self.updateDocument()
if not self.currentFilePath: 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: else:
filePath = self.currentFilePath filePath = self.currentFilePath
self.saveFile(filePath) self.saveFile(filePath)
def menuFileSaveAsAction(self): def menuFileSaveAsAction(self):
self.updateDocument() 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: if filePath:
self.saveFile(filePath) self.saveFile(filePath)
@ -267,7 +294,8 @@ class DocumentWindow(QMainWindow):
def menuFileSavePDFAction(self): def menuFileSavePDFAction(self):
self.updateDocument() self.updateDocument()
self.updatePreview() 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: if filePath:
savePDF(d, s, filePath) savePDF(d, s, filePath)
self.setPath("lastExportPath", filePath) self.setPath("lastExportPath", filePath)
@ -284,36 +312,36 @@ class DocumentWindow(QMainWindow):
self.saveWarning() self.saveWarning()
def menuFileAboutAction(self): def menuFileAboutAction(self):
aDialog = AboutDialog()
AboutDialog()
def menuEditUndoAction(self): def menuEditUndoAction(self):
try: try:
QApplication.focusWidget().undo() # see if the built in widget supports it QApplication.focusWidget().undo() # see if the built in widget supports it
except:
pass # if not just fail silently
except Exception:
pass #  if not just fail silently
def menuEditRedoAction(self): def menuEditRedoAction(self):
try: try:
QApplication.focusWidget().redo() QApplication.focusWidget().redo()
except:
except Exception:
pass pass
def menuEditCutAction(self): def menuEditCutAction(self):
try: try:
QApplication.focusWidget().cut() QApplication.focusWidget().cut()
except:
except Exception:
pass pass
def menuEditCopyAction(self): def menuEditCopyAction(self):
try: try:
QApplication.focusWidget().copy() QApplication.focusWidget().copy()
except:
except Exception:
pass pass
def menuEditPasteAction(self): def menuEditPasteAction(self):
try: try:
QApplication.focusWidget().paste() QApplication.focusWidget().paste()
except:
except Exception:
pass pass
def saveWarning(self): def saveWarning(self):
@ -322,14 +350,15 @@ class DocumentWindow(QMainWindow):
""" """
self.updateDocument() # update the document to catch all changes self.updateDocument() # update the document to catch all changes
if (self.lastDoc == self.doc):
if self.lastDoc == self.doc:
self.close() self.close()
else: else:
wantToSave = UnsavedMessageBox().exec() wantToSave = UnsavedMessageBox().exec()
if wantToSave == QMessageBox.Save: 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.currentFilePath = filePath[0]
self.doc.saveXML(self.currentFilePath) self.doc.saveXML(self.currentFilePath)
self.close() self.close()
@ -348,20 +377,21 @@ class DocumentWindow(QMainWindow):
def clearChordLineEdits(self): def clearChordLineEdits(self):
self.window.chordNameLineEdit.clear() self.window.chordNameLineEdit.clear()
self.window.guitarVoicingLineEdit.clear() self.window.guitarVoicingLineEdit.clear()
self.window.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() self.window.guitarVoicingLineEdit.repaint()
def updateChordDict(self): def updateChordDict(self):
""" """
Updates the dictionary used to generate the Chord menu (on the block tab) 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.clear()
self.window.blockChordComboBox.addItems(list(self.chordDict.keys())) self.window.blockChordComboBox.addItems(list(self.chordDict.keys()))
def removeChordAction(self): def removeChordAction(self):
if self.window.chordTableView.selectionModel().hasSelection(): # check for selection
if self.window.chordTableView.selectionModel().hasSelection(): #  check for selection
self.updateChords() self.updateChords()
row = self.window.chordTableView.selectionModel().currentIndex().row() row = self.window.chordTableView.selectionModel().currentIndex().row()
@ -380,12 +410,13 @@ class DocumentWindow(QMainWindow):
self.doc.chordList.append(Chord(cName)) self.doc.chordList.append(Chord(cName))
if self.window.guitarVoicingLineEdit.text(): if self.window.guitarVoicingLineEdit.text():
try: try:
self.doc.chordList[-1].voicings['guitar'] = parseFingering(self.window.guitarVoicingLineEdit.text(), 'guitar')
success = True # chord successfully parsed
except:
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 VoicingWarningMessageBox().exec() # Voicing is malformed, warn user
else: else:
success = True # chord successfully parsed
success = True #  chord successfully parsed
else: else:
NameWarningMessageBox().exec() # Chord has no name, warn user NameWarningMessageBox().exec() # Chord has no name, warn user
@ -396,7 +427,7 @@ class DocumentWindow(QMainWindow):
def updateChordAction(self): def updateChordAction(self):
success = False # see comments above success = False # see comments above
if self.window.chordTableView.selectionModel().hasSelection(): # check for selection
if self.window.chordTableView.selectionModel().hasSelection(): #  check for selection
self.updateChords() self.updateChords()
row = self.window.chordTableView.selectionModel().currentIndex().row() row = self.window.chordTableView.selectionModel().currentIndex().row()
cName = parseName(self.window.chordNameLineEdit.text()) cName = parseName(self.window.chordNameLineEdit.text())
@ -404,9 +435,10 @@ class DocumentWindow(QMainWindow):
self.doc.chordList[row] = Chord(cName) self.doc.chordList[row] = Chord(cName)
if self.window.guitarVoicingLineEdit.text(): if self.window.guitarVoicingLineEdit.text():
try: 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 success = True
except:
except Exception:
VoicingWarningMessageBox().exec() VoicingWarningMessageBox().exec()
else: else:
success = True success = True
@ -421,11 +453,12 @@ class DocumentWindow(QMainWindow):
def clearBlockLineEdits(self): def clearBlockLineEdits(self):
self.window.blockLengthLineEdit.clear() self.window.blockLengthLineEdit.clear()
self.window.blockNotesLineEdit.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() self.window.blockNotesLineEdit.repaint()
def removeBlockAction(self): def removeBlockAction(self):
if self.window.blockTableView.selectionModel().hasSelection(): # check for selection
if self.window.blockTableView.selectionModel().hasSelection(): #  check for selection
self.updateBlocks() self.updateBlocks()
row = self.window.blockTableView.selectionModel().currentIndex().row() row = self.window.blockTableView.selectionModel().currentIndex().row()
@ -437,33 +470,37 @@ class DocumentWindow(QMainWindow):
self.updateBlocks() self.updateBlocks()
try: 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 bLength = False
if bLength: # create the block if bLength: # create the block
self.doc.blockList.append(Block(bLength, 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.window.blockTableView.populate(self.doc.blockList)
self.clearBlockLineEdits() self.clearBlockLineEdits()
else: 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): def updateBlockAction(self):
if self.window.blockTableView.selectionModel().hasSelection(): # check for selection
if self.window.blockTableView.selectionModel().hasSelection(): #  check for selection
self.updateBlocks() self.updateBlocks()
try: try:
bLength = int(self.window.blockLengthLineEdit.text()) bLength = int(self.window.blockLengthLineEdit.text())
except:
except Exception:
bLength = False bLength = False
row = self.window.blockTableView.selectionModel().currentIndex().row() row = self.window.blockTableView.selectionModel().currentIndex().row()
if bLength: if bLength:
self.doc.blockList[row] = (Block(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.window.blockTableView.populate(self.doc.blockList)
self.clearBlockLineEdits() self.clearBlockLineEdits()
else: else:
@ -482,23 +519,28 @@ class DocumentWindow(QMainWindow):
savePDF(self.doc, self.style, self.currentPreview) savePDF(self.doc, self.style, self.currentPreview)
pdfView = fitz.Document(stream=self.currentPreview, filetype='pdf') 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 fmt = QImage.Format_RGB888
qtimg = QImage(pix.samples, pix.width, pix.height, pix.stride, fmt) 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) # -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): def updateTitleBar(self):
""" """
Update the application's title bar to reflect the current document. Update the application's title bar to reflect the current document.
""" """
if self.currentFilePath: if self.currentFilePath:
self.setWindowTitle(_version.appName + " – " + os.path.basename(self.currentFilePath))
self.setWindowTitle(_version.appName + " – " +
os.path.basename(self.currentFilePath))
else: else:
self.setWindowTitle(_version.appName) self.setWindowTitle(_version.appName)
@ -508,9 +550,11 @@ class DocumentWindow(QMainWindow):
""" """
chordTableList = [] chordTableList = []
for i in range(self.window.chordTableView.model.rowCount()): 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(): 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 self.doc.chordList = chordTableList
@ -520,10 +564,14 @@ class DocumentWindow(QMainWindow):
""" """
blockTableList = [] blockTableList = []
for i in range(self.window.blockTableView.model.rowCount()): 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 self.doc.blockList = blockTableList
@ -531,41 +579,57 @@ class DocumentWindow(QMainWindow):
""" """
Update the Document object by reading values from the UI. 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.pageSize = pageSizeDict[self.pageSizeSelected]
self.style.unit = unitDict[self.unitSelected] 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! # make sure the unit width isn't too wide to draw!
if self.window.beatWidthLineEdit.text(): 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): 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: 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.updateChords()
self.updateBlocks() 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 # something for the font box here
class GuitarDialog(QDialog): class GuitarDialog(QDialog):
""" """
Dialogue to allow the user to enter a guitar chord voicing. Not particularly advanced at present! Dialogue to allow the user to enter a guitar chord voicing. Not particularly advanced at present!
May be extended in future. May be extended in future.
""" """
def __init__(self): def __init__(self):
super().__init__() 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): def UIFileLoader(self, ui_file):
ui_file = QFile(ui_file) ui_file = QFile(ui_file)
@ -590,16 +654,19 @@ class GuitarDialog(QDialog):
else: else:
return None return None
class AboutDialog(QDialog): class AboutDialog(QDialog):
""" """
Dialogue showing information about the program. Dialogue showing information about the program.
""" """
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.UIFileLoader(str(os.path.join(scriptDir, 'ui','aboutdialog.ui')))
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))
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.versionLabel.setText("Version " + _version.version)
@ -612,10 +679,12 @@ class AboutDialog(QDialog):
self.dialog = uic.loadUi(ui_file) self.dialog = uic.loadUi(ui_file)
ui_file.close() ui_file.close()
class UnsavedMessageBox(QMessageBox): class UnsavedMessageBox(QMessageBox):
""" """
Message box to alert the user of unsaved changes and allow them to choose how to act. Message box to alert the user of unsaved changes and allow them to choose how to act.
""" """
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -623,13 +692,16 @@ class UnsavedMessageBox(QMessageBox):
self.setWindowTitle("Unsaved changes") self.setWindowTitle("Unsaved changes")
self.setText("The document has been modified.") self.setText("The document has been modified.")
self.setInformativeText("Do you want to save your changes?") 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) self.setDefaultButton(QMessageBox.Save)
class UnreadableMessageBox(QMessageBox): class UnreadableMessageBox(QMessageBox):
""" """
Message box to warn the user that the chosen file cannot be opened. Message box to warn the user that the chosen file cannot be opened.
""" """
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -640,10 +712,12 @@ class UnreadableMessageBox(QMessageBox):
self.setStandardButtons(QMessageBox.Ok) self.setStandardButtons(QMessageBox.Ok)
self.setDefaultButton(QMessageBox.Ok) self.setDefaultButton(QMessageBox.Ok)
class NameWarningMessageBox(QMessageBox): class NameWarningMessageBox(QMessageBox):
""" """
Message box to warn the user that a chord must have a name Message box to warn the user that a chord must have a name
""" """
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -654,31 +728,38 @@ class NameWarningMessageBox(QMessageBox):
self.setStandardButtons(QMessageBox.Ok) self.setStandardButtons(QMessageBox.Ok)
self.setDefaultButton(QMessageBox.Ok) self.setDefaultButton(QMessageBox.Ok)
class VoicingWarningMessageBox(QMessageBox): class VoicingWarningMessageBox(QMessageBox):
""" """
Message box to warn the user that the voicing entered could not be parsed Message box to warn the user that the voicing entered could not be parsed
""" """
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setIcon(QMessageBox.Warning) self.setIcon(QMessageBox.Warning)
self.setWindowTitle("Malformed voicing") 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.setStandardButtons(QMessageBox.Ok)
self.setDefaultButton(QMessageBox.Ok) self.setDefaultButton(QMessageBox.Ok)
class LengthWarningMessageBox(QMessageBox): class LengthWarningMessageBox(QMessageBox):
""" """
Message box to warn the user that a block must have a length Message box to warn the user that a block must have a length
""" """
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setIcon(QMessageBox.Warning) self.setIcon(QMessageBox.Warning)
self.setWindowTitle("Block without valid length") self.setWindowTitle("Block without valid length")
self.setText("Blocks must have a whole number 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.setStandardButtons(QMessageBox.Ok)
self.setDefaultButton(QMessageBox.Ok) self.setDefaultButton(QMessageBox.Ok)
@ -689,7 +770,9 @@ if __name__ == '__main__':
d = Document() d = Document()
s = Style() 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() w.show()
sys.exit(app.exec_()) sys.exit(app.exec_())

12
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')
Loading…
Cancel
Save