Browse Source

added Sections, multi-page capability

improved and expanded use of flowables
master
Ivan Holmes 5 years ago
parent
commit
5b2f1d84ab
  1. 2
      _version.py
  2. 12
      chordsheet/comboBox.py
  3. 170
      chordsheet/document.py
  4. 56
      chordsheet/pdfViewer.py
  5. 353
      chordsheet/render.py
  6. 110
      chordsheet/rlStylesheet.py
  7. 24
      chordsheet/tableView.py
  8. 4
      examples/ah.xml
  9. 309
      examples/ahlong.xml
  10. 1
      examples/angela.xml
  11. 2
      examples/example.xml
  12. 1
      examples/kissoflife.xml
  13. 2
      examples/test.xml
  14. 264
      gui.py
  15. 11
      test.py
  16. 234
      ui/mainwindow.ui

2
_version.py

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
appName = "Chordsheet" appName = "Chordsheet"
version = '0.4dev'
version = '0.4'

12
chordsheet/comboBox.py

@ -0,0 +1,12 @@
from PyQt5.QtWidgets import QComboBox
from PyQt5.QtCore import pyqtSignal
class MComboBox(QComboBox):
"""
Modified version of combobox that emits a signal with the current item when clicked.
"""
clicked = pyqtSignal(str)
def showPopup(self):
self.clicked.emit(self.currentText())
super().showPopup()

170
chordsheet/document.py

@ -7,36 +7,41 @@ from reportlab.lib.pagesizes import A4
defaultTimeSignature = 4 defaultTimeSignature = 4
class Style: class Style:
def __init__(self, **kwargs): def __init__(self, **kwargs):
# set up the style using sane defaults # set up the style using sane defaults
self.unit = kwargs.get('unit', mm) self.unit = kwargs.get('unit', mm)
self.pageSize = kwargs.get('pageSize', A4) self.pageSize = kwargs.get('pageSize', A4)
self.leftMargin = kwargs.get('leftMargin', 10) self.leftMargin = kwargs.get('leftMargin', 10)
self.topMargin = kwargs.get('topMargin', 10) self.topMargin = kwargs.get('topMargin', 10)
self.rightMargin = kwargs.get('rightMargin', 10)
self.bottomMargin = kwargs.get('bottomMargin', 10)
self.font = kwargs.get('font', 'FreeSans') self.font = kwargs.get('font', 'FreeSans')
self.lineSpacing = kwargs.get('lineSpacing', 1.15) self.lineSpacing = kwargs.get('lineSpacing', 1.15)
self.separatorSize = kwargs.get('separatorSize', 5)
self.unitWidth = kwargs.get('unitWidth', 10) self.unitWidth = kwargs.get('unitWidth', 10)
self.useIncludedFont = True self.useIncludedFont = True
self.separatorSize = 5*self.unit
self.stringHzSp = 20*self.unit self.stringHzSp = 20*self.unit
self.stringHzGap = 2*self.unit self.stringHzGap = 2*self.unit
self.stringHeight = 5*self.unit self.stringHeight = 5*self.unit
self.unitHeight = 20*self.unit self.unitHeight = 20*self.unit
self.beatsHeight = 5*self.unit self.beatsHeight = 5*self.unit
self.titleFontSize = 24 self.titleFontSize = 24
self.subtitleFontSize = 18 self.subtitleFontSize = 18
self.creditsFontSize = 12 self.creditsFontSize = 12
self.tempoFontSize = 12 self.tempoFontSize = 12
self.headingFontSize = 18
self.notesFontSize = 12 self.notesFontSize = 12
self.chordNameFontSize = 18 self.chordNameFontSize = 18
self.beatsFontSize = 12 self.beatsFontSize = 12
class Chord: class Chord:
def __init__(self, name, **kwargs): def __init__(self, name, **kwargs):
self.name = name self.name = name
@ -51,21 +56,33 @@ class Chord:
class Block: class Block:
def __init__(self, length, **kwargs):
def __init__(self, length, chord=None, notes=None):
self.length = length self.length = length
self.chord = kwargs.get('chord', None)
self.notes = kwargs.get('notes', None)
self.chord = chord
self.notes = notes
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, self.__class__): if isinstance(other, self.__class__):
return self.length == other.length and self.chord == other.chord and self.notes == other.notes return self.length == other.length and self.chord == other.chord and self.notes == other.notes
return NotImplemented
return NotImplemented
class Section:
def __init__(self, blockList=None, name=None):
self.blockList = blockList or []
self.name = name
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.blockList == other.blockList and self.name == other.name
return NotImplemented
class Document: class Document:
def __init__(self, chordList=None, blockList=None, title=None, subtitle=None, composer=None, arranger=None, timeSignature=defaultTimeSignature, tempo=None):
def __init__(self, chordList=None, sectionList=None, title=None, subtitle=None, composer=None, arranger=None, timeSignature=defaultTimeSignature, tempo=None):
self.chordList = chordList or [] self.chordList = chordList or []
self.blockList = blockList or []
self.title = title or '' # Do not initialise title empty
self.sectionList = sectionList or []
self.title = title or '' # Do not initialise title empty
self.subtitle = subtitle self.subtitle = subtitle
self.composer = composer self.composer = composer
self.arranger = arranger self.arranger = arranger
@ -74,98 +91,119 @@ class Document:
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, self.__class__): if isinstance(other, self.__class__):
textEqual = self.title == other.title and self.subtitle == other.subtitle and self.composer == other.composer and self.arranger == other.arranger and self.timeSignature == other.timeSignature and self.tempo == other.tempo # check all the text values for equality
return textEqual and self.chordList == other.chordList and self.blockList == other.blockList
textEqual = self.title == other.title and self.subtitle == other.subtitle and self.composer == other.composer and self.arranger == other.arranger and self.timeSignature == other.timeSignature and self.tempo == other.tempo # check all the text values for equality
return textEqual and self.chordList == other.chordList and self.sectionList == other.sectionList
return NotImplemented return NotImplemented
def loadXML(self, filepath): def loadXML(self, filepath):
""" """
Read an XML file and import its contents. Read an XML file and import its contents.
""" """
xmlDoc = ET.parse(filepath) xmlDoc = ET.parse(filepath)
root = xmlDoc.getroot() root = xmlDoc.getroot()
self.chordList = [] self.chordList = []
if root.find('chords') is not None:
if root.find('chords'):
for c in root.findall('chords/chord'): for c in root.findall('chords/chord'):
self.chordList.append(Chord(parseName(c.find('name').text))) self.chordList.append(Chord(parseName(c.find('name').text)))
for v in c.findall('voicing'): for v in c.findall('voicing'):
self.chordList[-1].voicings[v.attrib['instrument']] = parseFingering(v.text, v.attrib['instrument'])
self.blockList = []
if root.find('progression') is not None:
for b in root.findall('progression/block'):
blockChordName = parseName(b.find('chord').text) if b.find('chord') is not None else None
if blockChordName:
blockChord = None
for c in self.chordList:
if c.name == blockChordName:
blockChord = c
break
if blockChord is None:
exit("Chord {c} does not match any chord in {l}.".format(c=blockChordName, l=self.chordList))
else:
blockChord = None
blockNotes = (b.find('notes').text if b.find('notes') is not None else None)
self.blockList.append(Block(int(b.find('length').text), chord=blockChord, notes=blockNotes))
self.title = (root.find('title').text if root.find('title') is not None else '') # Do not initialise title empty
self.subtitle = (root.find('subtitle').text if root.find('subtitle') is not None else None)
self.composer = (root.find('composer').text if root.find('composer') is not None else None)
self.arranger = (root.find('arranger').text if root.find('arranger') is not None else None)
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(self, filepath):
self.chordList[-1].voicings[v.attrib['instrument']
] = parseFingering(v.text, v.attrib['instrument'])
self.sectionList = []
if root.find('section'):
for n, s in enumerate(root.findall('section')):
blockList = []
for b in s.findall('block'):
blockChordName = parseName(b.find('chord').text) if b.find(
'chord') is not None else None
if blockChordName:
blockChord = None
for c in self.chordList:
if c.name == blockChordName:
blockChord = c
break
if blockChord is None:
exit("Chord {c} does not match any chord in {l}.".format(
c=blockChordName, l=self.chordList))
else:
blockChord = None
blockNotes = (b.find('notes').text if b.find(
'notes') is not None else None)
blockList.append(
Block(float(b.find('length').text), chord=blockChord, notes=blockNotes))
# automatically name the section by its index if a name isn't given. The +1 is because indexing starts from 0.
self.sectionList.append(Section(blockList=blockList, name=(
s.attrib['name'] if 'name' in s.attrib else "Section {}".format(n + 1))))
self.title = (root.find('title').text if root.find(
'title') is not None else '') # Do not initialise title empty
self.subtitle = (root.find('subtitle').text if root.find(
'subtitle') is not None else None)
self.composer = (root.find('composer').text if root.find(
'composer') is not None else None)
self.arranger = (root.find('arranger').text if root.find(
'arranger') is not None else None)
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):
""" """
Create a new Document object directly from an XML file. Create a new Document object directly from an XML file.
""" """
doc = Document() doc = Document()
doc.loadXML(filepath) doc.loadXML(filepath)
return doc return doc
def saveXML(self, filepath): def saveXML(self, filepath):
""" """
Write the contents of the Document object to an XML file. Write the contents of the Document object to an XML file.
""" """
root = ET.Element("chordsheet") root = ET.Element("chordsheet")
ET.SubElement(root, "title").text = self.title ET.SubElement(root, "title").text = self.title
if self.subtitle is not None: if self.subtitle is not None:
ET.SubElement(root, "subtitle").text = self.subtitle ET.SubElement(root, "subtitle").text = self.subtitle
if self.arranger is not None: if self.arranger is not None:
ET.SubElement(root, "arranger").text = self.arranger ET.SubElement(root, "arranger").text = self.arranger
if self.composer is not None: if self.composer is not None:
ET.SubElement(root, "composer").text = self.composer ET.SubElement(root, "composer").text = self.composer
ET.SubElement(root, "timesignature").text = str(self.timeSignature) ET.SubElement(root, "timesignature").text = str(self.timeSignature)
if self.tempo is not None: if self.tempo is not None:
ET.SubElement(root, "tempo").text = self.tempo ET.SubElement(root, "tempo").text = self.tempo
chordsElement = ET.SubElement(root, "chords") chordsElement = ET.SubElement(root, "chords")
for c in self.chordList: for c in self.chordList:
chordElement = ET.SubElement(chordsElement, "chord") chordElement = ET.SubElement(chordsElement, "chord")
ET.SubElement(chordElement, "name").text = c.name ET.SubElement(chordElement, "name").text = c.name
for inst in c.voicings.keys(): for inst in c.voicings.keys():
if inst == 'guitar': if inst == 'guitar':
ET.SubElement(chordElement, "voicing", attrib={'instrument':'guitar'}).text = ','.join(c.voicings['guitar'])
ET.SubElement(chordElement, "voicing", attrib={
'instrument': 'guitar'}).text = ','.join(c.voicings['guitar'])
if inst == 'piano': if inst == 'piano':
ET.SubElement(chordElement, "voicing", attrib={'instrument':'piano'}).text = c.voicings['piano'][0] # return first element of list as feature has not been implemented
progressionElement = ET.SubElement(root, "progression")
for b in self.blockList:
blockElement = ET.SubElement(progressionElement, "block")
ET.SubElement(blockElement, "length").text = str(b.length)
if b.chord is not None:
ET.SubElement(blockElement, "chord").text = b.chord.name
if b.notes is not None:
ET.SubElement(blockElement, "notes").text = b.notes
# return first element of list as feature has not been implemented
ET.SubElement(chordElement, "voicing", attrib={
'instrument': 'piano'}).text = c.voicings['piano'][0]
for n, s in enumerate(self.sectionList):
sectionElement = ET.SubElement(root, "section", attrib={
'name': s.name if s.name else "Section {}".format(n + 1)})
for b in s.blockList:
blockElement = ET.SubElement(sectionElement, "block")
ET.SubElement(blockElement, "length").text = str(b.length)
if b.chord is not None:
ET.SubElement(blockElement, "chord").text = b.chord.name
if b.notes is not None:
ET.SubElement(blockElement, "notes").text = b.notes
tree = ET.ElementTree(root) tree = ET.ElementTree(root)
tree.write(filepath)
tree.write(filepath)

