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.

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