a44827b361
Creating thumbnails on autosave was already disabled in
4cd48850a3
but only for the first score autosave. Subsequent autosaves still
generated thumbnails. This commit makes all autosaves not create
thumbnails.
1332 lines
48 KiB
C++
1332 lines
48 KiB
C++
//=============================================================================
|
||
// MuseScore
|
||
// Music Composition & Notation
|
||
//
|
||
// Copyright (C) 2011-2012 Werner Schweer and others
|
||
//
|
||
// This program is free software; you can redistribute it and/or modify
|
||
// it under the terms of the GNU General Public License version 2
|
||
// as published by the Free Software Foundation and appearing in
|
||
// the file LICENSE.GPL
|
||
//=============================================================================
|
||
|
||
#include "config.h"
|
||
#include "score.h"
|
||
#include "xml.h"
|
||
#include "element.h"
|
||
#include "measure.h"
|
||
#include "segment.h"
|
||
#include "slur.h"
|
||
#include "chordrest.h"
|
||
#include "chord.h"
|
||
#include "tuplet.h"
|
||
#include "beam.h"
|
||
#include "revisions.h"
|
||
#include "page.h"
|
||
#include "part.h"
|
||
#include "staff.h"
|
||
#include "system.h"
|
||
#include "keysig.h"
|
||
#include "clef.h"
|
||
#include "text.h"
|
||
#include "ottava.h"
|
||
#include "volta.h"
|
||
#include "excerpt.h"
|
||
#include "mscore.h"
|
||
#include "stafftype.h"
|
||
#include "sym.h"
|
||
|
||
#ifdef OMR
|
||
#include "omr/omr.h"
|
||
#include "omr/omrpage.h"
|
||
#endif
|
||
|
||
#include "sig.h"
|
||
#include "undo.h"
|
||
#include "imageStore.h"
|
||
#include "audio.h"
|
||
#include "barline.h"
|
||
#include "thirdparty/qzip/qzipreader_p.h"
|
||
#include "thirdparty/qzip/qzipwriter_p.h"
|
||
#ifdef Q_OS_WIN
|
||
#include <windows.h>
|
||
#endif
|
||
|
||
namespace Ms {
|
||
|
||
//---------------------------------------------------------
|
||
// writeMeasure
|
||
//---------------------------------------------------------
|
||
|
||
static void writeMeasure(XmlWriter& xml, MeasureBase* m, int staffIdx, bool writeSystemElements, bool forceTimeSig)
|
||
{
|
||
//
|
||
// special case multi measure rest
|
||
//
|
||
if (m->isMeasure() || staffIdx == 0)
|
||
m->write(xml, staffIdx, writeSystemElements, forceTimeSig);
|
||
|
||
if (m->score()->styleB(Sid::createMultiMeasureRests) && m->isMeasure() && toMeasure(m)->mmRest())
|
||
toMeasure(m)->mmRest()->write(xml, staffIdx, writeSystemElements, forceTimeSig);
|
||
|
||
xml.setCurTick(m->endTick());
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// writeMovement
|
||
//---------------------------------------------------------
|
||
|
||
void Score::writeMovement(XmlWriter& xml, bool selectionOnly)
|
||
{
|
||
// if we have multi measure rests and some parts are hidden,
|
||
// then some layout information is missing:
|
||
// relayout with all parts set visible
|
||
|
||
QList<Part*> hiddenParts;
|
||
bool unhide = false;
|
||
if (styleB(Sid::createMultiMeasureRests)) {
|
||
for (Part* part : _parts) {
|
||
if (!part->show()) {
|
||
if (!unhide) {
|
||
startCmd();
|
||
unhide = true;
|
||
}
|
||
part->undoChangeProperty(Pid::VISIBLE, true);
|
||
hiddenParts.append(part);
|
||
}
|
||
}
|
||
}
|
||
if (unhide) {
|
||
doLayout();
|
||
for (Part* p : hiddenParts)
|
||
p->setShow(false);
|
||
}
|
||
|
||
xml.stag(this);
|
||
if (excerpt()) {
|
||
Excerpt* e = excerpt();
|
||
QMultiMap<int, int> trackList = e->tracks();
|
||
QMapIterator<int, int> i(trackList);
|
||
if (!(trackList.size() == e->parts().size() * VOICES) && !trackList.isEmpty()) {
|
||
while (i.hasNext()) {
|
||
i.next();
|
||
xml.tagE(QString("Tracklist sTrack=\"%1\" dstTrack=\"%2\"").arg(i.key()).arg(i.value()));
|
||
}
|
||
}
|
||
}
|
||
|
||
if (lineMode())
|
||
xml.tag("layoutMode", "line");
|
||
if (systemMode())
|
||
xml.tag("layoutMode", "system");
|
||
|
||
#ifdef OMR
|
||
if (masterScore()->omr() && xml.writeOmr())
|
||
masterScore()->omr()->write(xml);
|
||
#endif
|
||
if (isMaster() && masterScore()->showOmr() && xml.writeOmr())
|
||
xml.tag("showOmr", masterScore()->showOmr());
|
||
if (_audio && xml.writeOmr()) {
|
||
xml.tag("playMode", int(_playMode));
|
||
_audio->write(xml);
|
||
}
|
||
|
||
for (int i = 0; i < 32; ++i) {
|
||
if (!_layerTags[i].isEmpty()) {
|
||
xml.tag(QString("LayerTag id=\"%1\" tag=\"%2\"").arg(i).arg(_layerTags[i]),
|
||
_layerTagComments[i]);
|
||
}
|
||
}
|
||
int n = _layer.size();
|
||
for (int i = 1; i < n; ++i) { // don’t save default variant
|
||
const Layer& l = _layer[i];
|
||
xml.tagE(QString("Layer name=\"%1\" mask=\"%2\"").arg(l.name).arg(l.tags));
|
||
}
|
||
xml.tag("currentLayer", _currentLayer);
|
||
|
||
if (isTopScore() && !MScore::testMode)
|
||
_synthesizerState.write(xml);
|
||
|
||
if (pageNumberOffset())
|
||
xml.tag("page-offset", pageNumberOffset());
|
||
xml.tag("Division", MScore::division);
|
||
xml.setCurTrack(-1);
|
||
|
||
if (isTopScore()) // only top score
|
||
style().save(xml, true); // save only differences to buildin style
|
||
|
||
xml.tag("showInvisible", _showInvisible);
|
||
xml.tag("showUnprintable", _showUnprintable);
|
||
xml.tag("showFrames", _showFrames);
|
||
xml.tag("showMargins", _showPageborders);
|
||
xml.tag("markIrregularMeasures", _markIrregularMeasures, true);
|
||
|
||
QMapIterator<QString, QString> i(_metaTags);
|
||
while (i.hasNext()) {
|
||
i.next();
|
||
// do not output "platform" and "creationDate" in test and save template mode
|
||
if ((!MScore::testMode && !MScore::saveTemplateMode) || (i.key() != "platform" && i.key() != "creationDate"))
|
||
xml.tag(QString("metaTag name=\"%1\"").arg(i.key().toHtmlEscaped()), i.value());
|
||
}
|
||
|
||
xml.setCurTrack(0);
|
||
int staffStart;
|
||
int staffEnd;
|
||
MeasureBase* measureStart;
|
||
MeasureBase* measureEnd;
|
||
|
||
if (selectionOnly) {
|
||
staffStart = _selection.staffStart();
|
||
staffEnd = _selection.staffEnd();
|
||
// make sure we select full parts
|
||
Staff* sStaff = staff(staffStart);
|
||
Part* sPart = sStaff->part();
|
||
Staff* eStaff = staff(staffEnd - 1);
|
||
Part* ePart = eStaff->part();
|
||
staffStart = staffIdx(sPart);
|
||
staffEnd = staffIdx(ePart) + ePart->nstaves();
|
||
measureStart = _selection.startSegment()->measure();
|
||
if (measureStart->isMeasure() && toMeasure(measureStart)->isMMRest())
|
||
measureStart = toMeasure(measureStart)->mmRestFirst();
|
||
if (_selection.endSegment())
|
||
measureEnd = _selection.endSegment()->measure()->next();
|
||
else
|
||
measureEnd = 0;
|
||
}
|
||
else {
|
||
staffStart = 0;
|
||
staffEnd = nstaves();
|
||
measureStart = first();
|
||
measureEnd = 0;
|
||
}
|
||
|
||
// Let's decide: write midi mapping to a file or not
|
||
masterScore()->checkMidiMapping();
|
||
for (const Part* part : _parts) {
|
||
if (!selectionOnly || ((staffIdx(part) >= staffStart) && (staffEnd >= staffIdx(part) + part->nstaves())))
|
||
part->write(xml);
|
||
}
|
||
|
||
xml.setCurTrack(0);
|
||
xml.setTrackDiff(-staffStart * VOICES);
|
||
if (measureStart) {
|
||
for (int staffIdx = staffStart; staffIdx < staffEnd; ++staffIdx) {
|
||
xml.stag(staff(staffIdx), QString("id=\"%1\"").arg(staffIdx + 1 - staffStart));
|
||
xml.setCurTick(measureStart->tick());
|
||
xml.setTickDiff(xml.curTick());
|
||
xml.setCurTrack(staffIdx * VOICES);
|
||
bool writeSystemElements = (staffIdx == staffStart);
|
||
bool firstMeasureWritten = false;
|
||
bool forceTimeSig = false;
|
||
for (MeasureBase* m = measureStart; m != measureEnd; m = m->next()) {
|
||
// force timesig if first measure and selectionOnly
|
||
if (selectionOnly && m->isMeasure()) {
|
||
if (!firstMeasureWritten) {
|
||
forceTimeSig = true;
|
||
firstMeasureWritten = true;
|
||
}
|
||
else
|
||
forceTimeSig = false;
|
||
}
|
||
writeMeasure(xml, m, staffIdx, writeSystemElements, forceTimeSig);
|
||
}
|
||
xml.etag();
|
||
}
|
||
}
|
||
xml.setCurTrack(-1);
|
||
if (isMaster()) {
|
||
if (!selectionOnly) {
|
||
for (const Excerpt* excerpt : excerpts()) {
|
||
if (excerpt->partScore() != this)
|
||
excerpt->partScore()->write(xml, false); // recursion
|
||
}
|
||
}
|
||
}
|
||
else
|
||
xml.tag("name", excerpt()->title());
|
||
xml.etag();
|
||
|
||
if (unhide)
|
||
endCmd(true);
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// write
|
||
//---------------------------------------------------------
|
||
|
||
void Score::write(XmlWriter& xml, bool selectionOnly)
|
||
{
|
||
if (isMaster()) {
|
||
MasterScore* score = static_cast<MasterScore*>(this);
|
||
while (score->prev())
|
||
score = score->prev();
|
||
while (score) {
|
||
score->writeMovement(xml, selectionOnly);
|
||
score = score->next();
|
||
}
|
||
}
|
||
else
|
||
writeMovement(xml, selectionOnly);
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// Staff
|
||
//---------------------------------------------------------
|
||
|
||
void Score::readStaff(XmlReader& e)
|
||
{
|
||
int staff = e.intAttribute("id", 1) - 1;
|
||
int measureIdx = 0;
|
||
e.setCurrentMeasureIndex(0);
|
||
e.setTick(Fraction(0,1));
|
||
e.setTrack(staff * VOICES);
|
||
|
||
if (staff == 0) {
|
||
while (e.readNextStartElement()) {
|
||
const QStringRef& tag(e.name());
|
||
|
||
if (tag == "Measure") {
|
||
Measure* measure = 0;
|
||
measure = new Measure(this);
|
||
measure->setTick(e.tick());
|
||
e.setCurrentMeasureIndex(measureIdx++);
|
||
//
|
||
// inherit timesig from previous measure
|
||
//
|
||
Measure* m = e.lastMeasure(); // measure->prevMeasure();
|
||
Fraction f(m ? m->timesig() : Fraction(4,4));
|
||
measure->setTicks(f);
|
||
measure->setTimesig(f);
|
||
|
||
measure->read(e, staff);
|
||
measure->checkMeasure(staff);
|
||
if (!measure->isMMRest()) {
|
||
measures()->add(measure);
|
||
e.setLastMeasure(measure);
|
||
e.setTick(measure->tick() + measure->ticks());
|
||
}
|
||
else {
|
||
// this is a multi measure rest
|
||
// always preceded by the first measure it replaces
|
||
Measure* m1 = e.lastMeasure();
|
||
|
||
if (m1) {
|
||
m1->setMMRest(measure);
|
||
measure->setTick(m1->tick());
|
||
}
|
||
}
|
||
}
|
||
else if (tag == "HBox" || tag == "VBox" || tag == "TBox" || tag == "FBox") {
|
||
MeasureBase* mb = toMeasureBase(Element::name2Element(tag, this));
|
||
mb->read(e);
|
||
mb->setTick(e.tick());
|
||
measures()->add(mb);
|
||
}
|
||
else if (tag == "tick")
|
||
e.setTick(Fraction::fromTicks(fileDivision(e.readInt())));
|
||
else
|
||
e.unknown();
|
||
}
|
||
}
|
||
else {
|
||
Measure* measure = firstMeasure();
|
||
while (e.readNextStartElement()) {
|
||
const QStringRef& tag(e.name());
|
||
|
||
if (tag == "Measure") {
|
||
if (measure == 0) {
|
||
qDebug("Score::readStaff(): missing measure!");
|
||
measure = new Measure(this);
|
||
measure->setTick(e.tick());
|
||
measures()->add(measure);
|
||
}
|
||
e.setTick(measure->tick());
|
||
e.setCurrentMeasureIndex(measureIdx++);
|
||
measure->read(e, staff);
|
||
measure->checkMeasure(staff);
|
||
if (measure->isMMRest())
|
||
measure = e.lastMeasure()->nextMeasure();
|
||
else {
|
||
e.setLastMeasure(measure);
|
||
if (measure->mmRest())
|
||
measure = measure->mmRest();
|
||
else
|
||
measure = measure->nextMeasure();
|
||
}
|
||
}
|
||
else if (tag == "tick")
|
||
e.setTick(Fraction::fromTicks(fileDivision(e.readInt())));
|
||
else
|
||
e.unknown();
|
||
}
|
||
}
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// saveFile
|
||
/// If file has generated name, create a modal file save dialog
|
||
/// and ask filename.
|
||
/// Rename old file to backup file (.xxxx.msc?,).
|
||
/// Default is to save score in .mscz format,
|
||
/// Return true if OK and false on error.
|
||
//---------------------------------------------------------
|
||
|
||
bool MasterScore::saveFile()
|
||
{
|
||
if (readOnly())
|
||
return false;
|
||
QString suffix = info.suffix();
|
||
if (info.exists() && !info.isWritable()) {
|
||
MScore::lastError = tr("The following file is locked: \n%1 \n\nTry saving to a different location.").arg(info.filePath());
|
||
return false;
|
||
}
|
||
//
|
||
// step 1
|
||
// save into temporary file to prevent partially overwriting
|
||
// the original file in case of "disc full"
|
||
//
|
||
|
||
QString tempName = info.filePath() + QString(".temp");
|
||
QFile temp(tempName);
|
||
if (!temp.open(QIODevice::WriteOnly)) {
|
||
MScore::lastError = tr("Open Temp File\n%1\nfailed: %2").arg(tempName, strerror(errno));
|
||
return false;
|
||
}
|
||
bool rv = suffix == "mscx" ? Score::saveFile(&temp, false) : Score::saveCompressedFile(&temp, info, false);
|
||
if (!rv) {
|
||
return false;
|
||
}
|
||
|
||
if (temp.error() != QFile::NoError) {
|
||
MScore::lastError = tr("Save File failed: %1").arg(temp.errorString());
|
||
return false;
|
||
}
|
||
temp.close();
|
||
|
||
QString name(info.filePath());
|
||
QString basename(info.fileName());
|
||
QDir dir(info.path());
|
||
if (!saved()) {
|
||
// if file was already saved in this session
|
||
// save but don't overwrite backup again
|
||
|
||
//
|
||
// step 2
|
||
// remove old backup file if exists
|
||
//
|
||
QString backupName = QString(".") + info.fileName() + QString(",");
|
||
if (dir.exists(backupName)) {
|
||
if (!dir.remove(backupName)) {
|
||
// if (!MScore::noGui)
|
||
// QMessageBox::critical(0, QObject::tr("Save File"),
|
||
// tr("Removing old backup file %1 failed").arg(backupName));
|
||
}
|
||
}
|
||
|
||
//
|
||
// step 3
|
||
// rename old file into backup
|
||
//
|
||
if (dir.exists(basename)) {
|
||
if (!dir.rename(basename, backupName)) {
|
||
// if (!MScore::noGui)
|
||
// QMessageBox::critical(0, tr("Save File"),
|
||
// tr("Renaming old file <%1> to backup <%2> failed").arg(name, backupname);
|
||
}
|
||
}
|
||
|
||
QFileInfo fileBackup(dir, backupName);
|
||
_sessionStartBackupInfo = fileBackup;
|
||
|
||
#ifdef Q_OS_WIN
|
||
QString backupNativePath = QDir::toNativeSeparators(fileBackup.absoluteFilePath());
|
||
#if (defined (_MSCVER) || defined (_MSC_VER))
|
||
#if (defined (UNICODE))
|
||
SetFileAttributes((LPCTSTR)backupNativePath.unicode(), FILE_ATTRIBUTE_HIDDEN);
|
||
#else
|
||
// Use byte-based Windows function
|
||
SetFileAttributes((LPCTSTR)backupNativePath.toLocal8Bit(), FILE_ATTRIBUTE_HIDDEN);
|
||
#endif
|
||
#else
|
||
SetFileAttributes((LPCTSTR)backupNativePath.toLocal8Bit(), FILE_ATTRIBUTE_HIDDEN);
|
||
#endif
|
||
#endif
|
||
}
|
||
else {
|
||
// file has previously been saved - remove the old file
|
||
if (dir.exists(basename)) {
|
||
if (!dir.remove(basename)) {
|
||
// if (!MScore::noGui)
|
||
// QMessageBox::critical(0, tr("Save File"),
|
||
// tr("Removing old file %1 failed").arg(name));
|
||
}
|
||
}
|
||
}
|
||
|
||
//
|
||
// step 4
|
||
// rename temp name into file name
|
||
//
|
||
if (!QFile::rename(tempName, name)) {
|
||
MScore::lastError = tr("Renaming temp. file <%1> to <%2> failed:\n%3").arg(tempName, name, strerror(errno));
|
||
return false;
|
||
}
|
||
// make file readable by all
|
||
QFile::setPermissions(name, QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser
|
||
| QFile::ReadGroup | QFile::ReadOther);
|
||
|
||
undoStack()->setClean();
|
||
setSaved(true);
|
||
info.refresh();
|
||
update();
|
||
return true;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// saveCompressedFile
|
||
//---------------------------------------------------------
|
||
|
||
bool Score::saveCompressedFile(QFileInfo& info, bool onlySelection, bool createThumbnail)
|
||
{
|
||
if (readOnly() && info == *masterScore()->fileInfo())
|
||
return false;
|
||
QFile fp(info.filePath());
|
||
if (!fp.open(QIODevice::WriteOnly)) {
|
||
MScore::lastError = tr("Open File\n%1\nfailed: %2").arg(info.filePath(), strerror(errno));
|
||
return false;
|
||
}
|
||
return saveCompressedFile(&fp, info, onlySelection, createThumbnail);
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// createThumbnail
|
||
//---------------------------------------------------------
|
||
|
||
QImage Score::createThumbnail()
|
||
{
|
||
LayoutMode mode = layoutMode();
|
||
setLayoutMode(LayoutMode::PAGE);
|
||
doLayout();
|
||
|
||
Page* page = pages().at(0);
|
||
QRectF fr = page->abbox();
|
||
qreal mag = 256.0 / qMax(fr.width(), fr.height());
|
||
int w = int(fr.width() * mag);
|
||
int h = int(fr.height() * mag);
|
||
|
||
QImage pm(w, h, QImage::Format_ARGB32_Premultiplied);
|
||
|
||
int dpm = lrint(DPMM * 1000.0);
|
||
pm.setDotsPerMeterX(dpm);
|
||
pm.setDotsPerMeterY(dpm);
|
||
pm.fill(0xffffffff);
|
||
|
||
double pr = MScore::pixelRatio;
|
||
MScore::pixelRatio = 1.0;
|
||
|
||
QPainter p(&pm);
|
||
p.setRenderHint(QPainter::Antialiasing, true);
|
||
p.setRenderHint(QPainter::TextAntialiasing, true);
|
||
p.scale(mag, mag);
|
||
print(&p, 0);
|
||
p.end();
|
||
|
||
MScore::pixelRatio = pr;
|
||
|
||
if (layoutMode() != mode) {
|
||
setLayoutMode(mode);
|
||
doLayout();
|
||
}
|
||
return pm;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// saveCompressedFile
|
||
// file is already opened
|
||
//---------------------------------------------------------
|
||
|
||
bool Score::saveCompressedFile(QFileDevice* f, QFileInfo& info, bool onlySelection, bool doCreateThumbnail)
|
||
{
|
||
MQZipWriter uz(f);
|
||
|
||
QString fn = info.completeBaseName() + ".mscx";
|
||
QBuffer cbuf;
|
||
cbuf.open(QIODevice::ReadWrite);
|
||
XmlWriter xml(this, &cbuf);
|
||
xml << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
||
xml.stag("container");
|
||
xml.stag("rootfiles");
|
||
xml.stag(QString("rootfile full-path=\"%1\"").arg(XmlWriter::xmlString(fn)));
|
||
xml.etag();
|
||
for (ImageStoreItem* ip : imageStore) {
|
||
if (!ip->isUsed(this))
|
||
continue;
|
||
QString path = QString("Pictures/") + ip->hashName();
|
||
xml.tag("file", path);
|
||
}
|
||
|
||
xml.etag();
|
||
xml.etag();
|
||
cbuf.seek(0);
|
||
//uz.addDirectory("META-INF");
|
||
uz.addFile("META-INF/container.xml", cbuf.data());
|
||
|
||
QBuffer dbuf;
|
||
dbuf.open(QIODevice::ReadWrite);
|
||
saveFile(&dbuf, true, onlySelection);
|
||
dbuf.seek(0);
|
||
uz.addFile(fn, dbuf.data());
|
||
f->flush(); // flush to preserve score data in case of
|
||
// any failures on the further operations.
|
||
|
||
// save images
|
||
//uz.addDirectory("Pictures");
|
||
for (ImageStoreItem* ip : imageStore) {
|
||
if (!ip->isUsed(this))
|
||
continue;
|
||
QString path = QString("Pictures/") + ip->hashName();
|
||
uz.addFile(path, ip->buffer());
|
||
}
|
||
|
||
// create thumbnail
|
||
if (doCreateThumbnail && !pages().isEmpty()) {
|
||
QImage pm = createThumbnail();
|
||
|
||
QByteArray ba;
|
||
QBuffer b(&ba);
|
||
if (!b.open(QIODevice::WriteOnly))
|
||
qDebug("open buffer failed");
|
||
if (!pm.save(&b, "PNG"))
|
||
qDebug("save failed");
|
||
uz.addFile("Thumbnails/thumbnail.png", ba);
|
||
}
|
||
|
||
#ifdef OMR
|
||
//
|
||
// save OMR page images
|
||
//
|
||
if (masterScore()->omr()) {
|
||
int n = masterScore()->omr()->numPages();
|
||
for (int i = 0; i < n; ++i) {
|
||
QString path = QString("OmrPages/page%1.png").arg(i+1);
|
||
QBuffer cbuf1;
|
||
OmrPage* page = masterScore()->omr()->page(i);
|
||
const QImage& image = page->image();
|
||
if (!image.save(&cbuf1, "PNG")) {
|
||
MScore::lastError = tr("Save file: cannot save image (%1x%2)").arg(image.width(), image.height());
|
||
return false;
|
||
}
|
||
uz.addFile(path, cbuf1.data());
|
||
cbuf1.close();
|
||
}
|
||
}
|
||
#endif
|
||
//
|
||
// save audio
|
||
//
|
||
if (_audio)
|
||
uz.addFile("audio.ogg", _audio->data());
|
||
|
||
uz.close();
|
||
return true;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// saveFile
|
||
// return true on success
|
||
//---------------------------------------------------------
|
||
|
||
bool Score::saveFile(QFileInfo& info)
|
||
{
|
||
if (readOnly() && info == *masterScore()->fileInfo())
|
||
return false;
|
||
if (info.suffix().isEmpty())
|
||
info.setFile(info.filePath() + ".mscx");
|
||
QFile fp(info.filePath());
|
||
if (!fp.open(QIODevice::WriteOnly)) {
|
||
MScore::lastError = tr("Open File\n%1\nfailed: %2").arg(info.filePath(), strerror(errno));
|
||
return false;
|
||
}
|
||
saveFile(&fp, false, false);
|
||
fp.close();
|
||
return true;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// loadStyle
|
||
//---------------------------------------------------------
|
||
|
||
bool Score::loadStyle(const QString& fn, bool ign)
|
||
{
|
||
QFile f(fn);
|
||
if (f.open(QIODevice::ReadOnly)) {
|
||
MStyle st = style();
|
||
if (st.load(&f, ign)) {
|
||
undo(new ChangeStyle(this, st));
|
||
return true;
|
||
}
|
||
else {
|
||
MScore::lastError = tr("The style file is not compatible with this version of MuseScore.");
|
||
return false;
|
||
}
|
||
}
|
||
MScore::lastError = strerror(errno);
|
||
return false;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// saveStyle
|
||
//---------------------------------------------------------
|
||
|
||
bool Score::saveStyle(const QString& name)
|
||
{
|
||
QString ext(".mss");
|
||
QFileInfo info(name);
|
||
|
||
if (info.suffix().isEmpty())
|
||
info.setFile(info.filePath() + ext);
|
||
QFile f(info.filePath());
|
||
if (!f.open(QIODevice::WriteOnly)) {
|
||
MScore::lastError = tr("Open Style File\n%1\nfailed: %2").arg(info.filePath(), strerror(errno));
|
||
return false;
|
||
}
|
||
|
||
XmlWriter xml(this, &f);
|
||
xml.header();
|
||
xml.stag("museScore version=\"" MSC_VERSION "\"");
|
||
style().save(xml, false); // save complete style
|
||
xml.etag();
|
||
if (f.error() != QFile::NoError) {
|
||
MScore::lastError = tr("Write Style failed: %1").arg(f.errorString());
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// saveFile
|
||
// return true on success
|
||
//---------------------------------------------------------
|
||
|
||
extern QString revision;
|
||
|
||
bool Score::saveFile(QIODevice* f, bool msczFormat, bool onlySelection)
|
||
{
|
||
XmlWriter xml(this, f);
|
||
xml.setWriteOmr(msczFormat);
|
||
xml.header();
|
||
if (!MScore::testMode) {
|
||
xml.stag("museScore version=\"" MSC_VERSION "\"");
|
||
xml.tag("programVersion", VERSION);
|
||
xml.tag("programRevision", revision);
|
||
}
|
||
else
|
||
xml.stag("museScore version=\"3.01\"");
|
||
write(xml, onlySelection);
|
||
xml.etag();
|
||
if (isMaster())
|
||
masterScore()->revisions()->write(xml);
|
||
if (!onlySelection) {
|
||
//update version values for i.e. plugin access
|
||
_mscoreVersion = VERSION;
|
||
_mscoreRevision = revision.toInt(0, 16);
|
||
_mscVersion = MSCVERSION;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// readRootFile
|
||
//---------------------------------------------------------
|
||
|
||
QString readRootFile(MQZipReader* uz, QList<QString>& images)
|
||
{
|
||
QString rootfile;
|
||
|
||
QByteArray cbuf = uz->fileData("META-INF/container.xml");
|
||
if (cbuf.isEmpty()) {
|
||
qDebug("can't find container.xml");
|
||
return rootfile;
|
||
}
|
||
|
||
XmlReader e(cbuf);
|
||
|
||
while (e.readNextStartElement()) {
|
||
if (e.name() != "container") {
|
||
e.unknown();
|
||
continue;
|
||
}
|
||
while (e.readNextStartElement()) {
|
||
if (e.name() != "rootfiles") {
|
||
e.unknown();
|
||
continue;
|
||
}
|
||
while (e.readNextStartElement()) {
|
||
const QStringRef& tag(e.name());
|
||
|
||
if (tag == "rootfile") {
|
||
if (rootfile.isEmpty()) {
|
||
rootfile = e.attribute("full-path");
|
||
e.skipCurrentElement();
|
||
}
|
||
}
|
||
else if (tag == "file")
|
||
images.append(e.readElementText());
|
||
else
|
||
e.unknown();
|
||
}
|
||
}
|
||
}
|
||
return rootfile;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// loadCompressedMsc
|
||
// return false on error
|
||
//---------------------------------------------------------
|
||
|
||
Score::FileError MasterScore::loadCompressedMsc(QIODevice* io, bool ignoreVersionError)
|
||
{
|
||
MQZipReader uz(io);
|
||
|
||
QList<QString> sl;
|
||
QString rootfile = readRootFile(&uz, sl);
|
||
if (rootfile.isEmpty())
|
||
return FileError::FILE_NO_ROOTFILE;
|
||
|
||
//
|
||
// load images
|
||
//
|
||
if (!MScore::noImages) {
|
||
foreach(const QString& s, sl) {
|
||
QByteArray dbuf = uz.fileData(s);
|
||
imageStore.add(s, dbuf);
|
||
}
|
||
}
|
||
|
||
QByteArray dbuf = uz.fileData(rootfile);
|
||
if (dbuf.isEmpty()) {
|
||
QVector<MQZipReader::FileInfo> fil = uz.fileInfoList();
|
||
foreach(const MQZipReader::FileInfo& fi, fil) {
|
||
if (fi.filePath.endsWith(".mscx")) {
|
||
dbuf = uz.fileData(fi.filePath);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
XmlReader e(dbuf);
|
||
QBuffer readAheadBuf(&dbuf);
|
||
e.setReadAheadDevice(&readAheadBuf);
|
||
e.setDocName(masterScore()->fileInfo()->completeBaseName());
|
||
|
||
FileError retval = read1(e, ignoreVersionError);
|
||
|
||
#ifdef OMR
|
||
//
|
||
// load OMR page images
|
||
//
|
||
if (masterScore()->omr()) {
|
||
int n = masterScore()->omr()->numPages();
|
||
for (int i = 0; i < n; ++i) {
|
||
QString path = QString("OmrPages/page%1.png").arg(i+1);
|
||
QByteArray dbuf1 = uz.fileData(path);
|
||
OmrPage* page = masterScore()->omr()->page(i);
|
||
QImage image;
|
||
if (image.loadFromData(dbuf1, "PNG")) {
|
||
page->setImage(image);
|
||
}
|
||
else
|
||
qDebug("load image failed");
|
||
}
|
||
}
|
||
#endif
|
||
//
|
||
// read audio
|
||
//
|
||
if (audio()) {
|
||
QByteArray dbuf1 = uz.fileData("audio.ogg");
|
||
audio()->setData(dbuf1);
|
||
}
|
||
return retval;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// loadMsc
|
||
// return true on success
|
||
//---------------------------------------------------------
|
||
|
||
Score::FileError MasterScore::loadMsc(QString name, bool ignoreVersionError)
|
||
{
|
||
QFile f(name);
|
||
if (!f.open(QIODevice::ReadOnly)) {
|
||
MScore::lastError = f.errorString();
|
||
return FileError::FILE_OPEN_ERROR;
|
||
}
|
||
return loadMsc(name, &f, ignoreVersionError);
|
||
}
|
||
|
||
Score::FileError MasterScore::loadMsc(QString name, QIODevice* io, bool ignoreVersionError)
|
||
{
|
||
ScoreLoad sl;
|
||
fileInfo()->setFile(name);
|
||
|
||
if (name.endsWith(".mscz"))
|
||
return loadCompressedMsc(io, ignoreVersionError);
|
||
else {
|
||
XmlReader r(io);
|
||
r.setReadAheadDevice(io);
|
||
return read1(r, ignoreVersionError);
|
||
}
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// parseVersion
|
||
//---------------------------------------------------------
|
||
|
||
void MasterScore::parseVersion(const QString& val)
|
||
{
|
||
QRegExp re("(\\d+)\\.(\\d+)\\.(\\d+)");
|
||
int v1, v2, v3, rv1, rv2, rv3;
|
||
if (re.indexIn(VERSION) != -1) {
|
||
QStringList sl = re.capturedTexts();
|
||
if (sl.size() == 4) {
|
||
v1 = sl[1].toInt();
|
||
v2 = sl[2].toInt();
|
||
v3 = sl[3].toInt();
|
||
if (re.indexIn(val) != -1) {
|
||
sl = re.capturedTexts();
|
||
if (sl.size() == 4) {
|
||
rv1 = sl[1].toInt();
|
||
rv2 = sl[2].toInt();
|
||
rv3 = sl[3].toInt();
|
||
|
||
int currentVersion = v1 * 10000 + v2 * 100 + v3;
|
||
int readVersion = rv1 * 10000 + rv2 * 100 + rv3;
|
||
if (readVersion > currentVersion) {
|
||
qDebug("read future version");
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
QRegExp re1("(\\d+)\\.(\\d+)");
|
||
if (re1.indexIn(val) != -1) {
|
||
sl = re.capturedTexts();
|
||
if (sl.size() == 3) {
|
||
rv1 = sl[1].toInt();
|
||
rv2 = sl[2].toInt();
|
||
|
||
int currentVersion = v1 * 10000 + v2 * 100 + v3;
|
||
int readVersion = rv1 * 10000 + rv2 * 100;
|
||
if (readVersion > currentVersion) {
|
||
qDebug("read future version");
|
||
}
|
||
}
|
||
}
|
||
else
|
||
qDebug("1cannot parse <%s>", qPrintable(val));
|
||
}
|
||
}
|
||
}
|
||
else
|
||
qDebug("2cannot parse <%s>", VERSION);
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// read1
|
||
// return true on success
|
||
//---------------------------------------------------------
|
||
|
||
Score::FileError MasterScore::read1(XmlReader& e, bool ignoreVersionError)
|
||
{
|
||
while (e.readNextStartElement()) {
|
||
if (e.name() == "museScore") {
|
||
const QString& version = e.attribute("version");
|
||
QStringList sl = version.split('.');
|
||
setMscVersion(sl[0].toInt() * 100 + sl[1].toInt());
|
||
|
||
if (!ignoreVersionError) {
|
||
QString message;
|
||
if (mscVersion() > MSCVERSION)
|
||
return FileError::FILE_TOO_NEW;
|
||
if (mscVersion() < 114)
|
||
return FileError::FILE_TOO_OLD;
|
||
if (mscVersion() == 300)
|
||
return FileError::FILE_OLD_300_FORMAT;
|
||
}
|
||
Score::FileError error;
|
||
if (mscVersion() <= 114)
|
||
error = read114(e);
|
||
else if (mscVersion() <= 207)
|
||
error = read206(e);
|
||
else
|
||
error = read301(e);
|
||
setExcerptsChanged(false);
|
||
return error;
|
||
}
|
||
else
|
||
e.unknown();
|
||
}
|
||
return FileError::FILE_CORRUPTED;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// print
|
||
//---------------------------------------------------------
|
||
|
||
void Score::print(QPainter* painter, int pageNo)
|
||
{
|
||
_printing = true;
|
||
MScore::pdfPrinting = true;
|
||
Page* page = pages().at(pageNo);
|
||
QRectF fr = page->abbox();
|
||
|
||
QList<Element*> ell = page->items(fr);
|
||
qStableSort(ell.begin(), ell.end(), elementLessThan);
|
||
for (const Element* e : ell) {
|
||
if (!e->visible())
|
||
continue;
|
||
painter->save();
|
||
painter->translate(e->pagePos());
|
||
e->draw(painter);
|
||
painter->restore();
|
||
}
|
||
MScore::pdfPrinting = false;
|
||
_printing = false;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// readCompressedToBuffer
|
||
//---------------------------------------------------------
|
||
|
||
QByteArray MasterScore::readCompressedToBuffer()
|
||
{
|
||
MQZipReader uz(info.filePath());
|
||
if (!uz.exists()) {
|
||
qDebug("Score::readCompressedToBuffer: cannot read zip file");
|
||
return QByteArray();
|
||
}
|
||
QList<QString> images;
|
||
QString rootfile = readRootFile(&uz, images);
|
||
|
||
//
|
||
// load images
|
||
//
|
||
foreach(const QString& s, images) {
|
||
QByteArray dbuf = uz.fileData(s);
|
||
imageStore.add(s, dbuf);
|
||
}
|
||
|
||
if (rootfile.isEmpty()) {
|
||
qDebug("=can't find rootfile in: %s", qPrintable(info.filePath()));
|
||
return QByteArray();
|
||
}
|
||
return uz.fileData(rootfile);
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// readToBuffer
|
||
//---------------------------------------------------------
|
||
|
||
QByteArray MasterScore::readToBuffer()
|
||
{
|
||
QByteArray ba;
|
||
QString cs = info.suffix();
|
||
|
||
if (cs == "mscz") {
|
||
ba = readCompressedToBuffer();
|
||
}
|
||
if (cs.toLower() == "msc" || cs.toLower() == "mscx") {
|
||
QFile f(info.filePath());
|
||
if (f.open(QIODevice::ReadOnly)) {
|
||
ba = f.readAll();
|
||
f.close();
|
||
}
|
||
}
|
||
return ba;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// createRevision
|
||
//---------------------------------------------------------
|
||
|
||
void Score::createRevision()
|
||
{
|
||
#if 0
|
||
qDebug("createRevision");
|
||
QBuffer dbuf;
|
||
dbuf.open(QIODevice::ReadWrite);
|
||
saveFile(&dbuf, false, false);
|
||
dbuf.close();
|
||
|
||
QByteArray ba1 = readToBuffer();
|
||
|
||
QString os = QString::fromUtf8(ba1.data(), ba1.size());
|
||
QString ns = QString::fromUtf8(dbuf.buffer().data(), dbuf.buffer().size());
|
||
|
||
diff_match_patch dmp;
|
||
Revision* r = new Revision();
|
||
r->setDiff(dmp.patch_toText(dmp.patch_make(ns, os)));
|
||
r->setId("1");
|
||
_revisions->add(r);
|
||
|
||
// qDebug("patch:\n%s\n==========", qPrintable(patch));
|
||
//
|
||
#endif
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// writeVoiceMove
|
||
// write <move> and starting <voice> tags to denote
|
||
// change in position.
|
||
// Returns true if <voice> tag was written.
|
||
//---------------------------------------------------------
|
||
|
||
static bool writeVoiceMove(XmlWriter& xml, Segment* seg, const Fraction& startTick, int track, int* lastTrackWrittenPtr)
|
||
{
|
||
bool voiceTagWritten = false;
|
||
int& lastTrackWritten = *lastTrackWrittenPtr;
|
||
if ((lastTrackWritten < track) && !xml.clipboardmode()) {
|
||
while (lastTrackWritten < (track - 1)) {
|
||
xml.tagE("voice");
|
||
++lastTrackWritten;
|
||
}
|
||
xml.stag("voice");
|
||
xml.setCurTick(startTick);
|
||
xml.setCurTrack(track);
|
||
++lastTrackWritten;
|
||
voiceTagWritten = true;
|
||
}
|
||
|
||
if ((xml.curTick() != seg->tick()) || (track != xml.curTrack())) {
|
||
Location curr = Location::absolute();
|
||
Location dest = Location::absolute();
|
||
curr.setFrac(xml.curTick());
|
||
dest.setFrac(seg->tick());
|
||
curr.setTrack(xml.curTrack());
|
||
dest.setTrack(track);
|
||
|
||
dest.toRelative(curr);
|
||
dest.write(xml);
|
||
|
||
xml.setCurTick(seg->tick());
|
||
xml.setCurTrack(track);
|
||
}
|
||
|
||
return voiceTagWritten;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// writeSegments
|
||
// ls - write upto this segment (excluding)
|
||
// can be zero
|
||
//---------------------------------------------------------
|
||
|
||
void Score::writeSegments(XmlWriter& xml, int strack, int etrack,
|
||
Segment* sseg, Segment* eseg, bool writeSystemElements, bool forceTimeSig)
|
||
{
|
||
Fraction startTick = xml.curTick();
|
||
Fraction endTick = eseg ? eseg->tick() : lastMeasure()->endTick();
|
||
bool clip = xml.clipboardmode();
|
||
|
||
// in clipboard mode, ls might be in an mmrest
|
||
// since we are traversing regular measures,
|
||
// force them out of mmRest
|
||
if (clip) {
|
||
Measure* lm = eseg ? eseg->measure() : 0;
|
||
Measure* fm = sseg ? sseg->measure() : 0;
|
||
if (lm && lm->isMMRest()) {
|
||
lm = lm->mmRestLast();
|
||
if (lm)
|
||
eseg = lm->nextMeasure() ? lm->nextMeasure()->first() : nullptr;
|
||
else
|
||
qDebug("writeSegments: no measure for end segment in mmrest");
|
||
}
|
||
if (fm && fm->isMMRest()) {
|
||
fm = fm->mmRestFirst();
|
||
if (fm)
|
||
sseg = fm->first();
|
||
}
|
||
}
|
||
|
||
QList<Spanner*> spanners;
|
||
#if 0
|
||
auto endIt = spanner().upper_bound(endTick);
|
||
for (auto i = spanner().begin(); i != endIt; ++i) {
|
||
Spanner* s = i->second;
|
||
#else
|
||
auto sl = spannerMap().findOverlapping(sseg->tick().ticks(), endTick.ticks());
|
||
for (auto i : sl) {
|
||
Spanner* s = i.value;
|
||
#endif
|
||
if (s->generated() || !xml.canWrite(s))
|
||
continue;
|
||
// don't write voltas to clipboard
|
||
if (clip && s->isVolta())
|
||
continue;
|
||
spanners.push_back(s);
|
||
}
|
||
|
||
int lastTrackWritten = strack - 1; // for counting necessary <voice> tags
|
||
for (int track = strack; track < etrack; ++track) {
|
||
if (!xml.canWriteVoice(track))
|
||
continue;
|
||
|
||
bool voiceTagWritten = false;
|
||
|
||
bool timeSigWritten = false; // for forceTimeSig
|
||
bool crWritten = false; // for forceTimeSig
|
||
bool keySigWritten = false; // for forceTimeSig
|
||
|
||
for (Segment* segment = sseg; segment && segment != eseg; segment = segment->next1()) {
|
||
if (!segment->enabled())
|
||
continue;
|
||
if (track == 0)
|
||
segment->setWritten(false);
|
||
Element* e = segment->element(track);
|
||
|
||
//
|
||
// special case: - barline span > 1
|
||
// - part (excerpt) staff starts after
|
||
// barline element
|
||
bool needMove = (segment->tick() != xml.curTick() || (track > lastTrackWritten));
|
||
if ((segment->isEndBarLineType()) && !e && writeSystemElements && ((track % VOICES) == 0)) {
|
||
// search barline:
|
||
for (int idx = track - VOICES; idx >= 0; idx -= VOICES) {
|
||
if (segment->element(idx)) {
|
||
int oDiff = xml.trackDiff();
|
||
xml.setTrackDiff(idx); // staffIdx should be zero
|
||
segment->element(idx)->write(xml);
|
||
xml.setTrackDiff(oDiff);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
for (Element* e1 : segment->annotations()) {
|
||
if (e1->track() != track || e1->generated() || (e1->systemFlag() && !writeSystemElements))
|
||
continue;
|
||
if (needMove) {
|
||
voiceTagWritten |= writeVoiceMove(xml, segment, startTick, track, &lastTrackWritten);
|
||
needMove = false;
|
||
}
|
||
e1->write(xml);
|
||
}
|
||
Measure* m = segment->measure();
|
||
// don't write spanners for multi measure rests
|
||
|
||
if ((!(m && m->isMMRest())) && segment->isChordRestType()) {
|
||
for (Spanner* s : spanners) {
|
||
if (s->track() == track) {
|
||
bool end = false;
|
||
if (s->anchor() == Spanner::Anchor::CHORD || s->anchor() == Spanner::Anchor::NOTE)
|
||
end = s->tick2() < endTick;
|
||
else
|
||
end = s->tick2() <= endTick;
|
||
if (s->tick() == segment->tick() && (!clip || end) && !s->isSlur()) {
|
||
if (needMove) {
|
||
voiceTagWritten |= writeVoiceMove(xml, segment, startTick, track, &lastTrackWritten);
|
||
needMove = false;
|
||
}
|
||
s->writeSpannerStart(xml, segment, track);
|
||
}
|
||
}
|
||
if ((s->tick2() == segment->tick())
|
||
&& !s->isSlur()
|
||
&& (s->effectiveTrack2() == track)
|
||
&& (!clip || s->tick() >= sseg->tick())
|
||
) {
|
||
if (needMove) {
|
||
voiceTagWritten |= writeVoiceMove(xml, segment, startTick, track, &lastTrackWritten);
|
||
needMove = false;
|
||
}
|
||
s->writeSpannerEnd(xml, segment, track);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!e || !xml.canWrite(e))
|
||
continue;
|
||
if (e->generated())
|
||
continue;
|
||
if (forceTimeSig && track2voice(track) == 0 && segment->segmentType() == SegmentType::ChordRest && !timeSigWritten && !crWritten) {
|
||
// Ensure that <voice> tag is open
|
||
voiceTagWritten |= writeVoiceMove(xml, segment, startTick, track, &lastTrackWritten);
|
||
// we will miss a key sig!
|
||
if (!keySigWritten) {
|
||
Key k = score()->staff(track2staff(track))->key(segment->tick());
|
||
KeySig* ks = new KeySig(this);
|
||
ks->setKey(k);
|
||
ks->write(xml);
|
||
delete ks;
|
||
keySigWritten = true;
|
||
}
|
||
// we will miss a time sig!
|
||
Fraction tsf = sigmap()->timesig(segment->tick()).timesig();
|
||
TimeSig* ts = new TimeSig(this);
|
||
ts->setSig(tsf);
|
||
ts->write(xml);
|
||
delete ts;
|
||
timeSigWritten = true;
|
||
}
|
||
if (needMove) {
|
||
voiceTagWritten |= writeVoiceMove(xml, segment, startTick, track, &lastTrackWritten);
|
||
needMove = false;
|
||
}
|
||
if (e->isChordRest()) {
|
||
ChordRest* cr = toChordRest(e);
|
||
cr->writeTupletStart(xml);
|
||
}
|
||
// if (segment->isEndBarLine() && (m->mmRestCount() < 0 || m->mmRest())) {
|
||
// BarLine* bl = toBarLine(e);
|
||
//TODO bl->setBarLineType(m->endBarLineType());
|
||
// bl->setVisible(m->endBarLineVisible());
|
||
// }
|
||
e->write(xml);
|
||
|
||
if (e->isChordRest()) {
|
||
ChordRest* cr = toChordRest(e);
|
||
cr->writeTupletEnd(xml);
|
||
}
|
||
|
||
if (!(e->isRest() && toRest(e)->isGap()))
|
||
segment->write(xml); // write only once
|
||
if (forceTimeSig) {
|
||
if (segment->segmentType() == SegmentType::KeySig)
|
||
keySigWritten = true;
|
||
if (segment->segmentType() == SegmentType::TimeSig)
|
||
timeSigWritten = true;
|
||
if (segment->segmentType() == SegmentType::ChordRest)
|
||
crWritten = true;
|
||
}
|
||
}
|
||
|
||
//write spanner ending after the last segment, on the last tick
|
||
if (clip || eseg == 0) {
|
||
for (Spanner* s : spanners) {
|
||
if ((s->tick2() == endTick)
|
||
&& !s->isSlur()
|
||
&& (s->track2() == track || (s->track2() == -1 && s->track() == track))
|
||
&& (!clip || s->tick() >= sseg->tick())
|
||
) {
|
||
s->writeSpannerEnd(xml, lastMeasure(), track, endTick);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (voiceTagWritten)
|
||
xml.etag(); // </voice>
|
||
}
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// searchTuplet
|
||
// search complete Dom for tuplet id
|
||
// last resort in case of error
|
||
//---------------------------------------------------------
|
||
|
||
Tuplet* Score::searchTuplet(XmlReader& /*e*/, int /*id*/)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
}
|
||
|