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.

1018 lines
40 KiB

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