@ -7,10 +7,11 @@ Created on Wed May 29 00:02:24 2019
"""
import sys , fitz , io , subprocess , os
from copy import copy
from PyQt5.QtWidgets import QApplication , QAction , QLabel , QDialogButtonBox , QDialog , QFileDialog , QMessageBox , QPushButton , QLineEdit , QCheckBox , QSpinBox , QDoubleSpinBox , QTableWidget , QTableWidget Item, QTabWidget , QComboBox , QWidget , QScrollArea
from PyQt5.QtCore import QFile , QObject , Qt
from PyQt5.QtGui import QPixmap , QImage
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 , pyqtSlot , QSettings
from PyQt5.QtGui import QPixmap , QImage , QKeySequence
from PyQt5 import uic
from chordsheet.tableView import ChordTableView , BlockTableView , MItemModel , MProxyStyle
@ -23,38 +24,78 @@ from chordsheet.document import Document, Style, Chord, Block
from chordsheet.render import savePDF
from chordsheet.parsers import parseFingering , parseName
# 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__ ) ) )
QApplication . setAttribute ( Qt . AA_EnableHighDpiScaling , True )
QApplication . setAttribute ( Qt . AA_EnableHighDpiScaling , True ) # enable automatic high DPI scaling on Windows
QApplication . setOrganizationName ( " Ivan Holmes " )
QApplication . setOrganizationDomain ( " ivanholmes.co.uk " )
QApplication . setApplicationName ( " Chordsheet " )
settings = QSettings ( )
pdfmetrics . registerFont ( TTFont ( ' FreeSans ' , os . path . join ( scriptDir , ' fonts ' , ' FreeSans.ttf ' ) ) )
if sys . platform == " darwin " :
pdfmetrics . registerFont ( TTFont ( ' HelveticaNeue ' , ' HelveticaNeue.ttc ' , subfontIndex = 0 ) )
# dictionaries for combo boxes
pageSizeDict = { ' A4 ' : A4 , ' A5 ' : A5 , ' Letter ' : LETTER , ' Legal ' : LEGAL }
unitDict = { ' mm ' : mm , ' cm ' : cm , ' inch ' : inch , ' point ' : 1 , ' pica ' : pica } # point is 1 because reportlab's native unit is points.
class DocumentWindow ( QWidget ) :
def __init__ ( self , doc , style , parent = None ) :
super ( ) . __init__ ( parent )
class DocumentWindow ( QMainWindow ) :
"""
Class for the main window of the application .
"""
def __init__ ( self , doc , style , filename = None ) :
"""
Initialisation function for the main window of the application .
Arguments :
doc - - the Document object for the window to use
style - - the Style object for the window to use
"""
super ( ) . __init__ ( )
self . doc = doc
self . style = style
self . lastDoc = copy ( self . doc )
self . currentFilePath = filename
self . UIFileLoader ( str ( os . path . join ( scriptDir , ' ui ' , ' mainwindow.ui ' ) ) )
self . UIInitStyle ( )
# self.UIInitDocument()
self . setCentralWidget ( self . window . centralWidget )
self . setMenuBar ( self . window . menuBar )
self . setWindowTitle ( " Chordsheet " )
if filename :
try :
self . doc . loadXML ( filename )
except :
UnreadableMessageBox ( ) . exec ( )
def closeEvent ( self , event ) :
"""
Reimplement the built in closeEvent to allow asking the user to save .
"""
self . saveWarning ( )
def UIFileLoader ( self , ui_file ) :
"""
Loads the . ui file for this window and connects the UI elements to their actions .
"""
ui_file = QFile ( ui_file )
ui_file . open ( QFile . ReadOnly )
self . window = uic . loadUi ( ui_file )
ui_file . close ( )
# link all the UI elements
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 )
@ -63,6 +104,19 @@ class DocumentWindow(QWidget):
self . window . actionPrint . triggered . connect ( self . menuFilePrintAction )
self . window . actionClose . triggered . connect ( self . menuFileCloseAction )
self . window . actionNew . setShortcut ( QKeySequence . New )
self . window . actionOpen . setShortcut ( QKeySequence . Open )
self . window . actionSave . setShortcut ( QKeySequence . Save )
self . window . actionSave_as . setShortcut ( QKeySequence . SaveAs )
self . window . actionSave_PDF . setShortcut ( QKeySequence ( " Ctrl+E " ) )
self . window . actionPrint . setShortcut ( QKeySequence . Print )
self . window . actionClose . setShortcut ( QKeySequence . Close )
self . window . actionUndo . setShortcut ( QKeySequence . Undo )
self . window . actionRedo . setShortcut ( QKeySequence . Redo )
self . window . actionCut . setShortcut ( QKeySequence . Cut )
self . window . actionCopy . setShortcut ( QKeySequence . Copy )
self . window . actionPaste . setShortcut ( QKeySequence . Paste )
self . window . pageSizeComboBox . currentIndexChanged . connect ( self . pageSizeAction )
self . window . documentUnitsComboBox . currentIndexChanged . connect ( self . unitAction )
@ -83,21 +137,25 @@ class DocumentWindow(QWidget):
self . window . blockTableView . clicked . connect ( self . blockClickedAction )
def UIInitDocument ( self ) :
"""
Fills the window ' s fields with the values from its document.
"""
self . updateTitleBar ( )
# set all fields to appropriate values from document
self . window . titleLineEdit . setText ( self . doc . title )
self . window . composerLineEdit . setText ( self . doc . composer )
self . window . arrangerLineEdit . setText ( self . doc . arranger )
self . window . timeSignatureSpinBox . setValue ( self . doc . timeSignature )
# chord and block table lists here
self . window . chordTableView . populate ( self . doc . chordList )
self . window . blockTableView . populate ( self . doc . blockList )
self . updateChordDict ( )
self . window . tabWidget . setCurrentWidget ( self . window . tabWidget . findChild ( QWidget , ' Overview ' ) )
# self.updatePreview()
def UIInitStyle ( self ) :
"""
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 ] )
@ -133,37 +191,61 @@ class DocumentWindow(QWidget):
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 ( ) )
def getPath ( self , value ) :
"""
Wrapper for Qt settings to return home directory if no setting exists .
"""
return str ( ( settings . value ( value ) if settings . value ( value ) else os . path . expanduser ( " ~ " ) ) )
def setPath ( self , value , fullpath ) :
"""
Wrapper for Qt settings to set path to open / save from next time from current file location.
"""
return settings . setValue ( value , os . path . dirname ( fullpath ) )
def menuFileNewAction ( self ) :
self . doc = Document ( )
self . lastDoc = copy ( self . doc )
self . currentFilePath = None
self . UIInitDocument ( )
self . updatePreview ( )
def menuFileOpenAction ( self ) :
filePath = QFileDialog . getOpenFileName ( self . window . tabWidget , ' Open file ' , str ( os . path . expanduser ( " ~ " ) ) , " Chordsheet ML files (*.xml *.cml) " )
filePath = QFileDialog . getOpenFileName ( self . window . tabWidget , ' Open file ' , self . getPath ( " workingPath " ) , " Chordsheet ML files (*.xml *.cml) " )
if filePath [ 0 ] :
self . currentFilePath = filePath [ 0 ]
self . doc . loadXML ( filePath [ 0 ] )
self . lastDoc = copy ( self . doc )
self . setPath ( " workingPath " , self . currentFilePath )
self . UIInitDocument ( )
self . updatePreview ( )
def menuFileSaveAction ( self ) :
self . updateDocument ( )
if not ( hasattr ( self , ' currentFilePath ' ) and self . currentFilePath ) :
filePath = QFileDialog . getSaveFileName ( self . window . tabWidget , ' Save file ' , str ( os . path . expanduser ( " ~ " ) ) , " Chordsheet ML files (*.xml *.cml) " )
filePath = QFileDialog . getSaveFileName ( self . window . tabWidget , ' Save file ' , self . getPath ( " workingPath " ) , " Chordsheet ML files (*.xml *.cml) " )
self . currentFilePath = filePath [ 0 ]
self . doc . saveXML ( self . currentFilePath )
self . lastDoc = copy ( self . doc )
self . setPath ( " workingPath " , self . currentFilePath )
def menuFileSaveAsAction ( self ) :
self . updateDocument ( )
filePath = QFileDialog . getSaveFileName ( self . window . tabWidget , ' Save file ' , str ( os . path . expanduser ( " ~ " ) ) , " Chordsheet ML files (*.xml *.cml) " )
filePath = QFileDialog . getSaveFileName ( self . window . tabWidget , ' Save file ' , self . getPath ( " workingPath " ) , " Chordsheet ML files (*.xml *.cml) " )
if filePath [ 0 ] :
self . currentFilePath = filePath [ 0 ]
self . doc . saveXML ( self . currentFilePath )
self . lastDoc = copy ( self . doc )
self . setPath ( " workingPath " , self . currentFilePath )
self . updateTitleBar ( ) # as we now have a new filename
def menuFileSavePDFAction ( self ) :
self . updateDocument ( )
self . updatePreview ( )
filePath = QFileDialog . getSaveFileName ( self . window . tabWidget , ' Save file ' , str ( os . path . expanduser ( " ~ " ) ) , " PDF files (*.pdf) " )
filePath = QFileDialog . getSaveFileName ( self . window . tabWidget , ' Save file ' , self . getPath ( " lastExportPath " ) , " PDF files (*.pdf) " )
if filePath [ 0 ] :
savePDF ( d , s , filePath [ 0 ] )
self . setPath ( " lastExportPath " , filePath [ 0 ] )
def menuFilePrintAction ( self ) :
if sys . platform == " darwin " :
@ -172,8 +254,34 @@ class DocumentWindow(QWidget):
else :
pass
@pyqtSlot ( )
def menuFileCloseAction ( self ) :
pass
self . saveWarning ( )
def menuFileAboutAction ( self ) :
aboutDialog = QMessageBox . information ( self , " About " , " Chordsheet © Ivan Holmes, 2019 " , buttons = QMessageBox . Ok , defaultButton = QMessageBox . Ok )
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 ) :
self . close ( )
else :
wantToSave = UnsavedMessageBox ( ) . exec ( )
if wantToSave == QMessageBox . Save :
if not ( hasattr ( self , ' currentFilePath ' ) and 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 )
self . close ( )
elif wantToSave == QMessageBox . Discard :
self . close ( )
# if cancel or anything else do nothing at all
def guitarVoicingAction ( self ) :
gdialog = GuitarDialog ( )
@ -208,7 +316,7 @@ class DocumentWindow(QWidget):
self . doc . chordList . append ( Chord ( parseName ( self . window . chordNameLineEdit . text ( ) ) ) )
if self . window . guitarVoicingLineEdit . text ( ) :
setattr ( self . doc . chordList [ - 1 ] , ' guitar ' , parseFingering ( self . window . guitarVoicingLineEdit . text ( ) , ' guitar ' ) )
self . doc . chordList [ - 1 ] . voicings [ ' guitar ' ] = parseFingering ( self . window . guitarVoicingLineEdit . text ( ) , ' guitar ' )
self . window . chordTableView . populate ( self . doc . chordList )
self . clearChordLineEdits ( )
@ -220,7 +328,7 @@ class DocumentWindow(QWidget):
row = self . window . chordTableView . selectionModel ( ) . currentIndex ( ) . row ( )
self . doc . chordList [ row ] = Chord ( parseName ( self . window . chordNameLineEdit . text ( ) ) )
if self . window . guitarVoicingLineEdit . text ( ) :
setattr ( self . doc . chordList [ row ] , ' guitar ' , parseFingering ( self . window . guitarVoicingLineEdit . text ( ) , ' guitar ' ) )
self . doc . chordList [ - 1 ] . voicings [ ' guitar ' ] = parseFingering ( self . window . guitarVoicingLineEdit . text ( ) , ' guitar ' )
self . window . chordTableView . populate ( self . doc . chordList )
self . clearChordLineEdits ( )
@ -278,12 +386,19 @@ class DocumentWindow(QWidget):
self . window . imageLabel . setPixmap ( QPixmap . fromImage ( qtimg ) )
self . window . imageLabel . repaint ( ) # necessary on Mojave with PyInstaller (or previous contents will be shown)
def updateTitleBar ( self ) :
appName = " Chordsheet "
if self . currentFilePath :
self . setWindowTitle ( appName + " – " + os . path . basename ( self . currentFilePath ) )
else :
self . setWindowTitle ( appName )
def updateChords ( self ) :
chordTableList = [ ]
for i in range ( self . window . chordTableView . model . rowCount ( ) ) :
chordTableList . append ( Chord ( parseName ( self . window . chordTableView . model . item ( i , 0 ) . text ( ) ) ) ) ,
if self . window . chordTableView . model . item ( i , 1 ) . text ( ) :
chordTableList [ - 1 ] . guitar = parseFingering ( self . window . chordTableView . model . item ( i , 1 ) . text ( ) , ' guitar ' )
chordTableList [ - 1 ] . voicings [ ' guitar' ] = parseFingering ( self . window . chordTableView . model . item ( i , 1 ) . text ( ) , ' guitar ' )
self . doc . chordList = chordTableList
@ -326,6 +441,10 @@ class DocumentWindow(QWidget):
# 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 ' ) ) )
@ -350,13 +469,40 @@ class GuitarDialog(QDialog):
else :
return None
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 . 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 inform the user that the chosen file cannot be opened .
"""
def __init__ ( self ) :
super ( ) . __init__ ( )
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 )
if __name__ == ' __main__ ' :
app = QApplication ( sys . argv )
d = Document ( )
s = Style ( )
w = DocumentWindow ( d , s )
w . window . show ( )
w = DocumentWindow ( d , s , filename = ( sys . argv [ 1 ] if len ( sys . argv ) > 1 else None ) ) # pass first argument as filename
w . show ( )
sys . exit ( app . exec_ ( ) )