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.

778 lines
30 KiB

  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. from copy import copy
  13. from PyQt5.QtWidgets import QApplication, QAction, QLabel, QDialogButtonBox, QDialog, QFileDialog, QMessageBox, QPushButton, QLineEdit, QCheckBox, QSpinBox, QDoubleSpinBox, QTableWidgetItem, QTabWidget, QComboBox, QWidget, QScrollArea, QMainWindow, QShortcut
  14. from PyQt5.QtCore import QFile, QObject, Qt, pyqtSlot, QSettings
  15. from PyQt5.QtGui import QPixmap, QImage, QKeySequence
  16. from PyQt5 import uic
  17. from chordsheet.tableView import ChordTableView, BlockTableView
  18. from reportlab.lib.units import mm, cm, inch, pica
  19. from reportlab.lib.pagesizes import A4, A5, LETTER, LEGAL
  20. from reportlab.pdfbase import pdfmetrics
  21. from reportlab.pdfbase.ttfonts import TTFont
  22. from chordsheet.document import Document, Style, Chord, Block
  23. from chordsheet.render import savePDF
  24. from chordsheet.parsers import parseFingering, parseName
  25. import _version
  26. # set the directory where our files are depending on whether we're running a pyinstaller binary or not
  27. if getattr(sys, 'frozen', False):
  28. scriptDir = sys._MEIPASS
  29. else:
  30. scriptDir = os.path.abspath(os.path.dirname(os.path.abspath(__file__)))
  31. # enable automatic high DPI scaling on Windows
  32. QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
  33. QApplication.setOrganizationName("Ivan Holmes")
  34. QApplication.setOrganizationDomain("ivanholmes.co.uk")
  35. QApplication.setApplicationName("Chordsheet")
  36. settings = QSettings()
  37. pdfmetrics.registerFont(
  38. TTFont('FreeSans', os.path.join(scriptDir, 'fonts', 'FreeSans.ttf')))
  39. if sys.platform == "darwin":
  40. pdfmetrics.registerFont(
  41. TTFont('HelveticaNeue', 'HelveticaNeue.ttc', subfontIndex=0))
  42. # dictionaries for combo boxes
  43. pageSizeDict = {'A4': A4, 'A5': A5, 'Letter': LETTER, 'Legal': LEGAL}
  44. # point is 1 because reportlab's native unit is points.
  45. unitDict = {'mm': mm, 'cm': cm, 'inch': inch, 'point': 1, 'pica': pica}
  46. class DocumentWindow(QMainWindow):
  47. """
  48. Class for the main window of the application.
  49. """
  50. def __init__(self, doc, style, filename=None):
  51. """
  52. Initialisation function for the main window of the application.
  53. Arguments:
  54. doc -- the Document object for the window to use
  55. style -- the Style object for the window to use
  56. """
  57. super().__init__()
  58. self.doc = doc
  59. self.style = style
  60. self.lastDoc = copy(self.doc)
  61. self.currentFilePath = filename
  62. self.UIFileLoader(str(os.path.join(scriptDir, 'ui', 'mainwindow.ui')))
  63. self.UIInitStyle()
  64. self.updateChordDict()
  65. self.setCentralWidget(self.window.centralWidget)
  66. self.setMenuBar(self.window.menuBar)
  67. self.setWindowTitle("Chordsheet")
  68. if filename:
  69. try:
  70. self.openFile(filename)
  71. except Exception:
  72. UnreadableMessageBox().exec()
  73. def closeEvent(self, event):
  74. """
  75. Reimplement the built in closeEvent to allow asking the user to save.
  76. """
  77. self.saveWarning()
  78. def UIFileLoader(self, ui_file):
  79. """
  80. Loads the .ui file for this window and connects the UI elements to their actions.
  81. """
  82. ui_file = QFile(ui_file)
  83. ui_file.open(QFile.ReadOnly)
  84. self.window = uic.loadUi(ui_file)
  85. ui_file.close()
  86. # link all the UI elements
  87. self.window.actionAbout.triggered.connect(self.menuFileAboutAction)
  88. self.window.actionNew.triggered.connect(self.menuFileNewAction)
  89. self.window.actionOpen.triggered.connect(self.menuFileOpenAction)
  90. self.window.actionSave.triggered.connect(self.menuFileSaveAction)
  91. self.window.actionSave_as.triggered.connect(self.menuFileSaveAsAction)
  92. self.window.actionSave_PDF.triggered.connect(
  93. self.menuFileSavePDFAction)
  94. self.window.actionPrint.triggered.connect(self.menuFilePrintAction)
  95. self.window.actionClose.triggered.connect(self.menuFileCloseAction)
  96. self.window.actionUndo.triggered.connect(self.menuEditUndoAction)
  97. self.window.actionRedo.triggered.connect(self.menuEditRedoAction)
  98. self.window.actionCut.triggered.connect(self.menuEditCutAction)
  99. self.window.actionCopy.triggered.connect(self.menuEditCopyAction)
  100. self.window.actionPaste.triggered.connect(self.menuEditPasteAction)
  101. self.window.actionNew.setShortcut(QKeySequence.New)
  102. self.window.actionOpen.setShortcut(QKeySequence.Open)
  103. self.window.actionSave.setShortcut(QKeySequence.Save)
  104. self.window.actionSave_as.setShortcut(QKeySequence.SaveAs)
  105. self.window.actionSave_PDF.setShortcut(QKeySequence("Ctrl+E"))
  106. self.window.actionPrint.setShortcut(QKeySequence.Print)
  107. self.window.actionClose.setShortcut(QKeySequence.Close)
  108. self.window.actionUndo.setShortcut(QKeySequence.Undo)
  109. self.window.actionRedo.setShortcut(QKeySequence.Redo)
  110. self.window.actionCut.setShortcut(QKeySequence.Cut)
  111. self.window.actionCopy.setShortcut(QKeySequence.Copy)
  112. self.window.actionPaste.setShortcut(QKeySequence.Paste)
  113. self.window.pageSizeComboBox.currentIndexChanged.connect(
  114. self.pageSizeAction)
  115. self.window.documentUnitsComboBox.currentIndexChanged.connect(
  116. self.unitAction)
  117. self.window.includedFontCheckBox.stateChanged.connect(
  118. self.includedFontAction)
  119. self.window.generateButton.clicked.connect(self.generateAction)
  120. self.window.guitarVoicingButton.clicked.connect(
  121. self.guitarVoicingAction)
  122. self.window.addChordButton.clicked.connect(self.addChordAction)
  123. self.window.removeChordButton.clicked.connect(self.removeChordAction)
  124. self.window.updateChordButton.clicked.connect(self.updateChordAction)
  125. self.window.addBlockButton.clicked.connect(self.addBlockAction)
  126. self.window.removeBlockButton.clicked.connect(self.removeBlockAction)
  127. self.window.updateBlockButton.clicked.connect(self.updateBlockAction)
  128. self.window.chordTableView.clicked.connect(self.chordClickedAction)
  129. self.window.blockTableView.clicked.connect(self.blockClickedAction)
  130. def UIInitDocument(self):
  131. """
  132. Fills the window's fields with the values from its document.
  133. """
  134. self.updateTitleBar()
  135. # set all fields to appropriate values from document
  136. self.window.titleLineEdit.setText(self.doc.title)
  137. self.window.composerLineEdit.setText(self.doc.composer)
  138. self.window.arrangerLineEdit.setText(self.doc.arranger)
  139. self.window.timeSignatureSpinBox.setValue(self.doc.timeSignature)
  140. self.window.tempoLineEdit.setText(self.doc.tempo)
  141. self.window.chordTableView.populate(self.doc.chordList)
  142. self.window.blockTableView.populate(self.doc.blockList)
  143. self.updateChordDict()
  144. def UIInitStyle(self):
  145. """
  146. Fills the window's fields with the values from its style.
  147. """
  148. self.window.pageSizeComboBox.addItems(list(pageSizeDict.keys()))
  149. self.window.pageSizeComboBox.setCurrentText(
  150. list(pageSizeDict.keys())[0])
  151. self.window.documentUnitsComboBox.addItems(list(unitDict.keys()))
  152. self.window.documentUnitsComboBox.setCurrentText(
  153. list(unitDict.keys())[0])
  154. self.window.lineSpacingDoubleSpinBox.setValue(self.style.lineSpacing)
  155. self.window.leftMarginLineEdit.setText(str(self.style.leftMargin))
  156. self.window.topMarginLineEdit.setText(str(self.style.topMargin))
  157. self.window.fontComboBox.setDisabled(True)
  158. self.window.includedFontCheckBox.setChecked(True)
  159. self.window.beatWidthLineEdit.setText(str(self.style.unitWidth))
  160. def pageSizeAction(self, index):
  161. self.pageSizeSelected = self.window.pageSizeComboBox.itemText(index)
  162. def unitAction(self, index):
  163. self.unitSelected = self.window.documentUnitsComboBox.itemText(index)
  164. def includedFontAction(self):
  165. if self.window.includedFontCheckBox.isChecked():
  166. self.style.useIncludedFont = True
  167. else:
  168. self.style.useIncludedFont = False
  169. def chordClickedAction(self, index):
  170. # set the controls to the values from the selected chord
  171. self.window.chordNameLineEdit.setText(
  172. self.window.chordTableView.model.item(index.row(), 0).text())
  173. self.window.guitarVoicingLineEdit.setText(
  174. self.window.chordTableView.model.item(index.row(), 1).text())
  175. def blockClickedAction(self, index):
  176. # set the controls to the values from the selected block
  177. bChord = self.window.blockTableView.model.item(index.row(), 0).text()
  178. self.window.blockChordComboBox.setCurrentText(
  179. bChord if bChord else "None")
  180. self.window.blockLengthLineEdit.setText(
  181. self.window.blockTableView.model.item(index.row(), 1).text())
  182. self.window.blockNotesLineEdit.setText(
  183. self.window.blockTableView.model.item(index.row(), 2).text())
  184. def getPath(self, value):
  185. """
  186. Wrapper for Qt settings to return home directory if no setting exists.
  187. """
  188. return str((settings.value(value) if settings.value(value) else os.path.expanduser("~")))
  189. def setPath(self, value, fullpath):
  190. """
  191. Wrapper for Qt settings to set path to open/save from next time from current file location.
  192. """
  193. return settings.setValue(value, os.path.dirname(fullpath))
  194. def menuFileNewAction(self):
  195. self.doc = Document() #  new document object
  196. # copy this object as reference to check against on quitting
  197. self.lastDoc = copy(self.doc)
  198. #  reset file path (this document hasn't been saved yet)
  199. self.currentFilePath = None
  200. self.UIInitDocument()
  201. self.updatePreview()
  202. def menuFileOpenAction(self):
  203. filePath = QFileDialog.getOpenFileName(self.window.tabWidget, 'Open file', self.getPath(
  204. "workingPath"), "Chordsheet ML files (*.xml *.cml)")[0]
  205. if filePath:
  206. self.openFile(filePath)
  207. def openFile(self, filePath):
  208. """
  209. Opens a file from a file path and sets up the window accordingly.
  210. """
  211. self.currentFilePath = filePath
  212. self.doc.loadXML(self.currentFilePath)
  213. self.lastDoc = copy(self.doc)
  214. self.setPath("workingPath", self.currentFilePath)
  215. self.UIInitDocument()
  216. self.updatePreview()
  217. def menuFileSaveAction(self):
  218. self.updateDocument()
  219. if not self.currentFilePath:
  220. filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath(
  221. "workingPath"), "Chordsheet ML files (*.xml *.cml)")[0]
  222. else:
  223. filePath = self.currentFilePath
  224. self.saveFile(filePath)
  225. def menuFileSaveAsAction(self):
  226. self.updateDocument()
  227. filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath(
  228. "workingPath"), "Chordsheet ML files (*.xml *.cml)")[0]
  229. if filePath:
  230. self.saveFile(filePath)
  231. def saveFile(self, filePath):
  232. """
  233. Saves a file to given file path and sets up environment.
  234. """
  235. self.currentFilePath = filePath
  236. self.doc.saveXML(self.currentFilePath)
  237. self.lastDoc = copy(self.doc)
  238. self.setPath("workingPath", self.currentFilePath)
  239. self.updateTitleBar() # as we may have a new filename
  240. def menuFileSavePDFAction(self):
  241. self.updateDocument()
  242. self.updatePreview()
  243. filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath(
  244. "lastExportPath"), "PDF files (*.pdf)")[0]
  245. if filePath:
  246. savePDF(d, s, filePath)
  247. self.setPath("lastExportPath", filePath)
  248. def menuFilePrintAction(self):
  249. if sys.platform == "darwin":
  250. pass
  251. # subprocess.call()
  252. else:
  253. pass
  254. @pyqtSlot()
  255. def menuFileCloseAction(self):
  256. self.saveWarning()
  257. def menuFileAboutAction(self):
  258. AboutDialog()
  259. def menuEditUndoAction(self):
  260. try:
  261. QApplication.focusWidget().undo() # see if the built in widget supports it
  262. except Exception:
  263. pass #  if not just fail silently
  264. def menuEditRedoAction(self):
  265. try:
  266. QApplication.focusWidget().redo()
  267. except Exception:
  268. pass
  269. def menuEditCutAction(self):
  270. try:
  271. QApplication.focusWidget().cut()
  272. except Exception:
  273. pass
  274. def menuEditCopyAction(self):
  275. try:
  276. QApplication.focusWidget().copy()
  277. except Exception:
  278. pass
  279. def menuEditPasteAction(self):
  280. try:
  281. QApplication.focusWidget().paste()
  282. except Exception:
  283. pass
  284. def saveWarning(self):
  285. """
  286. Function to check if the document has unsaved data in it and offer to save it.
  287. """
  288. self.updateDocument() # update the document to catch all changes
  289. if self.lastDoc == self.doc:
  290. self.close()
  291. else:
  292. wantToSave = UnsavedMessageBox().exec()
  293. if wantToSave == QMessageBox.Save:
  294. if not self.currentFilePath:
  295. filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', str(
  296. os.path.expanduser("~")), "Chordsheet ML files (*.xml *.cml)")
  297. self.currentFilePath = filePath[0]
  298. self.doc.saveXML(self.currentFilePath)
  299. self.close()
  300. elif wantToSave == QMessageBox.Discard:
  301. self.close()
  302. # if cancel or anything else do nothing at all
  303. def guitarVoicingAction(self):
  304. gdialog = GuitarDialog()
  305. voicing = gdialog.getVoicing()
  306. if voicing:
  307. self.window.guitarVoicingLineEdit.setText(voicing)
  308. def clearChordLineEdits(self):
  309. self.window.chordNameLineEdit.clear()
  310. self.window.guitarVoicingLineEdit.clear()
  311. # necessary on Mojave with PyInstaller (or previous contents will be shown)
  312. self.window.chordNameLineEdit.repaint()
  313. self.window.guitarVoicingLineEdit.repaint()
  314. def updateChordDict(self):
  315. """
  316. Updates the dictionary used to generate the Chord menu (on the block tab)
  317. """
  318. self.chordDict = {'None': None}
  319. self.chordDict.update({c.name: c for c in self.doc.chordList})
  320. self.window.blockChordComboBox.clear()
  321. self.window.blockChordComboBox.addItems(list(self.chordDict.keys()))
  322. def removeChordAction(self):
  323. if self.window.chordTableView.selectionModel().hasSelection(): #  check for selection
  324. self.updateChords()
  325. row = self.window.chordTableView.selectionModel().currentIndex().row()
  326. self.doc.chordList.pop(row)
  327. self.window.chordTableView.populate(self.doc.chordList)
  328. self.clearChordLineEdits()
  329. self.updateChordDict()
  330. def addChordAction(self):
  331. success = False # initialise
  332. self.updateChords()
  333. cName = parseName(self.window.chordNameLineEdit.text())
  334. if cName:
  335. self.doc.chordList.append(Chord(cName))
  336. if self.window.guitarVoicingLineEdit.text():
  337. try:
  338. self.doc.chordList[-1].voicings['guitar'] = parseFingering(
  339. self.window.guitarVoicingLineEdit.text(), 'guitar')
  340. success = True #  chord successfully parsed
  341. except Exception:
  342. VoicingWarningMessageBox().exec() # Voicing is malformed, warn user
  343. else:
  344. success = True #  chord successfully parsed
  345. else:
  346. NameWarningMessageBox().exec() # Chord has no name, warn user
  347. if success == True: # if chord was parsed properly
  348. self.window.chordTableView.populate(self.doc.chordList)
  349. self.clearChordLineEdits()
  350. self.updateChordDict()
  351. def updateChordAction(self):
  352. success = False # see comments above
  353. if self.window.chordTableView.selectionModel().hasSelection(): #  check for selection
  354. self.updateChords()
  355. row = self.window.chordTableView.selectionModel().currentIndex().row()
  356. cName = parseName(self.window.chordNameLineEdit.text())
  357. if cName:
  358. self.doc.chordList[row] = Chord(cName)
  359. if self.window.guitarVoicingLineEdit.text():
  360. try:
  361. self.doc.chordList[row].voicings['guitar'] = parseFingering(
  362. self.window.guitarVoicingLineEdit.text(), 'guitar')
  363. success = True
  364. except Exception:
  365. VoicingWarningMessageBox().exec()
  366. else:
  367. success = True
  368. else:
  369. NameWarningMessageBox().exec()
  370. if success == True:
  371. self.window.chordTableView.populate(self.doc.chordList)
  372. self.clearChordLineEdits()
  373. self.updateChordDict()
  374. def clearBlockLineEdits(self):
  375. self.window.blockLengthLineEdit.clear()
  376. self.window.blockNotesLineEdit.clear()
  377. # necessary on Mojave with PyInstaller (or previous contents will be shown)
  378. self.window.blockLengthLineEdit.repaint()
  379. self.window.blockNotesLineEdit.repaint()
  380. def removeBlockAction(self):
  381. if self.window.blockTableView.selectionModel().hasSelection(): #  check for selection
  382. self.updateBlocks()
  383. row = self.window.blockTableView.selectionModel().currentIndex().row()
  384. self.doc.blockList.pop(row)
  385. self.window.blockTableView.populate(self.doc.blockList)
  386. def addBlockAction(self):
  387. self.updateBlocks()
  388. try:
  389. #  can the value entered for block length be cast as an integer
  390. bLength = int(self.window.blockLengthLineEdit.text())
  391. except Exception:
  392. bLength = False
  393. if bLength: # create the block
  394. self.doc.blockList.append(Block(bLength,
  395. chord=self.chordDict[self.window.blockChordComboBox.currentText(
  396. )],
  397. notes=(self.window.blockNotesLineEdit.text() if not "" else None)))
  398. self.window.blockTableView.populate(self.doc.blockList)
  399. self.clearBlockLineEdits()
  400. else:
  401. # show warning that length was not entered or in wrong format
  402. LengthWarningMessageBox().exec()
  403. def updateBlockAction(self):
  404. if self.window.blockTableView.selectionModel().hasSelection(): #  check for selection
  405. self.updateBlocks()
  406. try:
  407. bLength = int(self.window.blockLengthLineEdit.text())
  408. except Exception:
  409. bLength = False
  410. row = self.window.blockTableView.selectionModel().currentIndex().row()
  411. if bLength:
  412. self.doc.blockList[row] = (Block(bLength,
  413. chord=self.chordDict[self.window.blockChordComboBox.currentText(
  414. )],
  415. notes=(self.window.blockNotesLineEdit.text() if not "" else None)))
  416. self.window.blockTableView.populate(self.doc.blockList)
  417. self.clearBlockLineEdits()
  418. else:
  419. LengthWarningMessageBox().exec()
  420. def generateAction(self):
  421. self.updateDocument()
  422. self.updatePreview()
  423. def updatePreview(self):
  424. """
  425. Update the preview shown by rendering a new PDF and drawing it to the scroll area.
  426. """
  427. try:
  428. self.currentPreview = io.BytesIO()
  429. savePDF(self.doc, self.style, self.currentPreview)
  430. pdfView = fitz.Document(stream=self.currentPreview, filetype='pdf')
  431. # render at 4x resolution and scale
  432. pix = pdfView[0].getPixmap(matrix=fitz.Matrix(4, 4), alpha=False)
  433. fmt = QImage.Format_RGB888
  434. qtimg = QImage(pix.samples, pix.width, pix.height, pix.stride, fmt)
  435. self.window.imageLabel.setPixmap(QPixmap.fromImage(qtimg).scaled(self.window.scrollArea.width(
  436. )-30, self.window.scrollArea.height()-30, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation))
  437. # -30 because the scrollarea has a margin of 12 each side (extra for safety)
  438. # necessary on Mojave with PyInstaller (or previous contents will be shown)
  439. self.window.imageLabel.repaint()
  440. except Exception:
  441. QMessageBox.warning(self, "Preview failed", "Could not update the preview.",
  442. buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok)
  443. def updateTitleBar(self):
  444. """
  445. Update the application's title bar to reflect the current document.
  446. """
  447. if self.currentFilePath:
  448. self.setWindowTitle(_version.appName + " – " +
  449. os.path.basename(self.currentFilePath))
  450. else:
  451. self.setWindowTitle(_version.appName)
  452. def updateChords(self):
  453. """
  454. Update the chord list by reading the table.
  455. """
  456. chordTableList = []
  457. for i in range(self.window.chordTableView.model.rowCount()):
  458. chordTableList.append(
  459. Chord(parseName(self.window.chordTableView.model.item(i, 0).text()))),
  460. if self.window.chordTableView.model.item(i, 1).text():
  461. chordTableList[-1].voicings['guitar'] = parseFingering(
  462. self.window.chordTableView.model.item(i, 1).text(), 'guitar')
  463. self.doc.chordList = chordTableList
  464. def updateBlocks(self):
  465. """
  466. Update the block list by reading the table.
  467. """
  468. blockTableList = []
  469. for i in range(self.window.blockTableView.model.rowCount()):
  470. blockLength = int(
  471. self.window.blockTableView.model.item(i, 1).text())
  472. blockChord = self.chordDict[(self.window.blockTableView.model.item(
  473. i, 0).text() if self.window.blockTableView.model.item(i, 0).text() else "None")]
  474. blockNotes = self.window.blockTableView.model.item(i, 2).text(
  475. ) if self.window.blockTableView.model.item(i, 2).text() else None
  476. blockTableList.append(
  477. Block(blockLength, chord=blockChord, notes=blockNotes))
  478. self.doc.blockList = blockTableList
  479. def updateDocument(self):
  480. """
  481. Update the Document object by reading values from the UI.
  482. """
  483. self.doc.title = self.window.titleLineEdit.text(
  484. ) # Title can be empty string but not None
  485. self.doc.subtitle = (self.window.subtitleLineEdit.text(
  486. ) if self.window.subtitleLineEdit.text() else None)
  487. self.doc.composer = (self.window.composerLineEdit.text(
  488. ) if self.window.composerLineEdit.text() else None)
  489. self.doc.arranger = (self.window.arrangerLineEdit.text(
  490. ) if self.window.arrangerLineEdit.text() else None)
  491. self.doc.tempo = (self.window.tempoLineEdit.text()
  492. if self.window.tempoLineEdit.text() else None)
  493. self.doc.timeSignature = int(self.window.timeSignatureSpinBox.value(
  494. )) if self.window.timeSignatureSpinBox.value() else self.doc.timeSignature
  495. self.style.pageSize = pageSizeDict[self.pageSizeSelected]
  496. self.style.unit = unitDict[self.unitSelected]
  497. self.style.leftMargin = float(self.window.leftMarginLineEdit.text(
  498. )) if self.window.leftMarginLineEdit.text() else self.style.leftMargin
  499. self.style.topMargin = float(self.window.topMarginLineEdit.text(
  500. )) if self.window.topMarginLineEdit.text() else self.style.topMargin
  501. self.style.lineSpacing = float(self.window.lineSpacingDoubleSpinBox.value(
  502. )) if self.window.lineSpacingDoubleSpinBox.value() else self.style.lineSpacing
  503. # make sure the unit width isn't too wide to draw!
  504. if self.window.beatWidthLineEdit.text():
  505. if (self.style.pageSize[0] - 2 * self.style.leftMargin * mm) >= (float(self.window.beatWidthLineEdit.text()) * 2 * self.doc.timeSignature * mm):
  506. self.style.unitWidth = float(
  507. self.window.beatWidthLineEdit.text())
  508. else:
  509. maxBeatWidth = (
  510. self.style.pageSize[0] - 2 * self.style.leftMargin * mm) / (2 * self.doc.timeSignature * mm)
  511. QMessageBox.warning(self, "Out of range", "Beat width is out of range. It can be a maximum of {}.".format(
  512. maxBeatWidth), buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok)
  513. self.updateChords()
  514. self.updateBlocks()
  515. self.style.font = (
  516. 'FreeSans' if self.style.useIncludedFont else 'HelveticaNeue')
  517. # something for the font box here
  518. class GuitarDialog(QDialog):
  519. """
  520. Dialogue to allow the user to enter a guitar chord voicing. Not particularly advanced at present!
  521. May be extended in future.
  522. """
  523. def __init__(self):
  524. super().__init__()
  525. self.UIFileLoader(
  526. str(os.path.join(scriptDir, 'ui', 'guitardialog.ui')))
  527. def UIFileLoader(self, ui_file):
  528. ui_file = QFile(ui_file)
  529. ui_file.open(QFile.ReadOnly)
  530. self.dialog = uic.loadUi(ui_file)
  531. ui_file.close()
  532. def getVoicing(self):
  533. """
  534. Show the dialogue and return the voicing that has been entered.
  535. """
  536. if self.dialog.exec_() == QDialog.Accepted:
  537. result = [self.dialog.ELineEdit.text(),
  538. self.dialog.ALineEdit.text(),
  539. self.dialog.DLineEdit.text(),
  540. self.dialog.GLineEdit.text(),
  541. self.dialog.BLineEdit.text(),
  542. self.dialog.eLineEdit.text()]
  543. resultJoined = ",".join(result)
  544. return resultJoined
  545. else:
  546. return None
  547. class AboutDialog(QDialog):
  548. """
  549. Dialogue showing information about the program.
  550. """
  551. def __init__(self):
  552. super().__init__()
  553. self.UIFileLoader(str(os.path.join(scriptDir, 'ui', 'aboutdialog.ui')))
  554. icon = QImage(str(os.path.join(scriptDir, 'ui', 'icon.png')))
  555. self.dialog.iconLabel.setPixmap(QPixmap.fromImage(icon).scaled(self.dialog.iconLabel.width(
  556. ), self.dialog.iconLabel.height(), Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation))
  557. self.dialog.versionLabel.setText("Version " + _version.version)
  558. self.dialog.exec()
  559. def UIFileLoader(self, ui_file):
  560. ui_file = QFile(ui_file)
  561. ui_file.open(QFile.ReadOnly)
  562. self.dialog = uic.loadUi(ui_file)
  563. ui_file.close()
  564. class UnsavedMessageBox(QMessageBox):
  565. """
  566. Message box to alert the user of unsaved changes and allow them to choose how to act.
  567. """
  568. def __init__(self):
  569. super().__init__()
  570. self.setIcon(QMessageBox.Question)
  571. self.setWindowTitle("Unsaved changes")
  572. self.setText("The document has been modified.")
  573. self.setInformativeText("Do you want to save your changes?")
  574. self.setStandardButtons(
  575. QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel)
  576. self.setDefaultButton(QMessageBox.Save)
  577. class UnreadableMessageBox(QMessageBox):
  578. """
  579. Message box to warn the user that the chosen file cannot be opened.
  580. """
  581. def __init__(self):
  582. super().__init__()
  583. self.setIcon(QMessageBox.Warning)
  584. self.setWindowTitle("File cannot be opened")
  585. self.setText("The file you have selected cannot be opened.")
  586. self.setInformativeText("Please make sure it is in the right format.")
  587. self.setStandardButtons(QMessageBox.Ok)
  588. self.setDefaultButton(QMessageBox.Ok)
  589. class NameWarningMessageBox(QMessageBox):
  590. """
  591. Message box to warn the user that a chord must have a name
  592. """
  593. def __init__(self):
  594. super().__init__()
  595. self.setIcon(QMessageBox.Warning)
  596. self.setWindowTitle("Unnamed chord")
  597. self.setText("Chords must have a name.")
  598. self.setInformativeText("Please give your chord a name and try again.")
  599. self.setStandardButtons(QMessageBox.Ok)
  600. self.setDefaultButton(QMessageBox.Ok)
  601. class VoicingWarningMessageBox(QMessageBox):
  602. """
  603. Message box to warn the user that the voicing entered could not be parsed
  604. """
  605. def __init__(self):
  606. super().__init__()
  607. self.setIcon(QMessageBox.Warning)
  608. self.setWindowTitle("Malformed voicing")
  609. self.setText(
  610. "The voicing you entered was not understood and has not been applied.")
  611. self.setInformativeText(
  612. "Please try re-entering it in the correct format.")
  613. self.setStandardButtons(QMessageBox.Ok)
  614. self.setDefaultButton(QMessageBox.Ok)
  615. class LengthWarningMessageBox(QMessageBox):
  616. """
  617. Message box to warn the user that a block must have a length
  618. """
  619. def __init__(self):
  620. super().__init__()
  621. self.setIcon(QMessageBox.Warning)
  622. self.setWindowTitle("Block without valid length")
  623. self.setText("Blocks must have a whole number length.")
  624. self.setInformativeText(
  625. "Please enter a valid length for your block and try again.")
  626. self.setStandardButtons(QMessageBox.Ok)
  627. self.setDefaultButton(QMessageBox.Ok)
  628. if __name__ == '__main__':
  629. app = QApplication(sys.argv)
  630. d = Document()
  631. s = Style()
  632. # pass first argument as filename
  633. w = DocumentWindow(d, s, filename=(
  634. sys.argv[1] if len(sys.argv) > 1 else None))
  635. w.show()
  636. sys.exit(app.exec_())