From fcf1e72c96a984e8dcdf3ebbac97bfbbd6a8306a Mon Sep 17 00:00:00 2001 From: Ivan Holmes Date: Sat, 8 May 2021 22:42:01 +0100 Subject: [PATCH] MDI pretty much working --- _version.py | 2 +- chordsheet/common.py | 7 + chordsheet/dialogs.py | 67 +++ chordsheet/messageBox.py | 118 ++++ chordsheet/panels.py | 54 ++ chordsheet/pdfViewer.py | 44 +- gui.py | 1156 +++++++++++++++++--------------------- preview.pdf | Bin 0 -> 20738 bytes ui/blocks.ui | 225 ++++++++ ui/chords.ui | 190 +++++++ ui/docinfo.ui | 144 +++++ ui/document.ui | 57 ++ ui/new.ui | 172 ++++++ ui/pdfarea.ui | 32 ++ ui/preview.ui | 52 ++ ui/psetup.ui | 273 +++++++++ ui/sections.ui | 127 +++++ version.rc | 8 +- 18 files changed, 2070 insertions(+), 658 deletions(-) create mode 100644 chordsheet/common.py create mode 100644 chordsheet/dialogs.py create mode 100644 chordsheet/messageBox.py create mode 100644 chordsheet/panels.py create mode 100644 preview.pdf create mode 100644 ui/blocks.ui create mode 100644 ui/chords.ui create mode 100644 ui/docinfo.ui create mode 100644 ui/document.ui create mode 100644 ui/new.ui create mode 100644 ui/pdfarea.ui create mode 100644 ui/preview.ui create mode 100644 ui/psetup.ui create mode 100644 ui/sections.ui diff --git a/_version.py b/_version.py index 46e40bb..02f7836 100644 --- a/_version.py +++ b/_version.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- appName = "Chordsheet" -version = '0.4.6' +version = '0.5.0' diff --git a/chordsheet/common.py b/chordsheet/common.py new file mode 100644 index 0000000..5a972fe --- /dev/null +++ b/chordsheet/common.py @@ -0,0 +1,7 @@ +import sys, os + +# set the directory where our files are depending on whether we're running a pyinstaller binary or not +if getattr(sys, 'frozen', False): + scriptDir = sys._MEIPASS +else: + scriptDir = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)) \ No newline at end of file diff --git a/chordsheet/dialogs.py b/chordsheet/dialogs.py new file mode 100644 index 0000000..dcc03ed --- /dev/null +++ b/chordsheet/dialogs.py @@ -0,0 +1,67 @@ +import os +from PyQt5.QtWidgets import QApplication, QAction, QLabel, QDialogButtonBox, QDialog, QFileDialog, QMessageBox, QPushButton, QLineEdit, QCheckBox, QSpinBox, QDoubleSpinBox, QTableWidgetItem, QTabWidget, QComboBox, QWidget, QScrollArea, QMainWindow, QShortcut +from PyQt5.QtCore import QFile, QObject, Qt +from PyQt5.QtGui import QImage, QPixmap +from PyQt5 import uic + +from chordsheet.common import scriptDir +import _version + +class GuitarDialog(QDialog): + """ + Dialogue to allow the user to enter a guitar chord voicing. Not particularly advanced at present! + May be extended in future. + """ + + def __init__(self): + super().__init__() + self.UIFileLoader( + str(os.path.join(scriptDir, 'ui', 'guitardialog.ui'))) + + def UIFileLoader(self, ui_file): + ui_file = QFile(ui_file) + ui_file.open(QFile.ReadOnly) + + self.dialog = uic.loadUi(ui_file) + ui_file.close() + + def getVoicing(self): + """ + Show the dialogue and return the voicing that has been entered. + """ + if self.dialog.exec_() == QDialog.Accepted: + result = [self.dialog.ELineEdit.text(), + self.dialog.ALineEdit.text(), + self.dialog.DLineEdit.text(), + self.dialog.GLineEdit.text(), + self.dialog.BLineEdit.text(), + self.dialog.eLineEdit.text()] + resultJoined = ",".join(result) + return resultJoined + else: + return None + + +class AboutDialog(QDialog): + """ + Dialogue showing information about the program. + """ + + def __init__(self): + super().__init__() + self.UIFileLoader(str(os.path.join(scriptDir, 'ui', 'aboutdialog.ui'))) + + icon = QImage(str(os.path.join(scriptDir, 'ui', 'icon.png'))) + self.dialog.iconLabel.setPixmap(QPixmap.fromImage(icon).scaled(self.dialog.iconLabel.width( + ), self.dialog.iconLabel.height(), Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)) + + self.dialog.versionLabel.setText("Version " + _version.version) + + self.dialog.exec() + + def UIFileLoader(self, ui_file): + ui_file = QFile(ui_file) + ui_file.open(QFile.ReadOnly) + + self.dialog = uic.loadUi(ui_file) + ui_file.close() \ No newline at end of file diff --git a/chordsheet/messageBox.py b/chordsheet/messageBox.py new file mode 100644 index 0000000..87a620e --- /dev/null +++ b/chordsheet/messageBox.py @@ -0,0 +1,118 @@ +from PyQt5.QtWidgets import QApplication, QAction, QLabel, QDialogButtonBox, QDialog, QFileDialog, QMessageBox, QPushButton, QLineEdit, QCheckBox, QSpinBox, QDoubleSpinBox, QTableWidgetItem, QTabWidget, QComboBox, QWidget, QScrollArea, QMainWindow, QShortcut + +class UnsavedMessageBox(QMessageBox): + """ + Message box to alert the user of unsaved changes and allow them to choose how to act. + """ + + def __init__(self): + super().__init__() + + self.setIcon(QMessageBox.Question) + self.setWindowTitle("Unsaved changes") + self.setText("The document has been modified.") + self.setInformativeText("Do you want to save your changes?") + self.setStandardButtons( + QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) + self.setDefaultButton(QMessageBox.Save) + + +class UnreadableMessageBox(QMessageBox): + """ + Message box to warn the user that the chosen file cannot be opened. + """ + + def __init__(self): + super().__init__() + + self.setIcon(QMessageBox.Warning) + self.setWindowTitle("File cannot be opened") + self.setText("The file you have selected cannot be opened.") + self.setInformativeText("Please make sure it is in the right format.") + self.setStandardButtons(QMessageBox.Ok) + self.setDefaultButton(QMessageBox.Ok) + + +class ChordNameWarningMessageBox(QMessageBox): + """ + Message box to warn the user that a chord must have a name + """ + + def __init__(self): + super().__init__() + + self.setIcon(QMessageBox.Warning) + self.setWindowTitle("Unnamed chord") + self.setText("Chords must have a name.") + self.setInformativeText("Please give your chord a name and try again.") + self.setStandardButtons(QMessageBox.Ok) + self.setDefaultButton(QMessageBox.Ok) + + +class SectionNameWarningMessageBox(QMessageBox): + """ + Message box to warn the user that a section must have a name + """ + + def __init__(self): + super().__init__() + + self.setIcon(QMessageBox.Warning) + self.setWindowTitle("Unnamed section") + self.setText("Sections must have a unique name.") + self.setInformativeText( + "Please give your section a unique name and try again.") + self.setStandardButtons(QMessageBox.Ok) + self.setDefaultButton(QMessageBox.Ok) + + +class BlockMustHaveSectionWarningMessageBox(QMessageBox): + """ + Message box to warn the user that a block must belong to a section + """ + + def __init__(self): + super().__init__() + + self.setIcon(QMessageBox.Warning) + self.setWindowTitle("No sections found") + self.setText("Each block must belong to a section, but no sections have yet been created.") + self.setInformativeText( + "Please create a section before adding blocks.") + self.setStandardButtons(QMessageBox.Ok) + self.setDefaultButton(QMessageBox.Ok) + + +class VoicingWarningMessageBox(QMessageBox): + """ + Message box to warn the user that the voicing entered could not be parsed + """ + + def __init__(self): + super().__init__() + + self.setIcon(QMessageBox.Warning) + self.setWindowTitle("Malformed voicing") + self.setText( + "The voicing you entered was not understood and has not been applied.") + self.setInformativeText( + "Please try re-entering it in the correct format.") + self.setStandardButtons(QMessageBox.Ok) + self.setDefaultButton(QMessageBox.Ok) + + +class LengthWarningMessageBox(QMessageBox): + """ + Message box to warn the user that a block must have a length + """ + + def __init__(self): + super().__init__() + + self.setIcon(QMessageBox.Warning) + self.setWindowTitle("Block without valid length") + self.setText("Blocks must have a length.") + self.setInformativeText( + "Please enter a valid length for your block and try again.") + self.setStandardButtons(QMessageBox.Ok) + self.setDefaultButton(QMessageBox.Ok) diff --git a/chordsheet/panels.py b/chordsheet/panels.py new file mode 100644 index 0000000..41f0427 --- /dev/null +++ b/chordsheet/panels.py @@ -0,0 +1,54 @@ +import os +from PyQt5.QtWidgets import QApplication, QAction, QLabel, QDialogButtonBox, QDialog, QFileDialog, QMessageBox, QPushButton, QLineEdit, QCheckBox, QSpinBox, QDoubleSpinBox, QTableWidgetItem, QTabWidget, QComboBox, QWidget, QScrollArea, QMainWindow, QShortcut, QDockWidget +from PyQt5.QtCore import QFile, QObject, Qt +from PyQt5.QtGui import QImage, QPixmap +from PyQt5 import uic + +from chordsheet.common import scriptDir + +class UIFileDockWidget(QDockWidget): + def __init__(self): + super().__init__() + + def UIFileLoader(self, ui_file): + ui_file = QFile(os.path.join(scriptDir, 'ui', ui_file)) + ui_file.open(QFile.ReadOnly) + + self.setWidget(uic.loadUi(ui_file)) + ui_file.close() + +class DocInfoDockWidget(UIFileDockWidget): + def __init__(self): + super().__init__() + self.UIFileLoader('docinfo.ui') + self.setWindowTitle("Document information") + +class PageSetupDockWidget(UIFileDockWidget): + def __init__(self): + super().__init__() + self.UIFileLoader('psetup.ui') + self.setWindowTitle("Page setup") + +class ChordsDockWidget(UIFileDockWidget): + def __init__(self): + super().__init__() + self.UIFileLoader('chords.ui') + self.setWindowTitle("Chords") + +class SectionsDockWidget(UIFileDockWidget): + def __init__(self): + super().__init__() + self.UIFileLoader('sections.ui') + self.setWindowTitle("Sections") + +class BlocksDockWidget(UIFileDockWidget): + def __init__(self): + super().__init__() + self.UIFileLoader('blocks.ui') + self.setWindowTitle("Blocks") + +class PreviewDockWidget(UIFileDockWidget): + def __init__(self): + super().__init__() + self.UIFileLoader('preview.ui') + self.setWindowTitle("Preview") \ No newline at end of file diff --git a/chordsheet/pdfViewer.py b/chordsheet/pdfViewer.py index 6c63d80..0db75df 100644 --- a/chordsheet/pdfViewer.py +++ b/chordsheet/pdfViewer.py @@ -1,9 +1,29 @@ -from PyQt5.QtWidgets import QScrollArea, QLabel, QVBoxLayout, QWidget -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPixmap, QImage +from PyQt5.QtWidgets import QScrollArea, QLabel, QVBoxLayout, QWidget, QSizePolicy +from PyQt5.QtCore import Qt, QPoint, QSize +from PyQt5.QtGui import QPixmap, QImage, QResizeEvent, QPainter import fitz +class PDFLabel(QLabel): + def __init__(self, parent): + super().__init__(parent) + self.parent = parent + self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred) + + def paintEvent(self, event): + self.adjustSize() + if self.pixmap() is not None: + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + idealWidth = self.parent.width()-45 + pixSize = self.pixmap().size() + + pixSize.scale(idealWidth, 1000000, Qt.KeepAspectRatio) + + scaledPix = self.pixmap().scaled(pixSize, Qt.KeepAspectRatio, Qt.SmoothTransformation) + painter.drawPixmap(QPoint(), scaledPix) + self.setMaximumSize(pixSize) + class PDFViewer(QScrollArea): def __init__(self, parent): super().__init__(parent) @@ -14,13 +34,9 @@ class PDFViewer(QScrollArea): self.setWidgetResizable(True) self.scrollAreaContents.setLayout(self.scrollAreaLayout) - self.pixmapList = [] - - def resizeEvent(self, event): - pass - # do something about this later + self.pixmapList = [] - def update(self, pdf): + def update_pdf(self, pdf): self.render(pdf) self.clear() self.show() @@ -32,9 +48,9 @@ class PDFViewer(QScrollArea): self.pixmapList = [] pdfView = fitz.Document(stream=pdf, filetype='pdf') - # render at 4x resolution and scale + # render at 8x resolution and scale for page in pdfView: - self.pixmapList.append(page.getPixmap(matrix=fitz.Matrix(4, 4), alpha=False)) + self.pixmapList.append(page.getPixmap(matrix=fitz.Matrix(8, 8), alpha=False)) def clear(self): while self.scrollAreaLayout.count(): @@ -45,12 +61,14 @@ class PDFViewer(QScrollArea): def show(self): for p in self.pixmapList: - label = QLabel(parent=self.scrollAreaContents) + label = PDFLabel(parent=self) label.setAlignment(Qt.AlignHCenter) qtimg = QImage(p.samples, p.width, p.height, p.stride, QImage.Format_RGB888) # -45 because of various margins... value obtained by trial and error. - label.setPixmap(QPixmap.fromImage(qtimg).scaled(self.width()-45, self.height()*2, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)) + label.setPixmap(QPixmap.fromImage(qtimg)) self.scrollAreaLayout.addWidget(label) + self.scrollAreaLayout.addStretch(1) + # necessary on Mojave with PyInstaller (or previous contents will be shown) self.repaint() \ No newline at end of file diff --git a/gui.py b/gui.py index ed53da6..dc81a3d 100755 --- a/gui.py +++ b/gui.py @@ -14,7 +14,7 @@ import os import time from copy import copy -from PyQt5.QtWidgets import QApplication, QAction, QLabel, QDialogButtonBox, QDialog, QFileDialog, QMessageBox, QPushButton, QLineEdit, QCheckBox, QSpinBox, QDoubleSpinBox, QTableWidgetItem, QTabWidget, QComboBox, QWidget, QScrollArea, QMainWindow, QShortcut +from PyQt5.QtWidgets import QApplication, QAction, QLabel, QDialogButtonBox, QDialog, QFileDialog, QMessageBox, QPushButton, QLineEdit, QCheckBox, QSpinBox, QDoubleSpinBox, QTableWidgetItem, QTabWidget, QComboBox, QWidget, QScrollArea, QMainWindow, QShortcut, QMdiSubWindow from PyQt5.QtCore import QFile, QObject, Qt, pyqtSlot, QSettings from PyQt5.QtGui import QPixmap, QImage, QKeySequence from PyQt5 import uic @@ -27,18 +27,16 @@ from reportlab.lib.pagesizes import A4, A5, LETTER, LEGAL from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont +from chordsheet.common import scriptDir from chordsheet.document import Document, Style, Chord, Block, Section from chordsheet.render import Renderer from chordsheet.parsers import parseFingering, parseName +from chordsheet.messageBox import UnsavedMessageBox, UnreadableMessageBox, ChordNameWarningMessageBox, SectionNameWarningMessageBox, BlockMustHaveSectionWarningMessageBox, VoicingWarningMessageBox, LengthWarningMessageBox +from chordsheet.dialogs import GuitarDialog, AboutDialog +from chordsheet.panels import DocInfoDockWidget, PageSetupDockWidget, ChordsDockWidget, SectionsDockWidget, BlocksDockWidget, PreviewDockWidget import _version -# set the directory where our files are depending on whether we're running a pyinstaller binary or not -if getattr(sys, 'frozen', False): - scriptDir = sys._MEIPASS -else: - scriptDir = os.path.abspath(os.path.dirname(os.path.abspath(__file__))) - # enable automatic high DPI scaling on Windows QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) QApplication.setOrganizationName("Ivan Holmes") @@ -58,12 +56,12 @@ pageSizeDict = {'A4': A4, 'A5': A5, 'Letter': LETTER, 'Legal': LEGAL} unitDict = {'mm': mm, 'cm': cm, 'inch': inch, 'point': 1, 'pica': pica} -class DocumentWindow(QMainWindow): +class MainWindow(QMainWindow): """ Class for the main window of the application. """ - def __init__(self, doc, style, filename=None): + def __init__(self, filename=None): """ Initialisation function for the main window of the application. @@ -73,18 +71,7 @@ class DocumentWindow(QMainWindow): """ super().__init__() - self.doc = doc - self.style = style - self.renderer = Renderer(self.doc, self.style) - - self.lastDoc = copy(self.doc) - self.currentFilePath = filename - - self.UIFileLoader(str(os.path.join(scriptDir, 'ui', 'mainwindow.ui'))) - self.UIInitStyle() - self.updateChordDict() - self.updateSectionDict() - self.currentSection = None + self.UIFileLoader(str(os.path.join(scriptDir, 'ui', 'new.ui'))) self.setCentralWidget(self.window.centralWidget) self.setMenuBar(self.window.menuBar) @@ -100,7 +87,7 @@ class DocumentWindow(QMainWindow): """ Reimplement the built in closeEvent to allow asking the user to save. """ - if self.saveWarning(): + if not self.window.mdiArea.subWindowList(): self.close() def UIFileLoader(self, ui_file): @@ -112,10 +99,25 @@ class DocumentWindow(QMainWindow): self.window = uic.loadUi(ui_file) ui_file.close() - + + self.docinfo = DocInfoDockWidget() + self.psetup = PageSetupDockWidget() + self.chordsw = ChordsDockWidget() + self.sectionsw = SectionsDockWidget() + self.blocksw = BlocksDockWidget() + self.previeww = PreviewDockWidget() + + self.addDockWidget(Qt.LeftDockWidgetArea, self.docinfo) + self.addDockWidget(Qt.LeftDockWidgetArea, self.psetup) + self.addDockWidget(Qt.LeftDockWidgetArea, self.previeww) + self.addDockWidget(Qt.RightDockWidgetArea, self.chordsw) + self.addDockWidget(Qt.RightDockWidgetArea, self.sectionsw) + self.addDockWidget(Qt.RightDockWidgetArea, self.blocksw) + # link all the UI elements + self.window.mdiArea.subWindowActivated.connect(self.switchDocument) + self.window.actionAbout.triggered.connect(self.menuFileAboutAction) - self.window.actionNew.triggered.connect(self.menuFileNewAction) self.window.actionOpen.triggered.connect(self.menuFileOpenAction) self.window.actionSave.triggered.connect(self.menuFileSaveAction) @@ -143,152 +145,370 @@ class DocumentWindow(QMainWindow): self.window.actionCopy.setShortcut(QKeySequence.Copy) self.window.actionPaste.setShortcut(QKeySequence.Paste) - self.window.pageSizeComboBox.currentIndexChanged.connect( + self.psetup.widget().pageSizeComboBox.currentIndexChanged.connect( self.pageSizeAction) - self.window.documentUnitsComboBox.currentIndexChanged.connect( + self.psetup.widget().documentUnitsComboBox.currentIndexChanged.connect( self.unitAction) + + self.psetup.widget().pageSizeComboBox.addItems(list(pageSizeDict.keys())) + self.psetup.widget().pageSizeComboBox.setCurrentText( + list(pageSizeDict.keys())[0]) + + self.psetup.widget().documentUnitsComboBox.addItems(list(unitDict.keys())) + self.psetup.widget().documentUnitsComboBox.setCurrentText( + list(unitDict.keys())[0]) - self.window.includedFontCheckBox.stateChanged.connect( + self.psetup.widget().includedFontCheckBox.stateChanged.connect( self.includedFontAction) - self.window.generateButton.clicked.connect(self.generateAction) + self.previeww.widget().updatePreviewButton.clicked.connect(self.generateAction) # update whole document when any tab is selected - self.window.tabWidget.tabBarClicked.connect(self.tabBarUpdateAction) + # self.window.tabWidget.tabBarClicked.connect(self.tabBarUpdateAction) - self.window.guitarVoicingButton.clicked.connect( + self.chordsw.widget().guitarVoicingButton.clicked.connect( self.guitarVoicingAction) - self.window.addChordButton.clicked.connect(self.addChordAction) - self.window.removeChordButton.clicked.connect(self.removeChordAction) - self.window.updateChordButton.clicked.connect(self.updateChordAction) + self.chordsw.widget().addChordButton.clicked.connect(self.addChordAction) + self.chordsw.widget().removeChordButton.clicked.connect(self.removeChordAction) + self.chordsw.widget().updateChordButton.clicked.connect(self.updateChordAction) # connecting clicked only works for this combo box because it's my own modified version (MComboBox) - self.window.blockSectionComboBox.clicked.connect( + self.blocksw.widget().blockSectionComboBox.clicked.connect( self.blockSectionClickedAction) - self.window.blockSectionComboBox.currentIndexChanged.connect( + self.blocksw.widget().blockSectionComboBox.currentIndexChanged.connect( self.blockSectionChangedAction) - self.window.addBlockButton.clicked.connect(self.addBlockAction) - self.window.removeBlockButton.clicked.connect(self.removeBlockAction) - self.window.updateBlockButton.clicked.connect(self.updateBlockAction) + self.blocksw.widget().addBlockButton.clicked.connect(self.addBlockAction) + self.blocksw.widget().removeBlockButton.clicked.connect(self.removeBlockAction) + self.blocksw.widget().updateBlockButton.clicked.connect(self.updateBlockAction) - self.window.addSectionButton.clicked.connect(self.addSectionAction) - self.window.removeSectionButton.clicked.connect( + self.sectionsw.widget().addSectionButton.clicked.connect(self.addSectionAction) + self.sectionsw.widget().removeSectionButton.clicked.connect( self.removeSectionAction) - self.window.updateSectionButton.clicked.connect( + self.sectionsw.widget().updateSectionButton.clicked.connect( self.updateSectionAction) - self.window.chordTableView.clicked.connect(self.chordClickedAction) - self.window.sectionTableView.clicked.connect(self.sectionClickedAction) - self.window.blockTableView.clicked.connect(self.blockClickedAction) + self.chordsw.widget().chordTableView.clicked.connect(self.chordClickedAction) + self.sectionsw.widget().sectionTableView.clicked.connect(self.sectionClickedAction) + self.blocksw.widget().blockTableView.clicked.connect(self.blockClickedAction) # Set the tab widget to Overview tab - self.window.tabWidget.setCurrentIndex(0) + # self.window.tabWidget.setCurrentIndex(0) - def UIInitDocument(self): + def UIInitDocument(self, doc): """ Fills the window's fields with the values from its document. """ - self.updateTitleBar() + # self.updateTitleBar() # set all fields to appropriate values from document - self.window.titleLineEdit.setText(self.doc.title) - self.window.subtitleLineEdit.setText(self.doc.subtitle) - self.window.composerLineEdit.setText(self.doc.composer) - self.window.arrangerLineEdit.setText(self.doc.arranger) - self.window.timeSignatureSpinBox.setValue(self.doc.timeSignature) - self.window.tempoLineEdit.setText(self.doc.tempo) - - self.window.chordTableView.populate(self.doc.chordList) - self.window.sectionTableView.populate(self.doc.sectionList) + self.docinfo.widget().titleLineEdit.setText(doc.title) + self.docinfo.widget().subtitleLineEdit.setText(doc.subtitle) + self.docinfo.widget().composerLineEdit.setText(doc.composer) + self.docinfo.widget().arrangerLineEdit.setText(doc.arranger) + self.docinfo.widget().timeSignatureSpinBox.setValue(doc.timeSignature) + self.docinfo.widget().tempoLineEdit.setText(doc.tempo) + + self.chordsw.widget().chordTableView.populate(doc.chordList) + self.sectionsw.widget().sectionTableView.populate(doc.sectionList) # populate the block table with the first section, account for a document with no sections - self.currentSection = self.doc.sectionList[0] if len( - self.doc.sectionList) else None - self.window.blockTableView.populate( + self.currentSection = doc.sectionList[0] if len( + doc.sectionList) else None + self.blocksw.widget().blockTableView.populate( self.currentSection.blockList if self.currentSection else []) - self.updateSectionDict() - self.updateChordDict() + self.updateChordDict(doc) + self.updateSectionDict(doc) - def UIInitStyle(self): + def UIInitStyle(self, style): """ Fills the window's fields with the values from its style. """ - self.window.pageSizeComboBox.addItems(list(pageSizeDict.keys())) - self.window.pageSizeComboBox.setCurrentText( - list(pageSizeDict.keys())[0]) + self.psetup.widget().pageSizeComboBox.setCurrentText( + [k for k, v in pageSizeDict.items() if v==style.pageSize][0]) - self.window.documentUnitsComboBox.addItems(list(unitDict.keys())) - self.window.documentUnitsComboBox.setCurrentText( - list(unitDict.keys())[0]) + self.psetup.widget().documentUnitsComboBox.setCurrentText( + [k for k, v in unitDict.items() if v==style.unit][0]) + + self.psetup.widget().lineSpacingDoubleSpinBox.setValue(style.lineSpacing) - self.window.lineSpacingDoubleSpinBox.setValue(self.style.lineSpacing) + self.psetup.widget().leftMarginLineEdit.setText(str(style.leftMargin)) + self.psetup.widget().rightMarginLineEdit.setText(str(style.rightMargin)) + self.psetup.widget().topMarginLineEdit.setText(str(style.topMargin)) + self.psetup.widget().bottomMarginLineEdit.setText(str(style.bottomMargin)) - self.window.leftMarginLineEdit.setText(str(self.style.leftMargin)) - self.window.rightMarginLineEdit.setText(str(self.style.rightMargin)) - self.window.topMarginLineEdit.setText(str(self.style.topMargin)) - self.window.bottomMarginLineEdit.setText(str(self.style.bottomMargin)) + self.psetup.widget().fontComboBox.setDisabled(True) + self.psetup.widget().includedFontCheckBox.setChecked(True) + + self.psetup.widget().beatWidthLineEdit.setText(str(style.unitWidth)) + + def updateChordDict(self, doc): + """ + Updates the dictionary used to generate the Chord menu (on the block tab) + """ + self.chordDict = {'None': None} + self.chordDict.update({c.name: c for c in doc.chordList}) + self.blocksw.widget().blockChordComboBox.clear() + self.blocksw.widget().blockChordComboBox.addItems(list(self.chordDict.keys())) + + def updateSectionDict(self, doc): + """ + Updates the dictionary used to generate the Section menu (on the block tab) + """ + self.sectionDict = {s.name: s for s in doc.sectionList} + self.blocksw.widget().blockSectionComboBox.clear() + self.blocksw.widget().blockSectionComboBox.addItems( + list(self.sectionDict.keys())) + + def switchDocument(self): + if self.window.mdiArea.currentSubWindow() is not None: + self.UIInitDocument(self.window.mdiArea.currentSubWindow().doc) + self.UIInitStyle(self.window.mdiArea.currentSubWindow().style) + + def generateAction(self): + if self.window.mdiArea.currentSubWindow() is not None: + self.window.mdiArea.currentSubWindow().updateDocument() + self.window.mdiArea.currentSubWindow().updatePreview() + + def removeChordAction(self): + if self.chordsw.widget().chordTableView.selectionModel().hasSelection(): #  check for selection + self.window.mdiArea.currentSubWindow().updateChords() + + row = self.chordsw.widget().chordTableView.selectionModel().currentIndex().row() + oldName = self.chordsw.widget().chordTableView.model.item(row, 0).text() + self.window.mdiArea.currentSubWindow().doc.chordList.pop(row) - self.window.fontComboBox.setDisabled(True) - self.window.includedFontCheckBox.setChecked(True) + self.chordsw.widget().chordTableView.populate(self.window.mdiArea.currentSubWindow().doc.chordList) + # remove the chord if any of the blocks have it attached + if self.currentSection is not None: + for s in self.window.mdiArea.currentSubWindow().doc.sectionList: + for b in s.blockList: + if b.chord: + if b.chord.name == oldName: + b.chord = None + self.blocksw.widget().blockTableView.populate(self.currentSection.blockList) + self.clearChordLineEdits() + self.window.mdiArea.currentSubWindow().updateChordDict() - self.window.beatWidthLineEdit.setText(str(self.style.unitWidth)) + def addChordAction(self): + success = False # initialise + self.window.mdiArea.currentSubWindow().updateChords() - def tabBarUpdateAction(self, index): - self.updateDocument() - + cName = parseName(self.chordsw.widget().chordNameLineEdit.text()) + if cName: + self.window.mdiArea.currentSubWindow().doc.chordList.append(Chord(cName)) + if self.chordsw.widget().guitarVoicingLineEdit.text() or self.chordsw.widget().pianoVoicingLineEdit.text(): + if self.chordsw.widget().guitarVoicingLineEdit.text(): + try: + self.window.mdiArea.currentSubWindow().doc.chordList[-1].voicings['guitar'] = parseFingering( + self.chordsw.widget().guitarVoicingLineEdit.text(), 'guitar') + success = True #  chord successfully parsed + except Exception: + VoicingWarningMessageBox().exec() # Voicing is malformed, warn user + if self.chordsw.widget().pianoVoicingLineEdit.text(): + try: + self.window.mdiArea.currentSubWindow().doc.chordList[-1].voicings['piano'] = parseFingering( + self.chordsw.widget().pianoVoicingLineEdit.text(), 'piano') + success = True #  chord successfully parsed + except Exception: + VoicingWarningMessageBox().exec() # Voicing is malformed, warn user + else: + success = True #  chord successfully parsed + else: + ChordNameWarningMessageBox().exec() # Chord has no name, warn user + + if success == True: # if chord was parsed properly + self.chordsw.widget().chordTableView.populate(self.window.mdiArea.currentSubWindow().doc.chordList) + self.clearChordLineEdits() + self.window.mdiArea.currentSubWindow().updateChordDict() + + def updateChordAction(self): + success = False # see comments above + if self.chordsw.widget().chordTableView.selectionModel().hasSelection(): #  check for selection + self.window.mdiArea.currentSubWindow().updateChords() + row = self.chordsw.widget().chordTableView.selectionModel().currentIndex().row() + oldName = self.chordsw.widget().chordTableView.model.item(row, 0).text() + cName = parseName(self.chordsw.widget().chordNameLineEdit.text()) + if cName: + self.window.mdiArea.currentSubWindow().doc.chordList[row].name = cName + if self.chordsw.widget().guitarVoicingLineEdit.text() or self.chordsw.widget().pianoVoicingLineEdit.text(): + if self.chordsw.widget().guitarVoicingLineEdit.text(): + try: + self.window.mdiArea.currentSubWindow().doc.chordList[row].voicings['guitar'] = parseFingering( + self.chordsw.widget().guitarVoicingLineEdit.text(), 'guitar') + success = True #  chord successfully parsed + except Exception: + VoicingWarningMessageBox().exec() # Voicing is malformed, warn user + if self.chordsw.widget().pianoVoicingLineEdit.text(): + try: + self.window.mdiArea.currentSubWindow().doc.chordList[row].voicings['piano'] = parseFingering( + self.chordsw.widget().pianoVoicingLineEdit.text(), 'piano') + success = True #  chord successfully parsed + except Exception: + VoicingWarningMessageBox().exec() # Voicing is malformed, warn user + else: + success = True #  chord successfully parsed + else: + ChordNameWarningMessageBox().exec() + + if success == True: + self.window.mdiArea.currentSubWindow().updateChordDict() + self.chordsw.widget().chordTableView.populate(self.window.mdiArea.currentSubWindow().doc.chordList) + # update the names of chords in all blocklists in case they've already been used + for s in self.window.mdiArea.currentSubWindow().doc.sectionList: + for b in s.blockList: + if b.chord: + if b.chord.name == oldName: + b.chord.name = cName + if self.currentSection and self.currentSection.blockList: + self.blocksw.widget().blockTableView.populate(self.currentSection.blockList) + self.clearChordLineEdits() + + def removeSectionAction(self): + if self.sectionsw.widget().sectionTableView.selectionModel().hasSelection(): #  check for selection + self.window.mdiArea.currentSubWindow().updateSections() + + row = self.sectionsw.widget().sectionTableView.selectionModel().currentIndex().row() + self.window.mdiArea.currentSubWindow().doc.sectionList.pop(row) + + self.sectionsw.widget().sectionTableView.populate(self.window.mdiArea.currentSubWindow().doc.sectionList) + self.clearSectionLineEdits() + self.window.mdiArea.currentSubWindow().updateSectionDict() + + def addSectionAction(self): + self.window.mdiArea.currentSubWindow().updateSections() + + sName = self.sectionsw.widget().sectionNameLineEdit.text() + if sName and sName not in [s.name for s in self.window.mdiArea.currentSubWindow().doc.sectionList]: + self.window.mdiArea.currentSubWindow().doc.sectionList.append(Section(name=sName)) + self.sectionsw.widget().sectionTableView.populate(self.window.mdiArea.currentSubWindow().doc.sectionList) + self.clearSectionLineEdits() + self.window.mdiArea.currentSubWindow().updateSectionDict() + else: + # Section has no name or non unique, warn user + SectionNameWarningMessageBox().exec() + + def updateSectionAction(self): + if self.sectionsw.widget().sectionTableView.selectionModel().hasSelection(): #  check for selection + self.window.mdiArea.currentSubWindow().updateSections() + row = self.sectionsw.widget().sectionTableView.selectionModel().currentIndex().row() + + sName = self.sectionsw.widget().sectionNameLineEdit.text() + if sName and sName not in [s.name for s in self.window.mdiArea.currentSubWindow().doc.sectionList]: + self.window.mdiArea.currentSubWindow().doc.sectionList[row].name = sName + self.sectionsw.widget().sectionTableView.populate(self.window.mdiArea.currentSubWindow().doc.sectionList) + self.clearSectionLineEdits() + self.window.mdiArea.currentSubWindow().updateSectionDict() + else: + # Section has no name or non unique, warn user + SectionNameWarningMessageBox().exec() + + def removeBlockAction(self): + if self.blocksw.widget().blockTableView.selectionModel().hasSelection(): #  check for selection + self.window.mdiArea.currentSubWindow().updateBlocks(self.currentSection) + + row = self.blocksw.widget().blockTableView.selectionModel().currentIndex().row() + self.currentSection.blockList.pop(row) + + self.blocksw.widget().blockTableView.populate(self.currentSection.blockList) + + def addBlockAction(self): + self.window.mdiArea.currentSubWindow().updateBlocks(self.currentSection) + + try: + #  can the value entered for block length be cast as a float + bLength = float(self.blocksw.widget().blockLengthLineEdit.text()) + except Exception: + bLength = False + + if bLength: # create the block + self.currentSection.blockList.append(Block(bLength, + chord=self.chordDict[self.blocksw.widget().blockChordComboBox.currentText( + )], + notes=(self.blocksw.widget().blockNotesLineEdit.text() if not "" else None))) + self.blocksw.widget().blockTableView.populate(self.currentSection.blockList) + self.clearBlockLineEdits() + else: + # show warning that length was not entered or in wrong format + LengthWarningMessageBox().exec() + + def updateBlockAction(self): + if self.blocksw.widget().blockTableView.selectionModel().hasSelection(): #  check for selection + self.window.mdiArea.currentSubWindow().updateBlocks(self.currentSection) + + try: + #  can the value entered for block length be cast as a float + bLength = float(self.blocksw.widget().blockLengthLineEdit.text()) + except Exception: + bLength = False + + row = self.blocksw.widget().blockTableView.selectionModel().currentIndex().row() + if bLength: + self.currentSection.blockList[row] = (Block(bLength, + chord=self.chordDict[self.blocksw.widget().blockChordComboBox.currentText( + )], + notes=(self.blocksw.widget().blockNotesLineEdit.text() if not "" else None))) + self.blocksw.widget().blockTableView.populate( + self.currentSection.blockList) + self.clearBlockLineEdits() + else: + LengthWarningMessageBox().exec() + def pageSizeAction(self, index): - self.pageSizeSelected = self.window.pageSizeComboBox.itemText(index) + self.pageSizeSelected = self.psetup.widget().pageSizeComboBox.itemText(index) def unitAction(self, index): - self.unitSelected = self.window.documentUnitsComboBox.itemText(index) + self.unitSelected = self.psetup.widget().documentUnitsComboBox.itemText(index) def includedFontAction(self): - if self.window.includedFontCheckBox.isChecked(): - self.style.useIncludedFont = True - else: - self.style.useIncludedFont = False - + if self.window.mdiArea.currentSubWindow() is not None: + if self.psetup.widget().includedFontCheckBox.isChecked(): + self.window.mdiArea.currentSubWindow().style.useIncludedFont = True + else: + self.window.mdiArea.currentSubWindow().style.useIncludedFont = False + def chordClickedAction(self, index): # set the controls to the values from the selected chord - self.window.chordNameLineEdit.setText( - self.window.chordTableView.model.item(index.row(), 0).text()) - self.window.guitarVoicingLineEdit.setText( - self.window.chordTableView.model.item(index.row(), 1).text()) - self.window.pianoVoicingLineEdit.setText( - self.window.chordTableView.model.item(index.row(), 2).text()) + self.chordsw.widget().chordNameLineEdit.setText( + self.chordsw.widget().chordTableView.model.item(index.row(), 0).text()) + self.chordsw.widget().guitarVoicingLineEdit.setText( + self.chordsw.widget().chordTableView.model.item(index.row(), 1).text()) + self.chordsw.widget().pianoVoicingLineEdit.setText( + self.chordsw.widget().chordTableView.model.item(index.row(), 2).text()) def sectionClickedAction(self, index): # set the controls to the values from the selected section - self.window.sectionNameLineEdit.setText( - self.window.sectionTableView.model.item(index.row(), 0).text()) + self.sectionsw.widget().sectionNameLineEdit.setText( + self.sectionsw.widget().sectionTableView.model.item(index.row(), 0).text()) # also set the combo box on the block page to make it flow well - curSecName = self.window.sectionTableView.model.item( + curSecName = self.sectionsw.widget().sectionTableView.model.item( index.row(), 0).text() if curSecName: - self.window.blockSectionComboBox.setCurrentText( + self.blocksw.widget().blockSectionComboBox.setCurrentText( curSecName) def blockSectionClickedAction(self, text): if text: - self.updateBlocks(self.sectionDict[text]) + self.window.mdiArea.currentSubWindow().updateBlocks(self.sectionDict[text]) def blockSectionChangedAction(self, index): - sName = self.window.blockSectionComboBox.currentText() + sName = self.blocksw.widget().blockSectionComboBox.currentText() if sName: - self.currentSection = self.sectionDict[sName] - self.window.blockTableView.populate(self.currentSection.blockList) + if self.window.mdiArea.currentSubWindow() is not None: + self.currentSection = self.sectionDict.get(sName, None) + if self.currentSection is not None: + self.blocksw.widget().blockTableView.populate(self.currentSection.blockList) else: self.currentSection = None def blockClickedAction(self, index): # set the controls to the values from the selected block - bChord = self.window.blockTableView.model.item(index.row(), 0).text() - self.window.blockChordComboBox.setCurrentText( + bChord = self.blocksw.widget().blockTableView.model.item(index.row(), 0).text() + self.blocksw.widget().blockChordComboBox.setCurrentText( bChord if bChord else "None") - self.window.blockLengthLineEdit.setText( - self.window.blockTableView.model.item(index.row(), 1).text()) - self.window.blockNotesLineEdit.setText( - self.window.blockTableView.model.item(index.row(), 2).text()) + self.blocksw.widget().blockLengthLineEdit.setText( + self.blocksw.widget().blockTableView.model.item(index.row(), 1).text()) + self.blocksw.widget().blockNotesLineEdit.setText( + self.blocksw.widget().blockTableView.model.item(index.row(), 2).text()) def getPath(self, value): """ @@ -303,89 +523,56 @@ class DocumentWindow(QMainWindow): return settings.setValue(value, os.path.dirname(fullpath)) def menuFileNewAction(self): - if self.saveWarning(): # ask the user if they want to save - self.doc = Document() #  new document object - # copy this object as reference to check against on quitting - self.lastDoc = copy(self.doc) - #  reset file path (this document hasn't been saved yet) - self.currentFilePath = None - # new renderer - self.renderer = Renderer(self.doc, self.style) - self.UIInitDocument() - self.updatePreview() + dw = DocumentWindow(Document(), Style(), None) + self.window.mdiArea.addSubWindow(dw) + self.UIInitDocument(dw.doc) + dw.show() + def menuFileOpenAction(self): - if self.saveWarning(): # ask the user if they want to save - filePath = QFileDialog.getOpenFileName(self.window.tabWidget, 'Open file', self.getPath( - "workingPath"), "Chordsheet Markup Language files (*.xml *.cml);;Chordsheet Macro files (*.cma)")[0] - if filePath: - self.openFile(filePath) - - def openFile(self, filePath): - """ - Opens a file from a file path and sets up the window accordingly. - """ - self.currentFilePath = filePath - - fileExt = os.path.splitext(self.currentFilePath)[1].lower() - - if fileExt == ".cma": - self.doc.loadCSMacro(self.currentFilePath) - else: # if fileExt in [".xml", ".cml"]: - self.doc.loadXML(self.currentFilePath) + filePath = QFileDialog.getOpenFileName(self.window, 'Open file', self.getPath( + "workingPath"), "Chordsheet Markup Language files (*.xml *.cml);;Chordsheet Macro files (*.cma)")[0] + if filePath: + dw = DocumentWindow.openFile(filePath) + self.window.mdiArea.addSubWindow(dw) + self.UIInitDocument(dw.doc) + self.UIInitStyle(dw.style) + self.currentSection = None - self.lastDoc = copy(self.doc) - self.setPath("workingPath", self.currentFilePath) - self.UIInitDocument() - self.updatePreview() + dw.show() def menuFileSaveAction(self): - self.updateDocument() + if self.window.mdiArea.currentSubWindow() is not None: + self.window.mdiArea.currentSubWindow().updateDocument() + + if self.window.mdiArea.currentSubWindow().currentFilePath: + fileExt = os.path.splitext(self.window.mdiArea.currentSubWindow().currentFilePath)[1].lower() + if fileExt != ".cma": + # Chordsheet Macro files can't be saved at this time + self.window.mdiArea.currentSubWindow().saveFile(self.window.mdiArea.currentSubWindow().currentFilePath) + else: + filePath = QFileDialog.getSaveFileName(self.window, 'Save file', self.getPath( + "workingPath"), "Chordsheet ML files (*.xml *.cml)")[0] + if filePath: + self.window.mdiArea.currentSubWindow().saveFile(filePath) - if self.currentFilePath: - fileExt = os.path.splitext(self.currentFilePath)[1].lower() - if fileExt != ".cma": - # Chordsheet Macro files can't be saved at this time - self.saveFile(self.currentFilePath) - else: - filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath( + def menuFileSaveAsAction(self): + if self.window.mdiArea.currentSubWindow() is not None: + self.window.mdiArea.currentSubWindow().updateDocument() + filePath = QFileDialog.getSaveFileName(self.window, 'Save file', self.getPath( "workingPath"), "Chordsheet ML files (*.xml *.cml)")[0] if filePath: - self.saveFile(filePath) - - def menuFileSaveAsAction(self): - self.updateDocument() - filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath( - "workingPath"), "Chordsheet ML files (*.xml *.cml)")[0] - if filePath: - self.saveFile(filePath) - - def saveFile(self, filePath): - """ - Saves a file to given file path and sets up environment. - """ - self.currentFilePath = filePath - - fileExt = os.path.splitext(self.currentFilePath)[1].lower() - - if fileExt == ".cma": - # At this stage we should never get here - pass - else: # if fileExt in [".xml", ".cml"]: - self.doc.saveXML(self.currentFilePath) - - self.lastDoc = copy(self.doc) - self.setPath("workingPath", self.currentFilePath) - self.updateTitleBar() # as we may have a new filename + self.window.mdiArea.currentSubWindow().saveFile(filePath) def menuFileSavePDFAction(self): - self.updateDocument() - self.updatePreview() - filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', self.getPath( - "lastExportPath"), "PDF files (*.pdf)")[0] - if filePath: - self.renderer.savePDF(filePath) - self.setPath("lastExportPath", filePath) + if self.window.mdiArea.currentSubWindow() is not None: + self.window.mdiArea.currentSubWindow().updateDocument() + self.window.mdiArea.currentSubWindow().updatePreview() + filePath = QFileDialog.getSaveFileName(self.window, 'Save file', self.getPath( + "lastExportPath"), "PDF files (*.pdf)")[0] + if filePath: + self.window.mdiArea.currentSubWindow().renderer.savePDF(filePath) + self.window.mdiArea.currentSubWindow().setPath("lastExportPath", filePath) def menuFilePrintAction(self): if sys.platform == "darwin": @@ -431,264 +618,134 @@ class DocumentWindow(QMainWindow): except Exception: pass - def saveWarning(self): - """ - Function to check if the document has unsaved data in it and offer to save it. - """ - self.updateDocument() # update the document to catch all changes - - if self.lastDoc == self.doc: - return True - else: - wantToSave = UnsavedMessageBox().exec() - - if wantToSave == QMessageBox.Save: - if not self.currentFilePath: - filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', str( - os.path.expanduser("~")), "Chordsheet ML files (*.xml *.cml)") - self.currentFilePath = filePath[0] - self.doc.saveXML(self.currentFilePath) - return True - - elif wantToSave == QMessageBox.Discard: - return True - - else: - return False - def guitarVoicingAction(self): gdialog = GuitarDialog() voicing = gdialog.getVoicing() if voicing: - self.window.guitarVoicingLineEdit.setText(voicing) + self.chordsw.widget().guitarVoicingLineEdit.setText(voicing) def clearChordLineEdits(self): - self.window.chordNameLineEdit.clear() - self.window.guitarVoicingLineEdit.clear() - self.window.pianoVoicingLineEdit.clear() + self.chordsw.widget().chordNameLineEdit.clear() + self.chordsw.widget().guitarVoicingLineEdit.clear() + self.chordsw.widget().pianoVoicingLineEdit.clear() # necessary on Mojave with PyInstaller (or previous contents will be shown) - self.window.chordNameLineEdit.repaint() - self.window.guitarVoicingLineEdit.repaint() - self.window.pianoVoicingLineEdit.repaint() + self.chordsw.widget().chordNameLineEdit.repaint() + self.chordsw.widget().guitarVoicingLineEdit.repaint() + self.chordsw.widget().pianoVoicingLineEdit.repaint() def clearSectionLineEdits(self): - self.window.sectionNameLineEdit.clear() + self.sectionsw.widget().sectionNameLineEdit.clear() # necessary on Mojave with PyInstaller (or previous contents will be shown) - self.window.sectionNameLineEdit.repaint() + self.sectionsw.widget().sectionNameLineEdit.repaint() def clearBlockLineEdits(self): - self.window.blockLengthLineEdit.clear() - self.window.blockNotesLineEdit.clear() + self.blocksw.widget().blockLengthLineEdit.clear() + self.blocksw.widget().blockNotesLineEdit.clear() # necessary on Mojave with PyInstaller (or previous contents will be shown) - self.window.blockLengthLineEdit.repaint() - self.window.blockNotesLineEdit.repaint() + self.blocksw.widget().blockLengthLineEdit.repaint() + self.blocksw.widget().blockNotesLineEdit.repaint() - def updateChordDict(self): - """ - Updates the dictionary used to generate the Chord menu (on the block tab) - """ - self.chordDict = {'None': None} - self.chordDict.update({c.name: c for c in self.doc.chordList}) - self.window.blockChordComboBox.clear() - self.window.blockChordComboBox.addItems(list(self.chordDict.keys())) - def updateSectionDict(self): - """ - Updates the dictionary used to generate the Section menu (on the block tab) - """ - self.sectionDict = {s.name: s for s in self.doc.sectionList} - self.window.blockSectionComboBox.clear() - self.window.blockSectionComboBox.addItems( - list(self.sectionDict.keys())) - def removeChordAction(self): - if self.window.chordTableView.selectionModel().hasSelection(): #  check for selection - self.updateChords() - row = self.window.chordTableView.selectionModel().currentIndex().row() - oldName = self.window.chordTableView.model.item(row, 0).text() - self.doc.chordList.pop(row) - self.window.chordTableView.populate(self.doc.chordList) - # remove the chord if any of the blocks have it attached - if self.currentSection is not None: - for s in self.doc.sectionList: - for b in s.blockList: - if b.chord: - if b.chord.name == oldName: - b.chord = None - self.window.blockTableView.populate(self.currentSection.blockList) - self.clearChordLineEdits() - self.updateChordDict() - def addChordAction(self): - success = False # initialise - self.updateChords() - cName = parseName(self.window.chordNameLineEdit.text()) - if cName: - self.doc.chordList.append(Chord(cName)) - if self.window.guitarVoicingLineEdit.text() or self.window.pianoVoicingLineEdit.text(): - if self.window.guitarVoicingLineEdit.text(): - try: - self.doc.chordList[-1].voicings['guitar'] = parseFingering( - self.window.guitarVoicingLineEdit.text(), 'guitar') - success = True #  chord successfully parsed - except Exception: - VoicingWarningMessageBox().exec() # Voicing is malformed, warn user - if self.window.pianoVoicingLineEdit.text(): - try: - self.doc.chordList[-1].voicings['piano'] = parseFingering( - self.window.pianoVoicingLineEdit.text(), 'piano') - success = True #  chord successfully parsed - except Exception: - VoicingWarningMessageBox().exec() # Voicing is malformed, warn user - else: - success = True #  chord successfully parsed - else: - ChordNameWarningMessageBox().exec() # Chord has no name, warn user - - if success == True: # if chord was parsed properly - self.window.chordTableView.populate(self.doc.chordList) - self.clearChordLineEdits() - self.updateChordDict() - - def updateChordAction(self): - success = False # see comments above - if self.window.chordTableView.selectionModel().hasSelection(): #  check for selection - self.updateChords() - row = self.window.chordTableView.selectionModel().currentIndex().row() - oldName = self.window.chordTableView.model.item(row, 0).text() - cName = parseName(self.window.chordNameLineEdit.text()) - if cName: - self.doc.chordList[row].name = cName - if self.window.guitarVoicingLineEdit.text() or self.window.pianoVoicingLineEdit.text(): - if self.window.guitarVoicingLineEdit.text(): - try: - self.doc.chordList[row].voicings['guitar'] = parseFingering( - self.window.guitarVoicingLineEdit.text(), 'guitar') - success = True #  chord successfully parsed - except Exception: - VoicingWarningMessageBox().exec() # Voicing is malformed, warn user - if self.window.pianoVoicingLineEdit.text(): - try: - self.doc.chordList[row].voicings['piano'] = parseFingering( - self.window.pianoVoicingLineEdit.text(), 'piano') - success = True #  chord successfully parsed - except Exception: - VoicingWarningMessageBox().exec() # Voicing is malformed, warn user - else: - success = True #  chord successfully parsed - else: - ChordNameWarningMessageBox().exec() - - if success == True: - self.updateChordDict() - self.window.chordTableView.populate(self.doc.chordList) - # update the names of chords in all blocklists in case they've already been used - for s in self.doc.sectionList: - for b in s.blockList: - if b.chord: - if b.chord.name == oldName: - b.chord.name = cName - if self.currentSection and self.currentSection.blockList: - self.window.blockTableView.populate(self.currentSection.blockList) - self.clearChordLineEdits() - - def removeSectionAction(self): - if self.window.sectionTableView.selectionModel().hasSelection(): #  check for selection - self.updateSections() - - row = self.window.sectionTableView.selectionModel().currentIndex().row() - self.doc.sectionList.pop(row) - - self.window.sectionTableView.populate(self.doc.sectionList) - self.clearSectionLineEdits() - self.updateSectionDict() - - def addSectionAction(self): - self.updateSections() - - sName = self.window.sectionNameLineEdit.text() - if sName and sName not in [s.name for s in self.doc.sectionList]: - self.doc.sectionList.append(Section(name=sName)) - self.window.sectionTableView.populate(self.doc.sectionList) - self.clearSectionLineEdits() - self.updateSectionDict() - else: - # Section has no name or non unique, warn user - SectionNameWarningMessageBox().exec() - - def updateSectionAction(self): - if self.window.sectionTableView.selectionModel().hasSelection(): #  check for selection - self.updateSections() - row = self.window.sectionTableView.selectionModel().currentIndex().row() - - sName = self.window.sectionNameLineEdit.text() - if sName and sName not in [s.name for s in self.doc.sectionList]: - self.doc.sectionList[row].name = sName - self.window.sectionTableView.populate(self.doc.sectionList) - self.clearSectionLineEdits() - self.updateSectionDict() - else: - # Section has no name or non unique, warn user - SectionNameWarningMessageBox().exec() - - def removeBlockAction(self): - if self.window.blockTableView.selectionModel().hasSelection(): #  check for selection - self.updateBlocks(self.currentSection) - row = self.window.blockTableView.selectionModel().currentIndex().row() - self.currentSection.blockList.pop(row) +class DocumentWindow(QMdiSubWindow): + def __init__(self, doc, style, filename): + super().__init__() + + self.UIFileLoader(str(os.path.join(scriptDir, 'ui', 'document.ui'))) + + self.doc = doc + self.style = style + self.renderer = Renderer(self.doc, self.style) - self.window.blockTableView.populate(self.currentSection.blockList) + self.lastDoc = copy(self.doc) + self.currentFilePath = filename + + mw.updateChordDict(self.doc) + mw.updateSectionDict(self.doc) + self.currentSection = None + + def UIFileLoader(self, ui_file): + ui_file = QFile(ui_file) + ui_file.open(QFile.ReadOnly) - def addBlockAction(self): - self.updateBlocks(self.currentSection) + self.setWidget(uic.loadUi(ui_file)) + ui_file.close() - try: - #  can the value entered for block length be cast as a float - bLength = float(self.window.blockLengthLineEdit.text()) - except Exception: - bLength = False + @classmethod + def openFile(cls, filePath): + dw = cls(Document(), Style(), None) + dw.loadFile(filePath) + return dw + + def loadFile(self, filePath): + """ + Opens a file from a file path and sets up the window accordingly. + """ + self.currentFilePath = filePath + + fileExt = os.path.splitext(self.currentFilePath)[1].lower() + + if fileExt == ".cma": + self.doc.loadCSMacro(self.currentFilePath) + else: # if fileExt in [".xml", ".cml"]: + self.doc.loadXML(self.currentFilePath) + + self.lastDoc = copy(self.doc) + mw.setPath("workingPath", self.currentFilePath) + + mw.updateChordDict(self.doc) + mw.updateSectionDict(self.doc) + self.updateTitleBar() + self.updatePreview() + + def saveFile(self, filePath): + """ + Saves a file to given file path and sets up environment. + """ + self.currentFilePath = filePath + + fileExt = os.path.splitext(self.currentFilePath)[1].lower() + + if fileExt == ".cma": + # At this stage we should never get here + pass + else: # if fileExt in [".xml", ".cml"]: + self.doc.saveXML(self.currentFilePath) + + self.lastDoc = copy(self.doc) + mw.setPath("workingPath", self.currentFilePath) + self.updateTitleBar() # as we may have a new filename + + def saveWarning(self): + """ + Function to check if the document has unsaved data in it and offer to save it. + """ + self.updateDocument() # update the document to catch all changes - if bLength: # create the block - self.currentSection.blockList.append(Block(bLength, - chord=self.chordDict[self.window.blockChordComboBox.currentText( - )], - notes=(self.window.blockNotesLineEdit.text() if not "" else None))) - self.window.blockTableView.populate(self.currentSection.blockList) - self.clearBlockLineEdits() + if self.lastDoc == self.doc: + return True else: - # show warning that length was not entered or in wrong format - LengthWarningMessageBox().exec() - - def updateBlockAction(self): - if self.window.blockTableView.selectionModel().hasSelection(): #  check for selection - self.updateBlocks(self.currentSection) + wantToSave = UnsavedMessageBox().exec() - try: - #  can the value entered for block length be cast as a float - bLength = float(self.window.blockLengthLineEdit.text()) - except Exception: - bLength = False + if wantToSave == QMessageBox.Save: + if not self.currentFilePath: + filePath = QFileDialog.getSaveFileName(self.window, 'Save file', str( + os.path.expanduser("~")), "Chordsheet ML files (*.xml *.cml)") + self.currentFilePath = filePath[0] + self.doc.saveXML(self.currentFilePath) + return True - row = self.window.blockTableView.selectionModel().currentIndex().row() - if bLength: - self.currentSection.blockList[row] = (Block(bLength, - chord=self.chordDict[self.window.blockChordComboBox.currentText( - )], - notes=(self.window.blockNotesLineEdit.text() if not "" else None))) - self.window.blockTableView.populate( - self.currentSection.blockList) - self.clearBlockLineEdits() + elif wantToSave == QMessageBox.Discard: + return True + else: - LengthWarningMessageBox().exec() - - def generateAction(self): - self.updateDocument() - self.updatePreview() + return False def updatePreview(self): """ @@ -699,33 +756,36 @@ class DocumentWindow(QMainWindow): except Exception: QMessageBox.warning(self, "Preview failed", "Could not update the preview.", buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok) + + + with open('preview.pdf', 'wb') as f: + f.write(self.currentPreview.getbuffer()) - self.window.pdfArea.update(self.currentPreview) + self.widget().pdfArea.update_pdf(self.currentPreview) def updateTitleBar(self): """ Update the application's title bar to reflect the current document. """ if self.currentFilePath: - self.setWindowTitle(_version.appName + " – " + - os.path.basename(self.currentFilePath)) + self.widget().setWindowTitle(os.path.basename(self.currentFilePath)) else: - self.setWindowTitle(_version.appName) + self.widget().setWindowTitle("Unsaved") def updateChords(self): """ Update the chord list by reading the table. """ chordTableList = [] - for i in range(self.window.chordTableView.model.rowCount()): + for i in range(mw.chordsw.widget().chordTableView.model.rowCount()): chordTableList.append( - Chord(parseName(self.window.chordTableView.model.item(i, 0).text()))), - if self.window.chordTableView.model.item(i, 1).text(): + Chord(parseName(mw.chordsw.widget().chordTableView.model.item(i, 0).text()))), + if mw.chordsw.widget().chordTableView.model.item(i, 1).text(): chordTableList[-1].voicings['guitar'] = parseFingering( - self.window.chordTableView.model.item(i, 1).text(), 'guitar') - if self.window.chordTableView.model.item(i, 2).text(): + mw.chordsw.widget().chordTableView.model.item(i, 1).text(), 'guitar') + if mw.chordsw.widget().chordTableView.model.item(i, 2).text(): chordTableList[-1].voicings['piano'] = parseFingering( - self.window.chordTableView.model.item(i, 2).text(), 'piano') + mw.chordsw.widget().chordTableView.model.item(i, 2).text(), 'piano') self.doc.chordList = chordTableList @@ -748,9 +808,9 @@ class DocumentWindow(QMainWindow): Update the section list by reading the table """ sectionTableList = [] - for i in range(self.window.sectionTableView.model.rowCount()): + for i in range(mw.sectionsw.widget().sectionTableView.model.rowCount()): sectionTableList.append(self.matchSection( - self.window.sectionTableView.model.item(i, 0).text())) + mw.sectionsw.widget().sectionTableView.model.item(i, 0).text())) self.doc.sectionList = sectionTableList @@ -762,53 +822,52 @@ class DocumentWindow(QMainWindow): BlockMustHaveSectionWarningMessageBox().exec() else: blockTableList = [] - for i in range(self.window.blockTableView.model.rowCount()): + for i in range(mw.blocksw.widget().blockTableView.model.rowCount()): blockLength = float( - self.window.blockTableView.model.item(i, 1).text()) - blockChord = self.chordDict[(self.window.blockTableView.model.item( - i, 0).text() if self.window.blockTableView.model.item(i, 0).text() else "None")] - blockNotes = self.window.blockTableView.model.item(i, 2).text( - ) if self.window.blockTableView.model.item(i, 2).text() else None + mw.blocksw.widget().blockTableView.model.item(i, 1).text()) + blockChord = mw.chordDict[(mw.blocksw.widget().blockTableView.model.item( + i, 0).text() if mw.blocksw.widget().blockTableView.model.item(i, 0).text() else "None")] + blockNotes = mw.blocksw.widget().blockTableView.model.item(i, 2).text( + ) if mw.blocksw.widget().blockTableView.model.item(i, 2).text() else None blockTableList.append( Block(blockLength, chord=blockChord, notes=blockNotes)) section.blockList = blockTableList - def updateDocument(self): """ Update the Document object by reading values from the UI. """ - self.doc.title = self.window.titleLineEdit.text( + self.doc.title = mw.docinfo.widget().titleLineEdit.text( ) # Title can be empty string but not None - self.doc.subtitle = (self.window.subtitleLineEdit.text( - ) if self.window.subtitleLineEdit.text() else None) - self.doc.composer = (self.window.composerLineEdit.text( - ) if self.window.composerLineEdit.text() else None) - self.doc.arranger = (self.window.arrangerLineEdit.text( - ) if self.window.arrangerLineEdit.text() else None) - self.doc.tempo = (self.window.tempoLineEdit.text() - if self.window.tempoLineEdit.text() else None) - self.doc.timeSignature = int(self.window.timeSignatureSpinBox.value( - )) if self.window.timeSignatureSpinBox.value() else self.doc.timeSignature - - self.style.pageSize = pageSizeDict[self.pageSizeSelected] - self.style.unit = unitDict[self.unitSelected] - self.style.leftMargin = float(self.window.leftMarginLineEdit.text( - )) if self.window.leftMarginLineEdit.text() else self.style.leftMargin - self.style.rightMargin = float(self.window.rightMarginLineEdit.text( - )) if self.window.rightMarginLineEdit.text() else self.style.rightMargin - self.style.topMargin = float(self.window.topMarginLineEdit.text( - )) if self.window.topMarginLineEdit.text() else self.style.topMargin - self.style.bottomMargin = float(self.window.bottomMarginLineEdit.text( - )) if self.window.bottomMarginLineEdit.text() else self.style.bottomMargin - self.style.lineSpacing = float(self.window.lineSpacingDoubleSpinBox.value( - )) if self.window.lineSpacingDoubleSpinBox.value() else self.style.lineSpacing + self.doc.subtitle = (mw.docinfo.widget().subtitleLineEdit.text( + ) if mw.docinfo.widget().subtitleLineEdit.text() else None) + self.doc.composer = (mw.docinfo.widget().composerLineEdit.text( + ) if mw.docinfo.widget().composerLineEdit.text() else None) + self.doc.arranger = (mw.docinfo.widget().arrangerLineEdit.text( + ) if mw.docinfo.widget().arrangerLineEdit.text() else None) + self.doc.tempo = (mw.docinfo.widget().tempoLineEdit.text( + ) if mw.docinfo.widget().tempoLineEdit.text() else None) + self.doc.timeSignature = int(mw.docinfo.widget().timeSignatureSpinBox.value( + )) if mw.docinfo.widget().timeSignatureSpinBox.value() else self.doc.timeSignature + + self.style.pageSize = pageSizeDict[mw.pageSizeSelected] + self.style.unit = unitDict[mw.unitSelected] + self.style.leftMargin = float(mw.psetup.widget().leftMarginLineEdit.text( + )) if mw.psetup.widget().leftMarginLineEdit.text() else self.style.leftMargin + self.style.rightMargin = float(mw.psetup.widget().rightMarginLineEdit.text( + )) if mw.psetup.widget().rightMarginLineEdit.text() else self.style.rightMargin + self.style.topMargin = float(mw.psetup.widget().topMarginLineEdit.text( + )) if mw.psetup.widget().topMarginLineEdit.text() else self.style.topMargin + self.style.bottomMargin = float(mw.psetup.widget().bottomMarginLineEdit.text( + )) if mw.psetup.widget().bottomMarginLineEdit.text() else self.style.bottomMargin + self.style.lineSpacing = float(mw.psetup.widget().lineSpacingDoubleSpinBox.value( + )) if mw.psetup.widget().lineSpacingDoubleSpinBox.value() else self.style.lineSpacing # make sure the unit width isn't too wide to draw! - if self.window.beatWidthLineEdit.text(): - if (self.style.pageSize[0] - 2 * self.style.leftMargin * mm) >= (float(self.window.beatWidthLineEdit.text()) * 2 * self.doc.timeSignature * mm): + if mw.psetup.widget().beatWidthLineEdit.text(): + if (self.style.pageSize[0] - 2 * self.style.leftMargin * mm) >= (float(mw.psetup.widget().beatWidthLineEdit.text()) * 2 * self.doc.timeSignature * mm): self.style.unitWidth = float( - self.window.beatWidthLineEdit.text()) + mw.psetup.widget().beatWidthLineEdit.text()) else: maxBeatWidth = ( self.style.pageSize[0] - 2 * self.style.leftMargin * mm) / (2 * self.doc.timeSignature * mm) @@ -818,201 +877,18 @@ class DocumentWindow(QMainWindow): # update chords, sections, blocks self.updateChords() self.updateSections() - if self.currentSection: - self.updateBlocks(self.currentSection) + if mw.currentSection: + self.updateBlocks(mw.currentSection) self.style.font = ( 'FreeSans' if self.style.useIncludedFont else 'HelveticaNeue') # something for the font box here - -class GuitarDialog(QDialog): - """ - Dialogue to allow the user to enter a guitar chord voicing. Not particularly advanced at present! - May be extended in future. - """ - - def __init__(self): - super().__init__() - self.UIFileLoader( - str(os.path.join(scriptDir, 'ui', 'guitardialog.ui'))) - - def UIFileLoader(self, ui_file): - ui_file = QFile(ui_file) - ui_file.open(QFile.ReadOnly) - - self.dialog = uic.loadUi(ui_file) - ui_file.close() - - def getVoicing(self): - """ - Show the dialogue and return the voicing that has been entered. - """ - if self.dialog.exec_() == QDialog.Accepted: - result = [self.dialog.ELineEdit.text(), - self.dialog.ALineEdit.text(), - self.dialog.DLineEdit.text(), - self.dialog.GLineEdit.text(), - self.dialog.BLineEdit.text(), - self.dialog.eLineEdit.text()] - resultJoined = ",".join(result) - return resultJoined - else: - return None - - -class AboutDialog(QDialog): - """ - Dialogue showing information about the program. - """ - - def __init__(self): - super().__init__() - self.UIFileLoader(str(os.path.join(scriptDir, 'ui', 'aboutdialog.ui'))) - - icon = QImage(str(os.path.join(scriptDir, 'ui', 'icon.png'))) - self.dialog.iconLabel.setPixmap(QPixmap.fromImage(icon).scaled(self.dialog.iconLabel.width( - ), self.dialog.iconLabel.height(), Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)) - - self.dialog.versionLabel.setText("Version " + _version.version) - - self.dialog.exec() - - def UIFileLoader(self, ui_file): - ui_file = QFile(ui_file) - ui_file.open(QFile.ReadOnly) - - self.dialog = uic.loadUi(ui_file) - ui_file.close() - - -class UnsavedMessageBox(QMessageBox): - """ - Message box to alert the user of unsaved changes and allow them to choose how to act. - """ - - def __init__(self): - super().__init__() - - self.setIcon(QMessageBox.Question) - self.setWindowTitle("Unsaved changes") - self.setText("The document has been modified.") - self.setInformativeText("Do you want to save your changes?") - self.setStandardButtons( - QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) - self.setDefaultButton(QMessageBox.Save) - - -class UnreadableMessageBox(QMessageBox): - """ - Message box to warn the user that the chosen file cannot be opened. - """ - - def __init__(self): - super().__init__() - - self.setIcon(QMessageBox.Warning) - self.setWindowTitle("File cannot be opened") - self.setText("The file you have selected cannot be opened.") - self.setInformativeText("Please make sure it is in the right format.") - self.setStandardButtons(QMessageBox.Ok) - self.setDefaultButton(QMessageBox.Ok) - - -class ChordNameWarningMessageBox(QMessageBox): - """ - Message box to warn the user that a chord must have a name - """ - - def __init__(self): - super().__init__() - - self.setIcon(QMessageBox.Warning) - self.setWindowTitle("Unnamed chord") - self.setText("Chords must have a name.") - self.setInformativeText("Please give your chord a name and try again.") - self.setStandardButtons(QMessageBox.Ok) - self.setDefaultButton(QMessageBox.Ok) - - -class SectionNameWarningMessageBox(QMessageBox): - """ - Message box to warn the user that a section must have a name - """ - - def __init__(self): - super().__init__() - - self.setIcon(QMessageBox.Warning) - self.setWindowTitle("Unnamed section") - self.setText("Sections must have a unique name.") - self.setInformativeText( - "Please give your section a unique name and try again.") - self.setStandardButtons(QMessageBox.Ok) - self.setDefaultButton(QMessageBox.Ok) - - -class BlockMustHaveSectionWarningMessageBox(QMessageBox): - """ - Message box to warn the user that a block must belong to a section - """ - - def __init__(self): - super().__init__() - - self.setIcon(QMessageBox.Warning) - self.setWindowTitle("No sections found") - self.setText("Each block must belong to a section, but no sections have yet been created.") - self.setInformativeText( - "Please create a section before adding blocks.") - self.setStandardButtons(QMessageBox.Ok) - self.setDefaultButton(QMessageBox.Ok) - - -class VoicingWarningMessageBox(QMessageBox): - """ - Message box to warn the user that the voicing entered could not be parsed - """ - - def __init__(self): - super().__init__() - - self.setIcon(QMessageBox.Warning) - self.setWindowTitle("Malformed voicing") - self.setText( - "The voicing you entered was not understood and has not been applied.") - self.setInformativeText( - "Please try re-entering it in the correct format.") - self.setStandardButtons(QMessageBox.Ok) - self.setDefaultButton(QMessageBox.Ok) - - -class LengthWarningMessageBox(QMessageBox): - """ - Message box to warn the user that a block must have a length - """ - - def __init__(self): - super().__init__() - - self.setIcon(QMessageBox.Warning) - self.setWindowTitle("Block without valid length") - self.setText("Blocks must have a length.") - self.setInformativeText( - "Please enter a valid length for your block and try again.") - self.setStandardButtons(QMessageBox.Ok) - self.setDefaultButton(QMessageBox.Ok) - - if __name__ == '__main__': app = QApplication(sys.argv) - d = Document() - s = Style() - # pass first argument as filename - w = DocumentWindow(d, s, filename=( - sys.argv[1] if len(sys.argv) > 1 else None)) - w.show() + mw = MainWindow() + mw.show() sys.exit(app.exec_()) diff --git a/preview.pdf b/preview.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b51a7e93b53f0120accccce4066996b2d34af76c GIT binary patch literal 20738 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

