diff --git a/chordsheet/document.py b/chordsheet/document.py index 41c2b89..f76bb77 100644 --- a/chordsheet/document.py +++ b/chordsheet/document.py @@ -119,7 +119,7 @@ class Document: blockChord = c break if blockChord is None: - exit("Chord {c} does not match any chord in {l}.".format( + raise ValueError("Chord {c} does not match any chord in {l}.".format( c=blockChordName, l=self.chordList)) else: blockChord = None @@ -144,11 +144,12 @@ class Document: self.tempo = (root.find('tempo').text if root.find( 'tempo') is not None else None) - def newFromXML(filepath): + @classmethod + def newFromXML(cls, filepath): """ Create a new Document object directly from an XML file. """ - doc = Document() + doc = cls() doc.loadXML(filepath) return doc @@ -200,3 +201,92 @@ class Document: tree = ET.ElementTree(root) tree.write(filepath) + + def loadCSMacro(self, filepath): + """ + Read a Chordsheet Macro file and import its contents. + """ + self.chordList = [] + self.sectionList = [] + + aliasTable = {} + + def chord(args): + argList = args.split(" ") + chordName = argList.pop(0) + + self.chordList.append(Chord(parseName(chordName))) + + argIter = iter(argList) + subCmdsArgs = list(zip(argIter, argIter)) + + for subCmd, arg in subCmdsArgs: + if subCmd == "alias": + aliasTable[arg] = chordName + else: + self.chordList[-1].voicings[subCmd] = parseFingering(arg, subCmd) + + def section(args): + blockList = [] + + sectionName, blocks = [arg.strip() for arg in args.split("\n", 1)] + + self.sectionList.append(Section(name=sectionName)) + + for b in blocks.split(): + blockParams = b.split(",") + blockLength = float(blockParams[1]) + + if blockParams[0] in aliasTable: + blockChordName = aliasTable[blockParams[0]] + else: + blockChordName = blockParams[0] + + blockChordName = parseName(blockChordName) if blockChordName not in ["NC", "X"] else None + + blockChord = None + + if blockChordName: + for c in self.chordList: + if c.name == blockChordName: + blockChord = c + break + if blockChord is None: + raise ValueError("Chord {c} does not match any chord in {l}.".format( + c=blockChordName, l=self.chordList)) + + blockList.append(Block(blockLength, chord=blockChord)) + + self.sectionList[-1].blockList = blockList + + with open(filepath, 'r') as f: + cmatext = f.read() + + cmaCmdsArgs = [statement.split(" ", 1) for statement in \ + (rawStatement.strip() for rawStatement in cmatext.split("\\")[1:])] + + for cmd, args in cmaCmdsArgs: + if cmd == "chordsheet": + # There's only one version so no need to do anything with this + pass + elif cmd == "title": + self.title = args + elif cmd == "subtitle": + self.subtitle = args + elif cmd == "arranger": + self.arranger = args + elif cmd == "composer": + self.composer = args + elif cmd == "timesig": + self.timeSignature = int(args) + elif cmd == "tempo": + self.tempo = args + elif cmd == "chord": + chord(args) + elif cmd == "section": + section(args) + elif cmd in ["!", "rem"]: + # Simply ignore comments + pass + else: + raise ValueError(f"Command {cmd} not understood.") \ No newline at end of file diff --git a/examples/kissoflife.cma b/examples/kissoflife.cma index 6a152e6..289c437 100644 --- a/examples/kissoflife.cma +++ b/examples/kissoflife.cma @@ -1,4 +1,5 @@ \chordsheet 1 +\rem This is a comment to say that the first line isn't really needed (as there's only one version of Chordsheet Macro so far) \title Kiss of Life \subtitle Sade @@ -17,5 +18,7 @@ AM9,8 F#m11,8 DM7,1.5 C#m7,2 Bm7,4.5 F#m11,8 +\! This is another comment. Comments may not exist in the middle of other statements. + \section Chorus and bridge Bm7,3.5 F,4.5 Bm7,3.5 F#m11,4.5 \ No newline at end of file diff --git a/examples/kissoflife.xml b/examples/kissoflife.xml index 41b6fc0..4a9fd78 100644 --- a/examples/kissoflife.xml +++ b/examples/kissoflife.xml @@ -1 +1 @@ -Kiss of LifeSadeIvan HolmesSade Adu, Paul S. Denman, Andrew Hale, Stuart Matthewman4AM9A,B,C♯,EF♯m11A,B,C♯,EDM7A,C♯,F♯C♯m7G♯,B,EBm7A,D,F♯
8.0AM98.0F♯m111.5DM72.0C♯m74.5Bm78.0F♯m11
3.5Bm74.5F♯m113.5Bm74.5F♯m11
\ No newline at end of file +Kiss of LifeSadeIvan HolmesSade Adu, Paul S. Denman, Andrew Hale, Stuart Matthewman4120AM9A,B,C♯,EF♯m11A,B,C♯,EDM7A,C♯,F♯C♯m7G♯,B,EBm7A,D,F♯
8.0AM98.0F♯m111.5DM72.0C♯m74.5Bm78.0F♯m11
3.5Bm74.5F♯m113.5Bm74.5F♯m11
\ No newline at end of file diff --git a/gui.py b/gui.py index f38369e..f5addeb 100755 --- a/gui.py +++ b/gui.py @@ -314,7 +314,7 @@ class DocumentWindow(QMainWindow): def menuFileOpenAction(self): if self.saveWarning(): # ask the user if they want to save filePath = QFileDialog.getOpenFileName(self.window.tabWidget, 'Open file', self.getPath( - "workingPath"), "Chordsheet ML files (*.xml *.cml)")[0] + "workingPath"), "Chordsheet Markup Language files (*.xml *.cml);;Chordsheet Macro files (*.cma)")[0] if filePath: self.openFile(filePath) @@ -323,7 +323,14 @@ class DocumentWindow(QMainWindow): Opens a file from a file path and sets up the window accordingly. """ self.currentFilePath = filePath - self.doc.loadXML(self.currentFilePath) + + fileExt = os.path.splitext(self.currentFilePath)[1].lower() + + if fileExt == ".cma": + self.doc.loadCSMacro(self.currentFilePath) + else: # if fileExt in [".xml", ".cml"]: + self.doc.loadXML(self.currentFilePath) + self.lastDoc = copy(self.doc) self.setPath("workingPath", self.currentFilePath) self.UIInitDocument() @@ -570,7 +577,8 @@ class DocumentWindow(QMainWindow): if b.chord: if b.chord.name == oldName: b.chord.name = cName - self.window.blockTableView.populate(self.currentSection.blockList) + if self.currentSection and self.currentSection.blockList: + self.window.blockTableView.populate(self.currentSection.blockList) self.clearChordLineEdits() def removeSectionAction(self): diff --git a/linux.spec b/linux.spec new file mode 100644 index 0000000..5b53784 --- /dev/null +++ b/linux.spec @@ -0,0 +1,37 @@ +# -*- mode: python ; coding: utf-8 -*- + +block_cipher = None + + +a = Analysis(['gui.py'], + pathex=['/home/ivan/Code/chordsheet'], + binaries=[], + datas=[ + ('fonts', 'fonts'), + ('ui', 'ui') + ], + hiddenimports=[], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='chordsheet', + icon='ui/icon.ico', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False )