56
chordsheet/pdfViewer.py

@ -0,0 +1,56 @@
from PyQt5.QtWidgets import QScrollArea, QLabel, QVBoxLayout, QWidget
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap, QImage
import fitz
class PDFViewer(QScrollArea):
def __init__(self, parent):
super().__init__(parent)
self.scrollAreaContents = QWidget()
self.scrollAreaLayout = QVBoxLayout()
self.setWidget(self.scrollAreaContents)
self.setWidgetResizable(True)
self.scrollAreaContents.setLayout(self.scrollAreaLayout)
self.pixmapList = []
def resizeEvent(self, event):
pass
# do something about this later
def update(self, pdf):
self.render(pdf)
self.clear()
self.show()
def render(self, pdf):
"""
Update the preview shown by rendering a new PDF and drawing it to the scroll area.
"""
self.pixmapList = []
pdfView = fitz.Document(stream=pdf, filetype='pdf')
# render at 4x resolution and scale
for page in pdfView:
self.pixmapList.append(page.getPixmap(matrix=fitz.Matrix(4, 4), alpha=False))
def clear(self):
while self.scrollAreaLayout.count():
item = self.scrollAreaLayout.takeAt(0)
w = item.widget()
if w:
w.deleteLater()
def show(self):
for p in self.pixmapList:
label = QLabel(parent=self.scrollAreaContents)
label.setAlignment(Qt.AlignHCenter)
qtimg = QImage(p.samples, p.width, p.height, p.stride, QImage.Format_RGB888)
# -45 because of various margins... value obtained by trial and error.
label.setPixmap(QPixmap.fromImage(qtimg).scaled(self.width()-45, self.height()*2, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation))
self.scrollAreaLayout.addWidget(label)
# necessary on Mojave with PyInstaller (or previous contents will be shown)
self.repaint()

353
chordsheet/render.py

