1 //=============================================================================
2 // MuseScore
3 // Music Composition & Notation
4 //
5 // Copyright (C) 2011-2012 Werner Schweer and others
6 //
7 // This program is free software; you can redistribute it and/or modify
8 // it under the terms of the GNU General Public License version 2
9 // as published by the Free Software Foundation and appearing in
10 // the file LICENSE.GPL
11 //=============================================================================
12
13 #include "config.h"
14 #include "score.h"
15 #include "xml.h"
16 #include "element.h"
17 #include "measure.h"
18 #include "segment.h"
19 #include "slur.h"
20 #include "chordrest.h"
21 #include "chord.h"
22 #include "tuplet.h"
23 #include "beam.h"
24 #include "revisions.h"
25 #include "page.h"
26 #include "part.h"
27 #include "staff.h"
28 #include "system.h"
29 #include "keysig.h"
30 #include "clef.h"
31 #include "text.h"
32 #include "ottava.h"
33 #include "volta.h"
34 #include "excerpt.h"
35 #include "mscore.h"
36 #include "stafftype.h"
37 #include "sym.h"
38 #include "scoreOrder.h"
39
40 #include "mscore/preferences.h"
41
42 #ifdef OMR
43 #include "omr/omr.h"
44 #include "omr/omrpage.h"
45 #endif
46
47 #ifdef AVSOMR
48 #include "avsomr/msmrwriter.h"
49 #endif
50
51 #include "sig.h"
52 #include "undo.h"
53 #include "imageStore.h"
54 #include "audio.h"
55 #include "barline.h"
56 #include "thirdparty/qzip/qzipreader_p.h"
57 #include "thirdparty/qzip/qzipwriter_p.h"
58 #ifdef Q_OS_WIN
59 #include <windows.h>
60 #endif
61
62 namespace Ms {
63
64 //---------------------------------------------------------
65 // writeMeasure
66 //---------------------------------------------------------
67
writeMeasure(XmlWriter & xml,MeasureBase * m,int staffIdx,bool writeSystemElements,bool forceTimeSig)68 static void writeMeasure(XmlWriter& xml, MeasureBase* m, int staffIdx, bool writeSystemElements, bool forceTimeSig)
69 {
70 //
71 // special case multi measure rest
72 //
73 if (m->isMeasure() || staffIdx == 0)
74 m->write(xml, staffIdx, writeSystemElements, forceTimeSig);
75
76 if (m->score()->styleB(Sid::createMultiMeasureRests) && m->isMeasure() && toMeasure(m)->mmRest())
77 toMeasure(m)->mmRest()->write(xml, staffIdx, writeSystemElements, forceTimeSig);
78
79 xml.setCurTick(m->endTick());
80 }
81
82 //---------------------------------------------------------
83 // writeMovement
84 //---------------------------------------------------------
85
writeMovement(XmlWriter & xml,bool selectionOnly)86 void Score::writeMovement(XmlWriter& xml, bool selectionOnly)
87 {
88 // if we have multi measure rests and some parts are hidden,
89 // then some layout information is missing:
90 // relayout with all parts set visible
91
92 QList<Part*> hiddenParts;
93 bool unhide = false;
94 if (styleB(Sid::createMultiMeasureRests)) {
95 for (Part* part : qAsConst(_parts)) {
96 if (!part->show()) {
97 if (!unhide) {
98 startCmd();
99 unhide = true;
100 }
101 part->undoChangeProperty(Pid::VISIBLE, true);
102 hiddenParts.append(part);
103 }
104 }
105 }
106 if (unhide) {
107 doLayout();
108 for (Part* p : hiddenParts)
109 p->setShow(false);
110 }
111
112 xml.stag(this);
113 if (excerpt()) {
114 Excerpt* e = excerpt();
115 QMultiMap<int, int> trackList = e->tracks();
116 QMapIterator<int, int> i(trackList);
117 if (!(trackList.size() == e->nstaves() * VOICES) && !trackList.isEmpty()) {
118 while (i.hasNext()) {
119 i.next();
120 xml.tagE(QString("Tracklist sTrack=\"%1\" dstTrack=\"%2\"").arg(i.key()).arg(i.value()));
121 }
122 }
123 }
124
125 if (lineMode())
126 xml.tag("layoutMode", "line");
127 if (systemMode())
128 xml.tag("layoutMode", "system");
129
130 #ifdef OMR
131 if (masterScore()->omr() && xml.writeOmr())
132 masterScore()->omr()->write(xml);
133 #endif
134 if (isMaster() && masterScore()->showOmr() && xml.writeOmr())
135 xml.tag("showOmr", masterScore()->showOmr());
136 if (_audio && xml.writeOmr()) {
137 xml.tag("playMode", int(_playMode));
138 _audio->write(xml);
139 }
140
141 for (int i = 0; i < 32; ++i) {
142 if (!_layerTags[i].isEmpty()) {
143 xml.tag(QString("LayerTag id=\"%1\" tag=\"%2\"").arg(i).arg(_layerTags[i]),
144 _layerTagComments[i]);
145 }
146 }
147 int n = _layer.size();
148 for (int i = 1; i < n; ++i) { // don’t save default variant
149 const Layer& l = _layer[i];
150 xml.tagE(QString("Layer name=\"%1\" mask=\"%2\"").arg(l.name).arg(l.tags));
151 }
152 xml.tag("currentLayer", _currentLayer);
153
154 if (isTopScore() && !MScore::testMode)
155 _synthesizerState.write(xml);
156
157 if (pageNumberOffset())
158 xml.tag("page-offset", pageNumberOffset());
159 xml.tag("Division", MScore::division);
160 xml.setCurTrack(-1);
161
162 if (isTopScore()) // only top score
163 style().save(xml, true); // save only differences to buildin style
164
165 xml.tag("showInvisible", _showInvisible);
166 xml.tag("showUnprintable", _showUnprintable);
167 xml.tag("showFrames", _showFrames);
168 xml.tag("showMargins", _showPageborders);
169 xml.tag("markIrregularMeasures", _markIrregularMeasures, true);
170
171 QMapIterator<QString, QString> i(_metaTags);
172 while (i.hasNext()) {
173 i.next();
174 // do not output "platform" and "creationDate" in test and save template mode
175 if ((!MScore::testMode && !MScore::saveTemplateMode) || (i.key() != "platform" && i.key() != "creationDate"))
176 xml.tag(QString("metaTag name=\"%1\"").arg(i.key().toHtmlEscaped()), i.value());
177 }
178
179 if (_scoreOrder && !_scoreOrder->isCustom()) {
180 ScoreOrder* order = _scoreOrder->clone();
181 order->updateInstruments(this);
182 order->write(xml);
183 delete order;
184 }
185
186 xml.setCurTrack(0);
187 int staffStart;
188 int staffEnd;
189 MeasureBase* measureStart;
190 MeasureBase* measureEnd;
191
192 if (selectionOnly) {
193 staffStart = _selection.staffStart();
194 staffEnd = _selection.staffEnd();
195 // make sure we select full parts
196 Staff* sStaff = staff(staffStart);
197 Part* sPart = sStaff->part();
198 Staff* eStaff = staff(staffEnd - 1);
199 Part* ePart = eStaff->part();
200 staffStart = staffIdx(sPart);
201 staffEnd = staffIdx(ePart) + ePart->nstaves();
202 measureStart = _selection.startSegment()->measure();
203 if (measureStart->isMeasure() && toMeasure(measureStart)->isMMRest())
204 measureStart = toMeasure(measureStart)->mmRestFirst();
205 if (_selection.endSegment())
206 measureEnd = _selection.endSegment()->measure()->next();
207 else
208 measureEnd = 0;
209 }
210 else {
211 staffStart = 0;
212 staffEnd = nstaves();
213 measureStart = first();
214 measureEnd = 0;
215 }
216
217 // Let's decide: write midi mapping to a file or not
218 masterScore()->checkMidiMapping();
219 for (const Part* part : qAsConst(_parts)) {
220 if (!selectionOnly || ((staffIdx(part) >= staffStart) && (staffEnd >= staffIdx(part) + part->nstaves())))
221 part->write(xml);
222 }
223
224 xml.setCurTrack(0);
225 xml.setTrackDiff(-staffStart * VOICES);
226 if (measureStart) {
227 for (int staffIdx = staffStart; staffIdx < staffEnd; ++staffIdx) {
228 xml.stag(staff(staffIdx), QString("id=\"%1\"").arg(staffIdx + 1 - staffStart));
229 xml.setCurTick(measureStart->tick());
230 xml.setTickDiff(xml.curTick());
231 xml.setCurTrack(staffIdx * VOICES);
232 bool writeSystemElements = (staffIdx == staffStart);
233 bool firstMeasureWritten = false;
234 bool forceTimeSig = false;
235 for (MeasureBase* m = measureStart; m != measureEnd; m = m->next()) {
236 // force timesig if first measure and selectionOnly
237 if (selectionOnly && m->isMeasure()) {
238 if (!firstMeasureWritten) {
239 forceTimeSig = true;
240 firstMeasureWritten = true;
241 }
242 else
243 forceTimeSig = false;
244 }
245 writeMeasure(xml, m, staffIdx, writeSystemElements, forceTimeSig);
246 }
247 xml.etag();
248 }
249 }
250 xml.setCurTrack(-1);
251 if (isMaster()) {
252 if (!selectionOnly) {
253 for (const Excerpt* excerpt : excerpts()) {
254 if (excerpt->partScore() != this)
255 excerpt->partScore()->write(xml, false); // recursion
256 }
257 }
258 }
259 else
260 xml.tag("name", excerpt()->title());
261 xml.etag();
262
263 if (unhide)
264 endCmd(true);
265 }
266
267 //---------------------------------------------------------
268 // write
269 //---------------------------------------------------------
270
write(XmlWriter & xml,bool selectionOnly)271 void Score::write(XmlWriter& xml, bool selectionOnly)
272 {
273 if (isMaster()) {
274 MasterScore* score = static_cast<MasterScore*>(this);
275
276 while (score->prev())
277 score = score->prev();
278 while (score) {
279 score->writeMovement(xml, selectionOnly);
280 score = score->next();
281 }
282 }
283 else
284 writeMovement(xml, selectionOnly);
285 }
286
287 //---------------------------------------------------------
288 // Staff
289 //---------------------------------------------------------
290
readStaff(XmlReader & e)291 void Score::readStaff(XmlReader& e)
292 {
293 int staff = e.intAttribute("id", 1) - 1;
294 int measureIdx = 0;
295 e.setCurrentMeasureIndex(0);
296 e.setTick(Fraction(0,1));
297 e.setTrack(staff * VOICES);
298
299 if (staff == 0) {
300 while (e.readNextStartElement()) {
301 const QStringRef& tag(e.name());
302
303 if (tag == "Measure") {
304 Measure* measure = 0;
305 measure = new Measure(this);
306 measure->setTick(e.tick());
307 e.setCurrentMeasureIndex(measureIdx++);
308 //
309 // inherit timesig from previous measure
310 //
311 Measure* m = e.lastMeasure(); // measure->prevMeasure();
312 Fraction f(m ? m->timesig() : Fraction(4,4));
313 measure->setTicks(f);
314 measure->setTimesig(f);
315
316 measure->read(e, staff);
317 measure->checkMeasure(staff);
318 if (!measure->isMMRest()) {
319 measures()->add(measure);
320 e.setLastMeasure(measure);
321 e.setTick(measure->tick() + measure->ticks());
322 }
323 else {
324 // this is a multi measure rest
325 // always preceded by the first measure it replaces
326 Measure* m1 = e.lastMeasure();
327
328 if (m1) {
329 m1->setMMRest(measure);
330 measure->setTick(m1->tick());
331 }
332 }
333 }
334 else if (tag == "HBox" || tag == "VBox" || tag == "TBox" || tag == "FBox") {
335 MeasureBase* mb = toMeasureBase(Element::name2Element(tag, this));
336 mb->read(e);
337 mb->setTick(e.tick());
338 measures()->add(mb);
339 }
340 else if (tag == "tick")
341 e.setTick(Fraction::fromTicks(fileDivision(e.readInt())));
342 else
343 e.unknown();
344 }
345 }
346 else {
347 Measure* measure = firstMeasure();
348 while (e.readNextStartElement()) {
349 const QStringRef& tag(e.name());
350
351 if (tag == "Measure") {
352 if (measure == 0) {
353 qDebug("Score::readStaff(): missing measure!");
354 measure = new Measure(this);
355 measure->setTick(e.tick());
356 measures()->add(measure);
357 }
358 e.setTick(measure->tick());
359 e.setCurrentMeasureIndex(measureIdx++);
360 measure->read(e, staff);
361 measure->checkMeasure(staff);
362 if (measure->isMMRest())
363 measure = e.lastMeasure()->nextMeasure();
364 else {
365 e.setLastMeasure(measure);
366 if (measure->mmRest())
367 measure = measure->mmRest();
368 else
369 measure = measure->nextMeasure();
370 }
371 }
372 else if (tag == "tick")
373 e.setTick(Fraction::fromTicks(fileDivision(e.readInt())));
374 else
375 e.unknown();
376 }
377 }
378 }
379
380 //---------------------------------------------------------
381 // saveFile
382 /// If file has generated name, create a modal file save dialog
383 /// and ask filename.
384 /// Rename old file to backup file (.xxxx.msc?,).
385 /// Default is to save score in .mscz format,
386 /// Return true if OK and false on error.
387 //---------------------------------------------------------
388
saveFile(bool generateBackup)389 bool MasterScore::saveFile(bool generateBackup)
390 {
391 if (readOnly())
392 return false;
393 QString suffix = info.suffix();
394 if (info.exists() && !info.isWritable()) {
395 MScore::lastError = tr("The following file is locked: \n%1 \n\nTry saving to a different location.").arg(info.filePath());
396 return false;
397 }
398 //
399 // step 1
400 // save into temporary file to prevent partially overwriting
401 // the original file in case of "disc full"
402 //
403
404 QString tempName = info.filePath() + QString(".temp");
405 QFile temp(tempName);
406 if (!temp.open(QIODevice::WriteOnly)) {
407 MScore::lastError = tr("Open Temp File\n%1\nfailed: %2").arg(tempName, strerror(errno));
408 return false;
409 }
410
411 bool rv = false;
412 if ("mscx" == suffix) {
413 rv = Score::saveFile(&temp, false);
414 }
415 #ifdef AVSOMR
416 else if ("msmr" == suffix) {
417 Avs::MsmrWriter msmrWriter;
418 rv = msmrWriter.saveMsmrFile(this, &temp, info);
419 }
420 #endif
421 else {
422 QString fileName = info.completeBaseName() + ".mscx";
423 rv = Score::saveCompressedFile(&temp, fileName, false);
424 }
425
426 if (!rv) {
427 return false;
428 }
429
430 if (temp.error() != QFile::NoError) {
431 MScore::lastError = tr("Save File failed: %1").arg(temp.errorString());
432 return false;
433 }
434 temp.close();
435
436 const QString name(info.filePath());
437 const QString basename(info.fileName());
438 QDir dir(info.path());
439 if (!saved() && generateBackup) {
440 // if file was already saved in this session
441 // save but don't overwrite backup again
442
443 const QString backupSubdirString = preferences.getString(PREF_APP_BACKUP_SUBFOLDER);
444 const QString backupDirString = info.path() + QString(QDir::separator()) + backupSubdirString;
445 QDir backupDir(backupDirString);
446 if (!backupDir.exists()) {
447 dir.mkdir(backupSubdirString);
448 #ifdef Q_OS_WIN
449 const QString backupDirNativePath = QDir::toNativeSeparators(backupDirString);
450 #if (defined (_MSCVER) || defined (_MSC_VER))
451 #if (defined (UNICODE))
452 SetFileAttributes((LPCTSTR)backupDirNativePath.unicode(), FILE_ATTRIBUTE_HIDDEN);
453 #else
454 // Use byte-based Windows function
455 SetFileAttributes((LPCTSTR)backupDirNativePath.toLocal8Bit(), FILE_ATTRIBUTE_HIDDEN);
456 #endif
457 #else
458 SetFileAttributes((LPCTSTR)backupDirNativePath.toLocal8Bit(), FILE_ATTRIBUTE_HIDDEN);
459 #endif
460 #endif
461 }
462 const QString backupName = QString(".") + info.fileName() + QString(",");
463 if (backupDir.exists(backupName)) {
464 if (!backupDir.remove(backupName)) {
465 // if (!MScore::noGui)
466 // QMessageBox::critical(0, QObject::tr("Save File"),
467 // tr("Removing old backup file %1 failed").arg(backupName));
468 }
469 }
470 //
471 // step 2
472 // backup files prior to 3.5 were saved in the same directory as the file itself.
473 // remove these old backup files if needed
474 //
475 if (dir != backupDir && dir.exists(backupName)) {
476 if (!dir.remove(backupName)) {
477 // if (!MScore::noGui)
478 // QMessageBox::critical(0, QObject::tr("Save File"),
479 // tr("Removing old backup file %1 failed").arg(backupName));
480 }
481 }
482
483 //
484 // step 3
485 // rename old file into backup
486 //
487 if (dir.exists(basename)) {
488 if (!QFile::rename(name, backupDirString + (backupDirString.endsWith("/") ? "" : "/") + backupName)) {
489 // if (!MScore::noGui)
490 // QMessageBox::critical(0, tr("Save File"),
491 // tr("Renaming old file <%1> to backup <%2> failed").arg(name, backupDirString + "/" + backupName);
492 }
493 }
494
495 QFileInfo fileBackup(backupDir, backupName);
496 _sessionStartBackupInfo = fileBackup;
497 }
498 else {
499 // file has previously been saved - remove the old file
500 if (dir.exists(basename)) {
501 if (!dir.remove(basename)) {
502 // if (!MScore::noGui)
503 // QMessageBox::critical(0, tr("Save File"),
504 // tr("Removing old file %1 failed").arg(name));
505 }
506 }
507 }
508
509 //
510 // step 4
511 // rename temp name into file name
512 //
513 if (!QFile::rename(tempName, name)) {
514 MScore::lastError = tr("Renaming temp. file <%1> to <%2> failed:\n%3").arg(tempName, name, strerror(errno));
515 return false;
516 }
517 // make file readable by all
518 QFile::setPermissions(name, QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser
519 | QFile::ReadGroup | QFile::ReadOther);
520
521 undoStack()->setClean();
522 setSaved(true);
523 info.refresh();
524 update();
525 return true;
526 }
527
528 //---------------------------------------------------------
529 // saveCompressedFile
530 //---------------------------------------------------------
531
saveCompressedFile(QFileInfo & info,bool onlySelection,bool createThumbnail)532 bool Score::saveCompressedFile(QFileInfo& info, bool onlySelection, bool createThumbnail)
533 {
534 if (readOnly() && info == *masterScore()->fileInfo())
535 return false;
536 QFile fp(info.filePath());
537 if (!fp.open(QIODevice::WriteOnly)) {
538 MScore::lastError = tr("Open File\n%1\nfailed: %2").arg(info.filePath(), strerror(errno));
539 return false;
540 }
541
542 QString fileName = info.completeBaseName() + ".mscx";
543 return saveCompressedFile(&fp, fileName, onlySelection, createThumbnail);
544 }
545
546 //---------------------------------------------------------
547 // createThumbnail
548 //---------------------------------------------------------
549
createThumbnail()550 QImage Score::createThumbnail()
551 {
552 LayoutMode mode = layoutMode();
553 setLayoutMode(LayoutMode::PAGE);
554 doLayout();
555
556 Page* page = pages().at(0);
557 QRectF fr = page->abbox();
558 qreal mag = 256.0 / qMax(fr.width(), fr.height());
559 int w = int(fr.width() * mag);
560 int h = int(fr.height() * mag);
561
562 QImage pm(w, h, QImage::Format_ARGB32_Premultiplied);
563
564 int dpm = lrint(DPMM * 1000.0);
565 pm.setDotsPerMeterX(dpm);
566 pm.setDotsPerMeterY(dpm);
567 pm.fill(0xffffffff);
568
569 double pr = MScore::pixelRatio;
570 MScore::pixelRatio = 1.0;
571
572 QPainter p(&pm);
573 p.setRenderHint(QPainter::Antialiasing, true);
574 p.setRenderHint(QPainter::TextAntialiasing, true);
575 p.scale(mag, mag);
576 print(&p, 0);
577 p.end();
578
579 MScore::pixelRatio = pr;
580
581 if (layoutMode() != mode) {
582 setLayoutMode(mode);
583 doLayout();
584 }
585 return pm;
586 }
587
588 //---------------------------------------------------------
589 // saveCompressedFile
590 // file is already opened
591 //---------------------------------------------------------
592
saveCompressedFile(QIODevice * f,const QString & fn,bool onlySelection,bool doCreateThumbnail)593 bool Score::saveCompressedFile(QIODevice* f, const QString& fn, bool onlySelection, bool doCreateThumbnail)
594 {
595 MQZipWriter uz(f);
596
597 QBuffer cbuf;
598 cbuf.open(QIODevice::ReadWrite);
599 XmlWriter xml(this, &cbuf);
600 xml << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
601 xml.stag("container");
602 xml.stag("rootfiles");
603 xml.stag(QString("rootfile full-path=\"%1\"").arg(XmlWriter::xmlString(fn)));
604 xml.etag();
605 for (ImageStoreItem* ip : imageStore) {
606 if (!ip->isUsed(this))
607 continue;
608 QString path = QString("Pictures/") + ip->hashName();
609 xml.tag("file", path);
610 }
611
612 xml.etag();
613 xml.etag();
614 cbuf.seek(0);
615 //uz.addDirectory("META-INF");
616 uz.addFile("META-INF/container.xml", cbuf.data());
617
618 QBuffer dbuf;
619 dbuf.open(QIODevice::ReadWrite);
620 saveFile(&dbuf, true, onlySelection);
621 dbuf.seek(0);
622 uz.addFile(fn, dbuf.data());
623
624 QFileDevice* fd = dynamic_cast<QFileDevice*>(f);
625 if (fd) // if is file (may be buffer)
626 fd->flush(); // flush to preserve score data in case of
627 // any failures on the further operations.
628
629 // save images
630 //uz.addDirectory("Pictures");
631 for (ImageStoreItem* ip : imageStore) {
632 if (!ip->isUsed(this))
633 continue;
634 QString path = QString("Pictures/") + ip->hashName();
635 uz.addFile(path, ip->buffer());
636 }
637
638 // create thumbnail
639 if (doCreateThumbnail && !pages().isEmpty()) {
640 QImage pm = createThumbnail();
641
642 QByteArray ba;
643 QBuffer b(&ba);
644 if (!b.open(QIODevice::WriteOnly))
645 qDebug("open buffer failed");
646 if (!pm.save(&b, "PNG"))
647 qDebug("save failed");
648 uz.addFile("Thumbnails/thumbnail.png", ba);
649 }
650
651 #ifdef OMR
652 //
653 // save OMR page images
654 //
655 if (masterScore()->omr()) {
656 int n = masterScore()->omr()->numPages();
657 for (int i = 0; i < n; ++i) {
658 QString path = QString("OmrPages/page%1.png").arg(i+1);
659 QBuffer cbuf1;
660 OmrPage* page = masterScore()->omr()->page(i);
661 const QImage& image = page->image();
662 if (!image.save(&cbuf1, "PNG")) {
663 MScore::lastError = tr("Save file: cannot save image (%1x%2)").arg(image.width(), image.height());
664 return false;
665 }
666 uz.addFile(path, cbuf1.data());
667 cbuf1.close();
668 }
669 }
670 #endif
671 //
672 // save audio
673 //
674 if (_audio)
675 uz.addFile("audio.ogg", _audio->data());
676
677 uz.close();
678 return true;
679 }
680
681 //---------------------------------------------------------
682 // saveFile
683 // return true on success
684 //---------------------------------------------------------
685
saveFile(QFileInfo & info)686 bool Score::saveFile(QFileInfo& info)
687 {
688 if (readOnly() && info == *masterScore()->fileInfo())
689 return false;
690 if (info.suffix().isEmpty())
691 info.setFile(info.filePath() + ".mscx");
692 QFile fp(info.filePath());
693 if (!fp.open(QIODevice::WriteOnly)) {
694 MScore::lastError = tr("Open File\n%1\nfailed: %2").arg(info.filePath(), strerror(errno));
695 return false;
696 }
697 saveFile(&fp, false, false);
698 fp.close();
699 return true;
700 }
701
702 //---------------------------------------------------------
703 // loadStyle
704 //---------------------------------------------------------
705
loadStyle(const QString & fn,bool ign,const bool overlap)706 bool Score::loadStyle(const QString& fn, bool ign, const bool overlap)
707 {
708 QFile f(fn);
709 if (f.open(QIODevice::ReadOnly)) {
710 MStyle st = style();
711 if (st.load(&f, ign)) {
712 undo(new ChangeStyle(this, st, overlap));
713 return true;
714 }
715 else {
716 MScore::lastError = QObject::tr("The style file is not compatible with this version of MuseScore.");
717 return false;
718 }
719 }
720 MScore::lastError = strerror(errno);
721 return false;
722 }
723
724 //---------------------------------------------------------
725 // saveStyle
726 //---------------------------------------------------------
727
saveStyle(const QString & name)728 bool Score::saveStyle(const QString& name)
729 {
730 QString ext(".mss");
731 QFileInfo info(name);
732
733 if (info.suffix().isEmpty())
734 info.setFile(info.filePath() + ext);
735 QFile f(info.filePath());
736 if (!f.open(QIODevice::WriteOnly)) {
737 MScore::lastError = tr("Open Style File\n%1\nfailed: %2").arg(info.filePath(), strerror(errno));
738 return false;
739 }
740
741 XmlWriter xml(this, &f);
742 xml.header();
743 xml.stag("museScore version=\"" MSC_VERSION "\"");
744 style().save(xml, false); // save complete style
745 xml.etag();
746 if (f.error() != QFile::NoError) {
747 MScore::lastError = QObject::tr("Write Style failed: %1").arg(f.errorString());
748 return false;
749 }
750 return true;
751 }
752
753 //---------------------------------------------------------
754 // saveFile
755 // return true on success
756 //---------------------------------------------------------
757
758 extern QString revision;
759
saveFile(QIODevice * f,bool msczFormat,bool onlySelection)760 bool Score::saveFile(QIODevice* f, bool msczFormat, bool onlySelection)
761 {
762 XmlWriter xml(this, f);
763 xml.setWriteOmr(msczFormat);
764 xml.header();
765
766 xml.stag("museScore version=\"" MSC_VERSION "\"");
767
768 if (!MScore::testMode) {
769 xml.tag("programVersion", VERSION);
770 xml.tag("programRevision", revision);
771 }
772 write(xml, onlySelection);
773 xml.etag();
774 if (isMaster())
775 masterScore()->revisions()->write(xml);
776 if (!onlySelection) {
777 //update version values for i.e. plugin access
778 _mscoreVersion = VERSION;
779 _mscoreRevision = revision.toInt(0, 16);
780 _mscVersion = MSCVERSION;
781 }
782 return true;
783 }
784
785 //---------------------------------------------------------
786 // readRootFile
787 //---------------------------------------------------------
788
readRootFile(MQZipReader * uz,QList<QString> & images)789 QString readRootFile(MQZipReader* uz, QList<QString>& images)
790 {
791 QString rootfile;
792
793 QByteArray cbuf = uz->fileData("META-INF/container.xml");
794 if (cbuf.isEmpty()) {
795 qDebug("can't find container.xml");
796 return rootfile;
797 }
798
799 XmlReader e(cbuf);
800
801 while (e.readNextStartElement()) {
802 if (e.name() != "container") {
803 e.unknown();
804 continue;
805 }
806 while (e.readNextStartElement()) {
807 if (e.name() != "rootfiles") {
808 e.unknown();
809 continue;
810 }
811 while (e.readNextStartElement()) {
812 const QStringRef& tag(e.name());
813
814 if (tag == "rootfile") {
815 if (rootfile.isEmpty()) {
816 rootfile = e.attribute("full-path");
817 e.skipCurrentElement();
818 }
819 }
820 else if (tag == "file")
821 images.append(e.readElementText());
822 else
823 e.unknown();
824 }
825 }
826 }
827 return rootfile;
828 }
829
830 //---------------------------------------------------------
831 // loadCompressedMsc
832 // return false on error
833 //---------------------------------------------------------
834
loadCompressedMsc(QIODevice * io,bool ignoreVersionError)835 Score::FileError MasterScore::loadCompressedMsc(QIODevice* io, bool ignoreVersionError)
836 {
837 MQZipReader uz(io);
838
839 QList<QString> sl;
840 QString rootfile = readRootFile(&uz, sl);
841 if (rootfile.isEmpty())
842 return FileError::FILE_NO_ROOTFILE;
843
844 //
845 // load images
846 //
847 if (!MScore::noImages) {
848 foreach(const QString& s, sl) {
849 QByteArray dbuf = uz.fileData(s);
850 imageStore.add(s, dbuf);
851 }
852 }
853
854 QByteArray dbuf = uz.fileData(rootfile);
855 if (dbuf.isEmpty()) {
856 QVector<MQZipReader::FileInfo> fil = uz.fileInfoList();
857 foreach(const MQZipReader::FileInfo& fi, fil) {
858 if (fi.filePath.endsWith(".mscx")) {
859 dbuf = uz.fileData(fi.filePath);
860 break;
861 }
862 }
863 }
864
865 XmlReader e(dbuf);
866 e.setDocName(masterScore()->fileInfo()->completeBaseName());
867
868 FileError retval = read1(e, ignoreVersionError);
869
870 #ifdef OMR
871 //
872 // load OMR page images
873 //
874 if (masterScore()->omr()) {
875 int n = masterScore()->omr()->numPages();
876 for (int i = 0; i < n; ++i) {
877 QString path = QString("OmrPages/page%1.png").arg(i+1);
878 QByteArray dbuf1 = uz.fileData(path);
879 OmrPage* page = masterScore()->omr()->page(i);
880 QImage image;
881 if (image.loadFromData(dbuf1, "PNG")) {
882 page->setImage(image);
883 }
884 else
885 qDebug("load image failed");
886 }
887 }
888 #endif
889 //
890 // read audio
891 //
892 if (audio()) {
893 QByteArray dbuf1 = uz.fileData("audio.ogg");
894 audio()->setData(dbuf1);
895 }
896 return retval;
897 }
898
styleDefaultByMscVersion(const int mscVer) const899 int MasterScore::styleDefaultByMscVersion(const int mscVer) const
900 {
901 constexpr int LEGACY_MSC_VERSION_V3 = 301;
902 constexpr int LEGACY_MSC_VERSION_V2 = 206;
903 constexpr int LEGACY_MSC_VERSION_V1 = 114;
904
905 if (mscVer > LEGACY_MSC_VERSION_V2 && mscVer < MSCVERSION)
906 return LEGACY_MSC_VERSION_V3;
907
908 if (mscVer > LEGACY_MSC_VERSION_V1 && mscVer <= LEGACY_MSC_VERSION_V2)
909 return LEGACY_MSC_VERSION_V2;
910
911 if (mscVer <= LEGACY_MSC_VERSION_V1)
912 return LEGACY_MSC_VERSION_V1;
913
914 return MSCVERSION;
915 }
916
readStyleDefaultsVersion()917 int MasterScore::readStyleDefaultsVersion()
918 {
919 if (styleB(Sid::usePre_3_6_defaults))
920 return style().defaultStyleVersion();
921
922 XmlReader e(readToBuffer());
923 e.setDocName(masterScore()->fileInfo()->completeBaseName());
924
925 while (!e.atEnd()) {
926 e.readNext();
927 if (e.name() == "defaultsVersion")
928 return e.readInt();
929 }
930
931 return styleDefaultByMscVersion(mscVersion());
932 }
933
934 //---------------------------------------------------------
935 // loadMsc
936 // return true on success
937 //---------------------------------------------------------
938
loadMsc(QString name,bool ignoreVersionError)939 Score::FileError MasterScore::loadMsc(QString name, bool ignoreVersionError)
940 {
941 QFile f(name);
942 if (!f.open(QIODevice::ReadOnly)) {
943 MScore::lastError = f.errorString();
944 return FileError::FILE_OPEN_ERROR;
945 }
946 return loadMsc(name, &f, ignoreVersionError);
947 }
948
loadMsc(QString name,QIODevice * io,bool ignoreVersionError)949 Score::FileError MasterScore::loadMsc(QString name, QIODevice* io, bool ignoreVersionError)
950 {
951 ScoreLoad sl;
952 fileInfo()->setFile(name);
953
954 if (name.endsWith(".mscz") || name.endsWith(".mscz,"))
955 return loadCompressedMsc(io, ignoreVersionError);
956 else {
957 XmlReader r(io);
958 return read1(r, ignoreVersionError);
959 }
960 }
961
962 //---------------------------------------------------------
963 // parseVersion
964 //---------------------------------------------------------
965
parseVersion(const QString & val)966 void MasterScore::parseVersion(const QString& val)
967 {
968 QRegExp re("(\\d+)\\.(\\d+)\\.(\\d+)");
969 int v1, v2, v3, rv1, rv2, rv3;
970 if (re.indexIn(VERSION) != -1) {
971 QStringList sl = re.capturedTexts();
972 if (sl.size() == 4) {
973 v1 = sl[1].toInt();
974 v2 = sl[2].toInt();
975 v3 = sl[3].toInt();
976 if (re.indexIn(val) != -1) {
977 sl = re.capturedTexts();
978 if (sl.size() == 4) {
979 rv1 = sl[1].toInt();
980 rv2 = sl[2].toInt();
981 rv3 = sl[3].toInt();
982
983 int currentVersion = v1 * 10000 + v2 * 100 + v3;
984 int readVersion = rv1 * 10000 + rv2 * 100 + rv3;
985 if (readVersion > currentVersion) {
986 qDebug("read future version");
987 }
988 }
989 }
990 else {
991 QRegExp re1("(\\d+)\\.(\\d+)");
992 if (re1.indexIn(val) != -1) {
993 sl = re.capturedTexts();
994 if (sl.size() == 3) {
995 rv1 = sl[1].toInt();
996 rv2 = sl[2].toInt();
997
998 int currentVersion = v1 * 10000 + v2 * 100 + v3;
999 int readVersion = rv1 * 10000 + rv2 * 100;
1000 if (readVersion > currentVersion) {
1001 qDebug("read future version");
1002 }
1003 }
1004 }
1005 else
1006 qDebug("1cannot parse <%s>", qPrintable(val));
1007 }
1008 }
1009 }
1010 else
1011 qDebug("2cannot parse <%s>", VERSION);
1012 }
1013
1014 //---------------------------------------------------------
1015 // read1
1016 // return true on success
1017 //---------------------------------------------------------
1018
read1(XmlReader & e,bool ignoreVersionError)1019 Score::FileError MasterScore::read1(XmlReader& e, bool ignoreVersionError)
1020 {
1021 while (e.readNextStartElement()) {
1022 if (e.name() == "museScore") {
1023 const QString& version = e.attribute("version");
1024 QStringList sl = version.split('.');
1025 setMscVersion(sl[0].toInt() * 100 + sl[1].toInt());
1026
1027 if (!ignoreVersionError) {
1028 if (mscVersion() > MSCVERSION)
1029 return FileError::FILE_TOO_NEW;
1030 if (mscVersion() < 114)
1031 return FileError::FILE_TOO_OLD;
1032 if (mscVersion() == 300)
1033 return FileError::FILE_OLD_300_FORMAT;
1034 }
1035
1036 if (created() && !preferences.getString(PREF_SCORE_STYLE_DEFAULTSTYLEFILE).isEmpty()) {
1037 setStyle(MScore::defaultStyle());
1038 }
1039 else {
1040 int defaultsVersion = readStyleDefaultsVersion();
1041
1042 setStyle(*MStyle::resolveStyleDefaults(defaultsVersion));
1043 style().setDefaultStyleVersion(defaultsVersion);
1044 }
1045
1046 Score::FileError error;
1047 if (mscVersion() <= 114)
1048 error = read114(e);
1049 else if (mscVersion() <= 207)
1050 error = read206(e);
1051 else
1052 error = read302(e);
1053 setExcerptsChanged(false);
1054 return error;
1055 }
1056 else
1057 e.unknown();
1058 }
1059 return FileError::FILE_CORRUPTED;
1060 }
1061
1062 //---------------------------------------------------------
1063 // print
1064 //---------------------------------------------------------
1065
print(QPainter * painter,int pageNo)1066 void Score::print(QPainter* painter, int pageNo)
1067 {
1068 _printing = true;
1069 MScore::pdfPrinting = true;
1070 Page* page = pages().at(pageNo);
1071 QRectF fr = page->abbox();
1072
1073 QList<Element*> ell = page->items(fr);
1074 std::stable_sort(ell.begin(), ell.end(), elementLessThan);
1075 for (const Element* e : qAsConst(ell)) {
1076 if (!e->visible())
1077 continue;
1078 painter->save();
1079 painter->translate(e->pagePos());
1080 e->draw(painter);
1081 painter->restore();
1082 }
1083 MScore::pdfPrinting = false;
1084 _printing = false;
1085 }
1086
1087 //---------------------------------------------------------
1088 // readCompressedToBuffer
1089 //---------------------------------------------------------
1090
readCompressedToBuffer()1091 QByteArray MasterScore::readCompressedToBuffer()
1092 {
1093 MQZipReader uz(info.filePath());
1094 if (!uz.exists()) {
1095 qDebug("Score::readCompressedToBuffer: cannot read zip file");
1096 return QByteArray();
1097 }
1098 QList<QString> images;
1099 QString rootfile = readRootFile(&uz, images);
1100
1101 //
1102 // load images
1103 //
1104 foreach(const QString& s, images) {
1105 QByteArray dbuf = uz.fileData(s);
1106 imageStore.add(s, dbuf);
1107 }
1108
1109 if (rootfile.isEmpty()) {
1110 qDebug("=can't find rootfile in: %s", qPrintable(info.filePath()));
1111 return QByteArray();
1112 }
1113 return uz.fileData(rootfile);
1114 }
1115
1116 //---------------------------------------------------------
1117 // readToBuffer
1118 //---------------------------------------------------------
1119
readToBuffer()1120 QByteArray MasterScore::readToBuffer()
1121 {
1122 QByteArray ba;
1123 QString cs = info.suffix();
1124
1125 if (cs == "mscz") {
1126 ba = readCompressedToBuffer();
1127 }
1128 if (cs.toLower() == "msc" || cs.toLower() == "mscx") {
1129 QFile f(info.filePath());
1130 if (f.open(QIODevice::ReadOnly)) {
1131 ba = f.readAll();
1132 f.close();
1133 }
1134 }
1135 return ba;
1136 }
1137
1138 //---------------------------------------------------------
1139 // createRevision
1140 //---------------------------------------------------------
1141
createRevision()1142 void Score::createRevision()
1143 {
1144 #if 0
1145 qDebug("createRevision");
1146 QBuffer dbuf;
1147 dbuf.open(QIODevice::ReadWrite);
1148 saveFile(&dbuf, false, false);
1149 dbuf.close();
1150
1151 QByteArray ba1 = readToBuffer();
1152
1153 QString os = QString::fromUtf8(ba1.data(), ba1.size());
1154 QString ns = QString::fromUtf8(dbuf.buffer().data(), dbuf.buffer().size());
1155
1156 diff_match_patch dmp;
1157 Revision* r = new Revision();
1158 r->setDiff(dmp.patch_toText(dmp.patch_make(ns, os)));
1159 r->setId("1");
1160 _revisions->add(r);
1161
1162 // qDebug("patch:\n%s\n==========", qPrintable(patch));
1163 //
1164 #endif
1165 }
1166
1167 //---------------------------------------------------------
1168 // writeVoiceMove
1169 // write <move> and starting <voice> tags to denote
1170 // change in position.
1171 // Returns true if <voice> tag was written.
1172 //---------------------------------------------------------
1173
writeVoiceMove(XmlWriter & xml,Segment * seg,const Fraction & startTick,int track,int * lastTrackWrittenPtr)1174 static bool writeVoiceMove(XmlWriter& xml, Segment* seg, const Fraction& startTick, int track, int* lastTrackWrittenPtr)
1175 {
1176 bool voiceTagWritten = false;
1177 int& lastTrackWritten = *lastTrackWrittenPtr;
1178 if ((lastTrackWritten < track) && !xml.clipboardmode()) {
1179 while (lastTrackWritten < (track - 1)) {
1180 xml.tagE("voice");
1181 ++lastTrackWritten;
1182 }
1183 xml.stag("voice");
1184 xml.setCurTick(startTick);
1185 xml.setCurTrack(track);
1186 ++lastTrackWritten;
1187 voiceTagWritten = true;
1188 }
1189
1190 if ((xml.curTick() != seg->tick()) || (track != xml.curTrack())) {
1191 Location curr = Location::absolute();
1192 Location dest = Location::absolute();
1193 curr.setFrac(xml.curTick());
1194 dest.setFrac(seg->tick());
1195 curr.setTrack(xml.curTrack());
1196 dest.setTrack(track);
1197
1198 dest.toRelative(curr);
1199 dest.write(xml);
1200
1201 xml.setCurTick(seg->tick());
1202 xml.setCurTrack(track);
1203 }
1204
1205 return voiceTagWritten;
1206 }
1207
1208 //---------------------------------------------------------
1209 // writeSegments
1210 // ls - write upto this segment (excluding)
1211 // can be zero
1212 //---------------------------------------------------------
1213
writeSegments(XmlWriter & xml,int strack,int etrack,Segment * sseg,Segment * eseg,bool writeSystemElements,bool forceTimeSig)1214 void Score::writeSegments(XmlWriter& xml, int strack, int etrack,
1215 Segment* sseg, Segment* eseg, bool writeSystemElements, bool forceTimeSig)
1216 {
1217 Fraction startTick = xml.curTick();
1218 Fraction endTick = eseg ? eseg->tick() : lastMeasure()->endTick();
1219 bool clip = xml.clipboardmode();
1220
1221 // in clipboard mode, ls might be in an mmrest
1222 // since we are traversing regular measures,
1223 // force them out of mmRest
1224 if (clip) {
1225 Measure* lm = eseg ? eseg->measure() : 0;
1226 Measure* fm = sseg ? sseg->measure() : 0;
1227 if (lm && lm->isMMRest()) {
1228 lm = lm->mmRestLast();
1229 if (lm)
1230 eseg = lm->nextMeasure() ? lm->nextMeasure()->first() : nullptr;
1231 else
1232 qDebug("writeSegments: no measure for end segment in mmrest");
1233 }
1234 if (fm && fm->isMMRest()) {
1235 fm = fm->mmRestFirst();
1236 if (fm)
1237 sseg = fm->first();
1238 }
1239 }
1240
1241 QList<Spanner*> spanners;
1242 #if 0
1243 auto endIt = spanner().upper_bound(endTick);
1244 for (auto i = spanner().begin(); i != endIt; ++i) {
1245 Spanner* s = i->second;
1246 #else
1247 auto sl = spannerMap().findOverlapping(sseg->tick().ticks(), endTick.ticks());
1248 for (auto i : sl) {
1249 Spanner* s = i.value;
1250 #endif
1251 if (s->generated() || !xml.canWrite(s))
1252 continue;
1253 // don't write voltas to clipboard
1254 if (clip && s->isVolta())
1255 continue;
1256 spanners.push_back(s);
1257 }
1258
1259 int lastTrackWritten = strack - 1; // for counting necessary <voice> tags
1260 for (int track = strack; track < etrack; ++track) {
1261 if (!xml.canWriteVoice(track))
1262 continue;
1263
1264 bool voiceTagWritten = false;
1265
1266 bool timeSigWritten = false; // for forceTimeSig
1267 bool crWritten = false; // for forceTimeSig
1268 bool keySigWritten = false; // for forceTimeSig
1269
1270 for (Segment* segment = sseg; segment && segment != eseg; segment = segment->next1()) {
1271 if (!segment->enabled())
1272 continue;
1273 if (track == 0)
1274 segment->setWritten(false);
1275 Element* e = segment->element(track);
1276
1277 //
1278 // special case: - barline span > 1
1279 // - part (excerpt) staff starts after
1280 // barline element
1281 bool needMove = (segment->tick() != xml.curTick() || (track > lastTrackWritten));
1282 if ((segment->isEndBarLineType()) && !e && writeSystemElements && ((track % VOICES) == 0)) {
1283 // search barline:
1284 for (int idx = track - VOICES; idx >= 0; idx -= VOICES) {
1285 if (segment->element(idx)) {
1286 int oDiff = xml.trackDiff();
1287 xml.setTrackDiff(idx); // staffIdx should be zero
1288 segment->element(idx)->write(xml);
1289 xml.setTrackDiff(oDiff);
1290 break;
1291 }
1292 }
1293 }
1294 for (Element* e1 : segment->annotations()) {
1295 if (e1->track() != track || e1->generated() || (e1->systemFlag() && !writeSystemElements))
1296 continue;
1297 if (needMove) {
1298 voiceTagWritten |= writeVoiceMove(xml, segment, startTick, track, &lastTrackWritten);
1299 needMove = false;
1300 }
1301 e1->write(xml);
1302 }
1303 Measure* m = segment->measure();
1304 // don't write spanners for multi measure rests
1305
1306 if ((!(m && m->isMMRest())) && segment->isChordRestType()) {
1307 for (Spanner* s : spanners) {
1308 if (s->track() == track) {
1309 bool end = false;
1310 if (s->anchor() == Spanner::Anchor::CHORD || s->anchor() == Spanner::Anchor::NOTE)
1311 end = s->tick2() < endTick;
1312 else
1313 end = s->tick2() <= endTick;
1314 if (s->tick() == segment->tick() && (!clip || end) && !s->isSlur()) {
1315 if (needMove) {
1316 voiceTagWritten |= writeVoiceMove(xml, segment, startTick, track, &lastTrackWritten);
1317 needMove = false;
1318 }
1319 s->writeSpannerStart(xml, segment, track);
1320 }
1321 }
1322 if ((s->tick2() == segment->tick())
1323 && !s->isSlur()
1324 && (s->effectiveTrack2() == track)
1325 && (!clip || s->tick() >= sseg->tick())
1326 ) {
1327 if (needMove) {
1328 voiceTagWritten |= writeVoiceMove(xml, segment, startTick, track, &lastTrackWritten);
1329 needMove = false;
1330 }
1331 s->writeSpannerEnd(xml, segment, track);
1332 }
1333 }
1334 }
1335
1336 if (!e || !xml.canWrite(e))
1337 continue;
1338 if (e->generated())
1339 continue;
1340 if (forceTimeSig && track2voice(track) == 0 && segment->segmentType() == SegmentType::ChordRest && !timeSigWritten && !crWritten) {
1341 // Ensure that <voice> tag is open
1342 voiceTagWritten |= writeVoiceMove(xml, segment, startTick, track, &lastTrackWritten);
1343 // we will miss a key sig!
1344 if (!keySigWritten) {
1345 Key k = score()->staff(track2staff(track))->key(segment->tick());
1346 KeySig* ks = new KeySig(this);
1347 ks->setKey(k);
1348 ks->write(xml);
1349 delete ks;
1350 keySigWritten = true;
1351 }
1352 // we will miss a time sig!
1353 Fraction tsf = sigmap()->timesig(segment->tick()).timesig();
1354 TimeSig* ts = new TimeSig(this);
1355 ts->setSig(tsf);
1356 ts->write(xml);
1357 delete ts;
1358 timeSigWritten = true;
1359 }
1360 if (needMove) {
1361 voiceTagWritten |= writeVoiceMove(xml, segment, startTick, track, &lastTrackWritten);
1362 needMove = false;
1363 }
1364 if (e->isChordRest()) {
1365 ChordRest* cr = toChordRest(e);
1366 cr->writeTupletStart(xml);
1367 }
1368 // if (segment->isEndBarLine() && (m->mmRestCount() < 0 || m->mmRest())) {
1369 // BarLine* bl = toBarLine(e);
1370 //TODO bl->setBarLineType(m->endBarLineType());
1371 // bl->setVisible(m->endBarLineVisible());
1372 // }
1373 e->write(xml);
1374
1375 if (e->isChordRest()) {
1376 ChordRest* cr = toChordRest(e);
1377 cr->writeTupletEnd(xml);
1378 }
1379
1380 if (!(e->isRest() && toRest(e)->isGap()))
1381 segment->write(xml); // write only once
1382 if (forceTimeSig) {
1383 if (segment->segmentType() == SegmentType::KeySig)
1384 keySigWritten = true;
1385 if (segment->segmentType() == SegmentType::TimeSig)
1386 timeSigWritten = true;
1387 if (segment->segmentType() == SegmentType::ChordRest)
1388 crWritten = true;
1389 }
1390 }
1391
1392 //write spanner ending after the last segment, on the last tick
1393 if (clip || eseg == 0) {
1394 for (Spanner* s : spanners) {
1395 if ((s->tick2() == endTick)
1396 && !s->isSlur()
1397 && (s->track2() == track || (s->track2() == -1 && s->track() == track))
1398 && (!clip || s->tick() >= sseg->tick())
1399 ) {
1400 s->writeSpannerEnd(xml, lastMeasure(), track, endTick);
1401 }
1402 }
1403 }
1404
1405 if (voiceTagWritten)
1406 xml.etag(); // </voice>
1407 }
1408 }
1409
1410 //---------------------------------------------------------
1411 // searchTuplet
1412 // search complete Dom for tuplet id
1413 // last resort in case of error
1414 //---------------------------------------------------------
1415
1416 Tuplet* Score::searchTuplet(XmlReader& /*e*/, int /*id*/)
1417 {
1418 return 0;
1419 }
1420
1421 }
1422
1423