diff --git a/_version.py b/_version.py index cd1c2c5..b8e37f8 100644 --- a/_version.py +++ b/_version.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- +appName = "Chordsheet" version = '0.4' \ No newline at end of file diff --git a/chordsheet/document.py b/chordsheet/document.py index 850dc43..3dd2927 100644 --- a/chordsheet/document.py +++ b/chordsheet/document.py @@ -9,6 +9,7 @@ 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) @@ -18,7 +19,7 @@ class Style: self.lineSpacing = kwargs.get('lineSpacing', 1.15) self.separatorSize = kwargs.get('separatorSize', 5) - self.useIncludedFont = False + self.useIncludedFont = True self.stringHzSp = 20*self.unit self.stringHzGap = 2*self.unit @@ -72,6 +73,9 @@ class Document: return NotImplemented def loadXML(self, filepath): + """ + Read an XML file and import its contents. + """ xmlDoc = ET.parse(filepath) root = xmlDoc.getroot() @@ -105,11 +109,17 @@ class Document: self.timeSignature = (int(root.find('timesignature').text) if root.find('timesignature') is not None else defaultTimeSignature) 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 @@ -144,7 +154,4 @@ class Document: ET.SubElement(blockElement, "notes").text = b.notes tree = ET.ElementTree(root) - tree.write(filepath) - - - \ No newline at end of file + tree.write(filepath) \ No newline at end of file diff --git a/chordsheet/primitives.py b/chordsheet/primitives.py index 9c65490..67bac51 100644 --- a/chordsheet/primitives.py +++ b/chordsheet/primitives.py @@ -5,6 +5,9 @@ from reportlab.lib.units import mm from reportlab.graphics.shapes import * def writeText(currentCanvas, style, string, size, vpos, **kwargs): + """ + Wrapper function to conveniently write text according to my requirements... + """ margin = style.leftMargin*style.unit align = kwargs.get('align', 'centre') @@ -28,11 +31,17 @@ def writeText(currentCanvas, style, string, size, vpos, **kwargs): return size*style.lineSpacing def drawHorizLine(currentCanvas, startpoint, endpoint, v_pos, h_origin, v_origin): + """ + Draw a horizontal line on the canvas taking origin point into account. + """ x1 = h_origin+startpoint x2 = h_origin+endpoint currentCanvas.line(x1, v_pos, x2, v_pos) def drawVertLine(currentCanvas, startpoint, endpoint, h_pos, h_origin, v_origin): + """ + Draw a vertical line on the canvas taking origin point into account. + """ y1 = v_origin+startpoint y2 = v_origin+endpoint currentCanvas.line(h_pos, y1, h_pos, y2) diff --git a/chordsheet/tableView.py b/chordsheet/tableView.py index ddd0025..4dfb827 100644 --- a/chordsheet/tableView.py +++ b/chordsheet/tableView.py @@ -14,8 +14,7 @@ class MProxyStyle(QtWidgets.QProxyStyle): def drawPrimitive(self, element, option, painter, widget=None): """ Draw a line across the entire row rather than just the column - we're hovering over. This may not always work depending on global - style. + we're hovering over. """ if element == self.PE_IndicatorItemViewItemDrop and not option.rect.isNull(): option_new = QtWidgets.QStyleOption(option) @@ -26,7 +25,9 @@ class MProxyStyle(QtWidgets.QProxyStyle): super().drawPrimitive(element, option, painter, widget) class MTableView(QtWidgets.QTableView): - + """ + Subclass the built in TableView to customise it. + """ def __init__(self, parent): super().__init__(parent) @@ -46,13 +47,18 @@ class MTableView(QtWidgets.QTableView): self.setStyle(MProxyStyle()) class ChordTableView(MTableView): - + """ + Subclass MTableView to add properties just for the chord table. + """ def __init__(self, parent): super().__init__(parent) self.model.setHorizontalHeaderLabels(['Chord', 'Voicing']) def populate(self, cList): + """ + Fill the table from a list of Chord objects. + """ self.model.removeRows(0, self.model.rowCount()) for c in cList: rowList = [QtGui.QStandardItem(c.name), QtGui.QStandardItem(",".join(c.voicings['guitar'] if 'guitar' in c.voicings.keys() else ""))] @@ -64,13 +70,18 @@ class ChordTableView(MTableView): class BlockTableView(MTableView): - + """ + Subclass MTableView to add properties just for the block table. + """ def __init__(self, parent): super().__init__(parent) self.model.setHorizontalHeaderLabels(['Chord', 'Length', 'Notes']) def populate(self, bList): + """ + Fill the table from a list of Block objects. + """ self.model.removeRows(0, self.model.rowCount()) for b in bList: rowList = [QtGui.QStandardItem((b.chord.name if b.chord else "")), QtGui.QStandardItem(str(b.length)), QtGui.QStandardItem(b.notes)] diff --git a/gui.py b/gui.py index e947510..9499e74 100755 --- a/gui.py +++ b/gui.py @@ -24,7 +24,7 @@ from chordsheet.document import Document, Style, Chord, Block from chordsheet.render import savePDF from chordsheet.parsers import parseFingering, parseName -from _version import 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): @@ -191,10 +191,12 @@ class DocumentWindow(QMainWindow): self.style.useIncludedFont = False def chordClickedAction(self, index): + # set the controls to the values from the selected chord self.window.chordNameLineEdit.setText(self.window.chordTableView.model.item(index.row(), 0).text()) self.window.guitarVoicingLineEdit.setText(self.window.chordTableView.model.item(index.row(), 1).text()) def blockClickedAction(self, index): + # set the controls to the values from the selected block self.window.blockChordComboBox.setCurrentText(self.window.blockTableView.model.item(index.row(), 0).text()) self.window.blockLengthLineEdit.setText(self.window.blockTableView.model.item(index.row(), 1).text()) self.window.blockNotesLineEdit.setText(self.window.blockTableView.model.item(index.row(), 2).text()) @@ -212,18 +214,21 @@ class DocumentWindow(QMainWindow): return settings.setValue(value, os.path.dirname(fullpath)) def menuFileNewAction(self): - self.doc = Document() - self.lastDoc = copy(self.doc) - self.currentFilePath = None + self.doc = Document() # new document object + self.lastDoc = copy(self.doc) # copy this object as reference to check against on quitting + self.currentFilePath = None # reset file path (this document hasn't been saved yet) self.UIInitDocument() self.updatePreview() def menuFileOpenAction(self): - filePath = QFileDialog.getOpenFileName(self.window.tabWidget, 'Open file', self.getPath("workingPath"), "Chordsheet ML files (*.xml *.cml)") - if filePath[0]: - self.openFile(filePath[0]) + filePath = QFileDialog.getOpenFileName(self.window.tabWidget, 'Open file', self.getPath("workingPath"), "Chordsheet ML files (*.xml *.cml)")[0] + if filePath: + self.openFile(filePath) def openFile(self, filePath): + """ + Opens a file from a file path and sets up the window accordingly. + """ self.currentFilePath = filePath self.doc.loadXML(self.currentFilePath) self.lastDoc = copy(self.doc) @@ -233,30 +238,35 @@ class DocumentWindow(QMainWindow): def menuFileSaveAction(self): self.updateDocument() - if not (hasattr(self, 'currentFilePath') and self.currentFilePath): - filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath("workingPath"), "Chordsheet ML files (*.xml *.cml)") - self.currentFilePath = filePath[0] - self.doc.saveXML(self.currentFilePath) - self.lastDoc = copy(self.doc) - self.setPath("workingPath", self.currentFilePath) + if not self.currentFilePath: + filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath("workingPath"), "Chordsheet ML files (*.xml *.cml)")[0] + else: + filePath = self.currentFilePath + saveFile(filePath) def menuFileSaveAsAction(self): self.updateDocument() - filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath("workingPath"), "Chordsheet ML files (*.xml *.cml)") - if filePath[0]: - self.currentFilePath = filePath[0] - self.doc.saveXML(self.currentFilePath) - self.lastDoc = copy(self.doc) - self.setPath("workingPath", self.currentFilePath) + filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath("workingPath"), "Chordsheet ML files (*.xml *.cml)")[0] + if filePath: + saveFile(filePath) self.updateTitleBar() # as we now have a new filename + + def saveFile(self, filePath): + """ + Saves a file to given file path and sets up environment. + """ + self.currentFilePath = filePath + self.doc.saveXML(self.currentFilePath) + self.lastDoc = copy(self.doc) + self.setPath("workingPath", self.currentFilePath) def menuFileSavePDFAction(self): self.updateDocument() self.updatePreview() - filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath("lastExportPath"), "PDF files (*.pdf)") - if filePath[0]: - savePDF(d, s, filePath[0]) - self.setPath("lastExportPath", filePath[0]) + filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath("lastExportPath"), "PDF files (*.pdf)")[0] + if filePath: + savePDF(d, s, filePath) + self.setPath("lastExportPath", filePath) def menuFilePrintAction(self): if sys.platform == "darwin": @@ -274,9 +284,9 @@ class DocumentWindow(QMainWindow): def menuEditUndoAction(self): try: - QApplication.focusWidget().undo() + QApplication.focusWidget().undo() # see if the built in widget supports it except: - pass + pass # if not just fail silently def menuEditRedoAction(self): try: @@ -335,9 +345,12 @@ class DocumentWindow(QMainWindow): self.window.chordNameLineEdit.clear() self.window.guitarVoicingLineEdit.clear() self.window.chordNameLineEdit.repaint() # necessary on Mojave with PyInstaller (or previous contents will be shown) - self.window.guitarVoicingLineEdit.clear() + self.window.guitarVoicingLineEdit.repaint() def updateChordDict(self): + """ + Updates the dictionary used to generate the Chord menu (on the block tab) + """ self.chordDict = {'None':None} self.chordDict.update({c.name:c for c in self.doc.chordList}) self.window.blockChordComboBox.clear() @@ -354,7 +367,7 @@ class DocumentWindow(QMainWindow): self.updateChordDict() def addChordAction(self): - success = False + success = False # initialise self.updateChords() cName = parseName(self.window.chordNameLineEdit.text()) @@ -363,22 +376,22 @@ class DocumentWindow(QMainWindow): if self.window.guitarVoicingLineEdit.text(): try: self.doc.chordList[-1].voicings['guitar'] = parseFingering(self.window.guitarVoicingLineEdit.text(), 'guitar') - success = True + success = True # chord successfully parsed except: - VoicingWarningMessageBox().exec() + VoicingWarningMessageBox().exec() # Voicing is malformed, warn user else: - success = True + success = True # chord successfully parsed else: - NameWarningMessageBox().exec() + NameWarningMessageBox().exec() # Chord has no name, warn user - if success == True: + if success == True: # if chord was parsed properly self.window.chordTableView.populate(self.doc.chordList) self.clearChordLineEdits() self.updateChordDict() def updateChordAction(self): - success = False - if self.window.chordTableView.selectionModel().hasSelection(): + success = False # see comments above + if self.window.chordTableView.selectionModel().hasSelection(): # check for selection self.updateChords() row = self.window.chordTableView.selectionModel().currentIndex().row() cName = parseName(self.window.chordNameLineEdit.text()) @@ -418,21 +431,21 @@ class DocumentWindow(QMainWindow): self.updateBlocks() try: - bLength = int(self.window.blockLengthLineEdit.text()) + bLength = int(self.window.blockLengthLineEdit.text()) # can the value entered for block length be cast as an integer except: bLength = False - if bLength: + 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.clearBlockLineEdits() else: - LengthWarningMessageBox().exec() + LengthWarningMessageBox().exec() # show warning that length was not entered or in wrong format def updateBlockAction(self): - if self.window.blockTableView.selectionModel().hasSelection(): + if self.window.blockTableView.selectionModel().hasSelection(): # check for selection self.updateBlocks() try: @@ -455,6 +468,9 @@ class DocumentWindow(QMainWindow): self.updatePreview() def updatePreview(self): + """ + Update the preview shown by rendering a new PDF and drawing it to the scroll area. + """ self.currentPreview = io.BytesIO() savePDF(self.doc, self.style, self.currentPreview) @@ -469,13 +485,18 @@ class DocumentWindow(QMainWindow): self.window.imageLabel.repaint() # necessary on Mojave with PyInstaller (or previous contents will be shown) def updateTitleBar(self): - appName = "Chordsheet" + """ + Update the application's title bar to reflect the current document. + """ if self.currentFilePath: - self.setWindowTitle(appName + " – " + os.path.basename(self.currentFilePath)) + self.setWindowTitle(_version.appName + " – " + os.path.basename(self.currentFilePath)) else: - self.setWindowTitle(appName) + self.setWindowTitle(_version.appName) def updateChords(self): + """ + Update the chord list by reading the table. + """ chordTableList = [] for i in range(self.window.chordTableView.model.rowCount()): chordTableList.append(Chord(parseName(self.window.chordTableView.model.item(i, 0).text()))), @@ -485,6 +506,9 @@ class DocumentWindow(QMainWindow): self.doc.chordList = chordTableList def updateBlocks(self): + """ + Update the block list by reading the table. + """ blockTableList = [] for i in range(self.window.blockTableView.model.rowCount()): blockLength = int(self.window.blockTableView.model.item(i, 1).text()) @@ -495,6 +519,9 @@ class DocumentWindow(QMainWindow): self.doc.blockList = blockTableList def updateDocument(self): + """ + Update the Document object by reading values from the UI. + """ self.doc.title = self.window.titleLineEdit.text() # Title can be empty string but not None self.doc.composer = (self.window.composerLineEdit.text() if self.window.composerLineEdit.text() else None) self.doc.arranger = (self.window.arrangerLineEdit.text() if self.window.arrangerLineEdit.text() else None) @@ -529,6 +556,9 @@ class GuitarDialog(QDialog): ui_file.close() def getVoicing(self): + """ + Show the dialogue and return the voicing that has been entered. + """ if self.dialog.exec_() == QDialog.Accepted: result = [self.dialog.ELineEdit.text(), self.dialog.ALineEdit.text(), @@ -552,7 +582,7 @@ class AboutDialog(QDialog): 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.versionLabel.setText("Version " + _version.version) self.dialog.exec()