You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1137 lines
48 KiB

#!/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 xml.etree.ElementTree import ParseError
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, pyqtSignal, QSettings
from PyQt5.QtGui import QPixmap, QImage, QKeySequence, QFontInfo, QFont, QRawFont
from PyQt5 import uic
from csgui.tableView import ChordTableView, BlockTableView
from csgui.comboBox import MComboBox
from csgui.pdfViewer import PDFViewer
from csgui.messageBox import UnsavedMessageBox, UnreadableMessageBox, ChordNameWarningMessageBox, SectionNameWarningMessageBox, BlockMustHaveSectionWarningMessageBox, VoicingWarningMessageBox, LengthWarningMessageBox
from csgui.dialogs import GuitarDialog, AboutDialog
from csgui.panels import DocInfoDockWidget, PageSetupDockWidget, ChordsDockWidget, SectionsDockWidget, BlocksDockWidget, PreviewDockWidget
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
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}
def getPath(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(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))
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', 'mainwindow.ui')))
self.setCentralWidget(self.window.centralWidget)
self.setMenuBar(self.window.menuBar)
self.setWindowTitle("Chordsheet")
self.lastSubWindow = None
self.window.mdiArea.setActivationOrder(self.window.mdiArea.ActivationHistoryOrder)
if filename:
dw = DocumentWindow.openFile(filename)
if dw:
self.window.mdiArea.addSubWindow(dw)
# self.UIInitDocument(dw.doc)
# self.UIInitStyle(dw.style)
# self.currentSection = None
dw.show()
def closeEvent(self, event):
"""
Reimplement the built in closeEvent to allow asking the user to save.
"""
if not self.window.mdiArea.subWindowList():
event.accept()
else:
for subWindow in self.window.mdiArea.subWindowList():
if not subWindow.close():
event.ignore()
return
event.accept()
def UISetupPanels(self):
self.docinfo = DocInfoDockWidget()
self.psetup = PageSetupDockWidget()
self.chordsw = ChordsDockWidget()
self.sectionsw = SectionsDockWidget()
self.blocksw = BlocksDockWidget()
self.previeww = PreviewDockWidget()
self.panels_list = [self.docinfo, self.psetup, self.chordsw, self.sectionsw,
self.blocksw, self.previeww]
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)
self.previeww.hide()
# connect the change signals to handlers
self.docinfo.widget().titleLineEdit.editingFinished.connect(
self.updateTitle)
self.docinfo.widget().subtitleLineEdit.editingFinished.connect(
self.updateSubtitle)
self.docinfo.widget().composerLineEdit.editingFinished.connect(
self.updateComposer)
self.docinfo.widget().arrangerLineEdit.editingFinished.connect(
self.updateArranger)
self.docinfo.widget().timeSignatureSpinBox.valueChanged.connect(
self.updateTimeSignature)
self.docinfo.widget().tempoLineEdit.editingFinished.connect(
self.updateTempo)
self.psetup.widget().pageSizeComboBox.textActivated.connect(
self.updatePageSize)
self.psetup.widget().documentUnitsComboBox.textActivated.connect(
self.updateUnit)
self.psetup.widget().leftMarginLineEdit.editingFinished.connect(
self.updateLeftMargin)
self.psetup.widget().rightMarginLineEdit.editingFinished.connect(
self.updateRightMargin)
self.psetup.widget().topMarginLineEdit.editingFinished.connect(
self.updateTopMargin)
self.psetup.widget().bottomMarginLineEdit.editingFinished.connect(
self.updateBottomMargin)
self.psetup.widget().lineSpacingDoubleSpinBox.valueChanged.connect(
self.updateLineSpacing)
self.psetup.widget().beatWidthLineEdit.editingFinished.connect(
self.updateBeatWidth)
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().autoUpdatePreviewCheckBox.stateChanged.connect(self.autoUpdateEnabledAction)
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)
self.chordsw.widget().chordTableView.model.itemsDropped.connect(self.updateChords)
# 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.blocksw.widget().blockTableView.model.itemsDropped.connect(self.updateBlocksCurrentSection)
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.sectionsw.widget().sectionTableView.model.itemsDropped.connect(self.updateSections)
self.chordsw.widget().chordTableView.clicked.connect(self.chordClickedAction)
self.sectionsw.widget().sectionTableView.clicked.connect(self.sectionClickedAction)
self.blocksw.widget().blockTableView.clicked.connect(self.blockClickedAction)
self.disablePanels()
def unfloatPanels(self):
for panel in self.panels_list:
panel.setFloating(False)
def disablePanels(self):
for panel in self.panels_list:
panel.setEnabled(False)
panel.clear()
def enablePanels(self):
for panel in self.panels_list:
panel.setEnabled(True)
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.csw = self.window.mdiArea.currentSubWindow
self.UISetupPanels()
self.disableMenuItems()
# link all the UI elements
self.window.mdiArea.subWindowActivated.connect(self.switchDocument)
self.window.actionAbout.triggered.connect(self.menuHelpAboutAction)
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.actionQuit.triggered.connect(self.menuFileQuitAction)
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.actionCascadeSubWindows.triggered.connect(self.cascadeSubWindowsAction)
self.window.actionTileSubWindows.triggered.connect(self.tileSubWindowsAction)
self.window.actionActivateNextSubWindow.triggered.connect(self.activatePreviousSubWindowAction)
self.window.actionActivatePreviousSubWindow.triggered.connect(self.activateNextSubWindowAction)
self.window.actionRedockAllPanels.triggered.connect(self.unfloatPanels)
self.window.menuWindow.addSeparator()
self.window.menuWindow.addAction(self.docinfo.toggleViewAction())
self.window.menuWindow.addAction(self.psetup.toggleViewAction())
self.window.menuWindow.addAction(self.chordsw.toggleViewAction())
self.window.menuWindow.addAction(self.sectionsw.toggleViewAction())
self.window.menuWindow.addAction(self.blocksw.toggleViewAction())
self.window.menuWindow.addSeparator()
self.window.menuWindow.addAction(self.previeww.toggleViewAction())
# Set the tab widget to Overview tab
# self.window.tabWidget.setCurrentIndex(0)
def enableMenuItems(self):
self.window.actionSave.setEnabled(True)
self.window.actionSave_as.setEnabled(True)
self.window.actionSave_PDF.setEnabled(True)
# Comment this because we can't print
# self.window.actionPrint.setEnabled(True)
self.window.actionClose.setEnabled(True)
self.window.actionUndo.setEnabled(True)
self.window.actionRedo.setEnabled(True)
self.window.actionCut.setEnabled(True)
self.window.actionCopy.setEnabled(True)
self.window.actionPaste.setEnabled(True)
def disableMenuItems(self):
self.window.actionSave.setEnabled(False)
self.window.actionSave_as.setEnabled(False)
self.window.actionSave_PDF.setEnabled(False)
self.window.actionPrint.setEnabled(False)
self.window.actionClose.setEnabled(False)
self.window.actionUndo.setEnabled(False)
self.window.actionRedo.setEnabled(False)
self.window.actionCut.setEnabled(False)
self.window.actionCopy.setEnabled(False)
self.window.actionPaste.setEnabled(False)
def UIInitDocument(self):
"""
Fills the window's fields with the values from its document.
"""
doc = self.csw().doc
# 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.csw().updateChordDict()
self.csw().updateSectionDict()
self.updateBlockChordComboBox()
self.updateBlockSectionComboBox()
# populate the block table with the first section, account for a document with no sections
if self.csw().currentSection is None:
self.csw().currentSection = doc.sectionList[0] if doc.sectionList else None
else:
self.blocksw.widget().blockSectionComboBox.setCurrentText(
self.csw().currentSection.name)
self.blocksw.widget().blockTableView.populate(
self.csw().currentSection.blockList if self.csw().currentSection else [])
def UIInitStyle(self):
"""
Fills the window's fields with the values from its style.
"""
style = self.csw().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 fontChangeAction(self):
# if self.csw() is not None:
qFont = self.psetup.widget().fontComboBox.currentFont()
qFontReal = QRawFont.fromFont(qFont)
#fontInfo = QFontInfo(qFont)
#qFontReal = QRawFont(fontInfo.family())
print(qFont.rawName())
def autoUpdateEnabledAction(self, state):
self.previeww.widget().updatePreviewButton.setEnabled(not state)
def switchDocument(self, curWindow):
if curWindow is not None:
self.enablePanels()
self.enableMenuItems()
self.UIInitDocument()
self.UIInitStyle()
if self.csw().currentSection is not None:
self.blocksw.widget().blockSectionComboBox.setCurrentText(
self.csw().currentSection.name)
self.lastSubWindow = curWindow
else:
self.disablePanels()
self.disableMenuItems()
def updateBlockChordComboBox(self):
self.blocksw.widget().blockChordComboBox.clear()
self.blocksw.widget().blockChordComboBox.addItems(list(self.csw().chordDict.keys()))
def updateBlockSectionComboBox(self):
self.blocksw.widget().blockSectionComboBox.clear()
self.blocksw.widget().blockSectionComboBox.addItems(list(self.csw().sectionDict.keys()))
def updateTitle(self):
self.csw().doc.title = self.docinfo.widget().titleLineEdit.text()
# Title can be empty string but not None
self.autoUpdatePreview()
def updateSubtitle(self):
self.csw().doc.subtitle = (self.docinfo.widget().subtitleLineEdit.text() \
if self.docinfo.widget().subtitleLineEdit.text() else None)
self.autoUpdatePreview()
def updateComposer(self):
self.csw().doc.composer = (self.docinfo.widget().composerLineEdit.text() \
if self.docinfo.widget().composerLineEdit.text() else None)
self.autoUpdatePreview()
def updateArranger(self):
self.csw().doc.arranger = (self.docinfo.widget().arrangerLineEdit.text() \
if self.docinfo.widget().arrangerLineEdit.text() else None)
self.autoUpdatePreview()
def updateTempo(self):
self.csw().doc.tempo = (self.docinfo.widget().tempoLineEdit.text() \
if self.docinfo.widget().tempoLineEdit.text() else None)
self.autoUpdatePreview()
def updateTimeSignature(self):
if self.csw():
self.csw().doc.timeSignature = (int(self.docinfo.widget().timeSignatureSpinBox.value()) if self.docinfo.widget().timeSignatureSpinBox.value() else self.doc.timeSignature)
self.autoUpdatePreview()
def updatePageSize(self, index):
if self.csw():
self.csw().style.pageSize = pageSizeDict[self.psetup.widget().pageSizeComboBox.currentText()]
self.autoUpdatePreview()
def updateUnit(self, index):
self.csw().style.unit = unitDict[self.psetup.widget().documentUnitsComboBox.currentText()]
self.autoUpdatePreview()
def updateLeftMargin(self):
self.csw().style.leftMargin = float(self.psetup.widget().leftMarginLineEdit.text(
)) if self.psetup.widget().leftMarginLineEdit.text() else self.csw().style.leftMargin
self.autoUpdatePreview()
def updateRightMargin(self):
self.csw().style.rightMargin = float(self.psetup.widget().rightMarginLineEdit.text(
)) if self.psetup.widget().rightMarginLineEdit.text() else self.csw().style.rightMargin
self.autoUpdatePreview()
def updateTopMargin(self):
self.csw().style.topMargin = float(self.psetup.widget().topMarginLineEdit.text(
)) if self.psetup.widget().topMarginLineEdit.text() else self.csw().style.topMargin
self.autoUpdatePreview()
def updateBottomMargin(self):
self.csw().style.bottomMargin = float(self.psetup.widget().bottomMarginLineEdit.text(
)) if self.psetup.widget().bottomMarginLineEdit.text() else self.csw().style.bottomMargin
self.autoUpdatePreview()
def updateLineSpacing(self):
if self.csw():
self.csw().style.lineSpacing = float(self.psetup.widget().lineSpacingDoubleSpinBox.value()) if self.psetup.widget().lineSpacingDoubleSpinBox.value() else self.csw().style.lineSpacing
self.autoUpdatePreview()
def updateBeatWidth(self):
if self.psetup.widget().beatWidthLineEdit.text():
if (self.csw().style.pageSize[0] - 2 * self.csw().style.leftMargin * mm) >= (float(self.psetup.widget().beatWidthLineEdit.text()) * 2 * self.csw().doc.timeSignature * mm):
self.csw().style.unitWidth = float(
self.psetup.widget().beatWidthLineEdit.text())
else:
maxBeatWidth = (
self.csw().style.pageSize[0] - 2 * self.csw().style.leftMargin * mm) / (2 * self.csw().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)
self.autoUpdatePreview()
def updateChords(self):
"""
Update the chord list by reading the table.
"""
chordTableList = []
for i in range(self.chordsw.widget().chordTableView.model.rowCount()):
chordTableList.append(
Chord(parseName(self.chordsw.widget().chordTableView.model.item(i, 0).text()))),
if self.chordsw.widget().chordTableView.model.item(i, 1).text():
chordTableList[-1].voicings['guitar'] = parseFingering(
self.chordsw.widget().chordTableView.model.item(i, 1).text(), 'guitar')
if self.chordsw.widget().chordTableView.model.item(i, 2).text():
chordTableList[-1].voicings['piano'] = parseFingering(
self.chordsw.widget().chordTableView.model.item(i, 2).text(), 'piano')
if self.csw():
self.csw().doc.chordList = chordTableList
self.autoUpdatePreview()
def updateSections(self):
"""
Update the section list by reading the table
"""
sectionTableList = []
for i in range(self.sectionsw.widget().sectionTableView.model.rowCount()):
sectionTableList.append(self.csw().matchSection(
self.sectionsw.widget().sectionTableView.model.item(i, 0).text()))
self.csw().doc.sectionList = sectionTableList
self.autoUpdatePreview()
def updateBlocks(self, section):
"""
Update the block list by reading the table.
"""
if section is None:
BlockMustHaveSectionWarningMessageBox().exec()
else:
blockTableList = []
for i in range(self.blocksw.widget().blockTableView.model.rowCount()):
blockLength = float(
self.blocksw.widget().blockTableView.model.item(i, 1).text())
blockChord = self.csw().chordDict[(self.blocksw.widget().blockTableView.model.item(
i, 0).text() if self.blocksw.widget().blockTableView.model.item(i, 0).text() else "None")]
blockNotes = self.blocksw.widget().blockTableView.model.item(i, 2).text(
) if self.blocksw.widget().blockTableView.model.item(i, 2).text() else None
blockTableList.append(
Block(blockLength, chord=blockChord, notes=blockNotes))
section.blockList = blockTableList
self.autoUpdatePreview()
def updateBlocksCurrentSection(self):
if self.csw():
section = self.csw().currentSection
else:
section = None
self.updateBlocks(section)
def autoUpdatePreview(self):
if self.previeww.widget().autoUpdatePreviewCheckBox.isChecked():
self.csw().updatePreview()
def cascadeSubWindowsAction(self):
self.window.mdiArea.cascadeSubWindows()
def tileSubWindowsAction(self):
self.window.mdiArea.tileSubWindows()
def activatePreviousSubWindowAction(self):
self.window.mdiArea.activatePreviousSubWindow()
def activateNextSubWindowAction(self):
self.window.mdiArea.activateNextSubWindow()
def generateAction(self):
# if self.csw() is not None:
# self.csw().updateDocument()
self.csw().updatePreview()
def removeChordAction(self):
if self.chordsw.widget().chordTableView.selectionModel().hasSelection(): #  check for selection
self.updateChords()
row = self.chordsw.widget().chordTableView.selectionModel().currentIndex().row()
oldName = self.chordsw.widget().chordTableView.model.item(row, 0).text()
self.csw().doc.chordList.pop(row)
self.chordsw.widget().chordTableView.populate(self.csw().doc.chordList)
# remove the chord if any of the blocks have it attached
if self.csw().currentSection is not None:
for s in self.csw().doc.sectionList:
for b in s.blockList:
if b.chord:
if b.chord.name == oldName:
b.chord = None
self.blocksw.widget().blockTableView.populate(self.csw().currentSection.blockList)
self.clearChordLineEdits()
self.csw().updateChordDict()
self.updateBlockChordComboBox()
self.autoUpdatePreview()
def addChordAction(self):
success = False # initialise
self.updateChords()
cName = parseName(self.chordsw.widget().chordNameLineEdit.text())
if cName:
self.csw().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.csw().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.csw().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.csw().doc.chordList)
self.clearChordLineEdits()
self.csw().updateChordDict()
self.updateBlockChordComboBox()
self.autoUpdatePreview()
def updateChordAction(self):
success = False # see comments above
if self.chordsw.widget().chordTableView.selectionModel().hasSelection(): #  check for selection
self.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.csw().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.csw().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.csw().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.csw().updateChordDict()
self.updateBlockChordComboBox()
self.chordsw.widget().chordTableView.populate(self.csw().doc.chordList)
# update the names of chords in all blocklists in case they've already been used
for s in self.csw().doc.sectionList:
for b in s.blockList:
if b.chord:
if b.chord.name == oldName:
b.chord.name = cName
if self.csw().currentSection and self.csw().currentSection.blockList:
self.blocksw.widget().blockTableView.populate(self.csw().currentSection.blockList)
self.clearChordLineEdits()
self.autoUpdatePreview()
def removeSectionAction(self):
if self.sectionsw.widget().sectionTableView.selectionModel().hasSelection(): #  check for selection
self.updateSections()
row = self.sectionsw.widget().sectionTableView.selectionModel().currentIndex().row()
self.csw().doc.sectionList.pop(row)
self.sectionsw.widget().sectionTableView.populate(self.csw().doc.sectionList)
self.clearSectionLineEdits()
self.csw().updateSectionDict()
self.updateBlockSectionComboBox()
self.autoUpdatePreview()
def addSectionAction(self):
self.updateSections()
sName = self.sectionsw.widget().sectionNameLineEdit.text()
if sName and sName not in [s.name for s in self.csw().doc.sectionList]:
self.csw().doc.sectionList.append(Section(name=sName))
self.sectionsw.widget().sectionTableView.populate(self.csw().doc.sectionList)
self.clearSectionLineEdits()
self.csw().updateSectionDict()
self.updateBlockSectionComboBox()
self.autoUpdatePreview()
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.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.csw().doc.sectionList]:
self.csw().doc.sectionList[row].name = sName
self.sectionsw.widget().sectionTableView.populate(self.csw().doc.sectionList)
self.clearSectionLineEdits()
self.csw().updateSectionDict()
self.updateBlockSectionComboBox()
self.autoUpdatePreview()
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.updateBlocks(self.csw().currentSection)
row = self.blocksw.widget().blockTableView.selectionModel().currentIndex().row()
self.csw().currentSection.blockList.pop(row)
self.blocksw.widget().blockTableView.populate(self.csw().currentSection.blockList)
self.autoUpdatePreview()
def addBlockAction(self):
self.updateBlocks(self.csw().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.csw().currentSection.blockList.append(Block(bLength,
chord=self.csw().chordDict[self.blocksw.widget().blockChordComboBox.currentText(
)],
notes=(self.blocksw.widget().blockNotesLineEdit.text() if not "" else None)))
self.blocksw.widget().blockTableView.populate(self.csw().currentSection.blockList)
self.clearBlockLineEdits()
self.autoUpdatePreview()
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.updateBlocks(self.csw().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.csw().currentSection.blockList[row] = (Block(bLength,
chord=self.csw().chordDict[self.blocksw.widget().blockChordComboBox.currentText(
)],
notes=(self.blocksw.widget().blockNotesLineEdit.text() if not "" else None)))
self.blocksw.widget().blockTableView.populate(
self.csw().currentSection.blockList)
self.clearBlockLineEdits()
self.autoUpdatePreview()
else:
LengthWarningMessageBox().exec()
def includedFontAction(self):
# if self.csw() is not None:
if self.psetup.widget().includedFontCheckBox.isChecked():
self.csw().style.font = 'FreeSans'
else:
self.csw().style.font = 'HelveticaNeue'
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.updateBlocks(self.csw().sectionDict[text])
def blockSectionChangedAction(self, index):
sName = self.blocksw.widget().blockSectionComboBox.currentText()
if sName:
# if self.csw() is not None:
self.csw().currentSection = self.csw().sectionDict.get(sName, None)
# self.csw().currentSection = self.currentSection
if self.csw().currentSection is not None:
self.blocksw.widget().blockTableView.populate(self.csw().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 menuFileNewAction(self):
dw = DocumentWindow(Document(), Style(), None)
self.window.mdiArea.addSubWindow(dw)
dw.show()
def menuFileOpenAction(self):
filePath = QFileDialog.getOpenFileName(self.window, 'Open file', getPath(
"workingPath"), "Chordsheet Markup Language files (*.cml *.xml);;Chordsheet Macro files (*.cma)")[0]
if filePath:
dw = DocumentWindow.openFile(filePath)
if dw:
self.window.mdiArea.addSubWindow(dw)
dw.show()
def menuFileSaveAction(self):
pathRequired = False
if self.csw().currentFilePath:
fileExt = os.path.splitext(self.csw().currentFilePath)[1].lower()
if fileExt != ".cma":
# Chordsheet Macro files can't be saved at this time
self.csw().saveFile(self.csw().currentFilePath)
else:
pathRequired = True
else:
pathRequired = True
if pathRequired:
self.csw().saveFileChoosePath()
def menuFileSaveAsAction(self):
self.csw().saveFileChoosePath()
def menuFileSavePDFAction(self):
filePath = self.csw().savePDFFileChoosePath()
if filePath:
setPath("lastExportPath", filePath)
def menuFilePrintAction(self):
if sys.platform == "darwin":
pass
# subprocess.call()
else:
pass
def menuFileCloseAction(self):
self.csw().close()
def menuFileQuitAction(self):
self.close()
def menuHelpAboutAction(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
def guitarVoicingAction(self):
gdialog = GuitarDialog()
if self.chordsw.widget().guitarVoicingLineEdit.text():
existingVoicing = parseFingering(
self.chordsw.widget().guitarVoicingLineEdit.text(), 'guitar'
)
else:
existingVoicing = None
voicing = gdialog.getVoicing(existingVoicing)
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')))
# Set to delete on close
self.setAttribute(Qt.WA_DeleteOnClose)
self.doc = doc
self.style = style
self.renderer = Renderer(self.doc, self.style)
self.lastHash = self.doc.getHash()
self.currentFilePath = filename
self.currentSection = None
self.updateChordDict()
self.updateSectionDict()
def UIFileLoader(self, ui_file):
ui_file = QFile(ui_file)
ui_file.open(QFile.ReadOnly)
self.setWidget(uic.loadUi(ui_file))
ui_file.close()
def closeEvent(self, event):
if self.saveWarning():
event.accept()
else:
event.ignore()
@classmethod
def openFile(cls, filePath):
"""
Opens a file from a file path and sets up the window accordingly.
"""
dw = cls(Document(), Style(), None)
dw.currentFilePath = filePath
fileExt = os.path.splitext(dw.currentFilePath)[1].lower()
try:
if fileExt == ".cma":
dw.doc.loadCSMacro(dw.currentFilePath)
else: # if fileExt in [".xml", ".cml"]:
dw.doc.loadXML(dw.currentFilePath)
except (ParseError, ValueError):
UnreadableMessageBox(os.path.basename(dw.currentFilePath)).exec()
dw.close()
return None
else:
dw.updatePreview()
setPath("workingPath", dw.currentFilePath)
dw.updateChordDict()
dw.updateSectionDict()
dw.currentSection = (dw.doc.sectionList[0] if dw.doc.sectionList else None)
dw.updateTitleBar()
dw.lastHash = dw.doc.getHash()
return dw
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.lastHash = self.doc.saveXML(self.currentFilePath)
setPath("workingPath", self.currentFilePath)
self.updateTitleBar() # as we may have a new filename
def saveFileChoosePath(self):
filePath, selectedFilter = QFileDialog.getSaveFileName(self.parent(), 'Save file', getPath(
"workingPath"), "Chordsheet ML files (*.cml *.xml)", ".cml")
if filePath:
if filePath.split(".")[-1] not in ["cml", "xml"]:
filePath += ".cml"
self.saveFile(filePath)
return filePath
else:
return None
def savePDFFileChoosePath(self):
filePath, selectedFilter = QFileDialog.getSaveFileName(self.parent(), 'Save file', getPath(
"lastExportPath"), "PDF files (*.pdf)")
if filePath:
if filePath.split(".")[-1] != "pdf":
filePath += ".pdf"
self.renderer.savePDF(filePath)
return filePath
else:
return None
def saveWarning(self):
"""
Function to check if the document has unsaved data in it and offer to save it.
"""
if self.lastHash == self.doc.getHash():
return True
else:
if self.currentFilePath:
fileName = os.path.basename(self.currentFilePath)
else:
fileName = "Unsaved"
wantToSave = UnsavedMessageBox(fileName).exec()
if wantToSave == QMessageBox.Save:
if not self.currentFilePath:
self.saveFileChoosePath()
else:
self.saveFile(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)
self.widget().pdfArea.update_pdf(self.currentPreview)
def updateTitleBar(self):
"""
Update the window'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 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 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})
def updateSectionDict(self):
"""
Updates the dictionary used to generate the Section menu (on the block tab)
"""
self.sectionDict = {s.name: s for s in self.doc.sectionList}
if __name__ == '__main__':
app = QApplication(sys.argv)
# pass first argument as filename
mw = MainWindow()
mw.show()
sys.exit(app.exec_())