vV~t5~W$acS+|#w?ZQf{7~_&9luBqZ`ty!_V=bg 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 R*E%DaDDk6E_%ga5{{oqSOi%y- literal 0 HcmV?d00001 diff --git a/ui/blocks.ui b/ui/blocks.ui new file mode 100644 index 0000000..82ab355 --- /dev/null +++ b/ui/blocks.ui @@ -0,0 +1,225 @@ + + + blocksWidget + + + + 0 + 0 + 437 + 371 + + + + Blocks + + + + + + + + QFormLayout::ExpandingFieldsGrow + + + + + Section + + + + + + + + 0 + 0 + + + + + + + + + + + 0 + 0 + + + + QAbstractItemView::NoEditTriggers + + + true + + + false + + + QAbstractItemView::InternalMove + + + Qt::TargetMoveAction + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + false + + + + + + + + + Length + + + + + + + + + + + 0 + 0 + + + + Notes + + + + + + + Chord + + + + + + + + 0 + 0 + + + + + 40 + 16777215 + + + + + + + + + 0 + 0 + + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + + + + + Remove block + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + Update block + + + + + + + Add block + + + + + + + + + + + + BlockTableView + QTableView +

chordsheet/tableView.h
+ + + MComboBox + QComboBox +
chordsheet/comboBox.h
+
+ + + blockSectionComboBox + blockTableView + blockLengthLineEdit + blockChordComboBox + blockNotesLineEdit + removeBlockButton + updateBlockButton + addBlockButton + + + + diff --git a/ui/chords.ui b/ui/chords.ui new file mode 100644 index 0000000..7f64fe4 --- /dev/null +++ b/ui/chords.ui @@ -0,0 +1,190 @@ + + + chordsWidget + + + + 0 + 0 + 443 + 359 + + + + Chords + + + + + + + + + 0 + 0 + + + + QAbstractItemView::NoEditTriggers + + + true + + + false + + + QAbstractItemView::InternalMove + + + Qt::IgnoreAction + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + false + + + false + + + + + + + + + Chord name + + + + + + + + 0 + 0 + + + + + + + + Guitar voicing + + + + + + + + 0 + 0 + + + + + 100 + 16777215 + + + + + + + + + 16777215 + 16777215 + + + + Editor... + + + + + + + + + + Piano voicing + + + + + + + + + + + Remove chord + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + Update chord + + + + + + + Add chord + + + + + + + + + + + + ChordTableView + QTableView +
chordsheet/tableView.h
+
+
+ + chordTableView + chordNameLineEdit + guitarVoicingLineEdit + guitarVoicingButton + pianoVoicingLineEdit + removeChordButton + updateChordButton + addChordButton + + + +
diff --git a/ui/docinfo.ui b/ui/docinfo.ui new file mode 100644 index 0000000..3f5b625 --- /dev/null +++ b/ui/docinfo.ui @@ -0,0 +1,144 @@ + + + docInfoWidget + + + Qt::NonModal + + + + 0 + 0 + 400 + 202 + + + + + 0 + 0 + + + + Document information + + + + + + QFormLayout::ExpandingFieldsGrow + + + + + Title + + + + + + + + 0 + 0 + + + + + + + + Subtitle + + + + + + + + + + Composer + + + + + + + + 0 + 0 + + + + + + + + Arranger + + + + + + + + 0 + 0 + + + + + + + + Tempo + + + + + + + + 0 + 0 + + + + + 60 + 16777215 + + + + + + + + Time + + + + + + + + 40 + 16777215 + + + + + + + 4 + + + + + + + + + + diff --git a/ui/document.ui b/ui/document.ui new file mode 100644 index 0000000..9ed6722 --- /dev/null +++ b/ui/document.ui @@ -0,0 +1,57 @@ + + + docWindow + + + + 0 + 0 + 424 + 324 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 400 + 300 + + + + + + + + + PDFViewer + QWidget +
chordsheet/pdfViewer.h
+ 1 +
+
+ + +
diff --git a/ui/new.ui b/ui/new.ui new file mode 100644 index 0000000..60a8b94 --- /dev/null +++ b/ui/new.ui @@ -0,0 +1,172 @@ + + + MainWindow + + + + 0 + 0 + 1061 + 659 + + + + Chordsheet + + + false + + + QTabWidget::Rounded + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + true + + + true + + + true + + + + + + + + + 0 + 0 + 1061 + 22 + + + + + File + + + + + + + + + + + + + + + Edit + + + + + + + + + + + + + + + + New... + + + + + Open... + + + + + Save + + + + + Save PDF... + + + + + Print... + + + + + Close + + + + + Save as... + + + + + Quit + + + + + Undo + + + + + Redo + + + + + Cut + + + + + Copy + + + + + Paste + + + + + Preferences + + + + + About + + + + + + diff --git a/ui/pdfarea.ui b/ui/pdfarea.ui new file mode 100644 index 0000000..ce109c2 --- /dev/null +++ b/ui/pdfarea.ui @@ -0,0 +1,32 @@ + + + Form + + + + 0 + 0 + 400 + 300 + + + + PDF Viewer + + + + + + + + + + PDFViewer + QWidget +
chordsheet/pdfViewer.h
+ 1 +
+
+ + +
diff --git a/ui/preview.ui b/ui/preview.ui new file mode 100644 index 0000000..24e543a --- /dev/null +++ b/ui/preview.ui @@ -0,0 +1,52 @@ + + + previewPanel + + + + 0 + 0 + 400 + 40 + + + + + 0 + 0 + + + + + 0 + 40 + + + + Preview + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Update preview + + + + + + + + diff --git a/ui/psetup.ui b/ui/psetup.ui new file mode 100644 index 0000000..5ca2c8e --- /dev/null +++ b/ui/psetup.ui @@ -0,0 +1,273 @@ + + + psetupWidget + + + + 0 + 0 + 400 + 500 + + + + + 0 + 0 + + + + Page setup + + + + + + + 0 + 0 + + + + Page options + + + + + + + + Page size + + + + + + + + + + Document units + + + + + + + + + + Left margin + + + + + + + + 60 + 16777215 + + + + + + + + Top margin + + + + + + + + 60 + 16777215 + + + + + + + + Right margin + + + + + + + + 60 + 16777215 + + + + + + + + Bottom margin + + + + + + + + 60 + 16777215 + + + + + + + + + + + + + + 0 + 0 + + + + Font options + + + + + + + + + 40 + 16777215 + + + + Font + + + + + + + + 0 + 0 + + + + + + + + + + Use included FreeSans + + + + + + + + + + + 0 + 0 + + + + Text options + + + + + + + + Line spacing + + + + + + + + 70 + 0 + + + + + 70 + 16777215 + + + + + + + + + + + + + + 0 + 0 + + + + Block options + + + + + + + + Beat width + + + + + + + + 60 + 16777215 + + + + + + + + + + + + + pageSizeComboBox + documentUnitsComboBox + leftMarginLineEdit + rightMarginLineEdit + topMarginLineEdit + bottomMarginLineEdit + lineSpacingDoubleSpinBox + fontComboBox + includedFontCheckBox + beatWidthLineEdit + + + + diff --git a/ui/sections.ui b/ui/sections.ui new file mode 100644 index 0000000..9280c89 --- /dev/null +++ b/ui/sections.ui @@ -0,0 +1,127 @@ + + + sectionsWidget + + + + 0 + 0 + 431 + 325 + + + + Sections + + + + + + + + + 0 + 0 + + + + QAbstractItemView::NoEditTriggers + + + true + + + false + + + QAbstractItemView::InternalMove + + + Qt::TargetMoveAction + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + false + + + + + + + QFormLayout::ExpandingFieldsGrow + + + + + Name + + + + + + + + + + + + + + Remove section + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 0 + 20 + + + + + + + + Update section + + + + + + + Add section + + + + + + + + + + + + SectionTableView + QTableView +
chordsheet/tableView.h
+
+
+ + +
diff --git a/version.rc b/version.rc index 07aba0e..85a8fc9 100644 --- a/version.rc +++ b/version.rc @@ -6,8 +6,8 @@ VSVersionInfo( ffi=FixedFileInfo( # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) # Set not needed items to zero 0. -filevers=(0, 4, 6, 0), -prodvers=(0, 4, 6, 0), +filevers=(0, 5, 0, 0), +prodvers=(0, 5, 0, 0), # Contains a bitmask that specifies the valid bits 'flags'r mask=0x3f, # Contains a bitmask that specifies the Boolean attributes of the file. @@ -31,12 +31,12 @@ StringFileInfo( u'040904B0', [StringStruct(u'CompanyName', u'Ivan Holmes'), StringStruct(u'FileDescription', u'Chordsheet'), - StringStruct(u'FileVersion', u'0.4.6'), + StringStruct(u'FileVersion', u'0.5.0'), StringStruct(u'InternalName', u'Chordsheet'), StringStruct(u'LegalCopyright', u'Copyright (c) Ivan Holmes, 2020. Some rights reserved.'), StringStruct(u'OriginalFilename', u'chordsheet.exe'), StringStruct(u'ProductName', u'Chordsheet'), - StringStruct(u'ProductVersion', u'0.4.6')]) + StringStruct(u'ProductVersion', u'0.5.0')]) ]), VarFileInfo([VarStruct(u'Translation', [1033, 1200])]) ]