@ -1,13 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from math import trunc
from math import trunc, ceil
from io import BytesIO
from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas
from reportlab.lib.units import mm from reportlab.lib.units import mm
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import BaseDocTemplate, Spacer, Paragraph, Flowable, Frame, PageTemplate
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import BaseDocTemplate, Spacer, Paragraph, Flowable, Frame, PageTemplate, PageBreak
from chordsheet.document import Block from chordsheet.document import Block
from chordsheet.rlStylesheet import getStyleSheet
def writeText(canvas, style, string, size, vpos, width, **kwargs): def writeText(canvas, style, string, size, vpos, width, **kwargs):
@ -37,31 +39,27 @@ def writeText(canvas, style, string, size, vpos, width, **kwargs):
return size*style.lineSpacing return size*style.lineSpacing
def splitBlocks(blockList, maxWidth):
h_loc = 0
splitBlockList = []
for i in range(len(blockList)):
c_orig = blockList[i].chord
n_orig = blockList[i].notes
if h_loc == maxWidth:
h_loc = 0
if h_loc+blockList[i].length > maxWidth:
lengthList = [maxWidth - h_loc]
while sum(lengthList) < blockList[i].length:
if blockList[i].length - sum(lengthList) >= maxWidth:
lengthList.append(maxWidth)
else:
lengthList.append(blockList[i].length - sum(lengthList))
for l in lengthList:
# create a block with the given length
splitBlockList.append(Block(l, chord=c_orig, notes=n_orig))
h_loc = lengthList[-1]
else:
splitBlockList.append(blockList[i])
h_loc += blockList[i].length
return splitBlockList
class Tempo(Flowable):
"""
Flowable that draws the tempo. Necessary because Paragraph does not support the crotchet character.
"""
def __init__(self, tempo, paraStyle):
self.tempo = tempo
self.text = "♩ = {t} bpm".format(t=self.tempo)
self.fontSize = paraStyle.fontSize
self.fontname = paraStyle.fontname
self.leading = paraStyle.leading
def wrap(self, availWidth, availHeight):
self.width = availWidth
self.height = self.leading
return (self.width, self.height)
def draw(self):
canvas = self.canv
canvas.setFont(self.fontname, self.fontSize)
canvas.drawString(0, self.leading * 0.25, self.text)
class GuitarChart(Flowable): class GuitarChart(Flowable):
@ -77,66 +75,81 @@ class GuitarChart(Flowable):
self.nStrings = 6 self.nStrings = 6
self.headingSize = 18 self.headingSize = 18
self.spaceAfter = self.style.separatorSize * mm
self.spaceAfter = self.style.separatorSize
def splitChordList(self, l, n):
"""Yield successive n-sized chunks from l."""
for i in range(0, len(l), n):
yield l[i:i + n]
def wrap(self, availWidth, availHeight): 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
self.nChords = trunc((availWidth - self.chartMargin -
self.style.stringHzGap) / self.style.stringHzSp)
# the height of one layer of chart
self.oneHeight = self.style.stringHeight * (self.nStrings+1)
# only one line needed
if len(self.guitarChordList) <= self.nChords:
self.width = self.chartMargin + self.style.stringHzGap + self.style.stringHzSp * \
len(self.guitarChordList) # calculate the width
self.height = self.oneHeight # and its height
# multiple lines needed
else:
self.width = self.chartMargin + self.style.stringHzGap + \
self.style.stringHzSp * self.nChords
self.height = self.oneHeight * ceil(len(self.guitarChordList) / self.nChords) + \
(self.style.stringHeight *
trunc(len(self.guitarChordList) / self.nChords))
return (self.width, self.height) return (self.width, self.height)
def draw(self): def draw(self):
canvas = self.canv canvas = self.canv
title_height = writeText(canvas, self.style, "Guitar chord voicings",
self.headingSize, self.height, self.width, align="left")
chartmargin = self.chartMargin chartmargin = self.chartMargin
v_origin = self.height - title_height - 2*mm
h_origin = chartmargin
self.nStrings = 6
fontsize = 12
stringList = [
[c.voicings['guitar'][-(r+1)] for c in self.guitarChordList] for r in range(self.nStrings)]
stringList.append([c.name for c in self.guitarChordList])
for i in range(self.nStrings+1): # i is the string line currently being drawn
writeText(canvas, self.style, ['e', 'B', 'G', 'D', 'A', 'E', 'Name'][i], fontsize, v_origin-(
i*self.style.stringHeight), self.width, hpos=h_origin, align='right')
# j is which chord (0 is first chord, 1 is 2nd etc)
for j in range(len(stringList[-1])):
currentWidth = canvas.stringWidth(stringList[i][j])
if j == 0:
x = self.style.stringHzGap + chartmargin
l = self.style.stringHzSp/2 - self.style.stringHzGap - \
((currentWidth/2)) - self.style.stringHzGap
y = v_origin-(self.style.stringHeight*i) - \
self.style.stringHeight/2
canvas.line(x, y, x+l, y)
else:
x = chartmargin + self.style.stringHzSp * \
(j-0.5)+(lastWidth/2+self.style.stringHzGap)
l = self.style.stringHzSp - currentWidth / \
2 - lastWidth/2 - self.style.stringHzGap*2
y = v_origin-(self.style.stringHeight*i) - \
self.style.stringHeight/2
canvas.line(x, y, x+l, y)
if j == len(stringList[-1])-1:
x = chartmargin + self.style.stringHzSp * \
(j+0.5) + currentWidth/2 + self.style.stringHzGap
l = self.style.stringHzSp/2 - currentWidth/2 - self.style.stringHzGap
y = v_origin-(self.style.stringHeight*i) - \
self.style.stringHeight/2
canvas.line(x, y, x+l, y)
writeText(canvas, self.style, stringList[i][j], fontsize, v_origin-(
i*self.style.stringHeight), self.width, hpos=chartmargin+self.style.stringHzSp*(j+0.5))
lastWidth = currentWidth
for count, gcl in enumerate(self.splitChordList(self.guitarChordList, self.nChords)):
v_origin = self.height - count * (self.oneHeight + self.style.stringHeight)
self.nStrings = 6
fontsize = 12
stringList = [
[c.voicings['guitar'][-(r+1)] for c in gcl] for r in range(self.nStrings)]
stringList.append([c.name for c in gcl])
for i in range(self.nStrings+1): # i is the string line currently being drawn
writeText(canvas, self.style, ['e', 'B', 'G', 'D', 'A', 'E', 'Name'][i], fontsize, v_origin-(
i*self.style.stringHeight), self.width, hpos=chartmargin, align='right')
# j is which chord (0 is first chord, 1 is 2nd etc)
for j in range(len(stringList[-1])):
currentWidth = canvas.stringWidth(stringList[i][j])
if j == 0:
x = self.style.stringHzGap + chartmargin
l = self.style.stringHzSp/2 - self.style.stringHzGap - \
((currentWidth/2)) - self.style.stringHzGap
y = v_origin-(self.style.stringHeight*i) - \
self.style.stringHeight/2
canvas.line(x, y, x+l, y)
else:
x = chartmargin + self.style.stringHzSp * \
(j-0.5)+(lastWidth/2+self.style.stringHzGap)
l = self.style.stringHzSp - currentWidth / \
2 - lastWidth/2 - self.style.stringHzGap*2
y = v_origin-(self.style.stringHeight*i) - \
self.style.stringHeight/2
canvas.line(x, y, x+l, y)
if j == len(stringList[-1])-1:
x = chartmargin + self.style.stringHzSp * \
(j+0.5) + currentWidth/2 + self.style.stringHzGap
l = self.style.stringHzSp/2 - currentWidth/2 - self.style.stringHzGap
y = v_origin-(self.style.stringHeight*i) - \
self.style.stringHeight/2
canvas.line(x, y, x+l, y)
writeText(canvas, self.style, stringList[i][j], fontsize, v_origin-(
i*self.style.stringHeight), self.width, hpos=chartmargin+self.style.stringHzSp*(j+0.5))
lastWidth = currentWidth
class ChordProgression(Flowable): class ChordProgression(Flowable):
@ -144,33 +157,89 @@ class ChordProgression(Flowable):
Flowable that draws a chord progression made up of blocks. Flowable that draws a chord progression made up of blocks.
""" """
def __init__(self, style, blockList, timeSignature):
def __init__(self, style, heading, blockList, timeSignature):
self.style = style self.style = style
self.heading = heading # the title of the section
self.blockList = blockList self.blockList = blockList
self.timeSignature = timeSignature self.timeSignature = timeSignature
self.headingSize = 18 self.headingSize = 18
self.spaceAfter = self.style.separatorSize * mm
self.spaceAfter = self.style.separatorSize
def wrapBlocks(self, blockList, maxWidth):
"""
Splits any blocks that won't fit in the remaining space on the line.
"""
h_loc = 0
splitBlockList = []
for i in range(len(blockList)):
c_orig = blockList[i].chord
n_orig = blockList[i].notes
if h_loc == maxWidth:
h_loc = 0
if h_loc+blockList[i].length > maxWidth:
lengthList = [maxWidth - h_loc]
while sum(lengthList) < blockList[i].length:
if blockList[i].length - sum(lengthList) >= maxWidth:
lengthList.append(maxWidth)
# print(lengthList)
else:
lengthList.append(
blockList[i].length - sum(lengthList))
# print(lengthList)
for l in lengthList:
# create a block with the given length
splitBlockList.append(Block(l, chord=c_orig, notes=n_orig))
h_loc = lengthList[-1]
else:
splitBlockList.append(blockList[i])
h_loc += blockList[i].length
return splitBlockList
def splitBlockList(self, blockList, length):
"""
Splits a blockList into two lists, one of the given length (in beats) and one for the rest. Also wraps the blocks to
given length in case the split would fall in the middle of one.
"""
secondPart = self.wrapBlocks(blockList, length)
firstPart = []
currentBeat = 0
while currentBeat != length:
block = secondPart.pop(0)
firstPart.append(block)
currentBeat += block.length
return firstPart, secondPart
def wrap(self, availWidth, availHeight): def wrap(self, availWidth, availHeight):
self.widthInBeats = 2 * self.timeSignature * \ self.widthInBeats = 2 * self.timeSignature * \
trunc((availWidth/(self.style.unitWidth*self.style.unit)) / trunc((availWidth/(self.style.unitWidth*self.style.unit)) /
(2*self.timeSignature)) # width of each line, in beats (2*self.timeSignature)) # width of each line, in beats
self.width = self.widthInBeats * self.style.unitWidth * self.style.unit self.width = self.widthInBeats * self.style.unitWidth * self.style.unit
self.height = self.headingSize * self.style.lineSpacing + 2 * mm + self.style.beatsHeight + \
self.style.unitHeight * \
self.height = self.style.beatsHeight + self.style.unitHeight * \
sum([b.length for b in self.blockList]) / self.widthInBeats sum([b.length for b in self.blockList]) / self.widthInBeats
return(self.width, self.height) return(self.width, self.height)
def split(self, availWidth, availHeight):
if availHeight >= self.height:
return [self]
else:
vUnits = trunc(
(availHeight - self.style.beatsHeight) / self.style.unitHeight)
firstPart, secondPart = self.splitBlockList(
self.blockList, vUnits * self.widthInBeats)
return [ChordProgression(self.style, self.heading, firstPart, self.timeSignature),
PageBreak(),
ChordProgression(self.style, self.heading, secondPart, self.timeSignature)]
def draw(self): def draw(self):
canvas = self.canv canvas = self.canv
unitWidth = self.style.unitWidth*self.style.unit 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
v_origin = self.height - self.style.beatsHeight
h_loc = 0 h_loc = 0
v_loc = 0 v_loc = 0
@ -190,7 +259,7 @@ class ChordProgression(Flowable):
writeText(canvas, self.style, str((u % self.timeSignature)+1), self.style.beatsFontSize, writeText(canvas, self.style, str((u % self.timeSignature)+1), self.style.beatsFontSize,
v_origin+self.style.beatsHeight, self.width, hpos=x+unitWidth/2) v_origin+self.style.beatsHeight, self.width, hpos=x+unitWidth/2)
parsedBlockList = splitBlocks(self.blockList, maxWidth)
parsedBlockList = self.wrapBlocks(self.blockList, maxWidth)
for b in parsedBlockList: for b in parsedBlockList:
if h_loc == maxWidth: if h_loc == maxWidth:
@ -200,12 +269,12 @@ class ChordProgression(Flowable):
b.length*unitWidth, self.style.unitHeight) b.length*unitWidth, self.style.unitHeight)
if b.notes is not None: if b.notes is not None:
writeText(canvas, self.style, b.notes, self.style.notesFontSize, v_origin-((v_loc+1)*self.style.unitHeight)+( 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))
1.3*self.style.notesFontSize), self.width, hpos=((h_loc+b.length/2)*unitWidth))
v_offset = ((v_loc*self.style.unitHeight) + v_offset = ((v_loc*self.style.unitHeight) +
self.style.unitHeight/2)-self.style.chordNameFontSize/2 self.style.unitHeight/2)-self.style.chordNameFontSize/2
if b.chord is not None: if b.chord is not None:
writeText(canvas, self.style, b.chord.name, self.style.chordNameFontSize, writeText(canvas, self.style, b.chord.name, self.style.chordNameFontSize,
v_origin-v_offset, self.width, hpos=h_origin+((h_loc+b.length/2)*unitWidth))
v_origin-v_offset, self.width, hpos=((h_loc+b.length/2)*unitWidth))
h_loc += b.length h_loc += b.length
@ -218,73 +287,59 @@ def guitarChartCheck(cL):
return chordsPresent return chordsPresent
class TitleBlock(Flowable):
"""
Flowable that draws the title and other text at the top of the document.
"""
def __init__(self, style, document):
class Renderer:
def __init__(self, document, style):
self.document = document
self.style = style self.style = style
self.lS = style.lineSpacing
self.title = document.title
self.subtitle = document.subtitle
self.composer = document.composer
self.arranger = document.arranger
self.tempo = document.tempo
self.spaceAfter = self.style.separatorSize * mm
def wrap(self, availWidth, availHeight):
self.width = availWidth
self.height = sum([self.style.titleFontSize * self.lS if self.title else 0,
self.style.subtitleFontSize * self.lS if self.subtitle else 0,
self.style.creditsFontSize * self.lS if self.composer else 0,
self.style.titleFontSize * self.lS if self.arranger else 0,
self.style.tempoFontSize * self.lS if self.tempo else 0])
return(self.width, self.height)
def draw(self):
canvas = self.canv
curPos = self.height
def savePDF(self, pathToPDF):
template = PageTemplate(id='AllPages', frames=[Frame(self.style.leftMargin*mm, self.style.bottomMargin*mm,
self.style.pageSize[0] - self.style.leftMargin*mm - self.style.rightMargin*mm,
self.style.pageSize[1] - self.style.topMargin*mm - self.style.bottomMargin*mm,
leftPadding=0, bottomPadding=0, rightPadding=0, topPadding=0)])
if self.title:
curPos -= writeText(canvas, self.style,
self.title, 24, curPos, self.width)
rlDocList = []
rlDoc = BaseDocTemplate(
pathToPDF, pagesize=self.style.pageSize, pageTemplates=[template])
if self.subtitle:
curPos -= writeText(canvas, self.style,
self.subtitle, 18, curPos, self.width)
styles = getStyleSheet(self.style)
if self.composer:
curPos -= writeText(canvas, self.style,
"Composer: {c}".format(c=self.composer), 12, curPos, self.width)
if self.document.title:
rlDocList.append(Paragraph(self.document.title, styles['Title']))
if self.arranger:
curPos -= writeText(canvas, self.style,
"Arranger: {a}".format(a=self.arranger), 12, curPos, self.width)
if self.document.subtitle:
rlDocList.append(
Paragraph(self.document.subtitle, styles['Subtitle']))
if self.tempo:
curPos -= writeText(canvas, self.style, "♩ = {t} bpm".format(
t=self.tempo), 12, curPos, self.width, align="left")
if self.document.composer:
rlDocList.append(Paragraph("Composer: {c}".format(
c=self.document.composer), styles['Credits']))
if self.document.arranger:
rlDocList.append(Paragraph("Arranger: {a}".format(
a=self.document.arranger), styles['Credits']))
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)])
if self.document.tempo:
rlDocList.append(Tempo(self.document.tempo, styles['Tempo']))
rlDocList = []
rlDoc = BaseDocTemplate(
pathToPDF, pagesize=style.pageSize, pageTemplates=[template])
if self.document.title or self.document.subtitle or self.document.composer or self.document.arranger or self.document.tempo:
rlDocList.append(Spacer(0, self.style.separatorSize))
if document.title:
rlDocList.append(TitleBlock(style, document))
if guitarChartCheck(self.document.chordList):
rlDocList.extend([
Paragraph('Guitar chord voicings', styles['Heading']),
GuitarChart(self.style, self.document.chordList)])
if guitarChartCheck(document.chordList):
rlDocList.append(GuitarChart(style, document.chordList))
for s in self.document.sectionList:
rlDocList.append(Paragraph(s.name, styles['Heading']))
# only draw the chord progression if there are blocks
if s.blockList:
rlDocList.append(ChordProgression(
self.style, s.name, s.blockList, self.document.timeSignature))
if document.blockList:
rlDocList.append(ChordProgression(
style, document.blockList, document.timeSignature))
rlDoc.build(rlDocList)
rlDoc.build(rlDocList)
def stream(self):
virtualFile = BytesIO()
self.savePDF(virtualFile)
return virtualFile

