1 //=============================================================================
2 //  MuseScore
3 //  Music Composition & Notation
4 //
5 //  Copyright (C) 2002-2011 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 "lyrics.h"
14 
15 #include "chord.h"
16 #include "score.h"
17 #include "sym.h"
18 #include "system.h"
19 #include "xml.h"
20 #include "staff.h"
21 #include "segment.h"
22 #include "undo.h"
23 #include "textedit.h"
24 #include "measure.h"
25 
26 namespace Ms {
27 
28 //---------------------------------------------------------
29 //   lyricsElementStyle
30 //---------------------------------------------------------
31 
32 static const ElementStyle lyricsElementStyle {
33       { Sid::lyricsPlacement, Pid::PLACEMENT  },
34       };
35 
36 //---------------------------------------------------------
37 //   Lyrics
38 //---------------------------------------------------------
39 
Lyrics(Score * s)40 Lyrics::Lyrics(Score* s)
41    : TextBase(s, Tid::LYRICS_ODD)
42       {
43       _even       = false;
44       initElementStyle(&lyricsElementStyle);
45       _no         = 0;
46       _ticks      = Fraction(0,1);
47       _syllabic   = Syllabic::SINGLE;
48       _separator  = 0;
49       }
50 
Lyrics(const Lyrics & l)51 Lyrics::Lyrics(const Lyrics& l)
52    : TextBase(l)
53       {
54       _even      = l._even;
55       _no        = l._no;
56       _ticks     = l._ticks;
57       _syllabic  = l._syllabic;
58       _separator = 0;
59       }
60 
~Lyrics()61 Lyrics::~Lyrics()
62       {
63       if (_separator)
64             remove(_separator);
65       }
66 
67 //---------------------------------------------------------
68 //   scanElements
69 //---------------------------------------------------------
70 
scanElements(void * data,void (* func)(void *,Element *),bool)71 void Lyrics::scanElements(void* data, void (*func)(void*, Element*), bool /*all*/)
72       {
73       func(data, this);
74 /* DO NOT ADD EITHER THE LYRICSLINE OR THE SEGMENTS: segments are added through the system each belongs to;
75       LyricsLine is not needed, as it is internally manged.
76       if (_separator)
77             _separator->scanElements(data, func, all); */
78       }
79 
80 //---------------------------------------------------------
81 //   write
82 //---------------------------------------------------------
83 
write(XmlWriter & xml) const84 void Lyrics::write(XmlWriter& xml) const
85       {
86       if (!xml.canWrite(this))
87             return;
88       xml.stag(this);
89       writeProperty(xml, Pid::VERSE);
90       if (_syllabic != Syllabic::SINGLE) {
91             static const char* sl[] = {
92                   "single", "begin", "end", "middle"
93                   };
94             xml.tag("syllabic", sl[int(_syllabic)]);
95             }
96       xml.tag("ticks", _ticks.ticks(), 0); // pre-3.1 compatibility: write integer ticks under <ticks> tag
97       writeProperty(xml, Pid::LYRIC_TICKS);
98 
99       TextBase::writeProperties(xml);
100       xml.etag();
101       }
102 
103 //---------------------------------------------------------
104 //   read
105 //---------------------------------------------------------
106 
read(XmlReader & e)107 void Lyrics::read(XmlReader& e)
108       {
109       while (e.readNextStartElement()) {
110             if (!readProperties(e))
111                   e.unknown();
112             }
113       if (!isStyled(Pid::OFFSET) && !e.pasteMode()) {
114             // fix offset for pre-3.1 scores
115             // 3.0: y offset was meaningless if autoplace is set
116             QString version = masterScore()->mscoreVersion();
117             if (autoplace() && !version.isEmpty() && version < "3.1") {
118                   QPointF off = propertyDefault(Pid::OFFSET).toPointF();
119                   ryoffset() = off.y();
120                   }
121             }
122       }
123 
124 //---------------------------------------------------------
125 //   readProperties
126 //---------------------------------------------------------
127 
readProperties(XmlReader & e)128 bool Lyrics::readProperties(XmlReader& e)
129       {
130       const QStringRef& tag(e.name());
131 
132       if (tag == "no")
133             _no = e.readInt();
134       else if (tag == "syllabic") {
135             QString val(e.readElementText());
136             if (val == "single")
137                   _syllabic = Syllabic::SINGLE;
138             else if (val == "begin")
139                   _syllabic = Syllabic::BEGIN;
140             else if (val == "end")
141                   _syllabic = Syllabic::END;
142             else if (val == "middle")
143                   _syllabic = Syllabic::MIDDLE;
144             else
145                   qDebug("bad syllabic property");
146             }
147       else if (tag == "ticks")            // obsolete
148             _ticks = e.readFraction(); // will fall back to reading integer ticks on older scores
149       else if (tag == "ticks_f")
150             _ticks = e.readFraction();
151       else if (readProperty(tag, e, Pid::PLACEMENT))
152             ;
153       else if (!TextBase::readProperties(e))
154             return false;
155       return true;
156       }
157 
158 //---------------------------------------------------------
159 //   add
160 //---------------------------------------------------------
161 
add(Element * el)162 void Lyrics::add(Element* el)
163       {
164 //      el->setParent(this);
165 //      if (el->type() == ElementType::LINE)
166 //            _separator.append((Line*)el);           // ignore! Internally managed
167 //            ;
168 //      else
169             qDebug("Lyrics::add: unknown element %s", el->name());
170       }
171 
172 //---------------------------------------------------------
173 //   remove
174 //---------------------------------------------------------
175 
remove(Element * el)176 void Lyrics::remove(Element* el)
177       {
178       if (el->isLyricsLine()) {
179             // only if separator still exists and is the right one
180             if (_separator && el == _separator) {
181 #if 0
182                   // clear melismaEnd flag from end cr
183                   // find end cr from melisma itself, as ticks for lyrics may not be accurate at this point
184                   // note this clearing this might be premature, as there may be other lyrics that still end there
185                   // also, at this point we can't be sure if this is a melisma or a dash
186                   // but the flag will be regenerated on next layout
187                   Element* e = _separator->endElement();
188                   if (!e)
189                         e = score()->findCRinStaff(_separator->tick2(), track());
190                   if (e && e->isChordRest())
191                         toChordRest(e)->setMelismaEnd(false);
192 #endif
193                   // Lyrics::remove() and LyricsLine::removeUnmanaged() call each other;
194                   // be sure each finds a clean context
195                   LyricsLine* separ = _separator;
196                   _separator = 0;
197                   separ->setParent(0);
198                   separ->removeUnmanaged();
199 //done in undo/redo?                  delete separ;
200                   }
201             }
202       else
203             qDebug("Lyrics::remove: unknown element %s", el->name());
204       }
205 
206 //---------------------------------------------------------
207 //   isMelisma
208 //---------------------------------------------------------
209 
isMelisma() const210 bool Lyrics::isMelisma() const
211       {
212       // entered as melisma using underscore?
213       if (_ticks > Fraction(0,1))
214             return true;
215 
216       // hyphenated?
217       // if so, it is a melisma only if there is no lyric in same verse on next CR
218       if (_syllabic == Syllabic::BEGIN || _syllabic == Syllabic::MIDDLE) {
219             // find next CR on same track and check for existence of lyric in same verse
220             ChordRest* cr  = chordRest();
221             if (cr) {
222                   Segment* s     = cr->segment()->next1();
223                   ChordRest* ncr = s ? s->nextChordRest(cr->track()) : 0;
224                   if (ncr && !ncr->lyrics(_no, placement()))
225                         return true;
226                   }
227             }
228 
229       // default - not a melisma
230       return false;
231       }
232 
233 //---------------------------------------------------------
234 //   layout
235 //    - does not touch vertical position
236 //---------------------------------------------------------
237 
layout()238 void Lyrics::layout()
239       {
240       if (!parent()) { // palette & clone trick
241             setPos(QPointF());
242             TextBase::layout1();
243             return;
244             }
245 
246       //
247       // parse leading verse number and/or punctuation, so we can factor it into layout separately
248       //
249       bool hasNumber     = false; // _verseNumber;
250 
251       // find:
252       // 1) string of numbers and non-word characters at start of syllable
253       // 2) at least one other character (indicating start of actual lyric)
254       // 3) string of non-word characters at end of syllable
255       //QRegularExpression leadingPattern("(^[\\d\\W]+)([^\\d\\W]+)");
256 
257       const QString text = plainText();
258       QString leading;
259       QString trailing;
260 
261       if (score()->styleB(Sid::lyricsAlignVerseNumber)) {
262             QRegularExpression punctuationPattern("(^[\\d\\W]*)([^\\d\\W].*?)([\\d\\W]*$)", QRegularExpression::UseUnicodePropertiesOption);
263             QRegularExpressionMatch punctuationMatch = punctuationPattern.match(text);
264             if (punctuationMatch.hasMatch()) {
265                   // leading and trailing punctuation
266                   leading = punctuationMatch.captured(1);
267                   trailing = punctuationMatch.captured(3);
268                   //QString actualLyric = punctuationMatch.captured(2);
269                   if (!leading.isEmpty() && leading[0].isDigit())
270                         hasNumber = true;
271                   }
272             }
273 
274       bool styleDidChange = false;
275       if ((_no & 1) && !_even) {
276             initTid(Tid::LYRICS_EVEN, /* preserveDifferent */ true);
277             _even             = true;
278             styleDidChange    = true;
279             }
280       if (!(_no & 1) && _even) {
281             initTid(Tid::LYRICS_ODD, /* preserveDifferent */ true);
282             _even             = false;
283             styleDidChange    = true;
284             }
285 
286       if (styleDidChange)
287             styleChanged();
288 
289       if (isMelisma() || hasNumber) {
290             // use the melisma style alignment setting
291             if (isStyled(Pid::ALIGN))
292                   setAlign(score()->styleV(Sid::lyricsMelismaAlign).value<Align>());
293             }
294       else {
295             // use the text style alignment setting
296             if (isStyled(Pid::ALIGN))
297                   setAlign(propertyDefault(Pid::ALIGN).value<Align>());
298             }
299       QPointF o(propertyDefault(Pid::OFFSET).toPointF());
300       rxpos() = o.x();
301       qreal x = pos().x();
302       TextBase::layout1();
303 
304       qreal centerAdjust = 0.0;
305       qreal leftAdjust   = 0.0;
306 
307       if (score()->styleB(Sid::lyricsAlignVerseNumber)) {
308             // Calculate leading and trailing parts widths. Lyrics
309             // should have text layout to be able to do it correctly.
310             Q_ASSERT(rows() != 0);
311             if (!leading.isEmpty() || !trailing.isEmpty()) {
312 //                   qDebug("create leading, trailing <%s> -- <%s><%s>", qPrintable(text), qPrintable(leading), qPrintable(trailing));
313                   const TextBlock& tb = textBlock(0);
314 
315                   const qreal leadingWidth = tb.xpos(leading.length(), this) - tb.boundingRect().x();
316                   const int trailingPos = text.length() - trailing.length();
317                   const qreal trailingWidth = tb.boundingRect().right() - tb.xpos(trailingPos, this);
318 
319                   leftAdjust = leadingWidth;
320                   centerAdjust = leadingWidth - trailingWidth;
321                   }
322             }
323 
324       ChordRest* cr = chordRest();
325 
326       if (align() & Align::HCENTER) {
327             //
328             // center under notehead, not origin
329             // however, lyrics that are melismas or have verse numbers will be forced to left alignment
330             //
331             // center under note head
332             qreal nominalWidth = symWidth(SymId::noteheadBlack);
333             x += nominalWidth * .5 - cr->x() - centerAdjust * 0.5;
334             }
335       else if (!(align() & Align::RIGHT)) {
336             // even for left aligned syllables, ignore leading verse numbers and/or punctuation
337             x -= leftAdjust;
338             }
339 
340       rxpos() = x;
341 
342       if (_ticks > Fraction(0,1) || _syllabic == Syllabic::BEGIN || _syllabic == Syllabic::MIDDLE) {
343             if (!_separator) {
344                   _separator = new LyricsLine(score());
345                   _separator->setTick(cr->tick());
346                   score()->addUnmanagedSpanner(_separator);
347                   }
348             _separator->setParent(this);
349             _separator->setTick(cr->tick());
350             // HACK separator should have non-zero length to get its layout
351             // always triggered. A proper ticks length will be set later on the
352             // separator layout.
353             _separator->setTicks(Fraction::fromTicks(1));
354             _separator->setTrack(track());
355             _separator->setTrack2(track());
356             _separator->setVisible(visible());
357             // bbox().setWidth(bbox().width());  // ??
358             }
359       else {
360             if (_separator) {
361                   _separator->removeUnmanaged();
362                   delete _separator;
363                   _separator = 0;
364                   }
365             }
366 
367       if (_ticks.isNotZero()) {
368             // set melisma end
369             ChordRest* ecr = score()->findCR(endTick(), track());
370             if (ecr)
371                   ecr->setMelismaEnd(true);
372             }
373 
374       }
375 
376 //---------------------------------------------------------
377 //   layout2
378 //    compute vertical position
379 //---------------------------------------------------------
380 
layout2(int nAbove)381 void Lyrics::layout2(int nAbove)
382       {
383       qreal lh = lineSpacing() * score()->styleD(Sid::lyricsLineHeight);
384 
385       if (placeBelow()) {
386             qreal yo = segment()->measure()->system()->staff(staffIdx())->bbox().height();
387             rypos()  = lh * (_no - nAbove) + yo - chordRest()->y();
388             rpos()  += styleValue(Pid::OFFSET, Sid::lyricsPosBelow).toPointF();
389             }
390       else {
391             rypos() = -lh * (nAbove - _no - 1) - chordRest()->y();
392             rpos() += styleValue(Pid::OFFSET, Sid::lyricsPosAbove).toPointF();
393             }
394       }
395 
396 //---------------------------------------------------------
397 //   paste
398 //---------------------------------------------------------
399 
paste(EditData & ed)400 void Lyrics::paste(EditData& ed)
401       {
402       MuseScoreView* scoreview = ed.view;
403 #if defined(Q_OS_MAC) || defined(Q_OS_WIN)
404       QClipboard::Mode mode = QClipboard::Clipboard;
405 #else
406       QClipboard::Mode mode = QClipboard::Selection;
407 #endif
408       QString txt = QApplication::clipboard()->text(mode);
409       QString regex = QString("[^\\S") + QChar(0xa0) + QChar(0x202F) + "]+";
410       QStringList sl = txt.split(QRegExp(regex), QString::SkipEmptyParts);
411       if (sl.empty())
412             return;
413 
414       QStringList hyph = sl[0].split("-");
415       bool minus = false;
416       bool underscore = false;
417       score()->startCmd();
418 
419       if(hyph.length() > 1) {
420             score()->undo(new InsertText(cursor(ed), hyph[0]), &ed);
421             hyph.removeFirst();
422             sl[0] =  hyph.join("-");
423             minus = true;
424             }
425       else if (sl.length() > 1 && sl[1] == "-") {
426             score()->undo(new InsertText(cursor(ed), sl[0]), &ed);
427             sl.removeFirst();
428             sl.removeFirst();
429             minus = true;
430             }
431       else if (sl[0].startsWith("_")) {
432             sl[0].remove(0, 1);
433             if (sl[0].isEmpty())
434                   sl.removeFirst();
435             underscore = true;
436             }
437       else if (sl[0].contains("_")) {
438             int p = sl[0].indexOf("_");
439             score()->undo(new InsertText(cursor(ed), sl[0]), &ed);
440             sl[0] = sl[0].mid(p + 1);
441             if (sl[0].isEmpty())
442                   sl.removeFirst();
443             underscore = true;
444             }
445       else if (sl.length() > 1 && sl[1] == "_") {
446             score()->undo(new InsertText(cursor(ed), sl[0]), &ed);
447             sl.removeFirst();
448             sl.removeFirst();
449             underscore = true;
450             }
451       else {
452             score()->undo(new InsertText(cursor(ed), sl[0]), &ed);
453             sl.removeFirst();
454             }
455 
456       score()->endCmd();
457       txt = sl.join(" ");
458 
459       QApplication::clipboard()->setText(txt, mode);
460       if (minus)
461             scoreview->lyricsMinus();
462       else if (underscore)
463             scoreview->lyricsUnderscore();
464       else
465             scoreview->lyricsTab(false, false, true);
466       }
467 
468 //---------------------------------------------------------
469 //   endTick
470 //---------------------------------------------------------
471 
endTick() const472 Fraction Lyrics::endTick() const
473       {
474       return segment()->tick() + ticks();
475       }
476 
477 //---------------------------------------------------------
478 //   acceptDrop
479 //---------------------------------------------------------
480 
acceptDrop(EditData & data) const481 bool Lyrics::acceptDrop(EditData& data) const
482       {
483       return data.dropElement->isText() || TextBase::acceptDrop(data);
484       }
485 
486 //---------------------------------------------------------
487 //   drop
488 //---------------------------------------------------------
489 
drop(EditData & data)490 Element* Lyrics::drop(EditData& data)
491       {
492       ElementType type = data.dropElement->type();
493       if (type == ElementType::SYMBOL || type == ElementType::FSYMBOL) {
494             TextBase::drop(data);
495             return 0;
496             }
497       if (!data.dropElement->isText()) {
498             delete data.dropElement;
499             data.dropElement = 0;
500             return 0;
501             }
502       Text* e = toText(data.dropElement);
503       e->setParent(this);
504       score()->undoAddElement(e);
505       return e;
506       }
507 
508 //---------------------------------------------------------
509 //   endEdit
510 //---------------------------------------------------------
511 
endEdit(EditData & ed)512 void Lyrics::endEdit(EditData& ed)
513       {
514       TextBase::endEdit(ed);
515       triggerLayoutAll();
516       }
517 
518 //---------------------------------------------------------
519 //   removeFromScore
520 //---------------------------------------------------------
521 
removeFromScore()522 void Lyrics::removeFromScore()
523       {
524       if (_ticks.isNotZero()) {
525             // clear melismaEnd flag from end cr
526             ChordRest* ecr = score()->findCR(endTick(), track());
527             if (ecr)
528                   ecr->setMelismaEnd(false);
529             }
530       if (_separator) {
531             _separator->removeUnmanaged();
532             delete _separator;
533             _separator = 0;
534             }
535       }
536 
537 //---------------------------------------------------------
538 //   getProperty
539 //---------------------------------------------------------
540 
getProperty(Pid propertyId) const541 QVariant Lyrics::getProperty(Pid propertyId) const
542       {
543       switch (propertyId) {
544             case Pid::SYLLABIC:
545                   return int(_syllabic);
546             case Pid::LYRIC_TICKS:
547                   return _ticks;
548             case Pid::VERSE:
549                   return _no;
550             default:
551                   return TextBase::getProperty(propertyId);
552             }
553       }
554 
555 //---------------------------------------------------------
556 //   setProperty
557 //---------------------------------------------------------
558 
setProperty(Pid propertyId,const QVariant & v)559 bool Lyrics::setProperty(Pid propertyId, const QVariant& v)
560       {
561       switch (propertyId) {
562             case Pid::PLACEMENT:
563                   setPlacement(Placement(v.toInt()));
564                   break;
565             case Pid::SYLLABIC:
566                   _syllabic = Syllabic(v.toInt());
567                   break;
568             case Pid::LYRIC_TICKS:
569                   if (_ticks.isNotZero()) {
570                         // clear melismaEnd flag from previous end cr
571                         // this might be premature, as there may be other melismas ending there
572                         // but flag will be generated correctly on layout
573                         // TODO: after inserting a measure,
574                         // endTick info is wrong.
575                         // Somehow we need to fix this.
576                         // See https://musescore.org/en/node/285304 and https://musescore.org/en/node/311289
577                         ChordRest* ecr = score()->findCR(endTick(), track());
578                         if (ecr)
579                               ecr->setMelismaEnd(false);
580                         }
581                   _ticks = v.value<Fraction>();
582                   break;
583             case Pid::VERSE:
584                   _no = v.toInt();
585                   break;
586             default:
587                   if (!TextBase::setProperty(propertyId, v))
588                         return false;
589                   break;
590             }
591       triggerLayout();
592       return true;
593       }
594 
595 //---------------------------------------------------------
596 //   propertyDefault
597 //---------------------------------------------------------
598 
propertyDefault(Pid id) const599 QVariant Lyrics::propertyDefault(Pid id) const
600       {
601       switch (id) {
602             case Pid::SUB_STYLE:
603                   return int((_no & 1) ? Tid::LYRICS_EVEN : Tid::LYRICS_ODD);
604             case Pid::PLACEMENT:
605                   return score()->styleV(Sid::lyricsPlacement);
606             case Pid::SYLLABIC:
607                   return int(Syllabic::SINGLE);
608             case Pid::LYRIC_TICKS:
609                   return Fraction(0,1);
610             case Pid::VERSE:
611                   return 0;
612             case Pid::ALIGN:
613                   if (isMelisma())
614                         return score()->styleV(Sid::lyricsMelismaAlign);
615                   // fall through
616             default:
617                   return TextBase::propertyDefault(id);
618             }
619       }
620 
621 //---------------------------------------------------------
622 //   forAllLyrics
623 //---------------------------------------------------------
624 
forAllLyrics(std::function<void (Lyrics *)> f)625 void Score::forAllLyrics(std::function<void(Lyrics*)> f)
626       {
627       for (Segment* s = firstSegment(SegmentType::ChordRest); s; s = s->next1(SegmentType::ChordRest)) {
628             for (Element* e : s->elist()) {
629                   if (e) {
630                         for (Lyrics* l : toChordRest(e)->lyrics()) {
631                               f(l);
632                               }
633                         }
634                   }
635             }
636       }
637 
638 //---------------------------------------------------------
639 //   undoChangeProperty
640 //---------------------------------------------------------
641 
undoChangeProperty(Pid id,const QVariant & v,PropertyFlags ps)642 void Lyrics::undoChangeProperty(Pid id, const QVariant& v, PropertyFlags ps)
643       {
644       if (id == Pid::VERSE && no() != v.toInt()) {
645             for (Lyrics* l : chordRest()->lyrics()) {
646                   if (l->no() == v.toInt()) {
647                         // verse already exists, swap
648                         l->TextBase::undoChangeProperty(id, no(), ps);
649                         Placement p = l->placement();
650                         l->TextBase::undoChangeProperty(Pid::PLACEMENT, int(placement()), ps);
651                         TextBase::undoChangeProperty(Pid::PLACEMENT, int(p), ps);
652                         break;
653                         }
654                   }
655             TextBase::undoChangeProperty(id, v, ps);
656             return;
657             }
658       else if (id == Pid::AUTOPLACE && v.toBool() != autoplace()) {
659             if (v.toBool()) {
660                   // setting autoplace
661                   // reset offset
662                   undoResetProperty(Pid::OFFSET);
663                   }
664             else {
665                   // unsetting autoplace
666                   // rebase offset
667                   QPointF off = offset();
668                   qreal y = pos().y() - propertyDefault(Pid::OFFSET).toPointF().y();
669                   off.ry() = placeAbove() ? y : y - staff()->height();
670                   undoChangeProperty(Pid::OFFSET, off, PropertyFlags::UNSTYLED);
671                   }
672             TextBase::undoChangeProperty(id, v, ps);
673             return;
674             }
675 #if 0
676       // TODO: create new command to do this
677       if (id == Pid::PLACEMENT) {
678             if (Placement(v.toInt()) == Placement::ABOVE) {
679                   // change placment of all verse for the same voice upto this one to ABOVE
680                   score()->forAllLyrics([this,id,v,ps](Lyrics* l) {
681                         if (l->no() <= no() && l->voice() == voice())
682                               l->TextBase::undoChangeProperty(id, v, ps);
683                         });
684                   }
685             else {
686                   // change placment of all verse for the same voce starting from this one to BELOW
687                   score()->forAllLyrics([this,id,v,ps](Lyrics* l) {
688                         if (l->no() >= no() && l->voice() == voice())
689                               l->TextBase::undoChangeProperty(id, v, ps);
690                         });
691                   }
692             return;
693             }
694 #endif
695 
696       TextBase::undoChangeProperty(id, v, ps);
697       }
698 
699 }
700 
701