diff --git a/_version.py b/_version.py
index 986861a..5e1bb30 100644
--- a/_version.py
+++ b/_version.py
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
appName = "Chordsheet"
-version = '0.4dev'
+version = '0.4'
diff --git a/chordsheet/comboBox.py b/chordsheet/comboBox.py
new file mode 100644
index 0000000..9b8e185
--- /dev/null
+++ b/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()
\ No newline at end of file
diff --git a/chordsheet/document.py b/chordsheet/document.py
index 5c63088..c636c92 100644
--- a/chordsheet/document.py
+++ b/chordsheet/document.py
@@ -7,36 +7,41 @@ from reportlab.lib.pagesizes import A4
defaultTimeSignature = 4
+
class Style:
def __init__(self, **kwargs):
# set up the style using sane defaults
self.unit = kwargs.get('unit', mm)
-
self.pageSize = kwargs.get('pageSize', A4)
self.leftMargin = kwargs.get('leftMargin', 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.lineSpacing = kwargs.get('lineSpacing', 1.15)
- self.separatorSize = kwargs.get('separatorSize', 5)
self.unitWidth = kwargs.get('unitWidth', 10)
-
+
self.useIncludedFont = True
-
+
+ self.separatorSize = 5*self.unit
+
self.stringHzSp = 20*self.unit
self.stringHzGap = 2*self.unit
self.stringHeight = 5*self.unit
self.unitHeight = 20*self.unit
self.beatsHeight = 5*self.unit
-
+
self.titleFontSize = 24
self.subtitleFontSize = 18
self.creditsFontSize = 12
self.tempoFontSize = 12
+ self.headingFontSize = 18
self.notesFontSize = 12
self.chordNameFontSize = 18
self.beatsFontSize = 12
-
+
+
class Chord:
def __init__(self, name, **kwargs):
self.name = name
@@ -51,21 +56,33 @@ class Chord:
class Block:
- def __init__(self, length, **kwargs):
+ def __init__(self, length, chord=None, notes=None):
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):
if isinstance(other, self.__class__):
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:
- 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.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.composer = composer
self.arranger = arranger
@@ -74,98 +91,119 @@ class Document:
def __eq__(self, other):
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
-
+
def loadXML(self, filepath):
"""
Read an XML file and import its contents.
"""
xmlDoc = ET.parse(filepath)
root = xmlDoc.getroot()
-
+
self.chordList = []
- if root.find('chords') is not None:
+ if root.find('chords'):
for c in root.findall('chords/chord'):
self.chordList.append(Chord(parseName(c.find('name').text)))
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.
"""
doc = Document()
doc.loadXML(filepath)
return doc
-
+
def saveXML(self, filepath):
"""
Write the contents of the Document object to an XML file.
"""
root = ET.Element("chordsheet")
-
+
ET.SubElement(root, "title").text = self.title
-
+
if self.subtitle is not None:
ET.SubElement(root, "subtitle").text = self.subtitle
if self.arranger is not None:
ET.SubElement(root, "arranger").text = self.arranger
-
+
if self.composer is not None:
ET.SubElement(root, "composer").text = self.composer
-
+
ET.SubElement(root, "timesignature").text = str(self.timeSignature)
if self.tempo is not None:
ET.SubElement(root, "tempo").text = self.tempo
-
+
chordsElement = ET.SubElement(root, "chords")
-
+
for c in self.chordList:
chordElement = ET.SubElement(chordsElement, "chord")
ET.SubElement(chordElement, "name").text = c.name
for inst in c.voicings.keys():
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':
- 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.write(filepath)
\ No newline at end of file
+ tree.write(filepath)
diff --git a/chordsheet/pdfViewer.py b/chordsheet/pdfViewer.py
new file mode 100644
index 0000000..6c63d80
--- /dev/null
+++ b/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()
\ No newline at end of file
diff --git a/chordsheet/render.py b/chordsheet/render.py
index 3036d23..35ddcd3 100644
--- a/chordsheet/render.py
+++ b/chordsheet/render.py
@@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
-from math import trunc
+from math import trunc, ceil
+from io import BytesIO
from reportlab.pdfgen import canvas
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.rlStylesheet import getStyleSheet
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
-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):
@@ -77,66 +75,81 @@ class GuitarChart(Flowable):
self.nStrings = 6
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):
- 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)
def draw(self):
canvas = self.canv
- title_height = writeText(canvas, self.style, "Guitar chord voicings",
- self.headingSize, self.height, self.width, align="left")
-
chartmargin = self.chartMargin
- v_origin = self.height - title_height - 2*mm
- h_origin = chartmargin
- self.nStrings = 6
- fontsize = 12
-
- stringList = [
- [c.voicings['guitar'][-(r+1)] for c in self.guitarChordList] for r in range(self.nStrings)]
- stringList.append([c.name for c in self.guitarChordList])
-
- for i in range(self.nStrings+1): # i is the string line currently being drawn
- writeText(canvas, self.style, ['e', 'B', 'G', 'D', 'A', 'E', 'Name'][i], fontsize, v_origin-(
- i*self.style.stringHeight), self.width, hpos=h_origin, align='right')
-
- # j is which chord (0 is first chord, 1 is 2nd etc)
- for j in range(len(stringList[-1])):
- currentWidth = canvas.stringWidth(stringList[i][j])
- if j == 0:
- x = self.style.stringHzGap + chartmargin
- l = self.style.stringHzSp/2 - self.style.stringHzGap - \
- ((currentWidth/2)) - self.style.stringHzGap
- y = v_origin-(self.style.stringHeight*i) - \
- self.style.stringHeight/2
- canvas.line(x, y, x+l, y)
- else:
- x = chartmargin + self.style.stringHzSp * \
- (j-0.5)+(lastWidth/2+self.style.stringHzGap)
- l = self.style.stringHzSp - currentWidth / \
- 2 - lastWidth/2 - self.style.stringHzGap*2
- y = v_origin-(self.style.stringHeight*i) - \
- self.style.stringHeight/2
- canvas.line(x, y, x+l, y)
-
- if j == len(stringList[-1])-1:
- x = chartmargin + self.style.stringHzSp * \
- (j+0.5) + currentWidth/2 + self.style.stringHzGap
- l = self.style.stringHzSp/2 - currentWidth/2 - self.style.stringHzGap
- y = v_origin-(self.style.stringHeight*i) - \
- self.style.stringHeight/2
- canvas.line(x, y, x+l, y)
-
- writeText(canvas, self.style, stringList[i][j], fontsize, v_origin-(
- i*self.style.stringHeight), self.width, hpos=chartmargin+self.style.stringHzSp*(j+0.5))
-
- lastWidth = currentWidth
+
+ 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):
@@ -144,33 +157,89 @@ class ChordProgression(Flowable):
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.heading = heading # the title of the section
self.blockList = blockList
self.timeSignature = timeSignature
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):
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 * \
+ self.height = self.style.beatsHeight + self.style.unitHeight * \
sum([b.length for b in self.blockList]) / self.widthInBeats
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):
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
+ v_origin = self.height - self.style.beatsHeight
h_loc = 0
v_loc = 0
@@ -190,7 +259,7 @@ class ChordProgression(Flowable):
writeText(canvas, self.style, str((u % self.timeSignature)+1), self.style.beatsFontSize,
v_origin+self.style.beatsHeight, self.width, hpos=x+unitWidth/2)
- parsedBlockList = splitBlocks(self.blockList, maxWidth)
+ parsedBlockList = self.wrapBlocks(self.blockList, maxWidth)
for b in parsedBlockList:
if h_loc == maxWidth:
@@ -200,12 +269,12 @@ class ChordProgression(Flowable):
b.length*unitWidth, self.style.unitHeight)
if b.notes is not None:
writeText(canvas, self.style, b.notes, self.style.notesFontSize, v_origin-((v_loc+1)*self.style.unitHeight)+(
- 1.3*self.style.notesFontSize), self.width, hpos=h_origin+((h_loc+b.length/2)*unitWidth))
+ 1.3*self.style.notesFontSize), self.width, hpos=((h_loc+b.length/2)*unitWidth))
v_offset = ((v_loc*self.style.unitHeight) +
self.style.unitHeight/2)-self.style.chordNameFontSize/2
if b.chord is not None:
writeText(canvas, self.style, b.chord.name, self.style.chordNameFontSize,
- v_origin-v_offset, self.width, hpos=h_origin+((h_loc+b.length/2)*unitWidth))
+ v_origin-v_offset, self.width, hpos=((h_loc+b.length/2)*unitWidth))
h_loc += b.length
@@ -218,73 +287,59 @@ def guitarChartCheck(cL):
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.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
diff --git a/chordsheet/rlStylesheet.py b/chordsheet/rlStylesheet.py
new file mode 100644
index 0000000..f561216
--- /dev/null
+++ b/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
\ No newline at end of file
diff --git a/chordsheet/tableView.py b/chordsheet/tableView.py
index ab6170e..4cd5e17 100644
--- a/chordsheet/tableView.py
+++ b/chordsheet/tableView.py
@@ -81,6 +81,30 @@ class ChordTableView(MTableView):
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):
"""
Subclass MTableView to add properties just for the block table.
diff --git a/examples/ah.xml b/examples/ah.xml
index 3415169..0d11a0c 100644
--- a/examples/ah.xml
+++ b/examples/ah.xml
@@ -27,7 +27,7 @@
x69676
-
+
9
Gm9
@@ -73,5 +73,5 @@
D7#5#9
over Ab
-
+
\ No newline at end of file
diff --git a/examples/ahlong.xml b/examples/ahlong.xml
new file mode 100644
index 0000000..df0a2c5
--- /dev/null
+++ b/examples/ahlong.xml
@@ -0,0 +1,309 @@
+
+ "African Heritage"
+ A corroboration
+ Ivan Holmes
+ Ivan Holmes and Joe Buckley
+ 120
+ 6
+
+
+ Gm9
+ x,10,8,10,10,x
+
+
+ Abm9
+ x,11,9,11,11,x
+
+
+ Cm9
+ x,x,8,8,8,10
+
+
+ D7#5#9
+ x58565
+
+
+ Eb7#5#9
+ x69676
+
+
+
+
+ 9
+ Gm9
+
+
+ 3
+ Abm9
+
+
+ 12
+ Gm9
+
+
+ 9
+ Cm9
+
+
+ 3
+ D7#5#9
+
+
+ 12
+ Gm9
+
+
+ 6
+ Eb7#5#9
+
+
+ 6
+ D7#5#9
+
+
+ 6
+ Cm9
+
+
+ 3
+ D7#5#9
+
+
+ 3
+ D7#5#9
+ over Ab
+
+
+
+
+ 9
+ Gm9
+
+
+ 3
+ Abm9
+
+
+ 12
+ Gm9
+
+
+ 6
+ Eb7#5#9
+
+
+ 6
+ D7#5#9
+
+
+ 12
+ Cm9
+
+
+
+
+ 9
+ Gm9
+
+
+ 3
+ Abm9
+
+
+ 12
+ Gm9
+
+
+ 6
+ Eb7#5#9
+
+
+ 6
+ D7#5#9
+
+
+ 12
+ Cm9
+
+
+ 9
+ Gm9
+
+
+ 3
+ Abm9
+
+
+ 12
+ Gm9
+
+
+ 9
+ Cm9
+
+
+ 3
+ D7#5#9
+
+
+ 12
+ Gm9
+
+
+ 6
+ Eb7#5#9
+
+
+ 6
+ D7#5#9
+
+
+ 6
+ Cm9
+
+
+ 3
+ D7#5#9
+
+
+ 3
+ D7#5#9
+ over Ab
+
+
+ 9
+ Gm9
+
+
+ 3
+ Abm9
+
+
+ 12
+ Gm9
+
+
+ 9
+ Cm9
+
+
+ 3
+ D7#5#9
+
+
+ 12
+ Gm9
+
+
+ 6
+ Eb7#5#9
+
+
+ 6
+ D7#5#9
+
+
+ 6
+ Cm9
+
+
+ 3
+ D7#5#9
+
+
+ 3
+ D7#5#9
+ over Ab
+
+
+ 9
+ Gm9
+
+
+ 3
+ Abm9
+
+
+ 12
+ Gm9
+
+
+ 9
+ Cm9
+
+
+ 3
+ D7#5#9
+
+
+ 12
+ Gm9
+
+
+ 6
+ Eb7#5#9
+
+
+ 6
+ D7#5#9
+
+
+ 6
+ Cm9
+
+
+ 3
+ D7#5#9
+
+
+ 3
+ D7#5#9
+ over Ab
+
+
+ 9
+ Gm9
+
+
+ 3
+ Abm9
+
+
+ 12
+ Gm9
+
+
+ 9
+ Cm9
+
+
+ 3
+ D7#5#9
+
+
+ 12
+ Gm9
+
+
+ 6
+ Eb7#5#9
+
+
+ 6
+ D7#5#9
+
+
+ 6
+ Cm9
+
+
+ 3
+ D7#5#9
+
+
+ 3
+ D7#5#9
+ over Ab
+
+
+
\ No newline at end of file
diff --git a/examples/angela.xml b/examples/angela.xml
new file mode 100644
index 0000000..b46b457
--- /dev/null
+++ b/examples/angela.xml
@@ -0,0 +1 @@
+AngelaTheme from 'Taxi'MaxBob James4E♭maj7x,x,1,3,3,3A♭ma7x,x,1,1,1,3Gm73,x,3,3,3,xB♭/Dx,x,0,3,3,1A♭maj7/Cx,3,1,1,1,3B♭x,1,3,3,3,1A♭4,6,6,5,4,4E♭/Gx,x,5,3,4,3Fmx,x,3,1,1,1A♭/B♭6,x,6,5,4,4E♭x,6,5,3,4,3E♭7x,x,1,3,2,3A♭maj94,x,5,3,4,3Fm71,x,1,1,1,x2.0E♭maj72.0A♭ma72.0Gm72.0B♭/D2.0A♭maj7/C2.0B♭1.0A♭1.0E♭/G1.0Fm1.0A♭/B♭2.0E♭2.0E♭72.0A♭maj92.0E♭/G2.0Fm72.0B♭1.0A♭1.0E♭/G1.0Fm1.0A♭/B♭
\ No newline at end of file
diff --git a/examples/example.xml b/examples/example.xml
index b0f503a..70c62a9 100644
--- a/examples/example.xml
+++ b/examples/example.xml
@@ -1 +1 @@
-Example SongIvan Holmes4Cx,3,2,0,1,0F1,3,3,2,1,1G3,2,0,0,0,3C/G3,3,2,0,1,0Dmx,x,0,2,3,116CIntro, strum lightly4C4F8G4C4F8G4C4Dm4C/G4G4C4Contemplation time8CCrescendo until end
\ No newline at end of file
+Example SongIvan Holmes4Cx,3,2,0,1,0F1,3,3,2,1,1G3,2,0,0,0,3C/G3,3,2,0,1,0Dmx,x,0,2,3,116CIntro, strum lightly4C4F8G4C4F8G4C4Dm4C/G4G4C4Contemplation time8CCrescendo until end
\ No newline at end of file
diff --git a/examples/kissoflife.xml b/examples/kissoflife.xml
new file mode 100644
index 0000000..a03e96e
--- /dev/null
+++ b/examples/kissoflife.xml
@@ -0,0 +1 @@
+Kiss of LifeSadeIvan HolmesSade Adu, Paul S. Denman, Andrew Hale, Stuart Matthewman4AM9F♯m11DM7C♯m7Bm78.0AM98.0F♯m111.5DM72.0C♯m74.5Bm78.0F♯m113.5Bm74.5F♯m113.5Bm74.5F♯m11
\ No newline at end of file
diff --git a/examples/test.xml b/examples/test.xml
index ca83cb8..e6d8e11 100644
--- a/examples/test.xml
+++ b/examples/test.xml
@@ -1 +1 @@
-CompositionA. Person4Bx,x,2,3,4,1E0,2,2,1,0,0Cm9x,x,8,8,8,10D7♭5♯94BThese are notes.4E12Cm96D7♭5♯96For quiet contemplation.46D7♭5♯9A very long block to test wrapping!
\ No newline at end of file
+CompositionA. Person4Bx,x,2,3,4,1E0,2,2,1,0,0Cm9x,x,8,8,8,10D7♭5♯94BThese are notes.4E12Cm96D7♭5♯96For quiet contemplation.46D7♭5♯9A very long block to test wrapping!
\ No newline at end of file
diff --git a/gui.py b/gui.py
index f8ab90e..c3d83d0 100755
--- a/gui.py
+++ b/gui.py
@@ -11,6 +11,7 @@ import fitz
import io
import subprocess
import os
+import time
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
@@ -18,14 +19,16 @@ 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
+from chordsheet.comboBox import MComboBox
+from chordsheet.pdfViewer import PDFViewer
from reportlab.lib.units import mm, cm, inch, pica
from reportlab.lib.pagesizes import A4, A5, LETTER, LEGAL
from reportlab.pdfbase import pdfmetrics
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
import _version
@@ -72,6 +75,7 @@ class DocumentWindow(QMainWindow):
self.doc = doc
self.style = style
+ self.renderer = Renderer(self.doc, self.style)
self.lastDoc = copy(self.doc)
self.currentFilePath = filename
@@ -79,6 +83,8 @@ class DocumentWindow(QMainWindow):
self.UIFileLoader(str(os.path.join(scriptDir, 'ui', 'mainwindow.ui')))
self.UIInitStyle()
self.updateChordDict()
+ self.updateSectionDict()
+ self.currentSection = None
self.setCentralWidget(self.window.centralWidget)
self.setMenuBar(self.window.menuBar)
@@ -146,17 +152,32 @@ class DocumentWindow(QMainWindow):
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.guitarVoicingAction)
self.window.addChordButton.clicked.connect(self.addChordAction)
self.window.removeChordButton.clicked.connect(self.removeChordAction)
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.removeBlockButton.clicked.connect(self.removeBlockAction)
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.sectionTableView.clicked.connect(self.sectionClickedAction)
self.window.blockTableView.clicked.connect(self.blockClickedAction)
def UIInitDocument(self):
@@ -167,13 +188,20 @@ class DocumentWindow(QMainWindow):
# set all fields to appropriate values from document
self.window.titleLineEdit.setText(self.doc.title)
+ self.window.subtitleLineEdit.setText(self.doc.subtitle)
self.window.composerLineEdit.setText(self.doc.composer)
self.window.arrangerLineEdit.setText(self.doc.arranger)
self.window.timeSignatureSpinBox.setValue(self.doc.timeSignature)
self.window.tempoLineEdit.setText(self.doc.tempo)
self.window.chordTableView.populate(self.doc.chordList)
- self.window.blockTableView.populate(self.doc.blockList)
+ self.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()
def UIInitStyle(self):
@@ -191,13 +219,19 @@ class DocumentWindow(QMainWindow):
self.window.lineSpacingDoubleSpinBox.setValue(self.style.lineSpacing)
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.bottomMarginLineEdit.setText(str(self.style.bottomMargin))
+
self.window.fontComboBox.setDisabled(True)
self.window.includedFontCheckBox.setChecked(True)
self.window.beatWidthLineEdit.setText(str(self.style.unitWidth))
+ def tabBarUpdateAction(self, index):
+ self.updateDocument()
+
def pageSizeAction(self, index):
self.pageSizeSelected = self.window.pageSizeComboBox.itemText(index)
@@ -217,6 +251,29 @@ class DocumentWindow(QMainWindow):
self.window.guitarVoicingLineEdit.setText(
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):
# set the controls to the values from the selected block
bChord = self.window.blockTableView.model.item(index.row(), 0).text()
@@ -245,6 +302,8 @@ class DocumentWindow(QMainWindow):
self.lastDoc = copy(self.doc)
# reset file path (this document hasn't been saved yet)
self.currentFilePath = None
+ # new renderer
+ self.renderer = Renderer(self.doc, self.style)
self.UIInitDocument()
self.updatePreview()
@@ -297,7 +356,7 @@ class DocumentWindow(QMainWindow):
filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath(
"lastExportPath"), "PDF files (*.pdf)")[0]
if filePath:
- savePDF(d, s, filePath)
+ self.renderer.savePDF(filePath)
self.setPath("lastExportPath", filePath)
def menuFilePrintAction(self):
@@ -381,6 +440,18 @@ class DocumentWindow(QMainWindow):
self.window.chordNameLineEdit.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):
"""
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.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):
if self.window.chordTableView.selectionModel().hasSelection(): # check for selection
self.updateChords()
@@ -418,7 +498,7 @@ class DocumentWindow(QMainWindow):
else:
success = True # chord successfully parsed
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
self.window.chordTableView.populate(self.doc.chordList)
@@ -430,9 +510,10 @@ class DocumentWindow(QMainWindow):
if self.window.chordTableView.selectionModel().hasSelection(): # check for selection
self.updateChords()
row = self.window.chordTableView.selectionModel().currentIndex().row()
+ oldName = self.window.chordTableView.model.item(row, 0).text()
cName = parseName(self.window.chordNameLineEdit.text())
if cName:
- self.doc.chordList[row] = Chord(cName)
+ self.doc.chordList[row].name = cName
if self.window.guitarVoicingLineEdit.text():
try:
self.doc.chordList[row].voicings['guitar'] = parseFingering(
@@ -443,44 +524,83 @@ class DocumentWindow(QMainWindow):
else:
success = True
else:
- NameWarningMessageBox().exec()
+ ChordNameWarningMessageBox().exec()
if success == True:
+ self.updateChordDict()
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.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):
if self.window.blockTableView.selectionModel().hasSelection(): # check for selection
- self.updateBlocks()
+ self.updateBlocks(self.currentSection)
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):
- self.updateBlocks()
+ self.updateBlocks(self.currentSection)
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:
bLength = False
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()
else:
# show warning that length was not entered or in wrong format
@@ -488,20 +608,22 @@ class DocumentWindow(QMainWindow):
def updateBlockAction(self):
if self.window.blockTableView.selectionModel().hasSelection(): # check for selection
- self.updateBlocks()
+ self.updateBlocks(self.currentSection)
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:
bLength = False
row = self.window.blockTableView.selectionModel().currentIndex().row()
if bLength:
- self.doc.blockList[row] = (Block(bLength,
- chord=self.chordDict[self.window.blockChordComboBox.currentText(
- )],
- notes=(self.window.blockNotesLineEdit.text() if not "" else None)))
- 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()
else:
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.
"""
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:
QMessageBox.warning(self, "Preview failed", "Could not update the preview.",
buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok)
+ self.window.pdfArea.update(self.currentPreview)
+
def updateTitleBar(self):
"""
Update the application's title bar to reflect the current document.
@@ -558,13 +668,39 @@ class DocumentWindow(QMainWindow):
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.
"""
+
blockTableList = []
for i in range(self.window.blockTableView.model.rowCount()):
- blockLength = int(
+ blockLength = float(
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")]
@@ -573,7 +709,7 @@ class DocumentWindow(QMainWindow):
blockTableList.append(
Block(blockLength, chord=blockChord, notes=blockNotes))
- self.doc.blockList = blockTableList
+ section.blockList = blockTableList
def updateDocument(self):
"""
@@ -596,8 +732,12 @@ class DocumentWindow(QMainWindow):
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.rightMargin = float(self.window.rightMarginLineEdit.text(
+ )) if self.window.rightMarginLineEdit.text() else self.style.rightMargin
self.style.topMargin = float(self.window.topMarginLineEdit.text(
)) 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(
)) 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(
maxBeatWidth), buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok)
+ # update chords, sections, blocks
self.updateChords()
- self.updateBlocks()
+ self.updateSections()
+ if self.currentSection:
+ self.updateBlocks(self.currentSection)
self.style.font = (
'FreeSans' if self.style.useIncludedFont else 'HelveticaNeue')
@@ -713,7 +856,7 @@ class UnreadableMessageBox(QMessageBox):
self.setDefaultButton(QMessageBox.Ok)
-class NameWarningMessageBox(QMessageBox):
+class ChordNameWarningMessageBox(QMessageBox):
"""
Message box to warn the user that a chord must have a name
"""
@@ -729,6 +872,23 @@ class NameWarningMessageBox(QMessageBox):
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):
"""
Message box to warn the user that the voicing entered could not be parsed
diff --git a/test.py b/test.py
index 746669b..f91d85c 100644
--- a/test.py
+++ b/test.py
@@ -1,12 +1,15 @@
+import os
+
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
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')))
-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')
\ No newline at end of file
+ren.savePDF('test.pdf')
\ No newline at end of file
diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui
index c52fd9d..6f12a3d 100644
--- a/ui/mainwindow.ui
+++ b/ui/mainwindow.ui
@@ -7,7 +7,7 @@
0
0
1061
- 646
+ 659
@@ -268,14 +268,14 @@
- -
+
-
Top margin
- -
+
-
@@ -285,6 +285,40 @@
+ -
+
+
+ Right margin
+
+
+
+ -
+
+
+
+ 60
+ 16777215
+
+
+
+
+ -
+
+
+ Bottom margin
+
+
+
+ -
+
+
+
+ 60
+ 16777215
+
+
+
+
@@ -569,6 +603,112 @@
+
+
+ Sections
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ true
+
+
+ false
+
+
+ QAbstractItemView::InternalMove
+
+
+ Qt::TargetMoveAction
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+ false
+
+
+ false
+
+
+
+ -
+
+
+ QFormLayout::ExpandingFieldsGrow
+
+
-
+
+
+ Name
+
+
+
+ -
+
+
+
+
+ -
+
+
-
+
+
+ Remove section
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::MinimumExpanding
+
+
+
+ 0
+ 20
+
+
+
+
+ -
+
+
+ Update section
+
+
+
+ -
+
+
+ Add section
+
+
+
+
+
+
+
+
+
Blocks
@@ -576,6 +716,30 @@
-
+
-
+
+
+ QFormLayout::ExpandingFieldsGrow
+
+
-
+
+
+ Section
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
-
@@ -783,7 +947,7 @@
-
+
1
@@ -796,52 +960,9 @@
400
-
+
true
-
-
-
- 0
- 0
- 598
- 598
-
-
-
- true
-
-
-
- 12
-
-
- 12
-
-
- 12
-
-
- 12
-
- -
-
-
- true
-
-
-
-
-
- false
-
-
- 0
-
-
-
-
-
@@ -974,6 +1095,22 @@
QTableView
+
+ SectionTableView
+ QTableView
+
+
+
+ MComboBox
+ QComboBox
+
+
+
+ PDFViewer
+ QWidget
+
+ 1
+
generateButton
@@ -996,7 +1133,6 @@
blockTableView
addBlockButton
removeBlockButton
- scrollArea