110
chordsheet/rlStylesheet.py

@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
from reportlab.lib.styles import StyleSheet1, ParagraphStyle
from reportlab.lib.enums import *
from reportlab.lib.units import mm
from reportlab.lib.colors import black
def getStyleSheet(csStyle):
"""Returns a stylesheet object"""
stylesheet = StyleSheet1()
stylesheet.add(ParagraphStyle(name='Master',
fontname=csStyle.font))
stylesheet.add(ParagraphStyle(name='Title',
leading=csStyle.lineSpacing*csStyle.titleFontSize,
fontSize=csStyle.titleFontSize,
alignment=TA_CENTER,
parent=stylesheet['Master'])
)
stylesheet.add(ParagraphStyle(name='Subtitle',
leading=csStyle.lineSpacing*csStyle.subtitleFontSize,
fontSize=csStyle.subtitleFontSize,
alignment=TA_CENTER,
parent=stylesheet['Master'])
)
stylesheet.add(ParagraphStyle(name='Credits',
leading=csStyle.lineSpacing*csStyle.creditsFontSize,
fontSize=csStyle.creditsFontSize,
alignment=TA_CENTER,
parent=stylesheet['Master'])
)
stylesheet.add(ParagraphStyle(name='Tempo',
leading=csStyle.lineSpacing*csStyle.tempoFontSize,
fontSize=csStyle.tempoFontSize,
alignment=TA_LEFT,
parent=stylesheet['Master'])
)
stylesheet.add(ParagraphStyle(name='Heading',
leading=csStyle.lineSpacing*csStyle.headingFontSize,
fontSize=csStyle.headingFontSize,
alignment=TA_LEFT,
parent=stylesheet['Master'],
spaceAfter=2*mm)
)
# stylesheet.add(ParagraphStyle(name='Heading3',
# parent=stylesheet['Normal'],
# fontName = csStyle.font,
# fontSize=12,
# leading=14,
# spaceBefore=12,
# spaceAfter=6),
# alias='h3')
# stylesheet.add(ParagraphStyle(name='Heading4',
# parent=stylesheet['Normal'],
# fontName = csStyle.font,
# fontSize=10,
# leading=12,
# spaceBefore=10,
# spaceAfter=4),
# alias='h4')
# stylesheet.add(ParagraphStyle(name='Heading5',
# parent=stylesheet['Normal'],
# fontName = csStyle.font,
# fontSize=9,
# leading=10.8,
# spaceBefore=8,
# spaceAfter=4),
# alias='h5')
# stylesheet.add(ParagraphStyle(name='Heading6',
# parent=stylesheet['Normal'],
# fontName = csStyle.font,
# fontSize=7,
# leading=8.4,
# spaceBefore=6,
# spaceAfter=2),
# alias='h6')
# stylesheet.add(ParagraphStyle(name='Bullet',
# parent=stylesheet['Normal'],
# firstLineIndent=0,
# spaceBefore=3),
# alias='bu')
# stylesheet.add(ParagraphStyle(name='Definition',
# parent=stylesheet['Normal'],
# firstLineIndent=0,
# leftIndent=36,
# bulletIndent=0,
# spaceBefore=6,
# bulletFontName=csStyle.font),
# alias='df')
# stylesheet.add(ParagraphStyle(name='Code',
# parent=stylesheet['Normal'],
# fontName='Courier',
# fontSize=8,
# leading=8.8,
# firstLineIndent=0,
# leftIndent=36,
# hyphenationLang=''))
return stylesheet

24
chordsheet/tableView.py

@ -81,6 +81,30 @@ class ChordTableView(MTableView):
self.model.appendRow(rowList) self.model.appendRow(rowList)
class SectionTableView(MTableView):
"""
Subclass MTableView to add properties just for the section table.
"""
def __init__(self, parent):
super().__init__(parent)
self.model.setHorizontalHeaderLabels(['Name'])
def populate(self, sList):
"""
Fill the table from a list of Section objects.
"""
self.model.removeRows(0, self.model.rowCount())
for s in sList:
rowList = [QtGui.QStandardItem(s.name)]
for item in rowList:
item.setEditable(False)
item.setDropEnabled(False)
self.model.appendRow(rowList)
class BlockTableView(MTableView): class BlockTableView(MTableView):
""" """
Subclass MTableView to add properties just for the block table. Subclass MTableView to add properties just for the block table.

4
examples/ah.xml

@ -27,7 +27,7 @@
<voicing instrument="guitar">x69676</voicing> <voicing instrument="guitar">x69676</voicing>
</chord> </chord>
</chords> </chords>
<progression>
<section name="A section">
<block> <block>
<length>9</length> <length>9</length>
<chord>Gm9</chord> <chord>Gm9</chord>
@ -73,5 +73,5 @@
<chord>D7#5#9</chord> <chord>D7#5#9</chord>
<notes>over Ab</notes> <notes>over Ab</notes>
</block> </block>
</progression>
</section>
</chordsheet> </chordsheet>

309
examples/ahlong.xml

