Browse Source

input validation for chord and block editors

add icon
add about dialogue
improve preview rendering
master
Ivan Holmes 5 years ago
parent
commit
5f334d1f14
  1. 3
      _version.py
  2. 15
      chordsheet/parsers.py
  3. 3
      chordsheet/tableView.py
  4. BIN
      examples/example.pdf
  5. BIN
      examples/example.png
  6. 47
      examples/example.xml
  7. 188
      gui.py
  8. 2
      mac.spec
  9. 121
      ui/aboutdialog.ui
  10. BIN
      ui/icon.afdesign
  11. BIN
      ui/icon.icns
  12. BIN
      ui/icon.ico
  13. BIN
      ui/icon.png
  14. 1
      win.spec

3
_version.py

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
version = '0.4'

15
chordsheet/parsers.py

@ -1,24 +1,29 @@
# -*- coding: utf-8 -*-
from sys import exit
def parseFingering(fingering, instrument):
"""
Converts fingerings into the list format that Chord objects understand.
"""
if instrument == 'guitar':
numStrings = 6
if len(fingering) == numStrings:
if len(fingering) == numStrings: # if the fingering is entered in concise format e.g. xx4455
output = list(fingering)
else:
else: # if entered in long format e.g. x,x,10,10,11,11
output = [x for x in fingering.split(',')]
if len(output) == numStrings:
return output
else:
exit("Voicing <{v}> is malformed.".format(v=fingering))
raise Exception("Voicing <{}> is malformed.".format(fingering))
else:
return [fingering]
# dictionary holding text to be replaced in chord names
nameReplacements = { "b":"", "#":"" }
def parseName(chordName):
"""
Replaces symbols in chord names.
"""
parsedName = chordName
for i, j in nameReplacements.items():
parsedName = parsedName.replace(i, j)

3
chordsheet/tableView.py

@ -62,7 +62,6 @@ class ChordTableView(MTableView):
self.model.appendRow(rowList)
self.resizeColumnsToContents()
class BlockTableView(MTableView):
@ -80,5 +79,3 @@ class BlockTableView(MTableView):
item.setDropEnabled(False)
self.model.appendRow(rowList)
self.resizeColumnsToContents()

BIN
examples/example.pdf

BIN
examples/example.png

After

Width: 2480  |  Height: 3507  |  Size: 274 KiB

47
examples/example.xml

@ -1,46 +1 @@
<chordsheet>
<title>Composition</title>
<composer>A. Person</composer>
<timesignature>4</timesignature>
<chords>
<chord>
<name>B</name>
<voicing instrument="guitar">xx2341</voicing>
<voicing instrument="piano">abcdefg</voicing>
</chord>
<chord>
<name>E</name>
<voicing instrument="guitar">022100</voicing>
</chord>
<chord>
<name>Cm9</name>
<voicing instrument="guitar">x,x,8,8,8,10</voicing>
</chord>
<chord>
<name>D7b5#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>D7b5#9</chord>
</block>
<block>
<length>6</length>
<notes>For quiet contemplation.</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><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></progression></chordsheet>

188
gui.py

