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