@ -0,0 +1,309 @@
<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>
<name>Gm9</name>
<voicing instrument="guitar">x,10,8,10,10,x</voicing>
</chord>
<chord>
<name>Abm9</name>
<voicing instrument="guitar">x,11,9,11,11,x</voicing>
</chord>
<chord>
<name>Cm9</name>
<voicing instrument="guitar">x,x,8,8,8,10</voicing>
</chord>
<chord>
<name>D7#5#9</name>
<voicing instrument="guitar">x58565</voicing>
</chord>
<chord>
<name>Eb7#5#9</name>
<voicing instrument="guitar">x69676</voicing>
</chord>
</chords>
<section name="A section">
<block>
<length>9</length>
<chord>Gm9</chord>
</block>
<block>
<length>3</length>
<chord>Abm9</chord>
</block>
<block>
<length>12</length>
<chord>Gm9</chord>
</block>
<block>
<length>9</length>
<chord>Cm9</chord>
</block>
<block>
<length>3</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>12</length>
<chord>Gm9</chord>
</block>
<block>
<length>6</length>
<chord>Eb7#5#9</chord>
</block>
<block>
<length>6</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>6</length>
<chord>Cm9</chord>
</block>
<block>
<length>3</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>3</length>
<chord>D7#5#9</chord>
<notes>over Ab</notes>
</block>
</section>
<section name="B section">
<block>
<length>9</length>
<chord>Gm9</chord>
</block>
<block>
<length>3</length>
<chord>Abm9</chord>
</block>
<block>
<length>12</length>
<chord>Gm9</chord>
</block>
<block>
<length>6</length>
<chord>Eb7#5#9</chord>
</block>
<block>
<length>6</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>12</length>
<chord>Cm9</chord>
</block>
</section>
<section name="C section">
<block>
<length>9</length>
<chord>Gm9</chord>
</block>
<block>
<length>3</length>
<chord>Abm9</chord>
</block>
<block>
<length>12</length>
<chord>Gm9</chord>
</block>
<block>
<length>6</length>
<chord>Eb7#5#9</chord>
</block>
<block>
<length>6</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>12</length>
<chord>Cm9</chord>
</block>
<block>
<length>9</length>
<chord>Gm9</chord>
</block>
<block>
<length>3</length>
<chord>Abm9</chord>
</block>
<block>
<length>12</length>
<chord>Gm9</chord>
</block>
<block>
<length>9</length>
<chord>Cm9</chord>
</block>
<block>
<length>3</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>12</length>
<chord>Gm9</chord>
</block>
<block>
<length>6</length>
<chord>Eb7#5#9</chord>
</block>
<block>
<length>6</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>6</length>
<chord>Cm9</chord>
</block>
<block>
<length>3</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>3</length>
<chord>D7#5#9</chord>
<notes>over Ab</notes>
</block>
<block>
<length>9</length>
<chord>Gm9</chord>
</block>
<block>
<length>3</length>
<chord>Abm9</chord>
</block>
<block>
<length>12</length>
<chord>Gm9</chord>
</block>
<block>
<length>9</length>
<chord>Cm9</chord>
</block>
<block>
<length>3</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>12</length>
<chord>Gm9</chord>
</block>
<block>
<length>6</length>
<chord>Eb7#5#9</chord>
</block>
<block>
<length>6</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>6</length>
<chord>Cm9</chord>
</block>
<block>
<length>3</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>3</length>
<chord>D7#5#9</chord>
<notes>over Ab</notes>
</block>
<block>
<length>9</length>
<chord>Gm9</chord>
</block>
<block>
<length>3</length>
<chord>Abm9</chord>
</block>
<block>
<length>12</length>
<chord>Gm9</chord>
</block>
<block>
<length>9</length>
<chord>Cm9</chord>
</block>
<block>
<length>3</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>12</length>
<chord>Gm9</chord>
</block>
<block>
<length>6</length>
<chord>Eb7#5#9</chord>
</block>
<block>
<length>6</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>6</length>
<chord>Cm9</chord>
</block>
<block>
<length>3</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>3</length>
<chord>D7#5#9</chord>
<notes>over Ab</notes>
</block>
<block>
<length>9</length>
<chord>Gm9</chord>
</block>
<block>
<length>3</length>
<chord>Abm9</chord>
</block>
<block>
<length>12</length>
<chord>Gm9</chord>
</block>
<block>
<length>9</length>
<chord>Cm9</chord>
</block>
<block>
<length>3</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>12</length>
<chord>Gm9</chord>
</block>
<block>
<length>6</length>
<chord>Eb7#5#9</chord>
</block>
<block>
<length>6</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>6</length>
<chord>Cm9</chord>
</block>
<block>
<length>3</length>
<chord>D7#5#9</chord>
</block>
<block>
<length>3</length>
<chord>D7#5#9</chord>
<notes>over Ab</notes>
</block>
</section>
</chordsheet>

1
examples/angela.xml

@ -0,0 +1 @@
<chordsheet><title>Angela</title><subtitle>Theme from 'Taxi'</subtitle><arranger>Max</arranger><composer>Bob James</composer><timesignature>4</timesignature><chords><chord><name>E&#9837;maj7</name><voicing instrument="guitar">x,x,1,3,3,3</voicing></chord><chord><name>A&#9837;ma7</name><voicing instrument="guitar">x,x,1,1,1,3</voicing></chord><chord><name>Gm7</name><voicing instrument="guitar">3,x,3,3,3,x</voicing></chord><chord><name>B&#9837;/D</name><voicing instrument="guitar">x,x,0,3,3,1</voicing></chord><chord><name>A&#9837;maj7/C</name><voicing instrument="guitar">x,3,1,1,1,3</voicing></chord><chord><name>B&#9837;</name><voicing instrument="guitar">x,1,3,3,3,1</voicing></chord><chord><name>A&#9837;</name><voicing instrument="guitar">4,6,6,5,4,4</voicing></chord><chord><name>E&#9837;/G</name><voicing instrument="guitar">x,x,5,3,4,3</voicing></chord><chord><name>Fm</name><voicing instrument="guitar">x,x,3,1,1,1</voicing></chord><chord><name>A&#9837;/B&#9837;</name><voicing instrument="guitar">6,x,6,5,4,4</voicing></chord><chord><name>E&#9837;</name><voicing instrument="guitar">x,6,5,3,4,3</voicing></chord><chord><name>E&#9837;7</name><voicing instrument="guitar">x,x,1,3,2,3</voicing></chord><chord><name>A&#9837;maj9</name><voicing instrument="guitar">4,x,5,3,4,3</voicing></chord><chord><name>Fm7</name><voicing instrument="guitar">1,x,1,1,1,x</voicing></chord></chords><section name="Intro"><block><length>2.0</length><chord>E&#9837;maj7</chord></block><block><length>2.0</length><chord>A&#9837;ma7</chord></block><block><length>2.0</length><chord>Gm7</chord></block><block><length>2.0</length><chord>B&#9837;/D</chord></block><block><length>2.0</length><chord>A&#9837;maj7/C</chord></block><block><length>2.0</length><chord>B&#9837;</chord></block><block><length>1.0</length><chord>A&#9837;</chord></block><block><length>1.0</length><chord>E&#9837;/G</chord></block><block><length>1.0</length><chord>Fm</chord></block><block><length>1.0</length><chord>A&#9837;/B&#9837;</chord></block><block><length>2.0</length><chord>E&#9837;</chord></block><block><length>2.0</length><chord>E&#9837;7</chord></block><block><length>2.0</length><chord>A&#9837;maj9</chord></block><block><length>2.0</length><chord>E&#9837;/G</chord></block><block><length>2.0</length><chord>Fm7</chord></block><block><length>2.0</length><chord>B&#9837;</chord></block><block><length>1.0</length><chord>A&#9837;</chord></block><block><length>1.0</length><chord>E&#9837;/G</chord></block><block><length>1.0</length><chord>Fm</chord></block><block><length>1.0</length><chord>A&#9837;/B&#9837;</chord></block></section></chordsheet>

2
examples/example.xml

@ -1 +1 @@
<chordsheet><title>Example Song</title><composer>Ivan Holmes</composer><timesignature>4</timesignature><chords><chord><name>C</name><voicing instrument="guitar">x,3,2,0,1,0</voicing></chord><chord><name>F</name><voicing instrument="guitar">1,3,3,2,1,1</voicing></chord><chord><name>G</name><voicing instrument="guitar">3,2,0,0,0,3</voicing></chord><chord><name>C/G</name><voicing instrument="guitar">3,3,2,0,1,0</voicing></chord><chord><name>Dm</name><voicing instrument="guitar">x,x,0,2,3,1</voicing></chord></chords><progression><block><length>16</length><chord>C</chord><notes>Intro, strum lightly</notes></block><block><length>4</length><chord>C</chord></block><block><length>4</length><chord>F</chord></block><block><length>8</length><chord>G</chord></block><block><length>4</length><chord>C</chord></block><block><length>4</length><chord>F</chord></block><block><length>8</length><chord>G</chord></block><block><length>4</length><chord>C</chord></block><block><length>4</length><chord>Dm</chord></block><block><length>4</length><chord>C/G</chord></block><block><length>4</length><chord>G</chord></block><block><length>4</length><chord>C</chord></block><block><length>4</length><notes>Contemplation time</notes></block><block><length>8</length><chord>C</chord><notes>Crescendo until end</notes></block></progression></chordsheet>
<chordsheet><title>Example Song</title><composer>Ivan Holmes</composer><timesignature>4</timesignature><chords><chord><name>C</name><voicing instrument="guitar">x,3,2,0,1,0</voicing></chord><chord><name>F</name><voicing instrument="guitar">1,3,3,2,1,1</voicing></chord><chord><name>G</name><voicing instrument="guitar">3,2,0,0,0,3</voicing></chord><chord><name>C/G</name><voicing instrument="guitar">3,3,2,0,1,0</voicing></chord><chord><name>Dm</name><voicing instrument="guitar">x,x,0,2,3,1</voicing></chord></chords><section name="Example section"><block><length>16</length><chord>C</chord><notes>Intro, strum lightly</notes></block><block><length>4</length><chord>C</chord></block><block><length>4</length><chord>F</chord></block><block><length>8</length><chord>G</chord></block><block><length>4</length><chord>C</chord></block><block><length>4</length><chord>F</chord></block><block><length>8</length><chord>G</chord></block><block><length>4</length><chord>C</chord></block><block><length>4</length><chord>Dm</chord></block><block><length>4</length><chord>C/G</chord></block><block><length>4</length><chord>G</chord></block><block><length>4</length><chord>C</chord></block><block><length>4</length><notes>Contemplation time</notes></block><block><length>8</length><chord>C</chord><notes>Crescendo until end</notes></block></section></chordsheet>

1
examples/kissoflife.xml

