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. 3
      chordsheet/parsers.py
  6. 48
      chordsheet/primitives.py
  7. 305
      chordsheet/render.py
  8. 22
      chordsheet/tableView.py
  9. 2
      examples/ah.xml
  10. 239
      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.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.
"""

3
chordsheet/parsers.py

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

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 -*-
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 = []
@ -23,7 +54,8 @@ def splitBlocks(blockList, maxWidth):
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
# create a block with the given length
splitBlockList.append(Block(l, chord=c_orig, notes=n_orig))
h_loc = lengthList[-1]
else:
@ -31,101 +63,151 @@ def splitBlocks(blockList, maxWidth):
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
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
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:
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:
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
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):
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:
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
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:
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)
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(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:
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
return v_origin + (v_loc+1)*style.unitHeight + style.beatsHeight + title_height # calculate the height of the generated chart
def guitarChartCheck(cL):
chordsPresent = False
@ -135,37 +217,74 @@ def guitarChartCheck(cL):
break
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)

22
chordsheet/tableView.py

@ -1,7 +1,10 @@
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):
"""
@ -9,7 +12,11 @@ class MItemModel(QtGui.QStandardItemModel):
"""
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,10 +31,12 @@ 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)
@ -46,10 +55,12 @@ class MTableView(QtWidgets.QTableView):
# 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,7 +97,8 @@ 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)

2
examples/ah.xml

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

239
gui.py

@ -6,14 +6,18 @@ 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
@ -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.
# 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.
@ -77,7 +87,7 @@ class DocumentWindow(QMainWindow):
if filename:
try:
self.openFile(filename)
except:
except Exception:
UnreadableMessageBox().exec()
def closeEvent(self, event):
@ -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,14 +136,18 @@ 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)
@ -166,10 +181,12 @@ class DocumentWindow(QMainWindow):
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)
@ -195,15 +212,20 @@ class DocumentWindow(QMainWindow):
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):
"""
@ -219,13 +241,16 @@ class DocumentWindow(QMainWindow):
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)
# 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)
@ -243,14 +268,16 @@ class DocumentWindow(QMainWindow):
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)
@ -267,7 +294,8 @@ class DocumentWindow(QMainWindow):
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)
@ -284,36 +312,36 @@ 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:
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):
@ -322,14 +350,15 @@ class DocumentWindow(QMainWindow):
"""
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()
@ -348,7 +377,8 @@ 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):
@ -380,9 +410,10 @@ 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')
self.doc.chordList[-1].voicings['guitar'] = parseFingering(
self.window.guitarVoicingLineEdit.text(), 'guitar')
success = True #  chord successfully parsed
except:
except Exception:
VoicingWarningMessageBox().exec() # Voicing is malformed, warn user
else:
success = True #  chord successfully parsed
@ -404,9 +435,10 @@ 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
@ -421,7 +453,8 @@ class DocumentWindow(QMainWindow):
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):
@ -437,18 +470,21 @@ class DocumentWindow(QMainWindow):
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
self.doc.blockList.append(Block(bLength,
chord = self.chordDict[self.window.blockChordComboBox.currentText()],
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
@ -456,13 +492,14 @@ class DocumentWindow(QMainWindow):
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()],
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()
@ -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,9 +550,11 @@ 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
@ -520,10 +564,14 @@ class DocumentWindow(QMainWindow):
"""
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
@ -531,41 +579,57 @@ class DocumentWindow(QMainWindow):
"""
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)
@ -590,16 +654,19 @@ 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.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)
@ -612,10 +679,12 @@ class AboutDialog(QDialog):
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,31 +728,38 @@ 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)
@ -689,7 +770,9 @@ if __name__ == '__main__':
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_())

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