diff --git a/_version.py b/_version.py new file mode 100644 index 0000000..cd1c2c5 --- /dev/null +++ b/_version.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +version = '0.4' \ No newline at end of file diff --git a/chordsheet/parsers.py b/chordsheet/parsers.py index 12b1bd2..9585b08 100644 --- a/chordsheet/parsers.py +++ b/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) diff --git a/chordsheet/tableView.py b/chordsheet/tableView.py index de0ef29..ddd0025 100644 --- a/chordsheet/tableView.py +++ b/chordsheet/tableView.py @@ -62,7 +62,6 @@ class ChordTableView(MTableView): self.model.appendRow(rowList) - self.resizeColumnsToContents() class BlockTableView(MTableView): @@ -79,6 +78,4 @@ class BlockTableView(MTableView): item.setEditable(False) item.setDropEnabled(False) - self.model.appendRow(rowList) - - self.resizeColumnsToContents() \ No newline at end of file + self.model.appendRow(rowList) \ No newline at end of file diff --git a/examples/example.pdf b/examples/example.pdf new file mode 100644 index 0000000..b0dbd0e Binary files /dev/null and b/examples/example.pdf differ diff --git a/examples/example.png b/examples/example.png new file mode 100644 index 0000000..00dd3ea Binary files /dev/null and b/examples/example.png differ diff --git a/examples/example.xml b/examples/example.xml index aa4d0f4..4bbb21e 100644 --- a/examples/example.xml +++ b/examples/example.xml @@ -1,46 +1 @@ - - Composition - A. Person - 4 - - - B - xx2341 - abcdefg - - - E - 022100 - - - Cm9 - x,x,8,8,8,10 - - - D7b5#9 - - - - - 4 - B - These are notes - - - 4 - E - - - 12 - Cm9 - - - 6 - D7b5#9 - - - 6 - For quiet contemplation. - - - \ 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. \ No newline at end of file diff --git a/gui.py b/gui.py old mode 100644 new mode 100755 index 8ea6fe5..e947510 --- a/gui.py +++ b/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,12 +221,15 @@ 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.lastDoc = copy(self.doc) - self.setPath("workingPath", self.currentFilePath) - self.UIInitDocument() - self.updatePreview() + 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() + self.updatePreview() def menuFileSaveAction(self): self.updateDocument() @@ -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,27 +354,51 @@ class DocumentWindow(QMainWindow): self.updateChordDict() def addChordAction(self): + success = False self.updateChords() - self.doc.chordList.append(Chord(parseName(self.window.chordNameLineEdit.text()))) - if self.window.guitarVoicingLineEdit.text(): - self.doc.chordList[-1].voicings['guitar'] = parseFingering(self.window.guitarVoicingLineEdit.text(), 'guitar') + 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() - self.window.chordTableView.populate(self.doc.chordList) - self.clearChordLineEdits() - self.updateChordDict() + 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())) - if self.window.guitarVoicingLineEdit.text(): - self.doc.chordList[-1].voicings['guitar'] = parseFingering(self.window.guitarVoicingLineEdit.text(), 'guitar') - - self.window.chordTableView.populate(self.doc.chordList) - self.clearChordLineEdits() - self.updateChordDict() + 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() def clearBlockLineEdits(self): self.window.blockLengthLineEdit.clear() @@ -351,23 +417,38 @@ class DocumentWindow(QMainWindow): def addBlockAction(self): self.updateBlocks() - self.doc.blockList.append(Block(self.window.blockLengthLineEdit.text(), - chord = self.chordDict[self.window.blockChordComboBox.currentText()], - notes = (self.window.blockNotesLineEdit.text() if not "" else None))) + try: + bLength = int(self.window.blockLengthLineEdit.text()) + except: + bLength = False - self.window.blockTableView.populate(self.doc.blockList) - self.clearBlockLineEdits() + 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() - row = self.window.blockTableView.selectionModel().currentIndex().row() - self.doc.blockList[row] = (Block(self.window.blockLengthLineEdit.text(), - 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() + try: + bLength = int(self.window.blockLengthLineEdit.text()) + except: + 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.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) diff --git a/mac.spec b/mac.spec index 26f654a..e8042bd 100644 --- a/mac.spec +++ b/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', diff --git a/ui/aboutdialog.ui b/ui/aboutdialog.ui new file mode 100644 index 0000000..85dd956 --- /dev/null +++ b/ui/aboutdialog.ui @@ -0,0 +1,121 @@ + + + Dialog + + + true + + + + 0 + 0 + 400 + 200 + + + + + 0 + 0 + + + + + 400 + 200 + + + + About Chordsheet + + + + + + + + + 0 + 0 + + + + + 128 + 128 + + + + + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Qt::Vertical + + + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-size:24pt; font-weight:600;">Chordsheet</span><br/>by Ivan Holmes</p><p>Chordsheet is a piece of software that generates a chord sheet with a simple GUI. </p><p><a href="https://github.com/ivanholmes/chordsheet"><span style=" text-decoration: underline; color:#0000ff;">Github</span></a></p></body></html> + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + 0 + 0 + + + + Version not set + + + Qt::AlignBottom|Qt::AlignRight|Qt::AlignTrailing + + + + + + + + + + diff --git a/ui/icon.afdesign b/ui/icon.afdesign new file mode 100644 index 0000000..a4da1e0 Binary files /dev/null and b/ui/icon.afdesign differ diff --git a/ui/icon.icns b/ui/icon.icns new file mode 100644 index 0000000..8c9bbe9 Binary files /dev/null and b/ui/icon.icns differ diff --git a/ui/icon.ico b/ui/icon.ico new file mode 100644 index 0000000..3e17d2a Binary files /dev/null and b/ui/icon.ico differ diff --git a/ui/icon.png b/ui/icon.png new file mode 100644 index 0000000..09c16bc Binary files /dev/null and b/ui/icon.png differ diff --git a/win.spec b/win.spec index b78d7d3..f1d6347 100644 --- a/win.spec +++ b/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,