@ -0,0 +1 @@
<chordsheet><title>Kiss of Life</title><subtitle>Sade</subtitle><arranger>Ivan Holmes</arranger><composer>Sade Adu, Paul S. Denman, Andrew Hale, Stuart Matthewman</composer><timesignature>4</timesignature><chords><chord><name>AM9</name></chord><chord><name>F&#9839;m11</name></chord><chord><name>DM7</name></chord><chord><name>C&#9839;m7</name></chord><chord><name>Bm7</name></chord></chords><section name="Intro/Verse"><block><length>8.0</length><chord>AM9</chord></block><block><length>8.0</length><chord>F&#9839;m11</chord></block><block><length>1.5</length><chord>DM7</chord></block><block><length>2.0</length><chord>C&#9839;m7</chord></block><block><length>4.5</length><chord>Bm7</chord></block><block><length>8.0</length><chord>F&#9839;m11</chord></block></section><section name="Chorus/Bridge"><block><length>3.5</length><chord>Bm7</chord></block><block><length>4.5</length><chord>F&#9839;m11</chord></block><block><length>3.5</length><chord>Bm7</chord></block><block><length>4.5</length><chord>F&#9839;m11</chord></block></section></chordsheet>

2
examples/test.xml

@ -1 +1 @@
<chordsheet><title>Composition</title><composer>A. Person</composer><timesignature>4</timesignature><chords><chord><name>B</name><voicing instrument="guitar">x,x,2,3,4,1</voicing></chord><chord><name>E</name><voicing instrument="guitar">0,2,2,1,0,0</voicing></chord><chord><name>Cm9</name><voicing instrument="guitar">x,x,8,8,8,10</voicing></chord><chord><name>D7&#9837;5&#9839;9</name></chord></chords><progression><block><length>4</length><chord>B</chord><notes>These are notes.</notes></block><block><length>4</length><chord>E</chord></block><block><length>12</length><chord>Cm9</chord></block><block><length>6</length><chord>D7&#9837;5&#9839;9</chord></block><block><length>6</length><notes>For quiet contemplation.</notes></block><block><length>46</length><chord>D7&#9837;5&#9839;9</chord><notes>A very long block to test wrapping!</notes></block></progression></chordsheet>
<chordsheet><title>Composition</title><composer>A. Person</composer><timesignature>4</timesignature><chords><chord><name>B</name><voicing instrument="guitar">x,x,2,3,4,1</voicing></chord><chord><name>E</name><voicing instrument="guitar">0,2,2,1,0,0</voicing></chord><chord><name>Cm9</name><voicing instrument="guitar">x,x,8,8,8,10</voicing></chord><chord><name>D7&#9837;5&#9839;9</name></chord></chords><section name="Test section"><block><length>4</length><chord>B</chord><notes>These are notes.</notes></block><block><length>4</length><chord>E</chord></block><block><length>12</length><chord>Cm9</chord></block><block><length>6</length><chord>D7&#9837;5&#9839;9</chord></block><block><length>6</length><notes>For quiet contemplation.</notes></block><block><length>46</length><chord>D7&#9837;5&#9839;9</chord><notes>A very long block to test wrapping!</notes></block></section></chordsheet>

264
gui.py

