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.

1132 lines
48 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Created on Wed May 29 00:02:24 2019
  5. @author: ivan
  6. """
  7. import sys
  8. import fitz
  9. import io
  10. import subprocess
  11. import os
  12. import time
  13. from copy import copy
  14. from xml.etree.ElementTree import ParseError
  15. 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
  16. from PyQt5.QtCore import QFile, QObject, Qt, pyqtSlot, pyqtSignal, QSettings
  17. from PyQt5.QtGui import QPixmap, QImage, QKeySequence, QFontInfo, QFont, QRawFont
  18. from PyQt5 import uic
  19. from csgui.tableView import ChordTableView, BlockTableView
  20. from csgui.comboBox import MComboBox
  21. from csgui.pdfViewer import PDFViewer
  22. from csgui.messageBox import UnsavedMessageBox, UnreadableMessageBox, ChordNameWarningMessageBox, SectionNameWarningMessageBox, BlockMustHaveSectionWarningMessageBox, VoicingWarningMessageBox, LengthWarningMessageBox
  23. from csgui.dialogs import GuitarDialog, AboutDialog
  24. from csgui.panels import DocInfoDockWidget, PageSetupDockWidget, ChordsDockWidget, SectionsDockWidget, BlocksDockWidget, PreviewDockWidget
  25. from reportlab.lib.units import mm, cm, inch, pica
  26. from reportlab.lib.pagesizes import A4, A5, LETTER, LEGAL
  27. from reportlab.pdfbase import pdfmetrics
  28. from reportlab.pdfbase.ttfonts import TTFont
  29. from chordsheet.common import scriptDir
  30. from chordsheet.document import Document, Style, Chord, Block, Section
  31. from chordsheet.render import Renderer
  32. from chordsheet.parsers import parseFingering, parseName
  33. import _version
  34. # enable automatic high DPI scaling on Windows
  35. QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
  36. QApplication.setOrganizationName("Ivan Holmes")
  37. QApplication.setOrganizationDomain("ivanholmes.co.uk")
  38. QApplication.setApplicationName("Chordsheet")
  39. settings = QSettings()
  40. pdfmetrics.registerFont(
  41. TTFont('FreeSans', os.path.join(scriptDir, 'fonts', 'FreeSans.ttf')))
  42. if sys.platform == "darwin":
  43. pdfmetrics.registerFont(
  44. TTFont('HelveticaNeue', 'HelveticaNeue.ttc', subfontIndex=0))
  45. # dictionaries for combo boxes
  46. pageSizeDict = {'A4': A4, 'A5': A5, 'Letter': LETTER, 'Legal': LEGAL}
  47. # point is 1 because reportlab's native unit is points.
  48. unitDict = {'mm': mm, 'cm': cm, 'inch': inch, 'point': 1, 'pica': pica}
  49. def getPath(value):
  50. """
  51. Wrapper for Qt settings to return home directory if no setting exists.
  52. """
  53. return str((settings.value(value) if settings.value(value) else os.path.expanduser("~")))
  54. def setPath(value, fullpath):
  55. """
  56. Wrapper for Qt settings to set path to open/save from next time from current file location.
  57. """
  58. return settings.setValue(value, os.path.dirname(fullpath))
  59. class MainWindow(QMainWindow):
  60. """
  61. Class for the main window of the application.
  62. """
  63. def __init__(self, filename=None):
  64. """
  65. Initialisation function for the main window of the application.
  66. Arguments:
  67. doc -- the Document object for the window to use
  68. style -- the Style object for the window to use
  69. """
  70. super().__init__()
  71. self.UIFileLoader(str(os.path.join(scriptDir, 'ui', 'mainwindow.ui')))
  72. self.setCentralWidget(self.window.centralWidget)
  73. self.setMenuBar(self.window.menuBar)
  74. self.setWindowTitle("Chordsheet")
  75. self.lastSubWindow = None
  76. self.window.mdiArea.setActivationOrder(self.window.mdiArea.ActivationHistoryOrder)
  77. if filename:
  78. dw = DocumentWindow.openFile(filename)
  79. if dw:
  80. self.window.mdiArea.addSubWindow(dw)
  81. # self.UIInitDocument(dw.doc)
  82. # self.UIInitStyle(dw.style)
  83. # self.currentSection = None
  84. dw.show()
  85. def closeEvent(self, event):
  86. """
  87. Reimplement the built in closeEvent to allow asking the user to save.
  88. """
  89. if not self.window.mdiArea.subWindowList():
  90. event.accept()
  91. else:
  92. for subWindow in self.window.mdiArea.subWindowList():
  93. if not subWindow.close():
  94. event.ignore()
  95. return
  96. event.accept()
  97. def UISetupPanels(self):
  98. self.docinfo = DocInfoDockWidget()
  99. self.psetup = PageSetupDockWidget()
  100. self.chordsw = ChordsDockWidget()
  101. self.sectionsw = SectionsDockWidget()
  102. self.blocksw = BlocksDockWidget()
  103. self.previeww = PreviewDockWidget()
  104. self.panels_list = [self.docinfo, self.psetup, self.chordsw, self.sectionsw,
  105. self.blocksw, self.previeww]
  106. self.addDockWidget(Qt.LeftDockWidgetArea, self.docinfo)
  107. self.addDockWidget(Qt.LeftDockWidgetArea, self.psetup)
  108. self.addDockWidget(Qt.LeftDockWidgetArea, self.previeww)
  109. self.addDockWidget(Qt.RightDockWidgetArea, self.chordsw)
  110. self.addDockWidget(Qt.RightDockWidgetArea, self.sectionsw)
  111. self.addDockWidget(Qt.RightDockWidgetArea, self.blocksw)
  112. self.previeww.hide()
  113. # connect the change signals to handlers
  114. self.docinfo.widget().titleLineEdit.editingFinished.connect(
  115. self.updateTitle)
  116. self.docinfo.widget().subtitleLineEdit.editingFinished.connect(
  117. self.updateSubtitle)
  118. self.docinfo.widget().composerLineEdit.editingFinished.connect(
  119. self.updateComposer)
  120. self.docinfo.widget().arrangerLineEdit.editingFinished.connect(
  121. self.updateArranger)
  122. self.docinfo.widget().timeSignatureSpinBox.valueChanged.connect(
  123. self.updateTimeSignature)
  124. self.docinfo.widget().tempoLineEdit.editingFinished.connect(
  125. self.updateTempo)
  126. self.psetup.widget().pageSizeComboBox.textActivated.connect(
  127. self.updatePageSize)
  128. self.psetup.widget().documentUnitsComboBox.textActivated.connect(
  129. self.updateUnit)
  130. self.psetup.widget().leftMarginLineEdit.editingFinished.connect(
  131. self.updateLeftMargin)
  132. self.psetup.widget().rightMarginLineEdit.editingFinished.connect(
  133. self.updateRightMargin)
  134. self.psetup.widget().topMarginLineEdit.editingFinished.connect(
  135. self.updateTopMargin)
  136. self.psetup.widget().bottomMarginLineEdit.editingFinished.connect(
  137. self.updateBottomMargin)
  138. self.psetup.widget().lineSpacingDoubleSpinBox.valueChanged.connect(
  139. self.updateLineSpacing)
  140. self.psetup.widget().beatWidthLineEdit.editingFinished.connect(
  141. self.updateBeatWidth)
  142. self.psetup.widget().pageSizeComboBox.addItems(list(pageSizeDict.keys()))
  143. self.psetup.widget().pageSizeComboBox.setCurrentText(
  144. list(pageSizeDict.keys())[0])
  145. self.psetup.widget().documentUnitsComboBox.addItems(list(unitDict.keys()))
  146. self.psetup.widget().documentUnitsComboBox.setCurrentText(
  147. list(unitDict.keys())[0])
  148. self.psetup.widget().fontComboBox.currentFontChanged.connect(self.fontChangeAction)
  149. self.psetup.widget().includedFontCheckBox.stateChanged.connect(
  150. self.includedFontAction)
  151. self.previeww.widget().autoUpdatePreviewCheckBox.stateChanged.connect(self.autoUpdateEnabledAction)
  152. self.previeww.widget().updatePreviewButton.clicked.connect(self.generateAction)
  153. # update whole document when any tab is selected
  154. # self.window.tabWidget.tabBarClicked.connect(self.tabBarUpdateAction)
  155. self.chordsw.widget().guitarVoicingButton.clicked.connect(
  156. self.guitarVoicingAction)
  157. self.chordsw.widget().addChordButton.clicked.connect(self.addChordAction)
  158. self.chordsw.widget().removeChordButton.clicked.connect(self.removeChordAction)
  159. self.chordsw.widget().updateChordButton.clicked.connect(self.updateChordAction)
  160. self.chordsw.widget().chordTableView.model.itemsDropped.connect(self.updateChords)
  161. # connecting clicked only works for this combo box because it's my own modified version (MComboBox)
  162. self.blocksw.widget().blockSectionComboBox.clicked.connect(
  163. self.blockSectionClickedAction)
  164. self.blocksw.widget().blockSectionComboBox.currentIndexChanged.connect(
  165. self.blockSectionChangedAction)
  166. self.blocksw.widget().addBlockButton.clicked.connect(self.addBlockAction)
  167. self.blocksw.widget().removeBlockButton.clicked.connect(self.removeBlockAction)
  168. self.blocksw.widget().updateBlockButton.clicked.connect(self.updateBlockAction)
  169. self.blocksw.widget().blockTableView.model.itemsDropped.connect(self.updateBlocksCurrentSection)
  170. self.sectionsw.widget().addSectionButton.clicked.connect(self.addSectionAction)
  171. self.sectionsw.widget().removeSectionButton.clicked.connect(
  172. self.removeSectionAction)
  173. self.sectionsw.widget().updateSectionButton.clicked.connect(
  174. self.updateSectionAction)
  175. self.sectionsw.widget().sectionTableView.model.itemsDropped.connect(self.updateSections)
  176. self.chordsw.widget().chordTableView.clicked.connect(self.chordClickedAction)
  177. self.sectionsw.widget().sectionTableView.clicked.connect(self.sectionClickedAction)
  178. self.blocksw.widget().blockTableView.clicked.connect(self.blockClickedAction)
  179. self.disablePanels()
  180. def unfloatPanels(self):
  181. for panel in self.panels_list:
  182. panel.setFloating(False)
  183. def disablePanels(self):
  184. for panel in self.panels_list:
  185. panel.setEnabled(False)
  186. panel.clear()
  187. def enablePanels(self):
  188. for panel in self.panels_list:
  189. panel.setEnabled(True)
  190. def UIFileLoader(self, ui_file):
  191. """
  192. Loads the .ui file for this window and connects the UI elements to their actions.
  193. """
  194. ui_file = QFile(ui_file)
  195. ui_file.open(QFile.ReadOnly)
  196. self.window = uic.loadUi(ui_file)
  197. ui_file.close()
  198. self.csw = self.window.mdiArea.currentSubWindow
  199. self.UISetupPanels()
  200. self.disableMenuItems()
  201. # link all the UI elements
  202. self.window.mdiArea.subWindowActivated.connect(self.switchDocument)
  203. self.window.actionAbout.triggered.connect(self.menuHelpAboutAction)
  204. self.window.actionNew.triggered.connect(self.menuFileNewAction)
  205. self.window.actionOpen.triggered.connect(self.menuFileOpenAction)
  206. self.window.actionSave.triggered.connect(self.menuFileSaveAction)
  207. self.window.actionSave_as.triggered.connect(self.menuFileSaveAsAction)
  208. self.window.actionSave_PDF.triggered.connect(
  209. self.menuFileSavePDFAction)
  210. self.window.actionPrint.triggered.connect(self.menuFilePrintAction)
  211. self.window.actionClose.triggered.connect(self.menuFileCloseAction)
  212. self.window.actionQuit.triggered.connect(self.menuFileQuitAction)
  213. self.window.actionUndo.triggered.connect(self.menuEditUndoAction)
  214. self.window.actionRedo.triggered.connect(self.menuEditRedoAction)
  215. self.window.actionCut.triggered.connect(self.menuEditCutAction)
  216. self.window.actionCopy.triggered.connect(self.menuEditCopyAction)
  217. self.window.actionPaste.triggered.connect(self.menuEditPasteAction)
  218. self.window.actionNew.setShortcut(QKeySequence.New)
  219. self.window.actionOpen.setShortcut(QKeySequence.Open)
  220. self.window.actionSave.setShortcut(QKeySequence.Save)
  221. self.window.actionSave_as.setShortcut(QKeySequence.SaveAs)
  222. self.window.actionSave_PDF.setShortcut(QKeySequence("Ctrl+E"))
  223. self.window.actionPrint.setShortcut(QKeySequence.Print)
  224. self.window.actionClose.setShortcut(QKeySequence.Close)
  225. self.window.actionUndo.setShortcut(QKeySequence.Undo)
  226. self.window.actionRedo.setShortcut(QKeySequence.Redo)
  227. self.window.actionCut.setShortcut(QKeySequence.Cut)
  228. self.window.actionCopy.setShortcut(QKeySequence.Copy)
  229. self.window.actionPaste.setShortcut(QKeySequence.Paste)
  230. self.window.actionCascadeSubWindows.triggered.connect(self.cascadeSubWindowsAction)
  231. self.window.actionTileSubWindows.triggered.connect(self.tileSubWindowsAction)
  232. self.window.actionActivateNextSubWindow.triggered.connect(self.activatePreviousSubWindowAction)
  233. self.window.actionActivatePreviousSubWindow.triggered.connect(self.activateNextSubWindowAction)
  234. self.window.actionRedockAllPanels.triggered.connect(self.unfloatPanels)
  235. self.window.menuWindow.addSeparator()
  236. self.window.menuWindow.addAction(self.docinfo.toggleViewAction())
  237. self.window.menuWindow.addAction(self.psetup.toggleViewAction())
  238. self.window.menuWindow.addAction(self.chordsw.toggleViewAction())
  239. self.window.menuWindow.addAction(self.sectionsw.toggleViewAction())
  240. self.window.menuWindow.addAction(self.blocksw.toggleViewAction())
  241. self.window.menuWindow.addSeparator()
  242. self.window.menuWindow.addAction(self.previeww.toggleViewAction())
  243. # Set the tab widget to Overview tab
  244. # self.window.tabWidget.setCurrentIndex(0)
  245. def enableMenuItems(self):
  246. self.window.actionSave.setEnabled(True)
  247. self.window.actionSave_as.setEnabled(True)
  248. self.window.actionSave_PDF.setEnabled(True)
  249. # Comment this because we can't print
  250. # self.window.actionPrint.setEnabled(True)
  251. self.window.actionClose.setEnabled(True)
  252. self.window.actionUndo.setEnabled(True)
  253. self.window.actionRedo.setEnabled(True)
  254. self.window.actionCut.setEnabled(True)
  255. self.window.actionCopy.setEnabled(True)
  256. self.window.actionPaste.setEnabled(True)
  257. def disableMenuItems(self):
  258. self.window.actionSave.setEnabled(False)
  259. self.window.actionSave_as.setEnabled(False)
  260. self.window.actionSave_PDF.setEnabled(False)
  261. self.window.actionPrint.setEnabled(False)
  262. self.window.actionClose.setEnabled(False)
  263. self.window.actionUndo.setEnabled(False)
  264. self.window.actionRedo.setEnabled(False)
  265. self.window.actionCut.setEnabled(False)
  266. self.window.actionCopy.setEnabled(False)
  267. self.window.actionPaste.setEnabled(False)
  268. def UIInitDocument(self):
  269. """
  270. Fills the window's fields with the values from its document.
  271. """
  272. doc = self.csw().doc
  273. # set all fields to appropriate values from document
  274. self.docinfo.widget().titleLineEdit.setText(doc.title)
  275. self.docinfo.widget().subtitleLineEdit.setText(doc.subtitle)
  276. self.docinfo.widget().composerLineEdit.setText(doc.composer)
  277. self.docinfo.widget().arrangerLineEdit.setText(doc.arranger)
  278. self.docinfo.widget().timeSignatureSpinBox.setValue(doc.timeSignature)
  279. self.docinfo.widget().tempoLineEdit.setText(doc.tempo)
  280. self.chordsw.widget().chordTableView.populate(doc.chordList)
  281. self.sectionsw.widget().sectionTableView.populate(doc.sectionList)
  282. self.csw().updateChordDict()
  283. self.csw().updateSectionDict()
  284. self.updateBlockChordComboBox()
  285. self.updateBlockSectionComboBox()
  286. # populate the block table with the first section, account for a document with no sections
  287. if self.csw().currentSection is None:
  288. self.csw().currentSection = doc.sectionList[0] if doc.sectionList else None
  289. else:
  290. self.blocksw.widget().blockSectionComboBox.setCurrentText(
  291. self.csw().currentSection.name)
  292. self.blocksw.widget().blockTableView.populate(
  293. self.csw().currentSection.blockList if self.csw().currentSection else [])
  294. def UIInitStyle(self):
  295. """
  296. Fills the window's fields with the values from its style.
  297. """
  298. style = self.csw().style
  299. self.psetup.widget().pageSizeComboBox.setCurrentText(
  300. [k for k, v in pageSizeDict.items() if v==style.pageSize][0])
  301. self.psetup.widget().documentUnitsComboBox.setCurrentText(
  302. [k for k, v in unitDict.items() if v==style.unit][0])
  303. self.psetup.widget().lineSpacingDoubleSpinBox.setValue(style.lineSpacing)
  304. self.psetup.widget().leftMarginLineEdit.setText(str(style.leftMargin))
  305. self.psetup.widget().rightMarginLineEdit.setText(str(style.rightMargin))
  306. self.psetup.widget().topMarginLineEdit.setText(str(style.topMargin))
  307. self.psetup.widget().bottomMarginLineEdit.setText(str(style.bottomMargin))
  308. self.psetup.widget().fontComboBox.setDisabled(True)
  309. self.psetup.widget().includedFontCheckBox.setChecked(True)
  310. self.psetup.widget().beatWidthLineEdit.setText(str(style.unitWidth))
  311. def fontChangeAction(self):
  312. # if self.csw() is not None:
  313. qFont = self.psetup.widget().fontComboBox.currentFont()
  314. qFontReal = QRawFont.fromFont(qFont)
  315. #fontInfo = QFontInfo(qFont)
  316. #qFontReal = QRawFont(fontInfo.family())
  317. print(qFont.rawName())
  318. def autoUpdateEnabledAction(self, state):
  319. self.previeww.widget().updatePreviewButton.setEnabled(not state)
  320. def switchDocument(self, curWindow):
  321. if curWindow is not None:
  322. self.enablePanels()
  323. self.enableMenuItems()
  324. self.UIInitDocument()
  325. self.UIInitStyle()
  326. if self.csw().currentSection is not None:
  327. self.blocksw.widget().blockSectionComboBox.setCurrentText(
  328. self.csw().currentSection.name)
  329. self.lastSubWindow = curWindow
  330. else:
  331. self.disablePanels()
  332. self.disableMenuItems()
  333. def updateBlockChordComboBox(self):
  334. self.blocksw.widget().blockChordComboBox.clear()
  335. self.blocksw.widget().blockChordComboBox.addItems(list(self.csw().chordDict.keys()))
  336. def updateBlockSectionComboBox(self):
  337. self.blocksw.widget().blockSectionComboBox.clear()
  338. self.blocksw.widget().blockSectionComboBox.addItems(list(self.csw().sectionDict.keys()))
  339. def updateTitle(self):
  340. self.csw().doc.title = self.docinfo.widget().titleLineEdit.text()
  341. # Title can be empty string but not None
  342. self.autoUpdatePreview()
  343. def updateSubtitle(self):
  344. self.csw().doc.subtitle = (self.docinfo.widget().subtitleLineEdit.text() \
  345. if self.docinfo.widget().subtitleLineEdit.text() else None)
  346. self.autoUpdatePreview()
  347. def updateComposer(self):
  348. self.csw().doc.composer = (self.docinfo.widget().composerLineEdit.text() \
  349. if self.docinfo.widget().composerLineEdit.text() else None)
  350. self.autoUpdatePreview()
  351. def updateArranger(self):
  352. self.csw().doc.arranger = (self.docinfo.widget().arrangerLineEdit.text() \
  353. if self.docinfo.widget().arrangerLineEdit.text() else None)
  354. self.autoUpdatePreview()
  355. def updateTempo(self):
  356. self.csw().doc.tempo = (self.docinfo.widget().tempoLineEdit.text() \
  357. if self.docinfo.widget().tempoLineEdit.text() else None)
  358. self.autoUpdatePreview()
  359. def updateTimeSignature(self):
  360. if self.csw():
  361. self.csw().doc.timeSignature = (int(self.docinfo.widget().timeSignatureSpinBox.value()) if self.docinfo.widget().timeSignatureSpinBox.value() else self.doc.timeSignature)
  362. self.autoUpdatePreview()
  363. def updatePageSize(self, index):
  364. if self.csw():
  365. self.csw().style.pageSize = pageSizeDict[self.psetup.widget().pageSizeComboBox.currentText()]
  366. self.autoUpdatePreview()
  367. def updateUnit(self, index):
  368. self.csw().style.unit = unitDict[self.psetup.widget().documentUnitsComboBox.currentText()]
  369. self.autoUpdatePreview()
  370. def updateLeftMargin(self):
  371. self.csw().style.leftMargin = float(self.psetup.widget().leftMarginLineEdit.text(
  372. )) if self.psetup.widget().leftMarginLineEdit.text() else self.csw().style.leftMargin
  373. self.autoUpdatePreview()
  374. def updateRightMargin(self):
  375. self.csw().style.rightMargin = float(self.psetup.widget().rightMarginLineEdit.text(
  376. )) if self.psetup.widget().rightMarginLineEdit.text() else self.csw().style.rightMargin
  377. self.autoUpdatePreview()
  378. def updateTopMargin(self):
  379. self.csw().style.topMargin = float(self.psetup.widget().topMarginLineEdit.text(
  380. )) if self.psetup.widget().topMarginLineEdit.text() else self.csw().style.topMargin
  381. self.autoUpdatePreview()
  382. def updateBottomMargin(self):
  383. self.csw().style.bottomMargin = float(self.psetup.widget().bottomMarginLineEdit.text(
  384. )) if self.psetup.widget().bottomMarginLineEdit.text() else self.csw().style.bottomMargin
  385. self.autoUpdatePreview()
  386. def updateLineSpacing(self):
  387. if self.csw():
  388. self.csw().style.lineSpacing = float(self.psetup.widget().lineSpacingDoubleSpinBox.value()) if self.psetup.widget().lineSpacingDoubleSpinBox.value() else self.csw().style.lineSpacing
  389. self.autoUpdatePreview()
  390. def updateBeatWidth(self):
  391. if self.psetup.widget().beatWidthLineEdit.text():
  392. if (self.csw().style.pageSize[0] - 2 * self.csw().style.leftMargin * mm) >= (float(self.psetup.widget().beatWidthLineEdit.text()) * 2 * self.csw().doc.timeSignature * mm):
  393. self.csw().style.unitWidth = float(
  394. self.psetup.widget().beatWidthLineEdit.text())
  395. else:
  396. maxBeatWidth = (
  397. self.csw().style.pageSize[0] - 2 * self.csw().style.leftMargin * mm) / (2 * self.csw().doc.timeSignature * mm)
  398. QMessageBox.warning(self, "Out of range", "Beat width is out of range. It can be a maximum of {}.".format(
  399. maxBeatWidth), buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok)
  400. self.autoUpdatePreview()
  401. def updateChords(self):
  402. """
  403. Update the chord list by reading the table.
  404. """
  405. chordTableList = []
  406. for i in range(self.chordsw.widget().chordTableView.model.rowCount()):
  407. chordTableList.append(
  408. Chord(parseName(self.chordsw.widget().chordTableView.model.item(i, 0).text()))),
  409. if self.chordsw.widget().chordTableView.model.item(i, 1).text():
  410. chordTableList[-1].voicings['guitar'] = parseFingering(
  411. self.chordsw.widget().chordTableView.model.item(i, 1).text(), 'guitar')
  412. if self.chordsw.widget().chordTableView.model.item(i, 2).text():
  413. chordTableList[-1].voicings['piano'] = parseFingering(
  414. self.chordsw.widget().chordTableView.model.item(i, 2).text(), 'piano')
  415. if self.csw():
  416. self.csw().doc.chordList = chordTableList
  417. self.autoUpdatePreview()
  418. def updateSections(self):
  419. """
  420. Update the section list by reading the table
  421. """
  422. sectionTableList = []
  423. for i in range(self.sectionsw.widget().sectionTableView.model.rowCount()):
  424. sectionTableList.append(self.csw().matchSection(
  425. self.sectionsw.widget().sectionTableView.model.item(i, 0).text()))
  426. self.csw().doc.sectionList = sectionTableList
  427. self.autoUpdatePreview()
  428. def updateBlocks(self, section):
  429. """
  430. Update the block list by reading the table.
  431. """
  432. if section is None:
  433. BlockMustHaveSectionWarningMessageBox().exec()
  434. else:
  435. blockTableList = []
  436. for i in range(self.blocksw.widget().blockTableView.model.rowCount()):
  437. blockLength = float(
  438. self.blocksw.widget().blockTableView.model.item(i, 1).text())
  439. blockChord = self.csw().chordDict[(self.blocksw.widget().blockTableView.model.item(
  440. i, 0).text() if self.blocksw.widget().blockTableView.model.item(i, 0).text() else "None")]
  441. blockNotes = self.blocksw.widget().blockTableView.model.item(i, 2).text(
  442. ) if self.blocksw.widget().blockTableView.model.item(i, 2).text() else None
  443. blockTableList.append(
  444. Block(blockLength, chord=blockChord, notes=blockNotes))
  445. section.blockList = blockTableList
  446. self.autoUpdatePreview()
  447. def updateBlocksCurrentSection(self):
  448. if self.csw():
  449. section = self.csw().currentSection
  450. else:
  451. section = None
  452. self.updateBlocks(section)
  453. def autoUpdatePreview(self):
  454. if self.previeww.widget().autoUpdatePreviewCheckBox.isChecked():
  455. self.csw().updatePreview()
  456. def cascadeSubWindowsAction(self):
  457. self.window.mdiArea.cascadeSubWindows()
  458. def tileSubWindowsAction(self):
  459. self.window.mdiArea.tileSubWindows()
  460. def activatePreviousSubWindowAction(self):
  461. self.window.mdiArea.activatePreviousSubWindow()
  462. def activateNextSubWindowAction(self):
  463. self.window.mdiArea.activateNextSubWindow()
  464. def generateAction(self):
  465. # if self.csw() is not None:
  466. # self.csw().updateDocument()
  467. self.csw().updatePreview()
  468. def removeChordAction(self):
  469. if self.chordsw.widget().chordTableView.selectionModel().hasSelection(): #  check for selection
  470. self.updateChords()
  471. row = self.chordsw.widget().chordTableView.selectionModel().currentIndex().row()
  472. oldName = self.chordsw.widget().chordTableView.model.item(row, 0).text()
  473. self.csw().doc.chordList.pop(row)
  474. self.chordsw.widget().chordTableView.populate(self.csw().doc.chordList)
  475. # remove the chord if any of the blocks have it attached
  476. if self.csw().currentSection is not None:
  477. for s in self.csw().doc.sectionList:
  478. for b in s.blockList:
  479. if b.chord:
  480. if b.chord.name == oldName:
  481. b.chord = None
  482. self.blocksw.widget().blockTableView.populate(self.csw().currentSection.blockList)
  483. self.clearChordLineEdits()
  484. self.csw().updateChordDict()
  485. self.updateBlockChordComboBox()
  486. self.autoUpdatePreview()
  487. def addChordAction(self):
  488. success = False # initialise
  489. self.updateChords()
  490. cName = parseName(self.chordsw.widget().chordNameLineEdit.text())
  491. if cName:
  492. self.csw().doc.chordList.append(Chord(cName))
  493. if self.chordsw.widget().guitarVoicingLineEdit.text() or self.chordsw.widget().pianoVoicingLineEdit.text():
  494. if self.chordsw.widget().guitarVoicingLineEdit.text():
  495. try:
  496. self.csw().doc.chordList[-1].voicings['guitar'] = parseFingering(
  497. self.chordsw.widget().guitarVoicingLineEdit.text(), 'guitar')
  498. success = True #  chord successfully parsed
  499. except Exception:
  500. VoicingWarningMessageBox().exec() # Voicing is malformed, warn user
  501. if self.chordsw.widget().pianoVoicingLineEdit.text():
  502. try:
  503. self.csw().doc.chordList[-1].voicings['piano'] = parseFingering(
  504. self.chordsw.widget().pianoVoicingLineEdit.text(), 'piano')
  505. success = True #  chord successfully parsed
  506. except Exception:
  507. VoicingWarningMessageBox().exec() # Voicing is malformed, warn user
  508. else:
  509. success = True #  chord successfully parsed
  510. else:
  511. ChordNameWarningMessageBox().exec() # Chord has no name, warn user
  512. if success == True: # if chord was parsed properly
  513. self.chordsw.widget().chordTableView.populate(self.csw().doc.chordList)
  514. self.clearChordLineEdits()
  515. self.csw().updateChordDict()
  516. self.updateBlockChordComboBox()
  517. self.autoUpdatePreview()
  518. def updateChordAction(self):
  519. success = False # see comments above
  520. if self.chordsw.widget().chordTableView.selectionModel().hasSelection(): #  check for selection
  521. self.updateChords()
  522. row = self.chordsw.widget().chordTableView.selectionModel().currentIndex().row()
  523. oldName = self.chordsw.widget().chordTableView.model.item(row, 0).text()
  524. cName = parseName(self.chordsw.widget().chordNameLineEdit.text())
  525. if cName:
  526. self.csw().doc.chordList[row].name = cName
  527. if self.chordsw.widget().guitarVoicingLineEdit.text() or self.chordsw.widget().pianoVoicingLineEdit.text():
  528. if self.chordsw.widget().guitarVoicingLineEdit.text():
  529. try:
  530. self.csw().doc.chordList[row].voicings['guitar'] = parseFingering(
  531. self.chordsw.widget().guitarVoicingLineEdit.text(), 'guitar')
  532. success = True #  chord successfully parsed
  533. except Exception:
  534. VoicingWarningMessageBox().exec() # Voicing is malformed, warn user
  535. if self.chordsw.widget().pianoVoicingLineEdit.text():
  536. try:
  537. self.csw().doc.chordList[row].voicings['piano'] = parseFingering(
  538. self.chordsw.widget().pianoVoicingLineEdit.text(), 'piano')
  539. success = True #  chord successfully parsed
  540. except Exception:
  541. VoicingWarningMessageBox().exec() # Voicing is malformed, warn user
  542. else:
  543. success = True #  chord successfully parsed
  544. else:
  545. ChordNameWarningMessageBox().exec()
  546. if success == True:
  547. self.csw().updateChordDict()
  548. self.updateBlockChordComboBox()
  549. self.chordsw.widget().chordTableView.populate(self.csw().doc.chordList)
  550. # update the names of chords in all blocklists in case they've already been used
  551. for s in self.csw().doc.sectionList:
  552. for b in s.blockList:
  553. if b.chord:
  554. if b.chord.name == oldName:
  555. b.chord.name = cName
  556. if self.csw().currentSection and self.csw().currentSection.blockList:
  557. self.blocksw.widget().blockTableView.populate(self.csw().currentSection.blockList)
  558. self.clearChordLineEdits()
  559. self.autoUpdatePreview()
  560. def removeSectionAction(self):
  561. if self.sectionsw.widget().sectionTableView.selectionModel().hasSelection(): #  check for selection
  562. self.updateSections()
  563. row = self.sectionsw.widget().sectionTableView.selectionModel().currentIndex().row()
  564. self.csw().doc.sectionList.pop(row)
  565. self.sectionsw.widget().sectionTableView.populate(self.csw().doc.sectionList)
  566. self.clearSectionLineEdits()
  567. self.csw().updateSectionDict()
  568. self.updateBlockSectionComboBox()
  569. self.autoUpdatePreview()
  570. def addSectionAction(self):
  571. self.updateSections()
  572. sName = self.sectionsw.widget().sectionNameLineEdit.text()
  573. if sName and sName not in [s.name for s in self.csw().doc.sectionList]:
  574. self.csw().doc.sectionList.append(Section(name=sName))
  575. self.sectionsw.widget().sectionTableView.populate(self.csw().doc.sectionList)
  576. self.clearSectionLineEdits()
  577. self.csw().updateSectionDict()
  578. self.updateBlockSectionComboBox()
  579. self.autoUpdatePreview()
  580. else:
  581. # Section has no name or non unique, warn user
  582. SectionNameWarningMessageBox().exec()
  583. def updateSectionAction(self):
  584. if self.sectionsw.widget().sectionTableView.selectionModel().hasSelection(): #  check for selection
  585. self.updateSections()
  586. row = self.sectionsw.widget().sectionTableView.selectionModel().currentIndex().row()
  587. sName = self.sectionsw.widget().sectionNameLineEdit.text()
  588. if sName and sName not in [s.name for s in self.csw().doc.sectionList]:
  589. self.csw().doc.sectionList[row].name = sName
  590. self.sectionsw.widget().sectionTableView.populate(self.csw().doc.sectionList)
  591. self.clearSectionLineEdits()
  592. self.csw().updateSectionDict()
  593. self.updateBlockSectionComboBox()
  594. self.autoUpdatePreview()
  595. else:
  596. # Section has no name or non unique, warn user
  597. SectionNameWarningMessageBox().exec()
  598. def removeBlockAction(self):
  599. if self.blocksw.widget().blockTableView.selectionModel().hasSelection(): #  check for selection
  600. self.updateBlocks(self.csw().currentSection)
  601. row = self.blocksw.widget().blockTableView.selectionModel().currentIndex().row()
  602. self.csw().currentSection.blockList.pop(row)
  603. self.blocksw.widget().blockTableView.populate(self.csw().currentSection.blockList)
  604. self.autoUpdatePreview()
  605. def addBlockAction(self):
  606. self.updateBlocks(self.csw().currentSection)
  607. try:
  608. #  can the value entered for block length be cast as a float
  609. bLength = float(self.blocksw.widget().blockLengthLineEdit.text())
  610. except Exception:
  611. bLength = False
  612. if bLength: # create the block
  613. self.csw().currentSection.blockList.append(Block(bLength,
  614. chord=self.csw().chordDict[self.blocksw.widget().blockChordComboBox.currentText(
  615. )],
  616. notes=(self.blocksw.widget().blockNotesLineEdit.text() if not "" else None)))
  617. self.blocksw.widget().blockTableView.populate(self.csw().currentSection.blockList)
  618. self.clearBlockLineEdits()
  619. self.autoUpdatePreview()
  620. else:
  621. # show warning that length was not entered or in wrong format
  622. LengthWarningMessageBox().exec()
  623. def updateBlockAction(self):
  624. if self.blocksw.widget().blockTableView.selectionModel().hasSelection(): #  check for selection
  625. self.updateBlocks(self.csw().currentSection)
  626. try:
  627. #  can the value entered for block length be cast as a float
  628. bLength = float(self.blocksw.widget().blockLengthLineEdit.text())
  629. except Exception:
  630. bLength = False
  631. row = self.blocksw.widget().blockTableView.selectionModel().currentIndex().row()
  632. if bLength:
  633. self.csw().currentSection.blockList[row] = (Block(bLength,
  634. chord=self.csw().chordDict[self.blocksw.widget().blockChordComboBox.currentText(
  635. )],
  636. notes=(self.blocksw.widget().blockNotesLineEdit.text() if not "" else None)))
  637. self.blocksw.widget().blockTableView.populate(
  638. self.csw().currentSection.blockList)
  639. self.clearBlockLineEdits()
  640. self.autoUpdatePreview()
  641. else:
  642. LengthWarningMessageBox().exec()
  643. def includedFontAction(self):
  644. # if self.csw() is not None:
  645. if self.psetup.widget().includedFontCheckBox.isChecked():
  646. self.csw().style.font = 'FreeSans'
  647. else:
  648. self.csw().style.font = 'HelveticaNeue'
  649. def chordClickedAction(self, index):
  650. # set the controls to the values from the selected chord
  651. self.chordsw.widget().chordNameLineEdit.setText(
  652. self.chordsw.widget().chordTableView.model.item(index.row(), 0).text())
  653. self.chordsw.widget().guitarVoicingLineEdit.setText(
  654. self.chordsw.widget().chordTableView.model.item(index.row(), 1).text())
  655. self.chordsw.widget().pianoVoicingLineEdit.setText(
  656. self.chordsw.widget().chordTableView.model.item(index.row(), 2).text())
  657. def sectionClickedAction(self, index):
  658. # set the controls to the values from the selected section
  659. self.sectionsw.widget().sectionNameLineEdit.setText(
  660. self.sectionsw.widget().sectionTableView.model.item(index.row(), 0).text())
  661. # also set the combo box on the block page to make it flow well
  662. curSecName = self.sectionsw.widget().sectionTableView.model.item(
  663. index.row(), 0).text()
  664. if curSecName:
  665. self.blocksw.widget().blockSectionComboBox.setCurrentText(
  666. curSecName)
  667. def blockSectionClickedAction(self, text):
  668. if text:
  669. self.updateBlocks(self.csw().sectionDict[text])
  670. def blockSectionChangedAction(self, index):
  671. sName = self.blocksw.widget().blockSectionComboBox.currentText()
  672. if sName:
  673. # if self.csw() is not None:
  674. self.csw().currentSection = self.csw().sectionDict.get(sName, None)
  675. # self.csw().currentSection = self.currentSection
  676. if self.csw().currentSection is not None:
  677. self.blocksw.widget().blockTableView.populate(self.csw().currentSection.blockList)
  678. else:
  679. pass # self.currentSection = None
  680. def blockClickedAction(self, index):
  681. # set the controls to the values from the selected block
  682. bChord = self.blocksw.widget().blockTableView.model.item(index.row(), 0).text()
  683. self.blocksw.widget().blockChordComboBox.setCurrentText(
  684. bChord if bChord else "None")
  685. self.blocksw.widget().blockLengthLineEdit.setText(
  686. self.blocksw.widget().blockTableView.model.item(index.row(), 1).text())
  687. self.blocksw.widget().blockNotesLineEdit.setText(
  688. self.blocksw.widget().blockTableView.model.item(index.row(), 2).text())
  689. def menuFileNewAction(self):
  690. dw = DocumentWindow(Document(), Style(), None)
  691. self.window.mdiArea.addSubWindow(dw)
  692. dw.show()
  693. def menuFileOpenAction(self):
  694. filePath = QFileDialog.getOpenFileName(self.window, 'Open file', getPath(
  695. "workingPath"), "Chordsheet Markup Language files (*.cml *.xml);;Chordsheet Macro files (*.cma)")[0]
  696. if filePath:
  697. dw = DocumentWindow.openFile(filePath)
  698. if dw:
  699. self.window.mdiArea.addSubWindow(dw)
  700. dw.show()
  701. def menuFileSaveAction(self):
  702. pathRequired = False
  703. if self.csw().currentFilePath:
  704. fileExt = os.path.splitext(self.csw().currentFilePath)[1].lower()
  705. if fileExt != ".cma":
  706. # Chordsheet Macro files can't be saved at this time
  707. self.csw().saveFile(self.csw().currentFilePath)
  708. else:
  709. pathRequired = True
  710. else:
  711. pathRequired = True
  712. if pathRequired:
  713. self.csw().saveFileChoosePath()
  714. def menuFileSaveAsAction(self):
  715. self.csw().saveFileChoosePath()
  716. def menuFileSavePDFAction(self):
  717. filePath = self.csw().savePDFFileChoosePath()
  718. if filePath:
  719. setPath("lastExportPath", filePath)
  720. def menuFilePrintAction(self):
  721. if sys.platform == "darwin":
  722. pass
  723. # subprocess.call()
  724. else:
  725. pass
  726. def menuFileCloseAction(self):
  727. self.csw().close()
  728. def menuFileQuitAction(self):
  729. self.close()
  730. def menuHelpAboutAction(self):
  731. AboutDialog()
  732. def menuEditUndoAction(self):
  733. try:
  734. QApplication.focusWidget().undo() # see if the built in widget supports it
  735. except Exception:
  736. pass #  if not just fail silently
  737. def menuEditRedoAction(self):
  738. try:
  739. QApplication.focusWidget().redo()
  740. except Exception:
  741. pass
  742. def menuEditCutAction(self):
  743. try:
  744. QApplication.focusWidget().cut()
  745. except Exception:
  746. pass
  747. def menuEditCopyAction(self):
  748. try:
  749. QApplication.focusWidget().copy()
  750. except Exception:
  751. pass
  752. def menuEditPasteAction(self):
  753. try:
  754. QApplication.focusWidget().paste()
  755. except Exception:
  756. pass
  757. def guitarVoicingAction(self):
  758. gdialog = GuitarDialog()
  759. if self.chordsw.widget().guitarVoicingLineEdit.text():
  760. existingVoicing = parseFingering(
  761. self.chordsw.widget().guitarVoicingLineEdit.text(), 'guitar'
  762. )
  763. else:
  764. existingVoicing = None
  765. voicing = gdialog.getVoicing(existingVoicing)
  766. if voicing:
  767. self.chordsw.widget().guitarVoicingLineEdit.setText(voicing)
  768. def clearChordLineEdits(self):
  769. self.chordsw.widget().chordNameLineEdit.clear()
  770. self.chordsw.widget().guitarVoicingLineEdit.clear()
  771. self.chordsw.widget().pianoVoicingLineEdit.clear()
  772. # necessary on Mojave with PyInstaller (or previous contents will be shown)
  773. self.chordsw.widget().chordNameLineEdit.repaint()
  774. self.chordsw.widget().guitarVoicingLineEdit.repaint()
  775. self.chordsw.widget().pianoVoicingLineEdit.repaint()
  776. def clearSectionLineEdits(self):
  777. self.sectionsw.widget().sectionNameLineEdit.clear()
  778. # necessary on Mojave with PyInstaller (or previous contents will be shown)
  779. self.sectionsw.widget().sectionNameLineEdit.repaint()
  780. def clearBlockLineEdits(self):
  781. self.blocksw.widget().blockLengthLineEdit.clear()
  782. self.blocksw.widget().blockNotesLineEdit.clear()
  783. # necessary on Mojave with PyInstaller (or previous contents will be shown)
  784. self.blocksw.widget().blockLengthLineEdit.repaint()
  785. self.blocksw.widget().blockNotesLineEdit.repaint()
  786. class DocumentWindow(QMdiSubWindow):
  787. def __init__(self, doc, style, filename):
  788. super().__init__()
  789. self.UIFileLoader(str(os.path.join(scriptDir, 'ui', 'document.ui')))
  790. # Set to delete on close
  791. self.setAttribute(Qt.WA_DeleteOnClose)
  792. self.doc = doc
  793. self.style = style
  794. self.renderer = Renderer(self.doc, self.style)
  795. self.lastHash = self.doc.getHash()
  796. self.currentFilePath = filename
  797. self.currentSection = None
  798. self.updateChordDict()
  799. self.updateSectionDict()
  800. def UIFileLoader(self, ui_file):
  801. ui_file = QFile(ui_file)
  802. ui_file.open(QFile.ReadOnly)
  803. self.setWidget(uic.loadUi(ui_file))
  804. ui_file.close()
  805. def closeEvent(self, event):
  806. if self.saveWarning():
  807. event.accept()
  808. else:
  809. event.ignore()
  810. @classmethod
  811. def openFile(cls, filePath):
  812. """
  813. Opens a file from a file path and sets up the window accordingly.
  814. """
  815. dw = cls(Document(), Style(), None)
  816. dw.currentFilePath = filePath
  817. fileExt = os.path.splitext(dw.currentFilePath)[1].lower()
  818. try:
  819. if fileExt == ".cma":
  820. dw.doc.loadCSMacro(dw.currentFilePath)
  821. else: # if fileExt in [".xml", ".cml"]:
  822. dw.doc.loadXML(dw.currentFilePath)
  823. except (ParseError, ValueError):
  824. UnreadableMessageBox(os.path.basename(dw.currentFilePath)).exec()
  825. dw.close()
  826. return None
  827. else:
  828. dw.updatePreview()
  829. setPath("workingPath", dw.currentFilePath)
  830. dw.updateChordDict()
  831. dw.updateSectionDict()
  832. dw.currentSection = (dw.doc.sectionList[0] if dw.doc.sectionList else None)
  833. dw.updateTitleBar()
  834. dw.lastHash = dw.doc.getHash()
  835. return dw
  836. def saveFile(self, filePath):
  837. """
  838. Saves a file to given file path and sets up environment.
  839. """
  840. self.currentFilePath = filePath
  841. fileExt = os.path.splitext(self.currentFilePath)[1].lower()
  842. if fileExt == ".cma":
  843. # At this stage we should never get here
  844. pass
  845. else: # if fileExt in [".xml", ".cml"]:
  846. self.lastHash = self.doc.saveXML(self.currentFilePath)
  847. setPath("workingPath", self.currentFilePath)
  848. self.updateTitleBar() # as we may have a new filename
  849. def saveFileChoosePath(self):
  850. filePath, selectedFilter = QFileDialog.getSaveFileName(self.parent(), 'Save file', getPath(
  851. "workingPath"), "Chordsheet ML files (*.cml *.xml)", ".cml")
  852. if filePath:
  853. if filePath.split(".")[-1] not in ["cml", "xml"]:
  854. filePath += ".cml"
  855. self.saveFile(filePath)
  856. return filePath
  857. else:
  858. return None
  859. def savePDFFileChoosePath(self):
  860. filePath, selectedFilter = QFileDialog.getSaveFileName(self.parent(), 'Save file', getPath(
  861. "lastExportPath"), "PDF files (*.pdf)")
  862. if filePath:
  863. if filePath.split(".")[-1] != "pdf":
  864. filePath += ".pdf"
  865. self.renderer.savePDF(filePath)
  866. return filePath
  867. else:
  868. return None
  869. def saveWarning(self):
  870. """
  871. Function to check if the document has unsaved data in it and offer to save it.
  872. """
  873. if self.lastHash == self.doc.getHash():
  874. return True
  875. else:
  876. wantToSave = UnsavedMessageBox(os.path.basename(self.currentFilePath)).exec()
  877. if wantToSave == QMessageBox.Save:
  878. if not self.currentFilePath:
  879. self.saveFileChoosePath()
  880. else:
  881. self.saveFile(self.currentFilePath)
  882. return True
  883. elif wantToSave == QMessageBox.Discard:
  884. return True
  885. else:
  886. return False
  887. def updatePreview(self):
  888. """
  889. Update the preview shown by rendering a new PDF and drawing it to the scroll area.
  890. """
  891. try:
  892. self.currentPreview = self.renderer.stream()
  893. except Exception:
  894. QMessageBox.warning(self, "Preview failed", "Could not update the preview.",
  895. buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok)
  896. self.widget().pdfArea.update_pdf(self.currentPreview)
  897. def updateTitleBar(self):
  898. """
  899. Update the window's title bar to reflect the current document.
  900. """
  901. if self.currentFilePath:
  902. self.widget().setWindowTitle(os.path.basename(self.currentFilePath))
  903. else:
  904. self.widget().setWindowTitle("Unsaved")
  905. def matchSection(self, nameToMatch):
  906. """
  907. Given the name of a section, this function checks if it is already present in the document.
  908. If it is, it's returned. If not, a new section with the given name is returned.
  909. """
  910. section = None
  911. for s in self.doc.sectionList:
  912. if s.name == nameToMatch:
  913. section = s
  914. break
  915. if section is None:
  916. section = Section(name=nameToMatch)
  917. return section
  918. def updateChordDict(self):
  919. """
  920. Updates the dictionary used to generate the Chord menu (on the block tab)
  921. """
  922. self.chordDict = {'None': None}
  923. self.chordDict.update({c.name: c for c in self.doc.chordList})
  924. def updateSectionDict(self):
  925. """
  926. Updates the dictionary used to generate the Section menu (on the block tab)
  927. """
  928. self.sectionDict = {s.name: s for s in self.doc.sectionList}
  929. if __name__ == '__main__':
  930. app = QApplication(sys.argv)
  931. # pass first argument as filename
  932. mw = MainWindow()
  933. mw.show()
  934. sys.exit(app.exec_())