Browse Source

various modifications and improvements:

show name of document in title bar
add keyboard shortcuts
improve UI resizability
make close menu item functional
add primitive about dialog
add a warning if the user tries to close without saving
make chords, blocks, document equatable
remember where the user last saved/opened a document
master 0.3
Ivan Holmes 5 years ago
parent
commit
36a86d280f
  1. 1
      README.md
  2. 32
      chordsheet/document.py
  3. 8
      chordsheet/render.py
  4. 2
      chordsheet/tableView.py
  5. 196
      gui.py
  6. 2
      ui/guitardialog.ui
  7. 97
      ui/mainwindow.ui

1
README.md

@ -21,6 +21,7 @@ Chordsheet is alpha-grade software. At present, the program will crash readily g
- PDF preview is blurry on high DPI monitors
- Chord names and notes can spill out of their block if it's not big enough
- Poor font handling (choice of either FreeSans or Helvetica Neue if installed)
- No support for printing
## Dependencies
Chordsheet depends on pymupdf (to show the preview), reportlab (to generate the PDF), and PyQt5 (for the GUI).

32
chordsheet/document.py

@ -35,8 +35,15 @@ class Style:
class Chord:
def __init__(self, name, **kwargs):
self.name = name
self.voicings = {}
for inst, fing in kwargs.items():
setattr(self, inst, fing)
self.voicings[inst] = fing
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.name == other.name and self.voicings == other.voicings
return NotImplemented
class Block:
def __init__(self, length, **kwargs):
@ -44,6 +51,11 @@ class Block:
self.chord = kwargs.get('chord', None)
self.notes = kwargs.get('notes', None)
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.length == other.length and self.chord == other.chord and self.notes == other.notes
return NotImplemented
class Document:
def __init__(self, chordList=None, blockList=None, title=None, composer=None, arranger=None, timeSignature=defaultTimeSignature):
self.chordList = chordList or []
@ -52,6 +64,12 @@ class Document:
self.composer = composer
self.arranger = arranger
self.timeSignature = timeSignature
def __eq__(self, other):
if isinstance(other, self.__class__):
textEqual = self.title == other.title and self.composer == other.composer and self.arranger == other.arranger and self.timeSignature == other.timeSignature # check all the text values for equality
return textEqual and self.chordList == other.chordList and self.blockList == other.blockList
return NotImplemented
def loadXML(self, filepath):
xmlDoc = ET.parse(filepath)
@ -62,8 +80,7 @@ class Document:
for c in root.findall('chords/chord'):
self.chordList.append(Chord(parseName(c.find('name').text)))
for v in c.findall('voicing'):
setattr(self.chordList[-1], v.attrib['instrument'],
parseFingering(v.text, v.attrib['instrument']))
self.chordList[-1].voicings[v.attrib['instrument']] = parseFingering(v.text, v.attrib['instrument'])
self.blockList = []
if root.find('progression') is not None:
@ -110,10 +127,11 @@ class Document:
for c in self.chordList:
chordElement = ET.SubElement(chordsElement, "chord")
ET.SubElement(chordElement, "name").text = c.name
if hasattr(c, 'guitar'):
ET.SubElement(chordElement, "voicing", attrib={'instrument':'guitar'}).text = ','.join(c.guitar)
if hasattr(c, 'piano'):
ET.SubElement(chordElement, "voicing", attrib={'instrument':'piano'}).text = c.piano[0] # return first element of list as feature has not been implemented
for inst in c.voicings.keys():
if inst == 'guitar':
ET.SubElement(chordElement, "voicing", attrib={'instrument':'guitar'}).text = ','.join(c.voicings['guitar'])
if inst == 'piano':
ET.SubElement(chordElement, "voicing", attrib={'instrument':'piano'}).text = c.voicings['piano'][0] # return first element of list as feature has not been implemented
progressionElement = ET.SubElement(root, "progression")

8
chordsheet/render.py

@ -48,9 +48,9 @@ def guitarChart(currentCanvas, style, chordList, cur_pos):
nstrings = 6
fontsize = 12
guitarChordList = [[chordList[q].guitar[-(r+1)] for q in range(len(chordList)) if hasattr(chordList[q], 'guitar')] for r in range(6)]
guitarChordList.append([chordList[q].name for q in range(len(chordList)) if hasattr(chordList[q], 'guitar')])
guitarChordList = [[chordList[q].voicings['guitar'][-(r+1)] for q in range(len(chordList)) if 'guitar' in chordList[q].voicings.keys()] for r in range(nstrings)]
guitarChordList.append([chordList[q].name for q in range(len(chordList)) if 'guitar' in chordList[q].voicings.keys()])
for i in range(nstrings+1): # i is the string currently being drawn
writeText(currentCanvas, style, ['e','B','G','D','A','E','Name'][i], fontsize, v_origin+(i*string_height), hpos=h_origin, align='right')
@ -126,7 +126,7 @@ def chordProgression(currentCanvas, style, document, cur_pos):
def guitarChartCheck(cL):
chordsPresent = False
for c in cL:
if hasattr(c, 'guitar'):
if 'guitar' in c.voicings.keys():
chordsPresent = True
break
return chordsPresent

