1 //=============================================================================
2 // MuseScore
3 // Music Composition & Notation
4 //
5 // Copyright (C) 2018 Werner Schweer
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 LICENCE.GPL
11 //=============================================================================
12
13 #include "scorediff.h"
14
15 #include "duration.h"
16 #include "measure.h"
17 #include "score.h"
18 #include "staff.h"
19 #include "xml.h"
20
21 #include "dtl/dtl.hpp"
22
23 #include <algorithm>
24 #include <utility>
25
26 namespace Ms {
27
28 //---------------------------------------------------------
29 // MscxModeDiff
30 //---------------------------------------------------------
31
32 class MscxModeDiff {
33 static constexpr const char* tagRegExpStr = "</?(?<name>[A-z_][A-z_0-9\\-.:]*)( [A-z_0-9\\-.:\\s=\"]*)?/?>";
34 const QRegularExpression tagRegExp;
35
36 enum TagType { START_TAG, END_TAG };
37 struct Tag {
38 int line;
39 TagType type;
40 QString name;
41
TagMs::MscxModeDiff::Tag42 Tag(int l, TagType t, const QString& n) : line(l), type(t), name(n) {}
43 };
44
45 static DiffType fromDtlDiffType(dtl::edit_t dtlType);
46
47 void adjustSemanticsMscx(std::vector<TextDiff>&);
48 int adjustSemanticsMscxOneDiff(std::vector<TextDiff>& diffs, int index);
49 int nextDiffOnShiftIndex(const std::vector<TextDiff>& diffs, int index, bool down);
50 static QString getOuterLines(const QString& str, int lines, bool start);
51 bool assessShiftDiff(const std::vector<TextDiff>&, const std::vector<Tag>& extraTags, int index, int lines);
52 int performShiftDiff(std::vector<TextDiff>&, int index, int lines);
53 void readMscx(const QString& xml, std::vector<Tag>& extraTags);
54 void handleTag(int line, TagType type, const QString& name, std::vector<Tag>& extraTags);
55
56 public:
57 MscxModeDiff();
58
59 std::vector<TextDiff> lineModeDiff(const QString& s1, const QString& s2);
60 std::vector<TextDiff> mscxModeDiff(const QString& s1, const QString& s2);
61 };
62
63 //---------------------------------------------------------
64 // TextDiffParser
65 // Used by ScoreDiff class during parsing MSCX text diff
66 //---------------------------------------------------------
67
68 class TextDiffParser {
69 const int iScore;
70 const int iOtherScore;
71
72 int tagLevel = 0;
73 int currentDiffTagLevel = 0;
74 std::vector<const ScoreElement*> contextsStack;
75 std::vector<bool> tagIsElement;
76 const ScoreElement* lastElementEnded = nullptr;
77 QString markupContext;
78
79 BaseDiff* handleToken(const QXmlStreamReader& r, const ScoreElement* newElement, bool saveDiff);
insideDiffTag() const80 bool insideDiffTag() const { return currentDiffTagLevel != 0; }
81
82 public:
83 TextDiffParser(int iScore);
84
85 void makeDiffs(const QString& mscx, const std::vector<std::pair<const ScoreElement*, QString>>& elements, const std::vector<TextDiff>& textDiffs, std::vector<BaseDiff*>& diffs);
86 };
87
88 //---------------------------------------------------------
89 // MscxModeDiff::MscxModeDiff
90 //---------------------------------------------------------
91
MscxModeDiff()92 MscxModeDiff::MscxModeDiff()
93 : tagRegExp(tagRegExpStr)
94 {}
95
96 //---------------------------------------------------------
97 // MscxModeDiff::fromDtlDiffType
98 //---------------------------------------------------------
99
fromDtlDiffType(dtl::edit_t dtlType)100 DiffType MscxModeDiff::fromDtlDiffType(dtl::edit_t dtlType)
101 {
102 switch(dtlType) {
103 case dtl::SES_DELETE:
104 return DiffType::DELETE;
105 case dtl::SES_COMMON:
106 return DiffType::EQUAL;
107 case dtl::SES_ADD:
108 return DiffType::INSERT;
109 }
110 Q_ASSERT(false); // dtlType must have one of the values handled above.
111 return DiffType::EQUAL;
112 }
113
114 //---------------------------------------------------------
115 // MscxModeDiff::lineModeDiff
116 //---------------------------------------------------------
117
lineModeDiff(const QString & s1,const QString & s2)118 std::vector<TextDiff> MscxModeDiff::lineModeDiff(const QString& s1, const QString& s2)
119 {
120 // type declarations for dtl library
121 typedef QStringRef elem;
122 typedef std::pair<elem, dtl::elemInfo> sesElem;
123 typedef std::vector<sesElem> sesElemVec;
124
125 // QVector does not contain range constructor used inside dtl
126 // so we have to convert to std::vector.
127 std::vector<QStringRef> lines1 = s1.splitRef('\n').toStdVector();
128 std::vector<QStringRef> lines2 = s2.splitRef('\n').toStdVector();
129 dtl::Diff<QStringRef, std::vector<QStringRef>> diff(lines1, lines2);
130
131 diff.compose();
132
133 const sesElemVec changes = diff.getSes().getSequence();
134 std::vector<TextDiff> diffs;
135 int line[2][2] {{1, 1}, {1, 1}}; // for correct assigning line numbers to
136 // DELETE and INSERT diffs we need to
137 // count lines separately for these diff
138 // types (EQUAL can use both counters).
139
140 for (const sesElem& ch : changes) {
141 DiffType type = fromDtlDiffType(ch.second.type);
142 const int iThis = (type == DiffType::DELETE) ? 0 : 1; // for EQUAL doesn't matter
143
144 if (diffs.empty() || diffs.back().type != type) {
145 if (!diffs.empty()) {
146 // sync line numbers
147 DiffType prevType = diffs.back().type;
148 const int prevThis = (prevType == DiffType::DELETE) ? 0 : 1;
149 const int prevOther = (prevThis == 1) ? 0 : 1;
150 if (prevType == DiffType::EQUAL)
151 std::copy_n(line[prevThis], 2, line[prevOther]);
152 else
153 line[prevThis][prevOther] = line[prevOther][prevOther];
154
155 if (type == DiffType::EQUAL) {
156 const int iOther = (iThis == 1) ? 0 : 1;
157 line[iThis][iOther] = line[iOther][iOther];
158 }
159 }
160
161 diffs.emplace_back();
162 TextDiff& d = diffs.back();
163 d.type = type;
164 std::copy_n(line[iThis], 2, d.start);
165 d.end[0] = line[iThis][0] - 1; // equal line numbers would mean an actual change in that line.
166 d.end[1] = line[iThis][1] - 1;
167 }
168
169 TextDiff& d = diffs.back();
170 d.end[iThis] = (line[iThis][iThis]++);
171 d.text[iThis].append(ch.first).append('\n');
172 if (type == DiffType::EQUAL) {
173 const int iOther = (iThis == 1) ? 0 : 1;
174 d.end[iOther] = (line[iThis][iOther]++);
175 d.text[iOther].append(ch.first).append('\n');
176 }
177 }
178
179 return diffs;
180 }
181
182 //---------------------------------------------------------
183 // MscxModeDiff::mscxModeDiff
184 //---------------------------------------------------------
185
mscxModeDiff(const QString & s1,const QString & s2)186 std::vector<TextDiff> MscxModeDiff::mscxModeDiff(const QString& s1, const QString& s2)
187 {
188 std::vector<TextDiff> diffs = lineModeDiff(s1, s2);
189 adjustSemanticsMscx(diffs);
190 return diffs;
191 }
192
193 //---------------------------------------------------------
194 // MscxModeDiff::adjustSemanticsMscx
195 //---------------------------------------------------------
196
adjustSemanticsMscx(std::vector<TextDiff> & diffs)197 void MscxModeDiff::adjustSemanticsMscx(std::vector<TextDiff>& diffs)
198 {
199 for (unsigned i = 0; i < diffs.size(); ++i)
200 i = adjustSemanticsMscxOneDiff(diffs, i);
201 }
202
203 //---------------------------------------------------------
204 // MscxModeDiff::adjustSemanticsMscx
205 // Returns new index of the given diff
206 //---------------------------------------------------------
207
adjustSemanticsMscxOneDiff(std::vector<TextDiff> & diffs,int index)208 int MscxModeDiff::adjustSemanticsMscxOneDiff(std::vector<TextDiff>& diffs, int index)
209 {
210 TextDiff* diff = &diffs[index];
211 int iScore;
212 switch(diff->type) {
213 case DiffType::EQUAL:
214 case DiffType::REPLACE:
215 // TODO: split a REPLACE diff, though they should not be here
216 return index;
217 case DiffType::INSERT:
218 iScore = 1;
219 break;
220 case DiffType::DELETE:
221 default:
222 iScore = 0;
223 break;
224 }
225
226 std::vector<Tag> extraTags;
227 readMscx(diff->text[iScore], extraTags);
228
229 if (extraTags.empty())
230 return index; // good, nothing to adjust
231
232 auto firstStartExtra = extraTags.begin();
233 auto lastStartExtra = extraTags.begin();
234 auto firstEndExtra = extraTags.begin();
235 auto lastEndExtra = extraTags.begin();
236 for (auto tag = extraTags.begin(); tag != extraTags.end(); ++tag) {
237 TagType type = tag->type;
238 switch(type) {
239 case START_TAG:
240 if (firstStartExtra->type != START_TAG)
241 firstStartExtra = tag;
242 lastStartExtra = tag;
243 break;
244 case END_TAG:
245 if (firstStartExtra->type != END_TAG)
246 firstEndExtra = tag;
247 lastEndExtra = tag;
248 break;
249 default:
250 break;
251 }
252 }
253
254 // Try to shift current diff up and down and see whether this helps
255 int lines = lastEndExtra->line - firstEndExtra->line + 1;
256 if (assessShiftDiff(diffs, extraTags, index, lines))
257 return performShiftDiff(diffs, index, lines);
258 else {
259 lines = - (lastStartExtra->line - firstStartExtra->line + 1);
260 if (assessShiftDiff(diffs, extraTags, index, lines))
261 return performShiftDiff(diffs, index, lines);
262 }
263
264 return index;
265 }
266
267 //---------------------------------------------------------
268 // MscxModeDiff::assessShiftDiff
269 // Returns true if shift of the diff with the given
270 // index is possible and there would be no extra tags
271 // left after such a shift.
272 //---------------------------------------------------------
273
assessShiftDiff(const std::vector<TextDiff> & diffs,const std::vector<Tag> & origExtraTags,int index,int lines)274 bool MscxModeDiff::assessShiftDiff(const std::vector<TextDiff>& diffs, const std::vector<Tag>& origExtraTags, int index, int lines)
275 {
276 if (lines == 0)
277 return diffs.empty();
278 const bool down = (lines > 0);
279 const int nextDiffIdx = nextDiffOnShiftIndex(diffs, index, down);
280 if (nextDiffIdx == index)
281 return false;
282
283 const TextDiff& diff = diffs[index];
284 const TextDiff& nextDiff = diffs[nextDiffIdx];
285
286 Q_ASSERT(diff.type == DiffType::DELETE || diff.type == DiffType::INSERT);
287 const int iScore = (diff.type == DiffType::DELETE ? 0 : 1);
288
289 const QString& diffTxt = diff.text[iScore];
290 const QString& nextTxt = nextDiff.text[iScore];
291 const QString diffChunk = getOuterLines(diffTxt, lines, down);
292 const QString nextChunk = getOuterLines(nextTxt, lines, down);
293
294 if (diffChunk == nextChunk) {
295 std::vector<Tag> extraTags(origExtraTags);
296 std::vector<Tag> diffExtraTags;
297 readMscx(diffChunk, diffExtraTags);
298
299 // merge diff chunk reading result with the result
300 // for the whole chunk
301 while (!diffExtraTags.empty()) {
302 auto tag = down ? diffExtraTags.begin() : (diffExtraTags.end() - 1);
303
304 if (extraTags.size() < 2) // would normally erase 2 tags
305 return false;
306
307 auto firstExtra = extraTags.begin();
308 if ((firstExtra->type == END_TAG)
309 && (firstExtra->name == tag->name))
310 extraTags.erase(firstExtra);
311 else
312 return false;
313 auto lastExtra = extraTags.end() - 1;
314 if ((lastExtra->type == START_TAG)
315 && (lastExtra->name == tag->name))
316 extraTags.erase(lastExtra);
317 else
318 return false;
319
320 diffExtraTags.erase(tag);
321 }
322
323 if (extraTags.empty()) {
324 // We have succeeded
325 return true;
326 }
327 }
328 return false;
329 }
330
331 //---------------------------------------------------------
332 // MscxModeDiff::performShiftDiff
333 // Perform a shift of the diff with the given index and
334 // return a new index of the diff.
335 //---------------------------------------------------------
336
performShiftDiff(std::vector<TextDiff> & diffs,int index,int lines)337 int MscxModeDiff::performShiftDiff(std::vector<TextDiff>& diffs, int index, int lines)
338 {
339 if (lines == 0)
340 return index;
341 const bool down = (lines > 0);
342 const int absLines = qAbs(lines);
343 const int nextDiffIdx = nextDiffOnShiftIndex(diffs, index, down);
344 if (nextDiffIdx == index)
345 return index;
346
347 TextDiff& diff = diffs[index];
348 TextDiff& nextDiff = diffs[nextDiffIdx];
349
350 Q_ASSERT(diff.type == DiffType::DELETE || diff.type == DiffType::INSERT);
351 const int iScore = (diff.type == DiffType::DELETE ? 0 : 1);
352 QString& diffTxt = diff.text[iScore];
353
354 const QString diffChunk = getOuterLines(diffTxt, absLines, down);
355
356 Q_ASSERT(nextDiff.type == DiffType::EQUAL);
357 int chunkStart[2]; // line numbers of diffChunk in two scores
358 int chunkEnd[2];
359 if (down) {
360 std::copy(diff.start, diff.start + 2, chunkStart);
361 chunkEnd[0] = diff.start[0] + absLines - 1;
362 chunkEnd[1] = diff.start[1] + absLines - 1;
363 }
364 else {
365 chunkStart[0] = diff.end[0] - (absLines - 1);
366 chunkStart[1] = diff.end[1] - (absLines - 1);
367 std::copy(diff.end, diff.end + 2, chunkEnd);
368 }
369
370 if (down) {
371 diffTxt.remove(0, diffChunk.size());
372 diffTxt.append(diffChunk);
373 nextDiff.text[0].remove(0, diffChunk.size());
374 nextDiff.text[1].remove(0, diffChunk.size());
375 }
376 else {
377 diffTxt.chop(diffChunk.size());
378 diffTxt.prepend(diffChunk);
379 nextDiff.text[0].chop(diffChunk.size());
380 nextDiff.text[1].chop(diffChunk.size());
381 }
382 // shift line numbers: needed both for shifted diff and for diffs
383 // between the shifted diff and nextDiff
384 const int inc = down ? 1 : -1;
385 for (int i = index; i != nextDiffIdx; i += inc) {
386 TextDiff& d = diffs[i];
387 Q_UNUSED(d);
388 Q_ASSERT(d.type != DiffType::EQUAL); // nextDiff should be the first EQUAL diff in that direction
389 diff.start[0] += lines;
390 diff.end[0] += lines;
391 diff.start[1] += lines;
392 diff.end[1] += lines;
393 }
394 int* nextChunkLines = down ? nextDiff.start : nextDiff.end;
395 nextChunkLines[0] += lines;
396 nextChunkLines[1] += lines;
397
398 TextDiff eqDiff;
399 eqDiff.type = DiffType::EQUAL;
400 eqDiff.text[0] = diffChunk;
401 eqDiff.text[1] = diffChunk;
402 std::copy(chunkStart, chunkStart + 2, eqDiff.start);
403 std::copy(chunkEnd, chunkEnd + 2, eqDiff.end);
404
405 const int prevDiffIdx = index + (down ? -1 : 1);
406 bool merged = false;
407 if (diffs[prevDiffIdx].type == DiffType::EQUAL)
408 merged = diffs[prevDiffIdx].merge(eqDiff);
409
410 if (!merged) {
411 const int insertIdx = down ? index : (index + 1);
412 diffs.insert(diffs.begin() + insertIdx, eqDiff);
413 if (down)
414 ++index;
415 }
416
417 return index;
418 }
419
420 //---------------------------------------------------------
421 // MscxModeDiff::getOuterLines
422 //---------------------------------------------------------
423
getOuterLines(const QString & str,int lines,bool start)424 QString MscxModeDiff::getOuterLines(const QString& str, int lines, bool start) {
425 lines = qAbs(lines);
426 const int secIdxStart = start ? 0 : (-1 - (lines - 1));
427 const int secIdxEnd = start ? (lines - 1) : -1;
428 constexpr auto secFlags = QString::SectionIncludeTrailingSep | QString::SectionSkipEmpty;
429 return str.section('\n', secIdxStart, secIdxEnd, secFlags);
430 }
431
432 //---------------------------------------------------------
433 // MscxModeDiff::nextDiffOnShiftIndex
434 //---------------------------------------------------------
435
nextDiffOnShiftIndex(const std::vector<TextDiff> & diffs,int index,bool down)436 int MscxModeDiff::nextDiffOnShiftIndex(const std::vector<TextDiff>& diffs, int index, bool down)
437 {
438 const DiffType diffType = diffs[index].type;
439 Q_ASSERT(diffType == DiffType::DELETE || diffType == DiffType::INSERT);
440 int nextDiffIdx = index + (down ? 1 : -1);
441 while (nextDiffIdx >= 0 && nextDiffIdx < int(diffs.size())) {
442 const TextDiff& d = diffs[nextDiffIdx];
443 if (d.type == diffType) {
444 // Two INSERT or DELETE diffs in a row, probably shouldn't happen
445 return index;
446 }
447 if (d.type == DiffType::EQUAL)
448 return nextDiffIdx;
449 // REPLACE is not here, the other type can be skipped
450 Q_ASSERT(d.type != DiffType::REPLACE);
451 nextDiffIdx += (down ? 1 : -1);
452 }
453 return index;
454 }
455
456 //---------------------------------------------------------
457 // MscxModeDiff::readMscx
458 //---------------------------------------------------------
459
readMscx(const QString & xml,std::vector<Tag> & extraTags)460 void MscxModeDiff::readMscx(const QString& xml, std::vector<Tag>& extraTags)
461 {
462 int line = 0;
463 int nextLineIndex = xml.indexOf('\n');
464 QRegularExpressionMatchIterator m = tagRegExp.globalMatch(xml);
465 while (m.hasNext()) {
466 QRegularExpressionMatch match = m.next();
467 QStringRef tag(match.capturedRef("name"));
468 while ((match.capturedStart("name") > nextLineIndex) && (nextLineIndex > 0)) {
469 ++line;
470 nextLineIndex = xml.indexOf('\n', nextLineIndex + 1);
471 }
472 if (match.capturedRef().startsWith("</")) {
473 // end element
474 handleTag(line, END_TAG, tag.toString(), extraTags);
475 if (tag == "Tuplet") {
476 // special case: <Tuplet> / <endTuplet/> pair
477 handleTag(line, START_TAG, "__tupletBound", extraTags);
478 }
479 }
480 else if (match.capturedRef().endsWith("/>")) {
481 // empty element: do nothing
482 if (tag == "endTuplet") {
483 // special case: <Tuplet> / <endTuplet/> pair
484 handleTag(line, END_TAG, "__tupletBound", extraTags);
485 }
486 }
487 else {
488 // start element
489 handleTag(line, START_TAG, tag.toString(), extraTags);
490 }
491 }
492 }
493
494 //---------------------------------------------------------
495 // MscxModeDiff::handleTag
496 //---------------------------------------------------------
497
handleTag(int line,TagType type,const QString & name,std::vector<Tag> & extraTags)498 void MscxModeDiff::handleTag(int line, TagType type, const QString& name, std::vector<Tag>& extraTags)
499 {
500 switch(type) {
501 case START_TAG:
502 extraTags.emplace_back(line, type, name);
503 break;
504 case END_TAG:
505 {
506 auto lastTag = extraTags.empty() ? nullptr : &extraTags.back();
507 if (lastTag
508 && lastTag->type == START_TAG && lastTag->name == name)
509 extraTags.pop_back();
510 else
511 extraTags.emplace_back(line, type, name);
512 }
513 break;
514 }
515 }
516
517 //---------------------------------------------------------
518 // TextDiffParser
519 //---------------------------------------------------------
520
TextDiffParser(int iScore)521 TextDiffParser::TextDiffParser(int iScore)
522 : iScore(iScore), iOtherScore(iScore == 1 ? 0 : 1)
523 {
524 contextsStack.push_back(nullptr);
525 }
526
527 //---------------------------------------------------------
528 // TextDiffParser::makeDiffs
529 //---------------------------------------------------------
530
makeDiffs(const QString & mscx,const std::vector<std::pair<const ScoreElement *,QString>> & elements,const std::vector<TextDiff> & textDiffs,std::vector<BaseDiff * > & diffs)531 void TextDiffParser::makeDiffs(const QString& mscx, const std::vector<std::pair<const ScoreElement*, QString>>& elements, const std::vector<TextDiff>& textDiffs, std::vector<BaseDiff*>& diffs)
532 {
533 QXmlStreamReader r(mscx);
534 r.readNext(); // read first tag
535 auto textDiff = textDiffs.begin();
536 auto textDiffEnd = textDiffs.end();
537 auto nextElement = elements.begin();
538 auto nextElementEnd = elements.end();
539 while (!r.atEnd()) {
540 // Scroll to the correct diff to be handled now
541 while (textDiff != textDiffEnd
542 && textDiff->end[iScore] < r.lineNumber()) {
543 // Save ContextChange when leaving equal chunk
544 if (textDiff->type == DiffType::EQUAL) {
545 ContextChange* c = new ContextChange;
546 c->type = textDiff->type;
547 c->textDiff = &(*textDiff);
548 c->ctx[iScore] = contextsStack.back();
549 c->ctx[iOtherScore] = nullptr;
550 c->before[iScore] = lastElementEnded;
551 c->before[iOtherScore] = nullptr;
552 diffs.push_back(c);
553 }
554
555 ++textDiff;
556 }
557 if (textDiff == textDiffEnd)
558 break;
559
560 bool saveDiff = false;
561 if (!insideDiffTag()
562 && (textDiff->type == DiffType::DELETE || textDiff->type == DiffType::INSERT)
563 ) {
564 const int iDiffScore = (textDiff->type == DiffType::DELETE) ? 0 : 1;
565 if (iScore == iDiffScore)
566 saveDiff = true;
567 }
568
569 const ScoreElement* newElement = nullptr;
570 if (r.isStartElement()) {
571 if (nextElement != nextElementEnd
572 && nextElement->second == r.name())
573 newElement = (nextElement++)->first;
574
575 ++tagLevel;
576 tagIsElement.push_back(newElement);
577 }
578
579 BaseDiff* diff = handleToken(r, newElement, saveDiff);
580
581 if (diff) {
582 diff->type = textDiff->type;
583 diff->textDiff = &(*textDiff);
584 diff->before[iScore] = lastElementEnded;
585 diff->before[iOtherScore] = nullptr;
586 diffs.push_back(diff);
587
588 if (saveDiff) {
589 // It is assumed that diff items are created in handleToken
590 // only for start tags
591 Q_ASSERT(r.isStartElement());
592 ItemType type = diff->itemType();
593 if (type == ItemType::ELEMENT || type == ItemType::PROPERTY) {
594 currentDiffTagLevel = tagLevel;
595 if (type == ItemType::ELEMENT && contextsStack.back()->isSpanner()) {
596 // Consider <Spanner> tag as the current differing tag
597 --currentDiffTagLevel;
598 }
599 }
600 }
601 }
602
603 if (r.isEndElement()) {
604 if (currentDiffTagLevel == tagLevel)
605 currentDiffTagLevel = 0;
606 --tagLevel;
607 tagIsElement.pop_back();
608 }
609
610 r.readNext();
611 }
612 if (r.hasError())
613 qWarning("TextDiffParser::makeDiffs: error while reading MSCX output: %s", r.errorString().toLatin1().constData());
614 }
615
616 //---------------------------------------------------------
617 // TextDiffParser::handleToken
618 // Returns nullptr or a newly allocated BaseDiff with
619 // properly filled context and fields that are unique to
620 // descendants of BaseDiff (e.g. el[2] for ElementDiff).
621 // Type and textDiff are left unfilled.
622 //---------------------------------------------------------
623
handleToken(const QXmlStreamReader & r,const ScoreElement * newElement,bool saveDiff)624 BaseDiff* TextDiffParser::handleToken(const QXmlStreamReader& r, const ScoreElement* newElement, bool saveDiff)
625 {
626 if (!r.isStartElement() && !r.isEndElement())
627 return nullptr;
628 BaseDiff* diff = nullptr;
629 const QStringRef tag(r.name());
630
631 if (r.isStartElement()) {
632 if (newElement) {
633 if (saveDiff) {
634 ElementDiff* d = new ElementDiff;
635 diff = d;
636 d->ctx[iScore] = contextsStack.back();
637 d->el[iScore] = newElement;
638 d->el[iOtherScore] = nullptr;
639 }
640 contextsStack.push_back(newElement);
641 }
642 else if (tag == "Spanner")
643 markupContext = tag.toString();
644 else if (saveDiff) {
645 const ScoreElement* ctx = contextsStack.back();
646 if (markupContext == "Spanner" && !(ctx && ctx->isSpanner())) {
647 // some property inside the end of a spanner
648 }
649 else if (tag == "prev" || tag == "next") {
650 // Ignore these tags
651 }
652 else if (tag == "location" || tag == "voice") {
653 if (ctx && ctx->isMeasure()) {
654 MarkupDiff* d = new MarkupDiff;
655 diff = d;
656 d->ctx[iScore] = ctx;
657 d->name = tag.toString();
658 const ScoreElement* widerCtx = *(contextsStack.end() - 2);
659 if (widerCtx && widerCtx->isStaff())
660 d->info = toStaff(widerCtx)->idx();
661 }
662 }
663 else {
664 Pid propId = ctx ? ctx->propertyId(tag) : Pid::END;
665 if (propId != Pid::END) {
666 PropertyDiff* d = new PropertyDiff;
667 diff = d;
668 d->ctx[iScore] = contextsStack.back();
669 d->pid = propId;
670 }
671 else {
672 MarkupDiff* d = new MarkupDiff;
673 diff = d;
674 d->ctx[iScore] = contextsStack.back();
675 d->name = tag.toString();
676 if (tag == "metaTag")
677 d->info = r.attributes().value("name").toString();
678 }
679 }
680 }
681 }
682 else { // end element
683 if ((tagIsElement.back() && !contextsStack.back()->isSpanner())
684 || (tag == "Spanner" && contextsStack.back()->isSpanner())
685 ) {
686 lastElementEnded = contextsStack.back();
687 contextsStack.pop_back();
688 }
689 if (!markupContext.isEmpty() && tag == markupContext)
690 markupContext.clear();
691 }
692
693 if (diff)
694 diff->ctx[iOtherScore] = nullptr;
695 return diff;
696 }
697
698 //---------------------------------------------------------
699 // lineNumberSort
700 //---------------------------------------------------------
701
lineNumberSort(BaseDiff * d1,BaseDiff * d2)702 static bool lineNumberSort(BaseDiff* d1, BaseDiff* d2)
703 {
704 const TextDiff* t1 = d1->textDiff;
705 const TextDiff* t2 = d2->textDiff;
706 return ((t1->end[0] < t2->end[0]) || (t1->end[1] < t2->end[1]));
707 }
708
709 //---------------------------------------------------------
710 // positionSort
711 //---------------------------------------------------------
712
positionSort(BaseDiff * d1,BaseDiff * d2)713 static bool positionSort(BaseDiff* d1, BaseDiff* d2)
714 {
715 return (d1->afrac(0) < d2->afrac(0) || d1->afrac(1) < d2->afrac(1));
716 }
717
718 //---------------------------------------------------------
719 // scoreToMscx
720 //---------------------------------------------------------
721
scoreToMscx(Score * s,XmlWriter & xml)722 static QString scoreToMscx(Score* s, XmlWriter& xml)
723 {
724 QString mscx;
725 xml.setString(&mscx, QIODevice::WriteOnly);
726 xml.setRecordElements(true);
727 s->write(xml, /* onlySelection */ false);
728 xml.flush();
729 return mscx;
730 }
731
732 //---------------------------------------------------------
733 // ScoreDiff
734 // s1, s2 are scores to compare.
735 // ScoreDiff does NOT take ownership of the scores.
736 //---------------------------------------------------------
737
ScoreDiff(Score * s1,Score * s2,bool textDiffOnly)738 ScoreDiff::ScoreDiff(Score* s1, Score* s2, bool textDiffOnly)
739 : _s1(s1), _s2(s2), _textDiffOnly(textDiffOnly)
740 {
741 update();
742 }
743
744 //---------------------------------------------------------
745 // makeDiffs
746 //---------------------------------------------------------
747
makeDiffs(const QString & mscx1,const QString & mscx2,const XmlWriter & xml1,const XmlWriter & xml2,const std::vector<TextDiff> & textDiffs,std::vector<BaseDiff * > & diffs)748 static void makeDiffs(const QString& mscx1, const QString& mscx2, const XmlWriter& xml1, const XmlWriter& xml2, const std::vector<TextDiff>& textDiffs, std::vector<BaseDiff*>& diffs)
749 {
750 TextDiffParser p1(0);
751 p1.makeDiffs(mscx1, xml1.elements(), textDiffs, diffs);
752 TextDiffParser p2(1);
753 p2.makeDiffs(mscx2, xml2.elements(), textDiffs, diffs);
754
755 std::stable_sort(diffs.begin(), diffs.end(), lineNumberSort);
756
757 // Fill correct contexts for diffs
758 const ScoreElement* lastCtx[2] {};
759 const ScoreElement* lastBefore[2] {};
760 for (BaseDiff* diff : diffs) {
761 for (int i = 0; i < 2; ++i) {
762 if (diff->ctx[i])
763 lastCtx[i] = diff->ctx[i];
764 else
765 diff->ctx[i] = lastCtx[i];
766
767 if (diff->before[i])
768 lastBefore[i] = diff->before[i];
769 else
770 diff->before[i] = lastBefore[i];
771 }
772 }
773
774 // Filter out EQUAL diffs and dummy ContextChange items
775 std::vector<BaseDiff*> filteredDiffs;
776 for (BaseDiff* diff : diffs) {
777 if ((diff->itemType() == ItemType::CONTEXTCHANGE)
778 || (diff->type == DiffType::EQUAL))
779 delete diff;
780 else
781 filteredDiffs.push_back(diff);
782 }
783 std::swap(diffs, filteredDiffs);
784 }
785
786 //---------------------------------------------------------
787 // ScoreDiff::update
788 // Updates the diff according to the current state of
789 // the compared scores.
790 //---------------------------------------------------------
791
update()792 void ScoreDiff::update()
793 {
794 if (updated())
795 return;
796
797 qDeleteAll(_diffs);
798 _diffs.clear();
799 _textDiffs.clear();
800 qDeleteAll(_mergedTextDiffs);
801 _mergedTextDiffs.clear();
802
803 XmlWriter xml1(_s1);
804 XmlWriter xml2(_s2);
805 QString mscx1(scoreToMscx(_s1, xml1));
806 QString mscx2(scoreToMscx(_s2, xml2));
807
808 _textDiffs = MscxModeDiff().mscxModeDiff(mscx1, mscx2);
809
810 if (!_textDiffOnly) {
811 makeDiffs(mscx1, mscx2, xml1, xml2, _textDiffs, _diffs);
812
813 processMarkupDiffs();
814 mergeInsertDeleteDiffs();
815 mergeElementDiffs();
816 editPropertyDiffs();
817
818 std::stable_sort(_diffs.begin(), _diffs.end(), positionSort);
819 }
820
821 _scoreState1 = _s1->state();
822 _scoreState2 = _s2->state();
823 }
824
825 //---------------------------------------------------------
826 // ~ScoreDiff
827 //---------------------------------------------------------
828
~ScoreDiff()829 ScoreDiff::~ScoreDiff()
830 {
831 qDeleteAll(_mergedTextDiffs);
832 qDeleteAll(_diffs);
833 }
834
835 //---------------------------------------------------------
836 // deleteDiffs
837 //---------------------------------------------------------
838
deleteDiffs(std::vector<BaseDiff * > & diffsList,const std::vector<BaseDiff * > & toDelete)839 static void deleteDiffs(std::vector<BaseDiff*>& diffsList, const std::vector<BaseDiff*>& toDelete)
840 {
841 for (BaseDiff* diff : toDelete) {
842 for (auto it = diffsList.begin(); it != diffsList.end(); ++it) {
843 if (*it == diff) {
844 diffsList.erase(it);
845 break;
846 }
847 }
848 delete diff;
849 }
850 }
851
852 //---------------------------------------------------------
853 // measureToMscx
854 //---------------------------------------------------------
855
measureToMscx(const Measure * m,XmlWriter & xml,int staff)856 static QString measureToMscx(const Measure* m, XmlWriter& xml, int staff)
857 {
858 QString mscx;
859 xml.setString(&mscx, QIODevice::WriteOnly);
860 xml.setRecordElements(true);
861 m->write(xml, staff, false, false);
862 xml.flush();
863 return mscx;
864 }
865
866 //---------------------------------------------------------
867 // measureInfo
868 //---------------------------------------------------------
869
870 struct MeasureInfo {
871 const Measure* m1;
872 const Measure* m2;
873 int staff;
874
MeasureInfoMs::MeasureInfo875 MeasureInfo(const Measure* m1, const Measure* m2, int staff) : m1(m1), m2(m2), staff(staff) {}
876
operator ==Ms::MeasureInfo877 bool operator==(const MeasureInfo& other) { return m1 == other.m1 && m2 == other.m2 && staff == other.staff; }
operator !=Ms::MeasureInfo878 bool operator!=(const MeasureInfo& other) { return !(*this == other); }
879 };
880
881 //---------------------------------------------------------
882 // ScoreDiff::processMarkupDiffs
883 // Find MarkupDiffs and convert them to the appropriate
884 // diffs of other types if needed.
885 //---------------------------------------------------------
886
processMarkupDiffs()887 void ScoreDiff::processMarkupDiffs()
888 {
889 std::vector<BaseDiff*> abandonedDiffs;
890 std::vector<MeasureInfo> measuresToProcess;
891 Pid pids[] { Pid::TRACK, Pid::POSITION }; // list of pids to be accepted on measures processing
892 for (BaseDiff* diff : _diffs) {
893 if (diff->itemType() != ItemType::MARKUP)
894 continue;
895
896 MarkupDiff* d = static_cast<MarkupDiff*>(diff);
897 const QString& name = d->name;
898
899 if (name == "voice" || name == "location") {
900 if (d->ctx[0]->isMeasure() && d->ctx[1]->isMeasure()) {
901 MeasureInfo m(toMeasure(d->ctx[0]), toMeasure(d->ctx[1]), d->info.toInt());
902 bool add = true;
903 for (auto& mScheduled : measuresToProcess) {
904 if (mScheduled == m) {
905 add = false;
906 break;
907 }
908 }
909 if (add)
910 measuresToProcess.push_back(m);
911 abandonedDiffs.push_back(d);
912 }
913 }
914 }
915
916 std::vector<BaseDiff*> newDiffs;
917 for (auto& m : measuresToProcess) {
918 XmlWriter xml1(m.m1->score());
919 xml1.setWriteTrack(true);
920 xml1.setWritePosition(true);
921 QString mscx1 = measureToMscx(m.m1, xml1, m.staff);
922
923 XmlWriter xml2(m.m2->score());
924 xml2.setWriteTrack(true);
925 xml2.setWritePosition(true);
926 QString mscx2 = measureToMscx(m.m2, xml2, m.staff);
927
928 std::vector<TextDiff> textDiffs = MscxModeDiff().mscxModeDiff(mscx1, mscx2);
929 makeDiffs(mscx1, mscx2, xml1, xml2, textDiffs, newDiffs);
930 }
931
932 for (BaseDiff* newDiff : newDiffs) {
933 bool save = false;
934 if (newDiff->itemType() == ItemType::PROPERTY) {
935 PropertyDiff* pDiff = static_cast<PropertyDiff*>(newDiff);
936 for (Pid pid : pids) {
937 if (pDiff->pid == pid) {
938 save = true;
939 break;
940 }
941 }
942 if (pDiff->pid == Pid::TRACK)
943 pDiff->pid = Pid::VOICE;
944
945 if (pDiff->ctx[0]->isMeasure() || pDiff->ctx[1]->isMeasure())
946 save = false;
947 }
948
949
950 if (save) {
951 newDiff->textDiff = nullptr;
952 _diffs.push_back(newDiff);
953 }
954 else
955 delete newDiff;
956 }
957
958 deleteDiffs(_diffs, abandonedDiffs);
959 }
960
961 //---------------------------------------------------------
962 // ScoreDiff::mergeInsertDeleteDiffs
963 // Merge INSERT and DELETE diffs to REPLACE diffs where
964 // possible.
965 //---------------------------------------------------------
966
mergeInsertDeleteDiffs()967 void ScoreDiff::mergeInsertDeleteDiffs()
968 {
969 const ScoreElement* lastCtx1 = _s1;
970 const ScoreElement* lastCtx2 = _s2;
971 std::vector<BaseDiff*> diffsToMerge;
972 std::vector<BaseDiff*> abandonedDiffs;
973 for (BaseDiff* diff : _diffs) {
974 if ((diff->ctx[0] != lastCtx1) || (diff->ctx[1] != lastCtx2)) {
975 diffsToMerge.clear();
976 lastCtx1 = diff->ctx[0];
977 lastCtx2 = diff->ctx[1];
978 }
979 bool merge = false;
980 for (auto diff2It = diffsToMerge.begin(); diff2It != diffsToMerge.end(); ++diff2It) {
981 BaseDiff* diff2 = (*diff2It);
982 if (diff->type == DiffType::DELETE) {
983 if ((diff2->type == DiffType::INSERT) && diff->sameItem(*diff2))
984 merge = true;
985 }
986 else if (diff->type == DiffType::INSERT) {
987 if ((diff2->type == DiffType::DELETE) && diff->sameItem(*diff2))
988 merge = true;
989 }
990
991 if (merge) {
992 // To preserve the correct order of diffs, we should
993 // leave diff2 in place since it appeared earlier in
994 // the _diffs list.
995 diff2->type = DiffType::REPLACE;
996
997 if (diff->textDiff && diff2->textDiff) {
998 TextDiff* mergedTextDiff = new TextDiff(*diff2->textDiff);
999 mergedTextDiff->merge(*diff->textDiff);
1000 _mergedTextDiffs.push_back(mergedTextDiff);
1001 diff2->textDiff = mergedTextDiff;
1002 }
1003 else if (!diff2->textDiff)
1004 diff2->textDiff = diff->textDiff;
1005
1006 int idx1 = (diff->type == DiffType::DELETE) ? 0 : 1;
1007 switch(diff2->itemType()) {
1008 case ItemType::ELEMENT:
1009 {
1010 ElementDiff* d = static_cast<ElementDiff*>(diff);
1011 ElementDiff* d2 = static_cast<ElementDiff*>(diff2);
1012 // d2->el[idx2] is already filled
1013 d2->el[idx1] = d->el[idx1];
1014 }
1015 break;
1016 case ItemType::PROPERTY:
1017 case ItemType::MARKUP:
1018 // nothing to fill
1019 break;
1020 case ItemType::CONTEXTCHANGE:
1021 break;
1022 }
1023
1024 abandonedDiffs.push_back(diff);
1025 diffsToMerge.erase(diff2It);
1026 break; // loop on diffsToMerge
1027 }
1028 }
1029
1030 if (!merge)
1031 diffsToMerge.push_back(diff);
1032 }
1033
1034 deleteDiffs(_diffs, abandonedDiffs);
1035 }
1036
1037 //---------------------------------------------------------
1038 // ScoreDiff::mergeElementDiffs
1039 // Merge element diffs with equal elements
1040 //---------------------------------------------------------
1041
mergeElementDiffs()1042 void ScoreDiff::mergeElementDiffs()
1043 {
1044 std::map<const ScoreElement*, ElementDiff*> elementDiffs;
1045 std::vector<BaseDiff*> abandonedDiffs;
1046 for (BaseDiff* diff : _diffs) {
1047 if (diff->itemType() != ItemType::ELEMENT)
1048 continue;
1049
1050 ElementDiff* elDiff = static_cast<ElementDiff*>(diff);
1051 auto foundDiffIt1 = elementDiffs.find(elDiff->el[0]);
1052 auto foundDiffIt2 = elementDiffs.find(elDiff->el[1]);
1053 ElementDiff* foundDiff = nullptr;
1054 if (foundDiffIt1 != elementDiffs.end())
1055 foundDiff = foundDiffIt1->second;
1056 else if (foundDiffIt2 != elementDiffs.end())
1057 foundDiff = foundDiffIt2->second;
1058
1059 if (foundDiff && foundDiff->type == elDiff->type
1060 && foundDiff->el[0] == elDiff->el[0]
1061 && foundDiff->el[1] == elDiff->el[1]) {
1062 for (int i = 0; i < 2; ++i) {
1063 const ScoreElement* se = foundDiff->el[i];
1064 if (!se)
1065 continue;
1066 if (!se->isMeasure() && se->isElement()) {
1067 const Element* m = toElement(se)->findMeasure();
1068 if (m)
1069 foundDiff->ctx[i] = m;
1070 else
1071 foundDiff->ctx[i] = se->score();
1072 }
1073 else
1074 foundDiff->ctx[i] = se->score();
1075 }
1076 abandonedDiffs.push_back(elDiff);
1077 }
1078 else {
1079 for (int i = 0; i < 2; ++i) {
1080 if (const ScoreElement* se = elDiff->el[i])
1081 elementDiffs[se] = elDiff;
1082 }
1083 }
1084 }
1085
1086 deleteDiffs(_diffs, abandonedDiffs);
1087 }
1088
1089 //---------------------------------------------------------
1090 // ScoreDiff::editPropertyDiffs
1091 // Merge property diffs with equal elements, edit some
1092 // diffs if necessary
1093 //---------------------------------------------------------
1094
editPropertyDiffs()1095 void ScoreDiff::editPropertyDiffs()
1096 {
1097 std::multimap<const ScoreElement*, PropertyDiff*> propertyDiffs;
1098 std::vector<BaseDiff*> abandonedDiffs;
1099 for (BaseDiff* diff : _diffs) {
1100 if (diff->itemType() != ItemType::PROPERTY)
1101 continue;
1102
1103 PropertyDiff* pDiff = static_cast<PropertyDiff*>(diff);
1104
1105 if (pDiff->pid == Pid::TPC1 || pDiff->pid == Pid::TPC2) {
1106 // Special case: "tpc" and "pitch" properties are partially
1107 // overlapped so we will mark tpc changes as pitch changes
1108 pDiff->pid = Pid::PITCH;
1109 }
1110
1111 auto foundRange = propertyDiffs.equal_range(pDiff->ctx[0]);
1112 bool merged = false;
1113 for (auto it = foundRange.first; it != foundRange.second; ++it) {
1114 PropertyDiff* foundDiff = it->second;
1115 if (foundDiff && foundDiff->type == pDiff->type
1116 && foundDiff->ctx[0] == pDiff->ctx[0]
1117 && foundDiff->ctx[1] == pDiff->ctx[1]
1118 && foundDiff->pid == pDiff->pid
1119 ) {
1120 abandonedDiffs.push_back(pDiff);
1121 merged = true;
1122 break;
1123 }
1124 }
1125
1126 if (!merged)
1127 propertyDiffs.emplace(pDiff->ctx[0], pDiff);
1128 }
1129
1130 deleteDiffs(_diffs, abandonedDiffs);
1131 }
1132
1133 //---------------------------------------------------------
1134 // ScoreDiff::equal
1135 //---------------------------------------------------------
1136
equal() const1137 bool ScoreDiff::equal() const
1138 {
1139 for (const TextDiff& td : _textDiffs) {
1140 if (td.type != DiffType::EQUAL)
1141 return false;
1142 }
1143 return true;
1144 }
1145
1146 //---------------------------------------------------------
1147 // ScoreDiff::rawDiff
1148 //---------------------------------------------------------
1149
rawDiff(bool skipEqual) const1150 QString ScoreDiff::rawDiff(bool skipEqual) const
1151 {
1152 QStringList list;
1153 for (const TextDiff& td : _textDiffs) {
1154 if (!skipEqual || td.type != DiffType::EQUAL)
1155 list.push_back(td.toString(true));
1156 }
1157 return list.join('\n');
1158 }
1159
1160 //---------------------------------------------------------
1161 // ScoreDiff::userDiff
1162 //---------------------------------------------------------
1163
userDiff() const1164 QString ScoreDiff::userDiff() const
1165 {
1166 QStringList list;
1167 for (const BaseDiff* d : _diffs)
1168 list.push_back(d->toString());
1169 return list.join('\n');
1170 }
1171
1172 //---------------------------------------------------------
1173 // TextDiff::merge
1174 //---------------------------------------------------------
1175
merge(const TextDiff & other)1176 bool TextDiff::merge(const TextDiff& other)
1177 {
1178 if (type == other.type) {
1179 if (other.end[0] == (start[0] - 1) && other.end[1] == (start[1] - 1)) {
1180 start[0] = other.start[0];
1181 start[1] = other.start[1];
1182 text[0].prepend(other.text[0]);
1183 text[1].prepend(other.text[1]);
1184 }
1185 else if ((end[0] + 1) == other.start[0] && (end[1] + 1) == other.start[1]) {
1186 end[0] = other.end[0];
1187 end[1] = other.end[1];
1188 text[0].append(other.text[0]);
1189 text[1].append(other.text[1]);
1190 }
1191 else {
1192 qWarning("TextDiff:merge: invalid argument: wrong line numbers");
1193 return false;
1194 }
1195 }
1196 else if ((type == DiffType::INSERT && other.type == DiffType::DELETE)
1197 || (type == DiffType::DELETE && other.type == DiffType::INSERT)
1198 ) {
1199 type = DiffType::REPLACE;
1200 const int iOther = (other.type == DiffType::DELETE) ? 0 : 1;
1201 start[iOther] = other.start[iOther];
1202 end[iOther] = other.end[iOther];
1203 text[iOther] = other.text[iOther];
1204 }
1205 else {
1206 qWarning("TextDiff:merge: invalid argument: wrong types");
1207 return false;
1208 }
1209
1210 return true;
1211 }
1212
1213 //---------------------------------------------------------
1214 // addLinePrefix
1215 //---------------------------------------------------------
1216
addLinePrefix(const QString & str,const QString & prefix)1217 static QString addLinePrefix(const QString& str, const QString& prefix)
1218 {
1219 if (prefix.isEmpty())
1220 return str;
1221 QVector<QStringRef> lines = str.splitRef('\n');
1222 if (lines.back().isEmpty())
1223 lines.pop_back();
1224 QStringList processedLines;
1225 for (QStringRef& line : lines)
1226 processedLines.push_back(QString(prefix).append(line));
1227 return processedLines.join('\n');
1228 }
1229
1230 //---------------------------------------------------------
1231 // TextDiff::toString
1232 // type argument allows to define which part of diff
1233 // should be printed. For example, it allows to print
1234 // only deleted chunk for REPLACE diff item.
1235 //---------------------------------------------------------
1236
toString(DiffType dt,bool prefixLines) const1237 QString TextDiff::toString(DiffType dt, bool prefixLines) const
1238 {
1239 if (dt == DiffType::REPLACE) {
1240 QStringList l;
1241 l.push_back(toString(DiffType::DELETE, prefixLines));
1242 l.push_back(toString(DiffType::INSERT, prefixLines));
1243 return l.join('\n');
1244 }
1245
1246 int idx = (dt == DiffType::INSERT) ? 1 : 0;
1247 const char* prefix = (dt == DiffType::INSERT) ? ">" : "<";
1248
1249 QString lines[2];
1250 for (int i = 0; i < 2; ++i) {
1251 if ((i != idx && dt != DiffType::EQUAL)
1252 || end[i] <= start[i]
1253 ) {
1254 lines[i] = QString::number(start[i]);
1255 }
1256 else
1257 lines[i] = QString("%1--%2")
1258 .arg(start[i], end[i]);
1259 }
1260
1261 return QString("@%1;%2\n%3")
1262 .arg(lines[0], lines[1],
1263 prefixLines ? addLinePrefix(text[idx], prefix) : text[idx]);
1264 }
1265
1266 //---------------------------------------------------------
1267 // BaseDiff::sameItem
1268 //---------------------------------------------------------
1269
sameItem(const BaseDiff & other) const1270 bool BaseDiff::sameItem(const BaseDiff& other) const
1271 {
1272 if (itemType() != other.itemType()
1273 || ctx[0] != other.ctx[0] || ctx[1] != other.ctx[1])
1274 return false;
1275 if (textDiff == other.textDiff)
1276 return true;
1277 else if (!textDiff || !other.textDiff)
1278 return false;
1279 return (textDiff->start[0] == other.textDiff->start[0]
1280 && textDiff->start[1] == other.textDiff->start[1]
1281 );
1282 }
1283
1284 //---------------------------------------------------------
1285 // BaseDiff::afrac
1286 // Returns position of the changed chunk
1287 //---------------------------------------------------------
1288
afrac(int score) const1289 Fraction BaseDiff::afrac(int score) const
1290 {
1291 Q_ASSERT(score == 0 || score == 1);
1292 if (ctx[score] && ctx[score]->isElement())
1293 return toElement(ctx[score])->tick();
1294 if (before[score] && before[score]->isElement()) {
1295 const Element* bef = toElement(before[score]);
1296 Fraction f = bef->tick();
1297 if (bef->isDurationElement()) {
1298 const DurationElement* de = toDurationElement(bef);
1299 return f + de->actualTicks();
1300 }
1301 return f;
1302 }
1303 return Fraction(0,1);
1304 }
1305
1306 //---------------------------------------------------------
1307 // describeContext
1308 //---------------------------------------------------------
1309
describeContext(const ScoreElement * ctx)1310 static QString describeContext(const ScoreElement* ctx)
1311 {
1312 if (!ctx)
1313 return QString("no context");
1314 QString descr;
1315 if (ctx->isElement()) {
1316 const Element* e = toElement(ctx);
1317 if (const Measure* m = toMeasure(e->findMeasure()))
1318 descr += QString("%1 %2").arg(m->userName()).arg(m->no() + 1);
1319 }
1320 if (!ctx->isMeasure()) {
1321 if (!descr.isEmpty())
1322 descr += ": ";
1323 descr += ctx->userName();
1324 // TODO: add more info
1325 }
1326 return descr;
1327 }
1328
1329 //---------------------------------------------------------
1330 // ContextChange::toString
1331 // ContextChange elements are not intended to be shown
1332 // to user, this function is for debugging purposes.
1333 //---------------------------------------------------------
1334
toString() const1335 QString ContextChange::toString() const
1336 {
1337 return QString("Context change: ctx1 (%1), ctx2(%2)").arg(describeContext(ctx[0]), describeContext(ctx[1]));
1338 }
1339
1340 //---------------------------------------------------------
1341 // ElementDiff::sameItem
1342 //---------------------------------------------------------
1343
sameItem(const BaseDiff & other) const1344 bool ElementDiff::sameItem(const BaseDiff& other) const
1345 {
1346 return BaseDiff::sameItem(other);
1347 }
1348
1349 //---------------------------------------------------------
1350 // ElementDiff::afrac
1351 // Returns position of the changed chunk
1352 //---------------------------------------------------------
1353
afrac(int score) const1354 Fraction ElementDiff::afrac(int score) const
1355 {
1356 Q_ASSERT(score == 0 || score == 1);
1357 const ScoreElement* se = el[score];
1358 if (se && se->isElement())
1359 return toElement(se)->tick();
1360 return BaseDiff::afrac(score);
1361 }
1362
1363 //---------------------------------------------------------
1364 // ElementDiff::toString
1365 //---------------------------------------------------------
1366
toString() const1367 QString ElementDiff::toString() const
1368 {
1369 QString ctxDescr = describeContext(ctx[0]);
1370 switch(type) {
1371 case DiffType::DELETE:
1372 return QObject::tr("%1: removed element %2", "scorediff").arg(ctxDescr, el[0]->userName());
1373 case DiffType::INSERT:
1374 return QObject::tr("%1: inserted element %2", "scorediff").arg(ctxDescr, el[1]->userName());
1375 case DiffType::REPLACE:
1376 return QObject::tr("%1: replaced element %2 with element %3", "scorediff").arg(ctxDescr, el[0]->userName(), el[1]->userName());
1377 case DiffType::EQUAL:
1378 Q_ASSERT(el[0]->type() == el[1]->type());
1379 return QObject::tr("%1: equal element %2", "scorediff").arg(ctxDescr, el[0]->userName());
1380 }
1381 return ctxDescr;
1382 }
1383
1384 //---------------------------------------------------------
1385 // PropertyDiff::sameItem
1386 //---------------------------------------------------------
1387
sameItem(const BaseDiff & otherBase) const1388 bool PropertyDiff::sameItem(const BaseDiff& otherBase) const
1389 {
1390 if (!BaseDiff::sameItem(otherBase))
1391 return false;
1392 if (itemType() != otherBase.itemType())
1393 return false;
1394 const PropertyDiff& other = static_cast<const PropertyDiff&>(otherBase);
1395 return (pid == other.pid);
1396 }
1397
1398 //---------------------------------------------------------
1399 // PropertyDiff::toString
1400 //---------------------------------------------------------
1401
toString() const1402 QString PropertyDiff::toString() const
1403 {
1404 QString ctxDescr = describeContext(ctx[0]);
1405 QString propName = propertyUserName(pid);
1406 switch(propertyType(pid)) {
1407 case P_TYPE::BOOL:
1408 {
1409 bool b1 = ctx[0]->getProperty(pid).toBool();
1410 Q_ASSERT(b1 != ctx[1]->getProperty(pid).toBool());
1411 QString t;
1412 if (b1)
1413 t = QObject::tr("%1: property %2 is turned off", "scorediff");
1414 else
1415 t = QObject::tr("%1: property %2 is turned on", "scorediff");
1416 return t.arg(ctxDescr, propName);
1417 }
1418 default:
1419 {
1420 QString val1 = ctx[0]->propertyUserValue(pid);
1421 QString val2 = ctx[1]->propertyUserValue(pid);
1422 QString t = QObject::tr("%1: property %2 changed from %3 to %4", "scorediff");
1423 return t.arg(ctxDescr, propName, val1, val2);
1424 }
1425 }
1426 }
1427
1428 //---------------------------------------------------------
1429 // MarkupDiff::sameItem
1430 //---------------------------------------------------------
1431
sameItem(const BaseDiff & otherBase) const1432 bool MarkupDiff::sameItem(const BaseDiff& otherBase) const
1433 {
1434 if (!BaseDiff::sameItem(otherBase))
1435 return false;
1436 if (itemType() != otherBase.itemType())
1437 return false;
1438 const MarkupDiff& other = static_cast<const MarkupDiff&>(otherBase);
1439 return (name == other.name);
1440 }
1441
1442 //---------------------------------------------------------
1443 // MarkupDiff::toString
1444 //---------------------------------------------------------
1445
toString() const1446 QString MarkupDiff::toString() const
1447 {
1448 QString ctxDescr = describeContext(ctx[0]);
1449 if (name == "metaTag") {
1450 QString tagName = info.toString();
1451 return QObject::tr("%1: %2 changed from %3 to %4", "scorediff")
1452 .arg(ctxDescr, tagName,
1453 ctx[0]->score()->metaTag(tagName), ctx[1]->score()->metaTag(tagName));
1454 }
1455 return QObject::tr("%1: markup changes: %2", "scorediff")
1456 .arg(ctxDescr, name);
1457 }
1458 }
1459
1460