#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Created on Wed May 29 00:02:24 2019 @author: ivan """ import sys 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, QFontComboBox, QSpinBox, QDoubleSpinBox, QTableWidgetItem, QTabWidget, QComboBox, QWidget, QScrollArea, QMainWindow, QShortcut, QMdiSubWindow from PyQt5.QtCore import QFile, QObject, Qt, pyqtSlot, QSettings from PyQt5.QtGui import QPixmap, QImage, QKeySequence, QFontInfo, QFont, QRawFont 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.common import scriptDir from chordsheet.document import Document, Style, Chord, Block, Section from chordsheet.render import Renderer from chordsheet.parsers import parseFingering, parseName from chordsheet.messageBox import UnsavedMessageBox, UnreadableMessageBox, ChordNameWarningMessageBox, SectionNameWarningMessageBox, BlockMustHaveSectionWarningMessageBox, VoicingWarningMessageBox, LengthWarningMessageBox from chordsheet.dialogs import GuitarDialog, AboutDialog from chordsheet.panels import DocInfoDockWidget, PageSetupDockWidget, ChordsDockWidget, SectionsDockWidget, BlocksDockWidget, PreviewDockWidget import _version # enable automatic high DPI scaling on Windows QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) QApplication.setOrganizationName("Ivan Holmes") QApplication.setOrganizationDomain("ivanholmes.co.uk") QApplication.setApplicationName("Chordsheet") settings = QSettings() pdfmetrics.registerFont( TTFont('FreeSans', os.path.join(scriptDir, 'fonts', 'FreeSans.ttf'))) if sys.platform == "darwin": pdfmetrics.registerFont( TTFont('HelveticaNeue', 'HelveticaNeue.ttc', subfontIndex=0)) # dictionaries for combo boxes pageSizeDict = {'A4': A4, 'A5': A5, 'Letter': LETTER, 'Legal': LEGAL} # point is 1 because reportlab's native unit is points. unitDict = {'mm': mm, 'cm': cm, 'inch': inch, 'point': 1, 'pica': pica} class MainWindow(QMainWindow): """ Class for the main window of the application. """ def __init__(self, filename=None): """ Initialisation function for the main window of the application. Arguments: doc -- the Document object for the window to use style -- the Style object for the window to use """ super().__init__() self.UIFileLoader(str(os.path.join(scriptDir, 'ui', 'new.ui'))) self.setCentralWidget(self.window.centralWidget) self.setMenuBar(self.window.menuBar) self.setWindowTitle("Chordsheet") self.lastSubWindow = None self.currentSection = None if filename: try: self.openFile(filename) except Exception: UnreadableMessageBox().exec() def closeEvent(self, event): """ Reimplement the built in closeEvent to allow asking the user to save. """ if not self.window.mdiArea.subWindowList(): self.close() def UIFileLoader(self, ui_file): """ Loads the .ui file for this window and connects the UI elements to their actions. """ ui_file = QFile(ui_file) ui_file.open(QFile.ReadOnly) self.window = uic.loadUi(ui_file) ui_file.close() self.docinfo = DocInfoDockWidget() self.psetup = PageSetupDockWidget() self.chordsw = ChordsDockWidget() self.sectionsw = SectionsDockWidget() self.blocksw = BlocksDockWidget() self.previeww = PreviewDockWidget() self.addDockWidget(Qt.LeftDockWidgetArea, self.docinfo) self.addDockWidget(Qt.LeftDockWidgetArea, self.psetup) self.addDockWidget(Qt.LeftDockWidgetArea, self.previeww) self.addDockWidget(Qt.RightDockWidgetArea, self.chordsw) self.addDockWidget(Qt.RightDockWidgetArea, self.sectionsw) self.addDockWidget(Qt.RightDockWidgetArea, self.blocksw) # Check all the boxes in the window menu self.window.actionDocument_information.setChecked(True) self.window.actionPage_setup.setChecked(True) self.window.actionChords.setChecked(True) self.window.actionSections.setChecked(True) self.window.actionBlocks.setChecked(True) self.window.actionPreview.setChecked(True) # link all the UI elements self.window.mdiArea.subWindowActivated.connect(self.switchDocument) self.window.actionAbout.triggered.connect(self.menuFileAboutAction) self.window.actionNew.triggered.connect(self.menuFileNewAction) self.window.actionOpen.triggered.connect(self.menuFileOpenAction) self.window.actionSave.triggered.connect(self.menuFileSaveAction) self.window.actionSave_as.triggered.connect(self.menuFileSaveAsAction) 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) self.window.actionSave.setShortcut(QKeySequence.Save) self.window.actionSave_as.setShortcut(QKeySequence.SaveAs) self.window.actionSave_PDF.setShortcut(QKeySequence("Ctrl+E")) self.window.actionPrint.setShortcut(QKeySequence.Print) self.window.actionClose.setShortcut(QKeySequence.Close) self.window.actionUndo.setShortcut(QKeySequence.Undo) self.window.actionRedo.setShortcut(QKeySequence.Redo) self.window.actionCut.setShortcut(QKeySequence.Cut) self.window.actionCopy.setShortcut(QKeySequence.Copy) self.window.actionPaste.setShortcut(QKeySequence.Paste) self.window.actionDocument_information.triggered.connect(self.menuWindowDocInfoAction) self.window.actionPage_setup.triggered.connect(self.menuWindowPageSetupAction) self.window.actionChords.triggered.connect(self.menuWindowChordsAction) self.window.actionSections.triggered.connect(self.menuWindowSectionsAction) self.window.actionBlocks.triggered.connect(self.menuWindowBlocksAction) self.window.actionPreview.triggered.connect(self.menuWindowPreviewAction) self.psetup.widget().pageSizeComboBox.currentIndexChanged.connect( self.pageSizeAction) self.psetup.widget().documentUnitsComboBox.currentIndexChanged.connect( self.unitAction) self.psetup.widget().pageSizeComboBox.addItems(list(pageSizeDict.keys())) self.psetup.widget().pageSizeComboBox.setCurrentText( list(pageSizeDict.keys())[0]) self.psetup.widget().documentUnitsComboBox.addItems(list(unitDict.keys())) self.psetup.widget().documentUnitsComboBox.setCurrentText( list(unitDict.keys())[0]) self.psetup.widget().fontComboBox.currentFontChanged.connect(self.fontChangeAction) self.psetup.widget().includedFontCheckBox.stateChanged.connect( self.includedFontAction) self.previeww.widget().updatePreviewButton.clicked.connect(self.generateAction) # update whole document when any tab is selected # self.window.tabWidget.tabBarClicked.connect(self.tabBarUpdateAction) self.chordsw.widget().guitarVoicingButton.clicked.connect( self.guitarVoicingAction) self.chordsw.widget().addChordButton.clicked.connect(self.addChordAction) self.chordsw.widget().removeChordButton.clicked.connect(self.removeChordAction) self.chordsw.widget().updateChordButton.clicked.connect(self.updateChordAction) # connecting clicked only works for this combo box because it's my own modified version (MComboBox) self.blocksw.widget().blockSectionComboBox.clicked.connect( self.blockSectionClickedAction) self.blocksw.widget().blockSectionComboBox.currentIndexChanged.connect( self.blockSectionChangedAction) self.blocksw.widget().addBlockButton.clicked.connect(self.addBlockAction) self.blocksw.widget().removeBlockButton.clicked.connect(self.removeBlockAction) self.blocksw.widget().updateBlockButton.clicked.connect(self.updateBlockAction) self.sectionsw.widget().addSectionButton.clicked.connect(self.addSectionAction) self.sectionsw.widget().removeSectionButton.clicked.connect( self.removeSectionAction) self.sectionsw.widget().updateSectionButton.clicked.connect( self.updateSectionAction) self.chordsw.widget().chordTableView.clicked.connect(self.chordClickedAction) self.sectionsw.widget().sectionTableView.clicked.connect(self.sectionClickedAction) self.blocksw.widget().blockTableView.clicked.connect(self.blockClickedAction) # Set the tab widget to Overview tab # self.window.tabWidget.setCurrentIndex(0) def UIInitDocument(self, doc): """ Fills the window's fields with the values from its document. """ # self.updateTitleBar() # set all fields to appropriate values from document self.docinfo.widget().titleLineEdit.setText(doc.title) self.docinfo.widget().subtitleLineEdit.setText(doc.subtitle) self.docinfo.widget().composerLineEdit.setText(doc.composer) self.docinfo.widget().arrangerLineEdit.setText(doc.arranger) self.docinfo.widget().timeSignatureSpinBox.setValue(doc.timeSignature) self.docinfo.widget().tempoLineEdit.setText(doc.tempo) self.chordsw.widget().chordTableView.populate(doc.chordList) self.sectionsw.widget().sectionTableView.populate(doc.sectionList) self.updateChordDict(doc) self.updateSectionDict(doc) # populate the block table with the first section, account for a document with no sections if self.currentSection is None: self.currentSection = doc.sectionList[0] if doc.sectionList else None else: self.blocksw.widget().blockSectionComboBox.setCurrentText( self.currentSection.name) self.blocksw.widget().blockTableView.populate( self.currentSection.blockList if self.currentSection else []) def UIInitStyle(self, style): """ Fills the window's fields with the values from its style. """ self.psetup.widget().pageSizeComboBox.setCurrentText( [k for k, v in pageSizeDict.items() if v==style.pageSize][0]) self.psetup.widget().documentUnitsComboBox.setCurrentText( [k for k, v in unitDict.items() if v==style.unit][0]) self.psetup.widget().lineSpacingDoubleSpinBox.setValue(style.lineSpacing) self.psetup.widget().leftMarginLineEdit.setText(str(style.leftMargin)) self.psetup.widget().rightMarginLineEdit.setText(str(style.rightMargin)) self.psetup.widget().topMarginLineEdit.setText(str(style.topMargin)) self.psetup.widget().bottomMarginLineEdit.setText(str(style.bottomMargin)) # self.psetup.widget().fontComboBox.setDisabled(True) self.psetup.widget().includedFontCheckBox.setChecked(True) self.psetup.widget().beatWidthLineEdit.setText(str(style.unitWidth)) def updateChordDict(self, doc): """ 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 doc.chordList}) self.blocksw.widget().blockChordComboBox.clear() self.blocksw.widget().blockChordComboBox.addItems(list(self.chordDict.keys())) def updateSectionDict(self, doc): """ Updates the dictionary used to generate the Section menu (on the block tab) """ self.sectionDict = {s.name: s for s in doc.sectionList} self.blocksw.widget().blockSectionComboBox.clear() self.blocksw.widget().blockSectionComboBox.addItems( list(self.sectionDict.keys())) def fontChangeAction(self): if self.window.mdiArea.currentSubWindow() is not None: qFont = self.psetup.widget().fontComboBox.currentFont() qFontReal = QRawFont.fromFont(qFont) #fontInfo = QFontInfo(qFont) #qFontReal = QRawFont(fontInfo.family()) print(qFont.rawName()) def switchDocument(self, curWindow): if curWindow is not None: if self.lastSubWindow is not None: self.lastSubWindow.currentSection = self.currentSection self.UIInitDocument(curWindow.doc) self.UIInitStyle(curWindow.style) self.currentSection = curWindow.currentSection if self.currentSection is not None: self.blocksw.widget().blockSectionComboBox.setCurrentText( self.currentSection.name) self.lastSubWindow = curWindow def generateAction(self): if self.window.mdiArea.currentSubWindow() is not None: self.window.mdiArea.currentSubWindow().updateDocument() self.window.mdiArea.currentSubWindow().updatePreview() def removeChordAction(self): if self.chordsw.widget().chordTableView.selectionModel().hasSelection(): #  check for selection self.window.mdiArea.currentSubWindow().updateChords() row = self.chordsw.widget().chordTableView.selectionModel().currentIndex().row() oldName = self.chordsw.widget().chordTableView.model.item(row, 0).text() self.window.mdiArea.currentSubWindow().doc.chordList.pop(row) self.chordsw.widget().chordTableView.populate(self.window.mdiArea.currentSubWindow().doc.chordList) # remove the chord if any of the blocks have it attached if self.currentSection is not None: for s in self.window.mdiArea.currentSubWindow().doc.sectionList: for b in s.blockList: if b.chord: if b.chord.name == oldName: b.chord = None self.blocksw.widget().blockTableView.populate(self.currentSection.blockList) self.clearChordLineEdits() self.updateChordDict(self.window.mdiArea.currentSubWindow().doc) def addChordAction(self): success = False # initialise self.window.mdiArea.currentSubWindow().updateChords() cName = parseName(self.chordsw.widget().chordNameLineEdit.text()) if cName: self.window.mdiArea.currentSubWindow().doc.chordList.append(Chord(cName)) if self.chordsw.widget().guitarVoicingLineEdit.text() or self.chordsw.widget().pianoVoicingLineEdit.text(): if self.chordsw.widget().guitarVoicingLineEdit.text(): try: self.window.mdiArea.currentSubWindow().doc.chordList[-1].voicings['guitar'] = parseFingering( self.chordsw.widget().guitarVoicingLineEdit.text(), 'guitar') success = True #  chord successfully parsed except Exception: VoicingWarningMessageBox().exec() # Voicing is malformed, warn user if self.chordsw.widget().pianoVoicingLineEdit.text(): try: self.window.mdiArea.currentSubWindow().doc.chordList[-1].voicings['piano'] = parseFingering( self.chordsw.widget().pianoVoicingLineEdit.text(), 'piano') success = True #  chord successfully parsed except Exception: VoicingWarningMessageBox().exec() # Voicing is malformed, warn user else: success = True #  chord successfully parsed else: ChordNameWarningMessageBox().exec() # Chord has no name, warn user if success == True: # if chord was parsed properly self.chordsw.widget().chordTableView.populate(self.window.mdiArea.currentSubWindow().doc.chordList) self.clearChordLineEdits() self.updateChordDict(self.window.mdiArea.currentSubWindow().doc) def updateChordAction(self): success = False # see comments above if self.chordsw.widget().chordTableView.selectionModel().hasSelection(): #  check for selection self.window.mdiArea.currentSubWindow().updateChords() row = self.chordsw.widget().chordTableView.selectionModel().currentIndex().row() oldName = self.chordsw.widget().chordTableView.model.item(row, 0).text() cName = parseName(self.chordsw.widget().chordNameLineEdit.text()) if cName: self.window.mdiArea.currentSubWindow().doc.chordList[row].name = cName if self.chordsw.widget().guitarVoicingLineEdit.text() or self.chordsw.widget().pianoVoicingLineEdit.text(): if self.chordsw.widget().guitarVoicingLineEdit.text(): try: self.window.mdiArea.currentSubWindow().doc.chordList[row].voicings['guitar'] = parseFingering( self.chordsw.widget().guitarVoicingLineEdit.text(), 'guitar') success = True #  chord successfully parsed except Exception: VoicingWarningMessageBox().exec() # Voicing is malformed, warn user if self.chordsw.widget().pianoVoicingLineEdit.text(): try: self.window.mdiArea.currentSubWindow().doc.chordList[row].voicings['piano'] = parseFingering( self.chordsw.widget().pianoVoicingLineEdit.text(), 'piano') success = True #  chord successfully parsed except Exception: VoicingWarningMessageBox().exec() # Voicing is malformed, warn user else: success = True #  chord successfully parsed else: ChordNameWarningMessageBox().exec() if success == True: self.updateChordDict(self.window.mdiArea.currentSubWindow().doc) self.chordsw.widget().chordTableView.populate(self.window.mdiArea.currentSubWindow().doc.chordList) # update the names of chords in all blocklists in case they've already been used for s in self.window.mdiArea.currentSubWindow().doc.sectionList: for b in s.blockList: if b.chord: if b.chord.name == oldName: b.chord.name = cName if self.currentSection and self.currentSection.blockList: self.blocksw.widget().blockTableView.populate(self.currentSection.blockList) self.clearChordLineEdits() def removeSectionAction(self): if self.sectionsw.widget().sectionTableView.selectionModel().hasSelection(): #  check for selection self.window.mdiArea.currentSubWindow().updateSections() row = self.sectionsw.widget().sectionTableView.selectionModel().currentIndex().row() self.window.mdiArea.currentSubWindow().doc.sectionList.pop(row) self.sectionsw.widget().sectionTableView.populate(self.window.mdiArea.currentSubWindow().doc.sectionList) self.clearSectionLineEdits() self.updateSectionDict(self.window.mdiArea.currentSubWindow().doc) def addSectionAction(self): self.window.mdiArea.currentSubWindow().updateSections() sName = self.sectionsw.widget().sectionNameLineEdit.text() if sName and sName not in [s.name for s in self.window.mdiArea.currentSubWindow().doc.sectionList]: self.window.mdiArea.currentSubWindow().doc.sectionList.append(Section(name=sName)) self.sectionsw.widget().sectionTableView.populate(self.window.mdiArea.currentSubWindow().doc.sectionList) self.clearSectionLineEdits() self.updateSectionDict(self.window.mdiArea.currentSubWindow().doc) else: # Section has no name or non unique, warn user SectionNameWarningMessageBox().exec() def updateSectionAction(self): if self.sectionsw.widget().sectionTableView.selectionModel().hasSelection(): #  check for selection self.window.mdiArea.currentSubWindow().updateSections() row = self.sectionsw.widget().sectionTableView.selectionModel().currentIndex().row() sName = self.sectionsw.widget().sectionNameLineEdit.text() if sName and sName not in [s.name for s in self.window.mdiArea.currentSubWindow().doc.sectionList]: self.window.mdiArea.currentSubWindow().doc.sectionList[row].name = sName self.sectionsw.widget().sectionTableView.populate(self.window.mdiArea.currentSubWindow().doc.sectionList) self.clearSectionLineEdits() self.updateSectionDict(self.window.mdiArea.currentSubWindow().doc) else: # Section has no name or non unique, warn user SectionNameWarningMessageBox().exec() def removeBlockAction(self): if self.blocksw.widget().blockTableView.selectionModel().hasSelection(): #  check for selection self.window.mdiArea.currentSubWindow().updateBlocks(self.currentSection) row = self.blocksw.widget().blockTableView.selectionModel().currentIndex().row() self.currentSection.blockList.pop(row) self.blocksw.widget().blockTableView.populate(self.currentSection.blockList) def addBlockAction(self): self.window.mdiArea.currentSubWindow().updateBlocks(self.currentSection) try: #  can the value entered for block length be cast as a float bLength = float(self.blocksw.widget().blockLengthLineEdit.text()) except Exception: bLength = False if bLength: # create the block self.currentSection.blockList.append(Block(bLength, chord=self.chordDict[self.blocksw.widget().blockChordComboBox.currentText( )], notes=(self.blocksw.widget().blockNotesLineEdit.text() if not "" else None))) self.blocksw.widget().blockTableView.populate(self.currentSection.blockList) self.clearBlockLineEdits() else: # show warning that length was not entered or in wrong format LengthWarningMessageBox().exec() def updateBlockAction(self): if self.blocksw.widget().blockTableView.selectionModel().hasSelection(): #  check for selection self.window.mdiArea.currentSubWindow().updateBlocks(self.currentSection) try: #  can the value entered for block length be cast as a float bLength = float(self.blocksw.widget().blockLengthLineEdit.text()) except Exception: bLength = False row = self.blocksw.widget().blockTableView.selectionModel().currentIndex().row() if bLength: self.currentSection.blockList[row] = (Block(bLength, chord=self.chordDict[self.blocksw.widget().blockChordComboBox.currentText( )], notes=(self.blocksw.widget().blockNotesLineEdit.text() if not "" else None))) self.blocksw.widget().blockTableView.populate( self.currentSection.blockList) self.clearBlockLineEdits() else: LengthWarningMessageBox().exec() def pageSizeAction(self, index): self.pageSizeSelected = self.psetup.widget().pageSizeComboBox.itemText(index) def unitAction(self, index): self.unitSelected = self.psetup.widget().documentUnitsComboBox.itemText(index) def includedFontAction(self): if self.window.mdiArea.currentSubWindow() is not None: if self.psetup.widget().includedFontCheckBox.isChecked(): self.window.mdiArea.currentSubWindow().style.useIncludedFont = True else: self.window.mdiArea.currentSubWindow().style.useIncludedFont = False def chordClickedAction(self, index): # set the controls to the values from the selected chord self.chordsw.widget().chordNameLineEdit.setText( self.chordsw.widget().chordTableView.model.item(index.row(), 0).text()) self.chordsw.widget().guitarVoicingLineEdit.setText( self.chordsw.widget().chordTableView.model.item(index.row(), 1).text()) self.chordsw.widget().pianoVoicingLineEdit.setText( self.chordsw.widget().chordTableView.model.item(index.row(), 2).text()) def sectionClickedAction(self, index): # set the controls to the values from the selected section self.sectionsw.widget().sectionNameLineEdit.setText( self.sectionsw.widget().sectionTableView.model.item(index.row(), 0).text()) # also set the combo box on the block page to make it flow well curSecName = self.sectionsw.widget().sectionTableView.model.item( index.row(), 0).text() if curSecName: self.blocksw.widget().blockSectionComboBox.setCurrentText( curSecName) def blockSectionClickedAction(self, text): if text: self.window.mdiArea.currentSubWindow().updateBlocks(self.sectionDict[text]) def blockSectionChangedAction(self, index): sName = self.blocksw.widget().blockSectionComboBox.currentText() if sName: if self.window.mdiArea.currentSubWindow() is not None: self.currentSection = self.sectionDict.get(sName, None) # self.window.mdiArea.currentSubWindow().currentSection = self.currentSection if self.currentSection is not None: self.blocksw.widget().blockTableView.populate(self.currentSection.blockList) else: pass # self.currentSection = None def blockClickedAction(self, index): # set the controls to the values from the selected block bChord = self.blocksw.widget().blockTableView.model.item(index.row(), 0).text() self.blocksw.widget().blockChordComboBox.setCurrentText( bChord if bChord else "None") self.blocksw.widget().blockLengthLineEdit.setText( self.blocksw.widget().blockTableView.model.item(index.row(), 1).text()) self.blocksw.widget().blockNotesLineEdit.setText( self.blocksw.widget().blockTableView.model.item(index.row(), 2).text()) def getPath(self, value): """ Wrapper for Qt settings to return home directory if no setting exists. """ return str((settings.value(value) if settings.value(value) else os.path.expanduser("~"))) def setPath(self, value, fullpath): """ Wrapper for Qt settings to set path to open/save from next time from current file location. """ return settings.setValue(value, os.path.dirname(fullpath)) def menuFileNewAction(self): dw = DocumentWindow(Document(), Style(), None) self.window.mdiArea.addSubWindow(dw) self.UIInitDocument(dw.doc) dw.show() def menuFileOpenAction(self): filePath = QFileDialog.getOpenFileName(self.window, 'Open file', self.getPath( "workingPath"), "Chordsheet Markup Language files (*.xml *.cml);;Chordsheet Macro files (*.cma)")[0] if filePath: dw = DocumentWindow.openFile(filePath) self.window.mdiArea.addSubWindow(dw) self.UIInitDocument(dw.doc) self.UIInitStyle(dw.style) # self.currentSection = None dw.show() def menuFileSaveAction(self): if self.window.mdiArea.currentSubWindow() is not None: self.window.mdiArea.currentSubWindow().updateDocument() if self.window.mdiArea.currentSubWindow().currentFilePath: fileExt = os.path.splitext(self.window.mdiArea.currentSubWindow().currentFilePath)[1].lower() if fileExt != ".cma": # Chordsheet Macro files can't be saved at this time self.window.mdiArea.currentSubWindow().saveFile(self.window.mdiArea.currentSubWindow().currentFilePath) else: filePath = QFileDialog.getSaveFileName(self.window, 'Save file', self.getPath( "workingPath"), "Chordsheet ML files (*.xml *.cml)")[0] if filePath: self.window.mdiArea.currentSubWindow().saveFile(filePath) def menuFileSaveAsAction(self): if self.window.mdiArea.currentSubWindow() is not None: self.window.mdiArea.currentSubWindow().updateDocument() filePath = QFileDialog.getSaveFileName(self.window, 'Save file', self.getPath( "workingPath"), "Chordsheet ML files (*.xml *.cml)")[0] if filePath: self.window.mdiArea.currentSubWindow().saveFile(filePath) def menuFileSavePDFAction(self): if self.window.mdiArea.currentSubWindow() is not None: self.window.mdiArea.currentSubWindow().updateDocument() self.window.mdiArea.currentSubWindow().updatePreview() filePath = QFileDialog.getSaveFileName(self.window, 'Save file', self.getPath( "lastExportPath"), "PDF files (*.pdf)")[0] if filePath: self.window.mdiArea.currentSubWindow().renderer.savePDF(filePath) self.window.mdiArea.currentSubWindow().setPath("lastExportPath", filePath) def menuFilePrintAction(self): if sys.platform == "darwin": pass # subprocess.call() else: pass @pyqtSlot() def menuFileCloseAction(self): self.saveWarning() def menuFileAboutAction(self): AboutDialog() def menuEditUndoAction(self): try: QApplication.focusWidget().undo() # see if the built in widget supports it except Exception: pass #  if not just fail silently def menuEditRedoAction(self): try: QApplication.focusWidget().redo() except Exception: pass def menuEditCutAction(self): try: QApplication.focusWidget().cut() except Exception: pass def menuEditCopyAction(self): try: QApplication.focusWidget().copy() except Exception: pass def menuEditPasteAction(self): try: QApplication.focusWidget().paste() except Exception: pass # self.docinfo = DocInfoDockWidget() # self.psetup = PageSetupDockWidget() # self.chordsw = ChordsDockWidget() # self.sectionsw = SectionsDockWidget() # self.blocksw = BlocksDockWidget() # self.previeww = PreviewDockWidget() def menuWindowDocInfoAction(self): if self.window.actionDocument_information.isChecked(): self.docinfo.show() else: self.docinfo.hide() def menuWindowPageSetupAction(self): if self.window.actionPage_setup.isChecked(): self.psetup.show() else: self.psetup.hide() def menuWindowChordsAction(self): if self.window.actionChords.isChecked(): self.chordsw.show() else: self.chordsw.hide() def menuWindowSectionsAction(self): if self.window.actionSections.isChecked(): self.sectionsw.show() else: self.sectionsw.hide() def menuWindowBlocksAction(self): if self.window.actionBlocks.isChecked(): self.blocksw.show() else: self.blocksw.hide() def menuWindowPreviewAction(self): if self.window.actionPreview.isChecked(): self.previeww.show() else: self.previeww.hide() def guitarVoicingAction(self): gdialog = GuitarDialog() voicing = gdialog.getVoicing() if voicing: self.chordsw.widget().guitarVoicingLineEdit.setText(voicing) def clearChordLineEdits(self): self.chordsw.widget().chordNameLineEdit.clear() self.chordsw.widget().guitarVoicingLineEdit.clear() self.chordsw.widget().pianoVoicingLineEdit.clear() # necessary on Mojave with PyInstaller (or previous contents will be shown) self.chordsw.widget().chordNameLineEdit.repaint() self.chordsw.widget().guitarVoicingLineEdit.repaint() self.chordsw.widget().pianoVoicingLineEdit.repaint() def clearSectionLineEdits(self): self.sectionsw.widget().sectionNameLineEdit.clear() # necessary on Mojave with PyInstaller (or previous contents will be shown) self.sectionsw.widget().sectionNameLineEdit.repaint() def clearBlockLineEdits(self): self.blocksw.widget().blockLengthLineEdit.clear() self.blocksw.widget().blockNotesLineEdit.clear() # necessary on Mojave with PyInstaller (or previous contents will be shown) self.blocksw.widget().blockLengthLineEdit.repaint() self.blocksw.widget().blockNotesLineEdit.repaint() class DocumentWindow(QMdiSubWindow): def __init__(self, doc, style, filename): super().__init__() self.UIFileLoader(str(os.path.join(scriptDir, 'ui', 'document.ui'))) self.doc = doc self.style = style self.renderer = Renderer(self.doc, self.style) self.lastDoc = copy(self.doc) self.currentFilePath = filename self.currentSection = None mw.updateChordDict(self.doc) mw.updateSectionDict(self.doc) def UIFileLoader(self, ui_file): ui_file = QFile(ui_file) ui_file.open(QFile.ReadOnly) self.setWidget(uic.loadUi(ui_file)) ui_file.close() @classmethod def openFile(cls, filePath): dw = cls(Document(), Style(), None) dw.loadFile(filePath) return dw def loadFile(self, filePath): """ Opens a file from a file path and sets up the window accordingly. """ self.currentFilePath = filePath 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.updatePreview() self.lastDoc = copy(self.doc) mw.setPath("workingPath", self.currentFilePath) mw.updateChordDict(self.doc) mw.updateSectionDict(self.doc) self.currentSection = (self.doc.sectionList[0] if self.doc.sectionList else None) self.updateTitleBar() def saveFile(self, filePath): """ Saves a file to given file path and sets up environment. """ self.currentFilePath = filePath fileExt = os.path.splitext(self.currentFilePath)[1].lower() if fileExt == ".cma": # At this stage we should never get here pass else: # if fileExt in [".xml", ".cml"]: self.doc.saveXML(self.currentFilePath) self.lastDoc = copy(self.doc) mw.setPath("workingPath", self.currentFilePath) self.updateTitleBar() # as we may have a new filename def saveWarning(self): """ Function to check if the document has unsaved data in it and offer to save it. """ self.updateDocument() # update the document to catch all changes if self.lastDoc == self.doc: return True else: wantToSave = UnsavedMessageBox().exec() if wantToSave == QMessageBox.Save: if not self.currentFilePath: filePath = QFileDialog.getSaveFileName(self.window, 'Save file', str( os.path.expanduser("~")), "Chordsheet ML files (*.xml *.cml)") self.currentFilePath = filePath[0] self.doc.saveXML(self.currentFilePath) return True elif wantToSave == QMessageBox.Discard: return True else: return False def updatePreview(self): """ Update the preview shown by rendering a new PDF and drawing it to the scroll area. """ try: self.currentPreview = self.renderer.stream() except Exception: QMessageBox.warning(self, "Preview failed", "Could not update the preview.", buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok) with open('preview.pdf', 'wb') as f: f.write(self.currentPreview.getbuffer()) self.widget().pdfArea.update_pdf(self.currentPreview) def updateTitleBar(self): """ Update the application's title bar to reflect the current document. """ if self.currentFilePath: self.widget().setWindowTitle(os.path.basename(self.currentFilePath)) else: self.widget().setWindowTitle("Unsaved") def updateChords(self): """ Update the chord list by reading the table. """ chordTableList = [] for i in range(mw.chordsw.widget().chordTableView.model.rowCount()): chordTableList.append( Chord(parseName(mw.chordsw.widget().chordTableView.model.item(i, 0).text()))), if mw.chordsw.widget().chordTableView.model.item(i, 1).text(): chordTableList[-1].voicings['guitar'] = parseFingering( mw.chordsw.widget().chordTableView.model.item(i, 1).text(), 'guitar') if mw.chordsw.widget().chordTableView.model.item(i, 2).text(): chordTableList[-1].voicings['piano'] = parseFingering( mw.chordsw.widget().chordTableView.model.item(i, 2).text(), 'piano') self.doc.chordList = chordTableList 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(mw.sectionsw.widget().sectionTableView.model.rowCount()): sectionTableList.append(self.matchSection( mw.sectionsw.widget().sectionTableView.model.item(i, 0).text())) self.doc.sectionList = sectionTableList def updateBlocks(self, section): """ Update the block list by reading the table. """ if section is None: BlockMustHaveSectionWarningMessageBox().exec() else: blockTableList = [] for i in range(mw.blocksw.widget().blockTableView.model.rowCount()): blockLength = float( mw.blocksw.widget().blockTableView.model.item(i, 1).text()) blockChord = mw.chordDict[(mw.blocksw.widget().blockTableView.model.item( i, 0).text() if mw.blocksw.widget().blockTableView.model.item(i, 0).text() else "None")] blockNotes = mw.blocksw.widget().blockTableView.model.item(i, 2).text( ) if mw.blocksw.widget().blockTableView.model.item(i, 2).text() else None blockTableList.append( Block(blockLength, chord=blockChord, notes=blockNotes)) section.blockList = blockTableList def updateDocument(self): """ Update the Document object by reading values from the UI. """ self.doc.title = mw.docinfo.widget().titleLineEdit.text( ) # Title can be empty string but not None self.doc.subtitle = (mw.docinfo.widget().subtitleLineEdit.text( ) if mw.docinfo.widget().subtitleLineEdit.text() else None) self.doc.composer = (mw.docinfo.widget().composerLineEdit.text( ) if mw.docinfo.widget().composerLineEdit.text() else None) self.doc.arranger = (mw.docinfo.widget().arrangerLineEdit.text( ) if mw.docinfo.widget().arrangerLineEdit.text() else None) self.doc.tempo = (mw.docinfo.widget().tempoLineEdit.text( ) if mw.docinfo.widget().tempoLineEdit.text() else None) self.doc.timeSignature = int(mw.docinfo.widget().timeSignatureSpinBox.value( )) if mw.docinfo.widget().timeSignatureSpinBox.value() else self.doc.timeSignature self.style.pageSize = pageSizeDict[mw.pageSizeSelected] self.style.unit = unitDict[mw.unitSelected] self.style.leftMargin = float(mw.psetup.widget().leftMarginLineEdit.text( )) if mw.psetup.widget().leftMarginLineEdit.text() else self.style.leftMargin self.style.rightMargin = float(mw.psetup.widget().rightMarginLineEdit.text( )) if mw.psetup.widget().rightMarginLineEdit.text() else self.style.rightMargin self.style.topMargin = float(mw.psetup.widget().topMarginLineEdit.text( )) if mw.psetup.widget().topMarginLineEdit.text() else self.style.topMargin self.style.bottomMargin = float(mw.psetup.widget().bottomMarginLineEdit.text( )) if mw.psetup.widget().bottomMarginLineEdit.text() else self.style.bottomMargin self.style.lineSpacing = float(mw.psetup.widget().lineSpacingDoubleSpinBox.value( )) if mw.psetup.widget().lineSpacingDoubleSpinBox.value() else self.style.lineSpacing # make sure the unit width isn't too wide to draw! if mw.psetup.widget().beatWidthLineEdit.text(): if (self.style.pageSize[0] - 2 * self.style.leftMargin * mm) >= (float(mw.psetup.widget().beatWidthLineEdit.text()) * 2 * self.doc.timeSignature * mm): self.style.unitWidth = float( mw.psetup.widget().beatWidthLineEdit.text()) else: maxBeatWidth = ( self.style.pageSize[0] - 2 * self.style.leftMargin * mm) / (2 * self.doc.timeSignature * mm) 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.updateSections() if mw.currentSection: self.updateBlocks(mw.currentSection) self.style.font = ( 'FreeSans' if self.style.useIncludedFont else 'HelveticaNeue') # something for the font box here if __name__ == '__main__': app = QApplication(sys.argv) # pass first argument as filename mw = MainWindow() mw.show() sys.exit(app.exec_())