2
chordsheet/tableView.py

@ -55,7 +55,7 @@ class ChordTableView(MTableView):
def populate(self, cList):
self.model.removeRows(0, self.model.rowCount())
for c in cList:
rowList = [QtGui.QStandardItem(c.name), QtGui.QStandardItem(",".join(c.guitar if hasattr(c, 'guitar') else ""))]
rowList = [QtGui.QStandardItem(c.name), QtGui.QStandardItem(",".join(c.voicings['guitar'] if 'guitar' in c.voicings.keys() else ""))]
for item in rowList:
item.setEditable(False)
item.setDropEnabled(False)

196
gui.py

@ -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, QTableWidgetItem, 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)
@ -62,7 +103,20 @@ class DocumentWindow(QWidget):
self.window.actionSave_PDF.triggered.connect(self.menuFileSavePDFAction)
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":
@ -171,9 +253,35 @@ class DocumentWindow(QWidget):
# subprocess.call()
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()
@ -277,13 +385,20 @@ 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_())

2
ui/guitardialog.ui

@ -23,7 +23,7 @@
</size>
</property>
<property name="windowTitle">
<string>Guitar chord</string>
<string>Guitar chord editor</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>

97
ui/mainwindow.ui

@ -6,14 +6,20 @@
<rect>
<x>0</x>
<y>0</y>
<width>1113</width>
<height>746</height>
<width>761</width>
<height>513</height>
</rect>
</property>
<property name="windowTitle">
<string>Chordsheet</string>
</property>
<widget class="QWidget" name="centralwidget">
<property name="documentMode">
<bool>false</bool>
</property>
<property name="tabShape">
<enum>QTabWidget::Rounded</enum>
</property>
<widget class="QWidget" name="centralWidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QSplitter" name="splitter">
@ -24,16 +30,19 @@
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="leftPane" native="true">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>320</width>
<height>0</height>
<width>430</width>
<height>400</height>
</size>
</property>
<property name="maximumSize">
@ -51,17 +60,11 @@
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<sizepolicy hsizetype="Ignored" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>400</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>500</width>
@ -69,7 +72,7 @@
</size>
</property>
<property name="currentIndex">
<number>2</number>
<number>0</number>
</property>
<widget class="QWidget" name="tabWidgetOverview">
<attribute name="title">
@ -516,12 +519,6 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>200</height>
</size>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
@ -614,6 +611,9 @@
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
@ -719,6 +719,12 @@
</layout>
</widget>
<widget class="QScrollArea" name="scrollArea">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>300</width>
@ -733,8 +739,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>580</width>
<height>698</height>
<width>298</width>
<height>465</height>
</rect>
</property>
<property name="autoFillBackground">
@ -776,12 +782,12 @@
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1113</width>
<width>761</width>
<height>22</height>
</rect>
</property>
@ -800,7 +806,21 @@
<addaction name="separator"/>
<addaction name="actionClose"/>
</widget>
<widget class="QMenu" name="menuEdit">
<property name="title">
<string>Edit</string>
</property>
<addaction name="actionUndo"/>
<addaction name="actionRedo"/>
<addaction name="separator"/>
<addaction name="actionCut"/>
<addaction name="actionCopy"/>
<addaction name="actionPaste"/>
<addaction name="separator"/>
<addaction name="actionAbout"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuEdit"/>
</widget>
<action name="actionNew">
<property name="text">
@ -832,6 +852,26 @@
<string>Close</string>
</property>
</action>
<action name="actionSave_as">
<property name="text">
<string>Save as...</string>
</property>
</action>
<action name="actionQuit">
<property name="text">
<string>Quit</string>
</property>
</action>
<action name="actionUndo">
<property name="text">
<string>Undo</string>
</property>
</action>
<action name="actionRedo">
<property name="text">
<string>Redo</string>
</property>
</action>
<action name="actionCut">
<property name="text">
<string>Cut</string>
@ -847,9 +887,14 @@
<string>Paste</string>
</property>
</action>
<action name="actionSave_as">
<action name="actionPreferences">
<property name="text">
<string>Save as...</string>
<string>Preferences</string>
</property>
</action>
<action name="actionAbout">
<property name="text">
<string>About</string>
</property>
</action>
</widget>

Loading…
Cancel
Save