@ -11,6 +11,7 @@ import fitz
import io import io
import subprocess import subprocess
import os import os
import time
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
@ -18,14 +19,16 @@ 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 from chordsheet.tableView import ChordTableView, BlockTableView
from chordsheet.comboBox import MComboBox
from chordsheet.pdfViewer import PDFViewer
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
from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfbase.ttfonts import TTFont
from chordsheet.document import Document, Style, Chord, Block
from chordsheet.render import savePDF
from chordsheet.document import Document, Style, Chord, Block, Section
from chordsheet.render import Renderer
from chordsheet.parsers import parseFingering, parseName from chordsheet.parsers import parseFingering, parseName
import _version import _version
@ -72,6 +75,7 @@ class DocumentWindow(QMainWindow):
self.doc = doc self.doc = doc
self.style = style self.style = style
self.renderer = Renderer(self.doc, self.style)
self.lastDoc = copy(self.doc) self.lastDoc = copy(self.doc)
self.currentFilePath = filename self.currentFilePath = filename
@ -79,6 +83,8 @@ class DocumentWindow(QMainWindow):
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()
self.updateSectionDict()
self.currentSection = None
self.setCentralWidget(self.window.centralWidget) self.setCentralWidget(self.window.centralWidget)
self.setMenuBar(self.window.menuBar) self.setMenuBar(self.window.menuBar)
@ -146,17 +152,32 @@ class DocumentWindow(QMainWindow):
self.window.generateButton.clicked.connect(self.generateAction) self.window.generateButton.clicked.connect(self.generateAction)
# update whole document when any tab is selected
self.window.tabWidget.tabBarClicked.connect(self.tabBarUpdateAction)
self.window.guitarVoicingButton.clicked.connect( self.window.guitarVoicingButton.clicked.connect(
self.guitarVoicingAction) 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)
# connecting clicked only works for this combo box because it's my own modified version (MComboBox)
self.window.blockSectionComboBox.clicked.connect(
self.blockSectionClickedAction)
self.window.blockSectionComboBox.currentIndexChanged.connect(
self.blockSectionChangedAction)
self.window.addBlockButton.clicked.connect(self.addBlockAction) self.window.addBlockButton.clicked.connect(self.addBlockAction)
self.window.removeBlockButton.clicked.connect(self.removeBlockAction) self.window.removeBlockButton.clicked.connect(self.removeBlockAction)
self.window.updateBlockButton.clicked.connect(self.updateBlockAction) self.window.updateBlockButton.clicked.connect(self.updateBlockAction)
self.window.addSectionButton.clicked.connect(self.addSectionAction)
self.window.removeSectionButton.clicked.connect(
self.removeSectionAction)
self.window.updateSectionButton.clicked.connect(
self.updateSectionAction)
self.window.chordTableView.clicked.connect(self.chordClickedAction) self.window.chordTableView.clicked.connect(self.chordClickedAction)
self.window.sectionTableView.clicked.connect(self.sectionClickedAction)
self.window.blockTableView.clicked.connect(self.blockClickedAction) self.window.blockTableView.clicked.connect(self.blockClickedAction)
def UIInitDocument(self): def UIInitDocument(self):
@ -167,13 +188,20 @@ class DocumentWindow(QMainWindow):
# set all fields to appropriate values from document # set all fields to appropriate values from document
self.window.titleLineEdit.setText(self.doc.title) self.window.titleLineEdit.setText(self.doc.title)
self.window.subtitleLineEdit.setText(self.doc.subtitle)
self.window.composerLineEdit.setText(self.doc.composer) self.window.composerLineEdit.setText(self.doc.composer)
self.window.arrangerLineEdit.setText(self.doc.arranger) self.window.arrangerLineEdit.setText(self.doc.arranger)
self.window.timeSignatureSpinBox.setValue(self.doc.timeSignature) self.window.timeSignatureSpinBox.setValue(self.doc.timeSignature)
self.window.tempoLineEdit.setText(self.doc.tempo) self.window.tempoLineEdit.setText(self.doc.tempo)
self.window.chordTableView.populate(self.doc.chordList) self.window.chordTableView.populate(self.doc.chordList)
self.window.blockTableView.populate(self.doc.blockList)
self.window.sectionTableView.populate(self.doc.sectionList)
# populate the block table with the first section, account for a document with no sections
self.currentSection = self.doc.sectionList[0] if len(
self.doc.sectionList) else None
self.window.blockTableView.populate(
self.currentSection.blockList if self.currentSection else [])
self.updateSectionDict()
self.updateChordDict() self.updateChordDict()
def UIInitStyle(self): def UIInitStyle(self):
@ -191,13 +219,19 @@ class DocumentWindow(QMainWindow):
self.window.lineSpacingDoubleSpinBox.setValue(self.style.lineSpacing) self.window.lineSpacingDoubleSpinBox.setValue(self.style.lineSpacing)
self.window.leftMarginLineEdit.setText(str(self.style.leftMargin)) self.window.leftMarginLineEdit.setText(str(self.style.leftMargin))
self.window.rightMarginLineEdit.setText(str(self.style.rightMargin))
self.window.topMarginLineEdit.setText(str(self.style.topMargin)) self.window.topMarginLineEdit.setText(str(self.style.topMargin))
self.window.bottomMarginLineEdit.setText(str(self.style.bottomMargin))
self.window.fontComboBox.setDisabled(True) self.window.fontComboBox.setDisabled(True)
self.window.includedFontCheckBox.setChecked(True) self.window.includedFontCheckBox.setChecked(True)
self.window.beatWidthLineEdit.setText(str(self.style.unitWidth)) self.window.beatWidthLineEdit.setText(str(self.style.unitWidth))
def tabBarUpdateAction(self, index):
self.updateDocument()
def pageSizeAction(self, index): def pageSizeAction(self, index):
self.pageSizeSelected = self.window.pageSizeComboBox.itemText(index) self.pageSizeSelected = self.window.pageSizeComboBox.itemText(index)
@ -217,6 +251,29 @@ class DocumentWindow(QMainWindow):
self.window.guitarVoicingLineEdit.setText( self.window.guitarVoicingLineEdit.setText(
self.window.chordTableView.model.item(index.row(), 1).text()) self.window.chordTableView.model.item(index.row(), 1).text())
def sectionClickedAction(self, index):
# set the controls to the values from the selected section
self.window.sectionNameLineEdit.setText(
self.window.sectionTableView.model.item(index.row(), 0).text())
# also set the combo box on the block page to make it flow well
curSecName = self.window.sectionTableView.model.item(
index.row(), 0).text()
if curSecName:
self.window.blockSectionComboBox.setCurrentText(
curSecName)
def blockSectionClickedAction(self, text):
if text:
self.updateBlocks(self.sectionDict[text])
def blockSectionChangedAction(self, index):
sName = self.window.blockSectionComboBox.currentText()
if sName:
self.currentSection = self.sectionDict[sName]
self.window.blockTableView.populate(self.currentSection.blockList)
else:
self.currentSection = None
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()
@ -245,6 +302,8 @@ class DocumentWindow(QMainWindow):
self.lastDoc = copy(self.doc) self.lastDoc = copy(self.doc)
#  reset file path (this document hasn't been saved yet) #  reset file path (this document hasn't been saved yet)
self.currentFilePath = None self.currentFilePath = None
# new renderer
self.renderer = Renderer(self.doc, self.style)
self.UIInitDocument() self.UIInitDocument()
self.updatePreview() self.updatePreview()
@ -297,7 +356,7 @@ class DocumentWindow(QMainWindow):
filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath( filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath(
"lastExportPath"), "PDF files (*.pdf)")[0] "lastExportPath"), "PDF files (*.pdf)")[0]
if filePath: if filePath:
savePDF(d, s, filePath)
self.renderer.savePDF(filePath)
self.setPath("lastExportPath", filePath) self.setPath("lastExportPath", filePath)
def menuFilePrintAction(self): def menuFilePrintAction(self):
@ -381,6 +440,18 @@ class DocumentWindow(QMainWindow):
self.window.chordNameLineEdit.repaint() self.window.chordNameLineEdit.repaint()
self.window.guitarVoicingLineEdit.repaint() self.window.guitarVoicingLineEdit.repaint()
def clearSectionLineEdits(self):
self.window.sectionNameLineEdit.clear()
# necessary on Mojave with PyInstaller (or previous contents will be shown)
self.window.sectionNameLineEdit.repaint()
def clearBlockLineEdits(self):
self.window.blockLengthLineEdit.clear()
self.window.blockNotesLineEdit.clear()
# necessary on Mojave with PyInstaller (or previous contents will be shown)
self.window.blockLengthLineEdit.repaint()
self.window.blockNotesLineEdit.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)
@ -390,6 +461,15 @@ class DocumentWindow(QMainWindow):
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 updateSectionDict(self):
"""
Updates the dictionary used to generate the Section menu (on the block tab)
"""
self.sectionDict = {s.name: s for s in self.doc.sectionList}
self.window.blockSectionComboBox.clear()
self.window.blockSectionComboBox.addItems(
list(self.sectionDict.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()
@ -418,7 +498,7 @@ class DocumentWindow(QMainWindow):
else: else:
success = True #  chord successfully parsed success = True #  chord successfully parsed
else: else:
NameWarningMessageBox().exec() # Chord has no name, warn user
ChordNameWarningMessageBox().exec() # Chord has no name, warn user
if success == True: # if chord was parsed properly if success == True: # if chord was parsed properly
self.window.chordTableView.populate(self.doc.chordList) self.window.chordTableView.populate(self.doc.chordList)
@ -430,9 +510,10 @@ class DocumentWindow(QMainWindow):
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()
oldName = self.window.chordTableView.model.item(row, 0).text()
cName = parseName(self.window.chordNameLineEdit.text()) cName = parseName(self.window.chordNameLineEdit.text())
if cName: if cName:
self.doc.chordList[row] = Chord(cName)
self.doc.chordList[row].name = cName
if self.window.guitarVoicingLineEdit.text(): if self.window.guitarVoicingLineEdit.text():
try: try:
self.doc.chordList[row].voicings['guitar'] = parseFingering( self.doc.chordList[row].voicings['guitar'] = parseFingering(
@ -443,44 +524,83 @@ class DocumentWindow(QMainWindow):
else: else:
success = True success = True
else: else:
NameWarningMessageBox().exec()
ChordNameWarningMessageBox().exec()
if success == True: if success == True:
self.updateChordDict()
self.window.chordTableView.populate(self.doc.chordList) self.window.chordTableView.populate(self.doc.chordList)
# update the names of chords in all blocklists in case they've already been used
for s in self.doc.sectionList:
for b in s.blockList:
if b.chord:
if b.chord.name == oldName:
b.chord.name = cName
self.window.blockTableView.populate(self.currentSection.blockList)
self.clearChordLineEdits() self.clearChordLineEdits()
self.updateChordDict()
def clearBlockLineEdits(self):
self.window.blockLengthLineEdit.clear()
self.window.blockNotesLineEdit.clear()
# necessary on Mojave with PyInstaller (or previous contents will be shown)
self.window.blockLengthLineEdit.repaint()
self.window.blockNotesLineEdit.repaint()
def removeSectionAction(self):
if self.window.sectionTableView.selectionModel().hasSelection(): #  check for selection
self.updateSections()
row = self.window.sectionTableView.selectionModel().currentIndex().row()
self.doc.sectionList.pop(row)
self.window.sectionTableView.populate(self.doc.sectionList)
self.clearSectionLineEdits()
self.updateSectionDict()
def addSectionAction(self):
self.updateSections()
sName = self.window.sectionNameLineEdit.text()
if sName and sName not in [s.name for s in self.doc.sectionList]:
self.doc.sectionList.append(Section(name=sName))
self.window.sectionTableView.populate(self.doc.sectionList)
self.clearSectionLineEdits()
self.updateSectionDict()
else:
# Section has no name or non unique, warn user
SectionNameWarningMessageBox().exec()
def updateSectionAction(self):
if self.window.sectionTableView.selectionModel().hasSelection(): #  check for selection
self.updateSections()
row = self.window.sectionTableView.selectionModel().currentIndex().row()
sName = self.window.sectionNameLineEdit.text()
if sName and sName not in [s.name for s in self.doc.sectionList]:
self.doc.sectionList[row].name = sName
self.window.sectionTableView.populate(self.doc.sectionList)
self.clearSectionLineEdits()
self.updateSectionDict()
else:
# Section has no name or non unique, warn user
SectionNameWarningMessageBox().exec()
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(self.currentSection)
row = self.window.blockTableView.selectionModel().currentIndex().row() row = self.window.blockTableView.selectionModel().currentIndex().row()
self.doc.blockList.pop(row)
self.currentSection.blockList.pop(row)
self.window.blockTableView.populate(self.doc.blockList)
self.window.blockTableView.populate(self.currentSection.blockList)
def addBlockAction(self): def addBlockAction(self):
self.updateBlocks()
self.updateBlocks(self.currentSection)
try: try:
#  can the value entered for block length be cast as an integer
bLength = int(self.window.blockLengthLineEdit.text())
#  can the value entered for block length be cast as a float
bLength = float(self.window.blockLengthLineEdit.text())
except Exception: except Exception:
bLength = False bLength = False
if bLength: # create the block if bLength: # create the block
self.doc.blockList.append(Block(bLength,
chord=self.chordDict[self.window.blockChordComboBox.currentText(
)],
notes=(self.window.blockNotesLineEdit.text() if not "" else None)))
self.window.blockTableView.populate(self.doc.blockList)
self.currentSection.blockList.append(Block(bLength,
chord=self.chordDict[self.window.blockChordComboBox.currentText(
)],
notes=(self.window.blockNotesLineEdit.text() if not "" else None)))
self.window.blockTableView.populate(self.currentSection.blockList)
self.clearBlockLineEdits() self.clearBlockLineEdits()
else: else:
# show warning that length was not entered or in wrong format # show warning that length was not entered or in wrong format
@ -488,20 +608,22 @@ class DocumentWindow(QMainWindow):
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(self.currentSection)
try: try:
bLength = int(self.window.blockLengthLineEdit.text())
#  can the value entered for block length be cast as a float
bLength = float(self.window.blockLengthLineEdit.text())
except Exception: 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,
chord=self.chordDict[self.window.blockChordComboBox.currentText(
)],
notes=(self.window.blockNotesLineEdit.text() if not "" else None)))
self.window.blockTableView.populate(self.doc.blockList)
self.currentSection.blockList[row] = (Block(bLength,
chord=self.chordDict[self.window.blockChordComboBox.currentText(
)],
notes=(self.window.blockNotesLineEdit.text() if not "" else None)))
self.window.blockTableView.populate(
self.currentSection.blockList)
self.clearBlockLineEdits() self.clearBlockLineEdits()
else: else:
LengthWarningMessageBox().exec() LengthWarningMessageBox().exec()
@ -515,25 +637,13 @@ class DocumentWindow(QMainWindow):
Update the preview shown by rendering a new PDF and drawing it to the scroll area. Update the preview shown by rendering a new PDF and drawing it to the scroll area.
""" """
try: try:
self.currentPreview = io.BytesIO()
savePDF(self.doc, self.style, self.currentPreview)
pdfView = fitz.Document(stream=self.currentPreview, filetype='pdf')
# 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))
# -30 because the scrollarea has a margin of 12 each side (extra for safety)
# necessary on Mojave with PyInstaller (or previous contents will be shown)
self.window.imageLabel.repaint()
self.currentPreview = self.renderer.stream()
except Exception: except Exception:
QMessageBox.warning(self, "Preview failed", "Could not update the preview.", QMessageBox.warning(self, "Preview failed", "Could not update the preview.",
buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok) buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok)
self.window.pdfArea.update(self.currentPreview)
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.
@ -558,13 +668,39 @@ class DocumentWindow(QMainWindow):
self.doc.chordList = chordTableList self.doc.chordList = chordTableList
def updateBlocks(self):
def matchSection(self, nameToMatch):
"""
Given the name of a section, this function checks if it is already present in the document.
If it is, it's returned. If not, a new section with the given name is returned.
"""
section = None
for s in self.doc.sectionList:
if s.name == nameToMatch:
section = s
break
if section is None:
section = Section(name=nameToMatch)
return section
def updateSections(self):
"""
Update the section list by reading the table
"""
sectionTableList = []
for i in range(self.window.sectionTableView.model.rowCount()):
sectionTableList.append(self.matchSection(
self.window.sectionTableView.model.item(i, 0).text()))
self.doc.sectionList = sectionTableList
def updateBlocks(self, section):
""" """
Update the block list by reading the table. Update the block list by reading the table.
""" """
blockTableList = [] blockTableList = []
for i in range(self.window.blockTableView.model.rowCount()): for i in range(self.window.blockTableView.model.rowCount()):
blockLength = int(
blockLength = float(
self.window.blockTableView.model.item(i, 1).text()) self.window.blockTableView.model.item(i, 1).text())
blockChord = self.chordDict[(self.window.blockTableView.model.item( blockChord = self.chordDict[(self.window.blockTableView.model.item(
i, 0).text() if self.window.blockTableView.model.item(i, 0).text() else "None")] i, 0).text() if self.window.blockTableView.model.item(i, 0).text() else "None")]
@ -573,7 +709,7 @@ class DocumentWindow(QMainWindow):
blockTableList.append( blockTableList.append(
Block(blockLength, chord=blockChord, notes=blockNotes)) Block(blockLength, chord=blockChord, notes=blockNotes))
self.doc.blockList = blockTableList
section.blockList = blockTableList
def updateDocument(self): def updateDocument(self):
""" """
@ -596,8 +732,12 @@ class DocumentWindow(QMainWindow):
self.style.unit = unitDict[self.unitSelected] self.style.unit = unitDict[self.unitSelected]
self.style.leftMargin = float(self.window.leftMarginLineEdit.text( self.style.leftMargin = float(self.window.leftMarginLineEdit.text(
)) if self.window.leftMarginLineEdit.text() else self.style.leftMargin )) if self.window.leftMarginLineEdit.text() else self.style.leftMargin
self.style.rightMargin = float(self.window.rightMarginLineEdit.text(
)) if self.window.rightMarginLineEdit.text() else self.style.rightMargin
self.style.topMargin = float(self.window.topMarginLineEdit.text( self.style.topMargin = float(self.window.topMarginLineEdit.text(
)) if self.window.topMarginLineEdit.text() else self.style.topMargin )) if self.window.topMarginLineEdit.text() else self.style.topMargin
self.style.bottomMargin = float(self.window.bottomMarginLineEdit.text(
)) if self.window.bottomMarginLineEdit.text() else self.style.bottomMargin
self.style.lineSpacing = float(self.window.lineSpacingDoubleSpinBox.value( self.style.lineSpacing = float(self.window.lineSpacingDoubleSpinBox.value(
)) if self.window.lineSpacingDoubleSpinBox.value() else self.style.lineSpacing )) if self.window.lineSpacingDoubleSpinBox.value() else self.style.lineSpacing
@ -612,8 +752,11 @@ class DocumentWindow(QMainWindow):
QMessageBox.warning(self, "Out of range", "Beat width is out of range. It can be a maximum of {}.".format( 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), buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok)
# update chords, sections, blocks
self.updateChords() self.updateChords()
self.updateBlocks()
self.updateSections()
if self.currentSection:
self.updateBlocks(self.currentSection)
self.style.font = ( self.style.font = (
'FreeSans' if self.style.useIncludedFont else 'HelveticaNeue') 'FreeSans' if self.style.useIncludedFont else 'HelveticaNeue')
@ -713,7 +856,7 @@ class UnreadableMessageBox(QMessageBox):
self.setDefaultButton(QMessageBox.Ok) self.setDefaultButton(QMessageBox.Ok)
class NameWarningMessageBox(QMessageBox):
class ChordNameWarningMessageBox(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
""" """
@ -729,6 +872,23 @@ class NameWarningMessageBox(QMessageBox):
self.setDefaultButton(QMessageBox.Ok) self.setDefaultButton(QMessageBox.Ok)
class SectionNameWarningMessageBox(QMessageBox):
"""
Message box to warn the user that a section must have a name
"""
def __init__(self):
super().__init__()
self.setIcon(QMessageBox.Warning)
self.setWindowTitle("Unnamed section")
self.setText("Sections must have a unique name.")
self.setInformativeText(
"Please give your section a unique name and try again.")
self.setStandardButtons(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

11
test.py

@ -1,12 +1,15 @@
import os
from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfbase.ttfonts import TTFont
from chordsheet.document import Document, Style from chordsheet.document import Document, Style
from chordsheet.render import savePDF
from chordsheet.render import Renderer
pdfmetrics.registerFont(TTFont('FreeSans', os.path.join('fonts', 'FreeSans.ttf'))) pdfmetrics.registerFont(TTFont('FreeSans', os.path.join('fonts', 'FreeSans.ttf')))
doc = Document.newFromXML('examples/example.xml')
style = Style()
doc = Document.newFromXML('examples/angela.xml')
style = Style(unitWidth=20)
ren = Renderer(doc, style)
savePDF(doc, style, 'test.pdf')
ren.savePDF('test.pdf')

234
ui/mainwindow.ui

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1061</width> <width>1061</width>
<height>646</height>
<height>659</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -268,14 +268,14 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0">
<item row="4" column="0">
<widget class="QLabel" name="topMarginLabel"> <widget class="QLabel" name="topMarginLabel">
<property name="text"> <property name="text">
<string>Top margin</string> <string>Top margin</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1">
<item row="4" column="1">
<widget class="QLineEdit" name="topMarginLineEdit"> <widget class="QLineEdit" name="topMarginLineEdit">
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
@ -285,6 +285,40 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0">
<widget class="QLabel" name="rightMarginLabel">
<property name="text">
<string>Right margin</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="rightMarginLineEdit">
<property name="maximumSize">
<size>
<width>60</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="bottomMarginLabel">
<property name="text">
<string>Bottom margin</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="bottomMarginLineEdit">
<property name="maximumSize">
<size>
<width>60</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
</layout> </layout>
@ -569,6 +603,112 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tabWidgetSections">
<attribute name="title">
<string>Sections</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QVBoxLayout" name="sectionTabLayout">
<item>
<widget class="SectionTableView" name="sectionTableView">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode">
<bool>false</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::InternalMove</enum>
</property>
<property name="defaultDropAction">
<enum>Qt::TargetMoveAction</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid">
<bool>false</bool>
</property>
<property name="cornerButtonEnabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout_3">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="sectionNameLabel">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="sectionNameLineEdit"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="bottomSectionHorizontalLayout">
<item>
<widget class="QPushButton" name="removeSectionButton">
<property name="text">
<string>Remove section</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="updateSectionButton">
<property name="text">
<string>Update section</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="addSectionButton">
<property name="text">
<string>Add section</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabWidgetBlocks"> <widget class="QWidget" name="tabWidgetBlocks">
<attribute name="title"> <attribute name="title">
<string>Blocks</string> <string>Blocks</string>
@ -576,6 +716,30 @@
<layout class="QVBoxLayout" name="verticalLayout_6"> <layout class="QVBoxLayout" name="verticalLayout_6">
<item> <item>
<layout class="QVBoxLayout" name="blockTabLayout"> <layout class="QVBoxLayout" name="blockTabLayout">
<item>
<layout class="QFormLayout" name="formLayout_5">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="blockSectionLabel">
<property name="text">
<string>Section</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="MComboBox" name="blockSectionComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item> <item>
<widget class="BlockTableView" name="blockTableView"> <widget class="BlockTableView" name="blockTableView">
<property name="sizePolicy"> <property name="sizePolicy">
@ -783,7 +947,7 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QScrollArea" name="scrollArea">
<widget class="PDFViewer" name="pdfArea" native="true">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>1</horstretch> <horstretch>1</horstretch>
@ -796,52 +960,9 @@
<height>400</height> <height>400</height>
</size> </size>
</property> </property>
<property name="widgetResizable">
<property name="widgetResizable" stdset="0">
<bool>true</bool> <bool>true</bool>
</property> </property>
<widget class="QWidget" name="scrollAreaContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>598</width>
<height>598</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>12</number>
</property>
<property name="topMargin">
<number>12</number>
</property>
<property name="rightMargin">
<number>12</number>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<item row="0" column="0" alignment="Qt::AlignHCenter">
<widget class="QLabel" name="imageLabel">
<property name="autoFillBackground">
<bool>true</bool>
</property>
<property name="text">
<string/>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
<property name="margin">
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>
</widget> </widget>
</widget> </widget>
</item> </item>
@ -974,6 +1095,22 @@
<extends>QTableView</extends> <extends>QTableView</extends>
<header>chordsheet/tableView.h</header> <header>chordsheet/tableView.h</header>
</customwidget> </customwidget>
<customwidget>
<class>SectionTableView</class>
<extends>QTableView</extends>
<header>chordsheet/tableView.h</header>
</customwidget>
<customwidget>
<class>MComboBox</class>
<extends>QComboBox</extends>
<header>chordsheet/comboBox.h</header>
</customwidget>
<customwidget>
<class>PDFViewer</class>
<extends>QWidget</extends>
<header>chordsheet/pdfViewer.h</header>
<container>1</container>
</customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>generateButton</tabstop> <tabstop>generateButton</tabstop>
@ -996,7 +1133,6 @@
<tabstop>blockTableView</tabstop> <tabstop>blockTableView</tabstop>
<tabstop>addBlockButton</tabstop> <tabstop>addBlockButton</tabstop>
<tabstop>removeBlockButton</tabstop> <tabstop>removeBlockButton</tabstop>
<tabstop>scrollArea</tabstop>
</tabstops> </tabstops>
<resources/> <resources/>
<connections/> <connections/>

Loading…
Cancel
Save