@ -24,6 +24,8 @@ from chordsheet.document import Document, Style, Chord, Block
from chordsheet.render import savePDF
from chordsheet.parsers import parseFingering, parseName
from _version import version
# set the directory where our files are depending on whether we're running a pyinstaller binary or not
if getattr(sys, 'frozen', False):
scriptDir = sys._MEIPASS
@ -66,6 +68,7 @@ class DocumentWindow(QMainWindow):
self.UIFileLoader(str(os.path.join(scriptDir, 'ui','mainwindow.ui')))
self.UIInitStyle()
self.updateChordDict()
self.setCentralWidget(self.window.centralWidget)
self.setMenuBar(self.window.menuBar)
@ -73,7 +76,7 @@ class DocumentWindow(QMainWindow):
if filename:
try:
self.doc.loadXML(filename)
self.openFile(filename)
except:
UnreadableMessageBox().exec()
@ -103,6 +106,11 @@ class DocumentWindow(QMainWindow):
self.window.actionSave_PDF.triggered.connect(self.menuFileSavePDFAction)
self.window.actionPrint.triggered.connect(self.menuFilePrintAction)
self.window.actionClose.triggered.connect(self.menuFileCloseAction)
self.window.actionUndo.triggered.connect(self.menuEditUndoAction)
self.window.actionRedo.triggered.connect(self.menuEditRedoAction)
self.window.actionCut.triggered.connect(self.menuEditCutAction)
self.window.actionCopy.triggered.connect(self.menuEditCopyAction)
self.window.actionPaste.triggered.connect(self.menuEditPasteAction)
self.window.actionNew.setShortcut(QKeySequence.New)
self.window.actionOpen.setShortcut(QKeySequence.Open)
@ -213,8 +221,11 @@ class DocumentWindow(QMainWindow):
def menuFileOpenAction(self):
filePath = QFileDialog.getOpenFileName(self.window.tabWidget, 'Open file', self.getPath("workingPath"), "Chordsheet ML files (*.xml *.cml)")
if filePath[0]:
self.currentFilePath = filePath[0]
self.doc.loadXML(filePath[0])
self.openFile(filePath[0])
def openFile(self, filePath):
self.currentFilePath = filePath
self.doc.loadXML(self.currentFilePath)
self.lastDoc = copy(self.doc)
self.setPath("workingPath", self.currentFilePath)
self.UIInitDocument()
@ -259,7 +270,37 @@ class DocumentWindow(QMainWindow):
self.saveWarning()
def menuFileAboutAction(self):
aboutDialog = QMessageBox.information(self, "About", "Chordsheet © Ivan Holmes, 2019", buttons = QMessageBox.Ok, defaultButton = QMessageBox.Ok)
aDialog = AboutDialog()
def menuEditUndoAction(self):
try:
QApplication.focusWidget().undo()
except:
pass
def menuEditRedoAction(self):
try:
QApplication.focusWidget().redo()
except:
pass
def menuEditCutAction(self):
try:
QApplication.focusWidget().cut()
except:
pass
def menuEditCopyAction(self):
try:
QApplication.focusWidget().copy()
except:
pass
def menuEditPasteAction(self):
try:
QApplication.focusWidget().paste()
except:
pass
def saveWarning(self):
"""
@ -297,7 +338,8 @@ class DocumentWindow(QMainWindow):
self.window.guitarVoicingLineEdit.clear()
def updateChordDict(self):
self.chordDict = {c.name:c for c in self.doc.chordList}
self.chordDict = {'None':None}
self.chordDict.update({c.name:c for c in self.doc.chordList})
self.window.blockChordComboBox.clear()
self.window.blockChordComboBox.addItems(list(self.chordDict.keys()))
@ -312,24 +354,48 @@ class DocumentWindow(QMainWindow):
self.updateChordDict()
def addChordAction(self):
success = False
self.updateChords()
self.doc.chordList.append(Chord(parseName(self.window.chordNameLineEdit.text())))
cName = parseName(self.window.chordNameLineEdit.text())
if cName:
self.doc.chordList.append(Chord(cName))
if self.window.guitarVoicingLineEdit.text():
try:
self.doc.chordList[-1].voicings['guitar'] = parseFingering(self.window.guitarVoicingLineEdit.text(), 'guitar')
success = True
except:
VoicingWarningMessageBox().exec()
else:
success = True
else:
NameWarningMessageBox().exec()
if success == True:
self.window.chordTableView.populate(self.doc.chordList)
self.clearChordLineEdits()
self.updateChordDict()
def updateChordAction(self):
success = False
if self.window.chordTableView.selectionModel().hasSelection():
self.updateChords()
row = self.window.chordTableView.selectionModel().currentIndex().row()
self.doc.chordList[row] = Chord(parseName(self.window.chordNameLineEdit.text()))
cName = parseName(self.window.chordNameLineEdit.text())
if cName:
self.doc.chordList[row] = Chord(cName)
if self.window.guitarVoicingLineEdit.text():
try:
self.doc.chordList[-1].voicings['guitar'] = parseFingering(self.window.guitarVoicingLineEdit.text(), 'guitar')
success = True
except:
VoicingWarningMessageBox().exec()
else:
success = True
else:
NameWarningMessageBox().exec()
if success == True:
self.window.chordTableView.populate(self.doc.chordList)
self.clearChordLineEdits()
self.updateChordDict()
@ -351,23 +417,38 @@ class DocumentWindow(QMainWindow):
def addBlockAction(self):
self.updateBlocks()
self.doc.blockList.append(Block(self.window.blockLengthLineEdit.text(),
try:
bLength = int(self.window.blockLengthLineEdit.text())
except:
bLength = False
if bLength:
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.clearBlockLineEdits()
else:
LengthWarningMessageBox().exec()
def updateBlockAction(self):
if self.window.blockTableView.selectionModel().hasSelection():
self.updateBlocks()
try:
bLength = int(self.window.blockLengthLineEdit.text())
except:
bLength = False
row = self.window.blockTableView.selectionModel().currentIndex().row()
self.doc.blockList[row] = (Block(self.window.blockLengthLineEdit.text(),
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.clearBlockLineEdits()
else:
LengthWarningMessageBox().exec()
def generateAction(self):
self.updateDocument()
@ -378,12 +459,13 @@ class DocumentWindow(QMainWindow):
savePDF(self.doc, self.style, self.currentPreview)
pdfView = fitz.Document(stream=self.currentPreview, filetype='pdf')
pix = pdfView[0].getPixmap(alpha = False)
pix = pdfView[0].getPixmap(matrix = fitz.Matrix(4, 4), alpha = False) # render at 4x resolution and scale
fmt = QImage.Format_RGB888
qtimg = QImage(pix.samples, pix.width, pix.height, pix.stride, fmt)
self.window.imageLabel.setPixmap(QPixmap.fromImage(qtimg))
self.window.imageLabel.setPixmap(QPixmap.fromImage(qtimg).scaled(self.window.scrollArea.width()-30, self.window.scrollArea.height()-30, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation))
# -30 because the scrollarea has a margin of 12 each side (extra for safety)
self.window.imageLabel.repaint() # necessary on Mojave with PyInstaller (or previous contents will be shown)
def updateTitleBar(self):
@ -406,17 +488,7 @@ class DocumentWindow(QMainWindow):
blockTableList = []
for i in range(self.window.blockTableView.model.rowCount()):
blockLength = int(self.window.blockTableView.model.item(i, 1).text())
blockChordName = parseName(self.window.blockTableView.model.item(i, 0).text())
if blockChordName:
blockChord = None
for c in self.doc.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.doc.chordList))
else:
blockChord = None
blockChord = self.chordDict[(self.window.blockTableView.model.item(i, 0).text() if self.window.blockTableView.model.item(i, 0).text() else "None")]
blockNotes = self.window.blockTableView.model.item(i, 2).text() if self.window.blockTableView.model.item(i, 2).text() else None
blockTableList.append(Block(blockLength, chord=blockChord, notes=blockNotes))
@ -469,6 +541,28 @@ class GuitarDialog(QDialog):
else:
return None
class AboutDialog(QDialog):
"""
Dialogue showing information about the program.
"""
def __init__(self):
super().__init__()
self.UIFileLoader(str(os.path.join(scriptDir, 'ui','aboutdialog.ui')))
icon = QImage(str(os.path.join(scriptDir, 'ui','icon.png')))
self.dialog.iconLabel.setPixmap(QPixmap.fromImage(icon).scaled(self.dialog.iconLabel.width(), self.dialog.iconLabel.height(), Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation))
self.dialog.versionLabel.setText("Version " + version)
self.dialog.exec()
def UIFileLoader(self, ui_file):
ui_file = QFile(ui_file)
ui_file.open(QFile.ReadOnly)
self.dialog = uic.loadUi(ui_file)
ui_file.close()
class UnsavedMessageBox(QMessageBox):
"""
Message box to alert the user of unsaved changes and allow them to choose how to act.
@ -476,6 +570,7 @@ class UnsavedMessageBox(QMessageBox):
def __init__(self):
super().__init__()
self.setIcon(QMessageBox.Question)
self.setWindowTitle("Unsaved changes")
self.setText("The document has been modified.")
self.setInformativeText("Do you want to save your changes?")
@ -484,17 +579,60 @@ class UnsavedMessageBox(QMessageBox):
class UnreadableMessageBox(QMessageBox):
"""
Message box to inform the user that the chosen file cannot be opened.
Message box to warn the user that the chosen file cannot be opened.
"""
def __init__(self):
super().__init__()
self.setIcon(QMessageBox.Warning)
self.setWindowTitle("File cannot be opened")
self.setText("The file you have selected cannot be opened.")
self.setInformativeText("Please make sure it is in the right format.")
self.setStandardButtons(QMessageBox.Ok)
self.setDefaultButton(QMessageBox.Ok)
class NameWarningMessageBox(QMessageBox):
"""
Message box to warn the user that a chord must have a name
"""
def __init__(self):
super().__init__()
self.setIcon(QMessageBox.Warning)
self.setWindowTitle("Unnamed chord")
self.setText("Chords must have a name.")
self.setInformativeText("Please give your chord a 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
"""
def __init__(self):
super().__init__()
self.setIcon(QMessageBox.Warning)
self.setWindowTitle("Malformed voicing")
self.setText("The voicing you entered was not understood and has not been applied.")
self.setInformativeText("Please try re-entering it in the correct format.")
self.setStandardButtons(QMessageBox.Ok)
self.setDefaultButton(QMessageBox.Ok)
class LengthWarningMessageBox(QMessageBox):
"""
Message box to warn the user that a block must have a length
"""
def __init__(self):
super().__init__()
self.setIcon(QMessageBox.Warning)
self.setWindowTitle("Block without valid length")
self.setText("Blocks must have a whole number length.")
self.setInformativeText("Please enter a valid length for your block and try again.")
self.setStandardButtons(QMessageBox.Ok)
self.setDefaultButton(QMessageBox.Ok)
if __name__ == '__main__':
app = QApplication(sys.argv)

2
mac.spec

@ -36,7 +36,7 @@ exe = EXE(pyz,
console=False )
app = BUNDLE(exe,
name='Chordsheet.app',
icon=None,
icon='ui/icon.icns',
bundle_identifier=None,
info_plist={
'NSPrincipalClass': 'NSApplication',

121
ui/aboutdialog.ui

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>200</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>200</height>
</size>
</property>
<property name="windowTitle">
<string>About Chordsheet</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QVBoxLayout" name="leftPane">
<item>
<widget class="QLabel" name="iconLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>128</width>
<height>128</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="rightPane">
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:24pt; font-weight:600;&quot;&gt;Chordsheet&lt;/span&gt;&lt;br/&gt;by Ivan Holmes&lt;/p&gt;&lt;p&gt;Chordsheet is a piece of software that generates a chord sheet with a simple GUI. &lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/ivanholmes/chordsheet&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;Github&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="versionLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Version not set</string>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignRight|Qt::AlignTrailing</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

BIN
ui/icon.afdesign

BIN
ui/icon.icns

BIN
ui/icon.ico

BIN
ui/icon.png

After

Width: 1024  |  Height: 1024  |  Size: 236 KiB

1
win.spec

@ -27,6 +27,7 @@ exe = EXE(pyz,
a.datas,
[],
name='Chordsheet',
icon='ui\\icon.ico',
debug=False,
bootloader_ignore_signals=False,
strip=False,

Loading…
Cancel
Save