Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2021 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include "lyrics.h"
24 :
25 : #include "types/translatablestring.h"
26 :
27 : #include "../editing/navigation.h"
28 : #include "../editing/textedit.h"
29 :
30 : #include "measure.h"
31 : #include "score.h"
32 : #include "segment.h"
33 : #include "staff.h"
34 : #include "text.h"
35 : #include "utils.h"
36 :
37 : #include "log.h"
38 :
39 : using namespace mu;
40 : using namespace mu::engraving;
41 :
42 : namespace mu::engraving {
43 : //---------------------------------------------------------
44 : // lyricsElementStyle
45 : //---------------------------------------------------------
46 :
47 : static const ElementStyle lyricsElementStyle {
48 : { Sid::lyricsPlacement, Pid::PLACEMENT },
49 : { Sid::lyricsAvoidBarlines, Pid::AVOID_BARLINES },
50 : };
51 :
52 : //---------------------------------------------------------
53 : // Lyrics
54 : //---------------------------------------------------------
55 :
56 1781 : Lyrics::Lyrics(DummyParentOr<ChordRest> parent)
57 1781 : : TextBase(ElementType::LYRICS, parent, TextStyleType::LYRICS_ODD, ElementFlag::ON_STAFF)
58 : {
59 1781 : m_separator = 0;
60 1781 : initElementStyle(&lyricsElementStyle);
61 1781 : m_verse = 0;
62 1781 : m_ticks = Fraction(0, 1);
63 1781 : m_syllabic = LyricsSyllabic::SINGLE;
64 1781 : }
65 :
66 1331 : Lyrics::Lyrics(const Lyrics& l)
67 1331 : : TextBase(l)
68 : {
69 1331 : m_verse = l.m_verse;
70 1331 : m_ticks = l.m_ticks;
71 1331 : m_syllabic = l.m_syllabic;
72 1331 : m_separator = 0;
73 1331 : }
74 :
75 6180 : Lyrics::~Lyrics()
76 : {
77 3090 : if (m_separator) {
78 225 : remove(m_separator);
79 : }
80 6180 : }
81 :
82 0 : TranslatableString Lyrics::subtypeUserName() const
83 : {
84 0 : return TranslatableString("engraving", "Verse %1").arg(m_verse + 1);
85 : }
86 :
87 : //---------------------------------------------------------
88 : // add
89 : //---------------------------------------------------------
90 :
91 0 : void Lyrics::add(EngravingItem* el)
92 : {
93 0 : if (el->isLyricsLine()) {
94 0 : LyricsLine* separator = toLyricsLine(el);
95 0 : m_separator = separator;
96 0 : score()->addUnmanagedSpanner(separator);
97 : } else {
98 0 : LOGD("Lyrics::add: unknown element %s", el->typeName());
99 : }
100 0 : }
101 :
102 : //---------------------------------------------------------
103 : // remove
104 : //---------------------------------------------------------
105 :
106 464 : void Lyrics::remove(EngravingItem* el)
107 : {
108 464 : if (el->isLyricsLine()) {
109 : // only if separator still exists and is the right one
110 464 : if (m_separator && el == m_separator) {
111 : // Lyrics::remove() and LyricsLine::removeUnmanaged() call each other;
112 : // be sure each finds a clean context
113 232 : LyricsLine* separ = m_separator;
114 232 : m_separator = 0;
115 232 : separ->removeUnmanaged();
116 : }
117 : } else {
118 0 : LOGD("Lyrics::remove: unknown element %s", el->typeName());
119 : }
120 464 : }
121 :
122 : //---------------------------------------------------------
123 : // isMelisma
124 : //---------------------------------------------------------
125 :
126 4651 : bool Lyrics::isMelisma() const
127 : {
128 : // entered as melisma using underscore?
129 4651 : if (m_ticks > Fraction(0, 1)) {
130 102 : return true;
131 : }
132 :
133 : // hyphenated?
134 : // if so, it is a melisma only if there is no lyric in same verse on next CR
135 4549 : if (m_separator && (m_syllabic == LyricsSyllabic::BEGIN || m_syllabic == LyricsSyllabic::MIDDLE)) {
136 : // find next CR and check for existence of lyric in same verse and placement (in any voice)
137 367 : const ChordRest* cr = chordRest();
138 367 : if (cr) {
139 367 : const Segment* s = cr->segment()->next1();
140 367 : const track_idx_t strack = staffIdx() * VOICES;
141 367 : const track_idx_t etrack = strack + VOICES;
142 367 : const track_idx_t lyrTrack = track();
143 367 : const ChordRest* lyrVoiceNextCR = s ? s->nextChordRest(lyrTrack) : nullptr;
144 601 : for (track_idx_t track = strack; track < etrack; ++track) {
145 544 : const ChordRest* trackNextCR = s ? s->nextChordRest(track) : nullptr;
146 544 : if (trackNextCR) {
147 7 : if (lyrTrack != track && lyrVoiceNextCR
148 380 : && !lyrVoiceNextCR->lyrics(m_verse, placement()) && lyrVoiceNextCR->tick() < trackNextCR->tick()) {
149 : // There is an intermediary note in a different voice, this is a melisma
150 1 : return true;
151 : }
152 372 : if (trackNextCR->lyrics(m_verse, placement())) {
153 : // Next note has lyrics, not a melisma just a dash
154 309 : return false;
155 : }
156 : }
157 : }
158 57 : return true;
159 : }
160 : }
161 :
162 : // default - not a melisma
163 4182 : return false;
164 : }
165 :
166 : //---------------------------------------------------------
167 : // paste
168 : //---------------------------------------------------------
169 :
170 0 : void Lyrics::paste(const String& txt)
171 : {
172 0 : if (txt.startsWith('<') && txt.contains('>')) {
173 0 : TextBase::paste(txt);
174 0 : return;
175 : }
176 :
177 0 : String regex = String(u"[^\\S") + Char(0xa0) + Char(0x202F) + u"]+";
178 0 : StringList sl = txt.split(std::regex(regex.toStdString()), muse::SkipEmptyParts);
179 0 : if (sl.empty()) {
180 0 : return;
181 : }
182 :
183 0 : StringList hyph = sl.at(0).split(u'-');
184 0 : score()->startCmd(TranslatableString("undoableAction", "Paste lyrics"));
185 :
186 0 : deleteSelectedText();
187 :
188 0 : if (hyph.size() > 1) {
189 0 : score()->undo(new InsertText(cursor(), hyph[0]));
190 0 : hyph.removeAt(0);
191 0 : sl[0] = hyph.join(u"-");
192 0 : } else if (sl.size() > 1 && sl[1] == u"-") {
193 0 : score()->undo(new InsertText(cursor(), sl[0]));
194 0 : sl.removeAt(0);
195 0 : sl.removeAt(0);
196 0 : } else if (sl[0].startsWith(u"_")) {
197 0 : sl[0].remove(0, 1);
198 0 : if (sl[0].isEmpty()) {
199 0 : sl.removeAt(0);
200 : }
201 0 : } else if (sl[0].contains(u"_")) {
202 0 : size_t p = sl[0].indexOf(u'_');
203 0 : score()->undo(new InsertText(cursor(), sl[0]));
204 0 : sl[0] = sl[0].mid(p + 1);
205 0 : if (sl[0].isEmpty()) {
206 0 : sl.removeAt(0);
207 : }
208 0 : } else if (sl.size() > 1 && sl[1] == "_") {
209 0 : score()->undo(new InsertText(cursor(), sl[0]));
210 0 : sl.removeAt(0);
211 0 : sl.removeAt(0);
212 : } else {
213 0 : score()->undo(new InsertText(cursor(), sl[0]));
214 0 : sl.removeAt(0);
215 : }
216 :
217 0 : score()->endCmd();
218 0 : }
219 :
220 : //---------------------------------------------------------
221 : // endTick
222 : //---------------------------------------------------------
223 :
224 203 : Fraction Lyrics::endTick() const
225 : {
226 203 : return segment()->tick() + ticks();
227 : }
228 :
229 : //---------------------------------------------------------
230 : // acceptDrop
231 : //---------------------------------------------------------
232 :
233 0 : bool Lyrics::acceptDrop(EditData& data) const
234 : {
235 0 : return data.dropElement->isText() || TextBase::acceptDrop(data);
236 : }
237 :
238 : //---------------------------------------------------------
239 : // drop
240 : //---------------------------------------------------------
241 :
242 0 : EngravingItem* Lyrics::drop(Transaction& tx, EditData& data)
243 : {
244 0 : ElementType type = data.dropElement->type();
245 0 : if (type == ElementType::SYMBOL || type == ElementType::FSYMBOL) {
246 0 : TextBase::drop(tx, data);
247 0 : return 0;
248 : }
249 0 : if (!data.dropElement->isText()) {
250 0 : delete data.dropElement;
251 0 : data.dropElement = 0;
252 0 : return 0;
253 : }
254 0 : Text* e = toText(data.dropElement);
255 0 : e->setOwnershipParent(this);
256 0 : score()->undoAddElement(e);
257 0 : return e;
258 : }
259 :
260 0 : bool Lyrics::isEditAllowed(EditData& ed) const
261 : {
262 0 : if (isTextNavigationKey(ed.key, ed.modifiers)) {
263 0 : return false;
264 : }
265 :
266 : static const std::set<KeyboardModifiers> navigationModifiers {
267 : NoModifier,
268 : KeypadModifier,
269 : ShiftModifier
270 0 : };
271 :
272 0 : if (navigationModifiers.find(ed.modifiers) != navigationModifiers.end()) {
273 : static const std::set<int> navigationKeys {
274 : Key_Underscore,
275 : Key_Minus,
276 : Key_Enter,
277 : Key_Return,
278 : Key_Up,
279 : Key_Down
280 0 : };
281 :
282 0 : if (navigationKeys.find(ed.key) != navigationKeys.end()) {
283 0 : return false;
284 : }
285 : }
286 :
287 0 : if (ed.key == Key_Left || ed.key == Key_Backspace) {
288 0 : return cursor()->column() != 0 || cursor()->hasSelection();
289 : }
290 :
291 0 : if (ed.key == Key_Right) {
292 0 : bool cursorInLastColumn = cursor()->column() == cursor()->curLine().columns();
293 0 : return !cursorInLastColumn || cursor()->hasSelection();
294 : }
295 :
296 0 : return TextBase::isEditAllowed(ed);
297 : }
298 :
299 0 : void Lyrics::adjustPrevious()
300 : {
301 0 : Lyrics* prev = Navigation::prevLyrics(toLyrics(this));
302 0 : if (prev && prev->ticks().isNotZero()) {
303 : // search for lyric spanners to split at this point if necessary
304 0 : if (prev->tick() + prev->ticks() >= tick()) {
305 : // the previous lyric has a spanner attached that goes through this one
306 : // we need to shorten it
307 0 : Segment* s = score()->tick2segment(tick());
308 0 : if (s) {
309 0 : s = s->prev1(SegmentType::ChordRest);
310 0 : if (s->tick() > prev->tick()) {
311 0 : prev->undoChangeProperty(Pid::LYRIC_TICKS, s->tick() - prev->tick());
312 : } else {
313 0 : prev->undoChangeProperty(Pid::LYRIC_TICKS, Fraction::eps());
314 : }
315 : }
316 : }
317 0 : prev->setNeedRemoveInvalidSegments();
318 0 : prev->triggerLayout();
319 : }
320 0 : }
321 :
322 2 : void Lyrics::setNeedRemoveInvalidSegments()
323 : {
324 : // Allow "invalid" segments when there is a following repeat item
325 :
326 2 : const Measure* meas = measure();
327 2 : const ChordRest* separatorEndChord = m_separator ? toChordRest(m_separator->endElement()) : nullptr;
328 2 : const ChordRest* lastChordRest = meas ? meas->lastChordRest(track()) : nullptr;
329 2 : const bool endChordIsLastInMeasure = separatorEndChord == lastChordRest;
330 2 : const bool hasFollowingJump = lastChordRest ? lastChordRest->hasFollowingJumpItem() : false;
331 :
332 2 : if (endChordIsLastInMeasure && hasFollowingJump) {
333 0 : return;
334 : }
335 2 : m_needRemoveInvalidSegments = true;
336 : }
337 :
338 : //---------------------------------------------------------
339 : // endEdit
340 : //---------------------------------------------------------
341 :
342 0 : void Lyrics::endEdit(EditData& ed)
343 : {
344 0 : TextBase::endEdit(ed);
345 :
346 0 : triggerLayout();
347 0 : if (m_separator) {
348 0 : m_separator->triggerLayout();
349 : }
350 0 : }
351 :
352 : //---------------------------------------------------------
353 : // removeFromScore
354 : //---------------------------------------------------------
355 :
356 44 : void Lyrics::removeFromScore()
357 : {
358 44 : if (m_ticks.isNotZero()) {
359 : // clear melismaEnd flag from end cr
360 0 : ChordRest* ecr = score()->findCR(endTick(), track());
361 0 : if (ecr) {
362 0 : ecr->setMelismaEnd(false);
363 : }
364 : }
365 :
366 44 : if (!plainText().isEmpty()) {
367 44 : PartialLyricsLine* partialDash = findPrevPartialLyricsLineDash(this);
368 44 : if (partialDash) {
369 0 : score()->undoRemoveElement(partialDash);
370 : }
371 : }
372 :
373 44 : if (m_separator) {
374 0 : m_separator->removeUnmanaged();
375 0 : delete m_separator;
376 0 : m_separator = 0;
377 : }
378 44 : Lyrics* prev = Navigation::prevLyrics(this);
379 44 : if (prev) {
380 : // check to make sure we haven't created an invalid segment by deleting this lyric
381 0 : prev->setNeedRemoveInvalidSegments();
382 : }
383 44 : }
384 :
385 : //---------------------------------------------------------
386 : // getProperty
387 : //---------------------------------------------------------
388 :
389 13338 : PropertyValue Lyrics::getProperty(Pid propertyId) const
390 : {
391 13338 : switch (propertyId) {
392 0 : case Pid::SYLLABIC:
393 0 : return int(m_syllabic);
394 457 : case Pid::LYRIC_TICKS:
395 457 : return m_ticks;
396 455 : case Pid::VERSE:
397 455 : return m_verse;
398 0 : case Pid::AVOID_BARLINES:
399 0 : return m_avoidBarlines;
400 12426 : default:
401 12426 : return TextBase::getProperty(propertyId);
402 : }
403 : }
404 :
405 : //---------------------------------------------------------
406 : // setProperty
407 : //---------------------------------------------------------
408 :
409 40436 : bool Lyrics::setProperty(Pid propertyId, const PropertyValue& v)
410 : {
411 40436 : ChordRest* scr = nullptr;
412 40436 : ChordRest* ecr = nullptr;
413 :
414 40436 : switch (propertyId) {
415 1800 : case Pid::PLACEMENT:
416 : {
417 1800 : PlacementV newVal = v.value<PlacementV>();
418 1800 : if (newVal != placement()) {
419 0 : if (Lyrics* l = Navigation::prevLyrics(this)) {
420 0 : l->setNeedRemoveInvalidSegments();
421 : }
422 0 : if (Navigation::nextLyrics(this)) {
423 0 : setNeedRemoveInvalidSegments();
424 : }
425 0 : setPlacement(newVal);
426 : }
427 : }
428 1800 : break;
429 0 : case Pid::SYLLABIC:
430 0 : m_syllabic = LyricsSyllabic(v.toInt());
431 0 : break;
432 6 : case Pid::LYRIC_TICKS:
433 6 : if (m_ticks.isNotZero()) {
434 : // clear melismaEnd flag from previous end cr
435 : // this might be premature, as there may be other melismas ending there
436 : // but flag will be generated correctly on layout
437 : // TODO: after inserting a measure,
438 : // endTick info is wrong.
439 : // Somehow we need to fix this.
440 : // See https://musescore.org/en/node/285304 and https://musescore.org/en/node/311289
441 6 : ecr = score()->findCR(endTick(), track());
442 6 : if (ecr) {
443 6 : ecr->setMelismaEnd(false);
444 : }
445 : }
446 6 : scr = score()->findCR(tick(), track());
447 6 : m_ticks = v.value<Fraction>();
448 6 : if (scr && m_ticks <= scr->ticks()) {
449 : // if no ticks, we have to relayout in order to remove invalid melisma segments
450 2 : setNeedRemoveInvalidSegments();
451 : }
452 6 : break;
453 4 : case Pid::VERSE: {
454 4 : if (Lyrics* l = Navigation::prevLyrics(this)) {
455 0 : l->setNeedRemoveInvalidSegments();
456 : }
457 4 : bool followTextStyle = getProperty(Pid::TEXT_STYLE) == propertyDefault(Pid::TEXT_STYLE);
458 4 : m_verse = v.toInt();
459 4 : if (followTextStyle) {
460 4 : setProperty(Pid::TEXT_STYLE, propertyDefault(Pid::TEXT_STYLE));
461 : }
462 4 : break;
463 : }
464 1798 : case Pid::AVOID_BARLINES:
465 1798 : m_avoidBarlines = v.toBool();
466 1798 : break;
467 2 : case Pid::VISIBLE:
468 2 : setVisible(v.toBool());
469 2 : break;
470 36826 : default:
471 36826 : if (!TextBase::setProperty(propertyId, v)) {
472 0 : return false;
473 : }
474 36826 : break;
475 : }
476 40436 : triggerLayout();
477 40436 : return true;
478 : }
479 :
480 : //---------------------------------------------------------
481 : // propertyDefault
482 : //---------------------------------------------------------
483 :
484 23539 : PropertyValue Lyrics::propertyDefault(Pid id) const
485 : {
486 23539 : switch (id) {
487 451 : case Pid::TEXT_STYLE:
488 451 : return isEven() ? TextStyleType::LYRICS_EVEN : TextStyleType::LYRICS_ODD;
489 131 : case Pid::PLACEMENT:
490 131 : return style().styleV(Sid::lyricsPlacement);
491 0 : case Pid::SYLLABIC:
492 0 : return int(LyricsSyllabic::SINGLE);
493 443 : case Pid::LYRIC_TICKS:
494 886 : return Fraction(0, 1);
495 443 : case Pid::VERSE:
496 443 : return 0;
497 0 : case Pid::AVOID_BARLINES:
498 0 : return style().styleB(Sid::lyricsAvoidBarlines);
499 2469 : case Pid::POSITION:
500 2469 : if (isMelisma()) {
501 0 : return style().styleV(Sid::lyricsMelismaAlign).value<Align>().horizontal;
502 : }
503 : // fall through
504 : default:
505 22071 : return TextBase::propertyDefault(id);
506 : }
507 : }
508 :
509 82193 : void Lyrics::triggerLayout() const
510 : {
511 82193 : if (m_separator) {
512 : // The separator may extend to next system(s), so we must use Spanner::triggerLayout()
513 145 : m_separator->triggerLayout();
514 : } else {
515 : // In this case is ok to use EngravingItem::triggerLayout()
516 82048 : EngravingItem::triggerLayout();
517 : }
518 82193 : }
519 :
520 4330 : double Lyrics::yRelativeToStaff() const
521 : {
522 4330 : const double yOff = staffOffsetY();
523 4330 : return pos().y() + chordRest()->pos().y() + yOff;
524 : }
525 :
526 2180 : void Lyrics::setYRelativeToStaff(double y)
527 : {
528 2180 : const double yOff = staffOffsetY();
529 2180 : mutldata()->setPosY(y - chordRest()->pos().y() - yOff);
530 2180 : }
531 :
532 : //---------------------------------------------------------
533 : // forAllLyrics
534 : //---------------------------------------------------------
535 :
536 0 : void Score::forAllLyrics(std::function<void(Lyrics*)> f)
537 : {
538 0 : for (Segment* s = firstSegment(SegmentType::ChordRest); s; s = s->next1(SegmentType::ChordRest)) {
539 0 : for (EngravingItem* e : s->elist()) {
540 0 : if (e) {
541 0 : for (Lyrics* l : toChordRest(e)->lyrics()) {
542 0 : f(l);
543 : }
544 : }
545 : }
546 : }
547 0 : }
548 :
549 : //---------------------------------------------------------
550 : // undoChangeProperty
551 : //---------------------------------------------------------
552 :
553 9 : void Lyrics::undoChangeProperty(Pid id, const PropertyValue& v, PropertyFlags ps)
554 : {
555 9 : if (id == Pid::VERSE && verse() != v.toInt()) {
556 2 : PartialLyricsLine* prevPartial = findPrevPartialLyricsLineDash(this);
557 :
558 4 : for (Lyrics* l : chordRest()->lyrics()) {
559 4 : if (l->verse() == v.toInt()) {
560 : // verse already exists, swap
561 2 : l->TextBase::undoChangeProperty(id, verse(), ps);
562 2 : const PlacementV p = l->placement();
563 2 : l->TextBase::undoChangeProperty(Pid::PLACEMENT, placement(), ps);
564 2 : TextBase::undoChangeProperty(Pid::PLACEMENT, p, ps);
565 2 : break;
566 : }
567 : }
568 2 : TextBase::undoChangeProperty(id, v, ps);
569 2 : if (prevPartial && prevPartial->verse() != v.toInt()) {
570 : // Skip logic to update Lyrics by calling parent class
571 1 : prevPartial->LyricsLine::undoChangeProperty(id, v, ps);
572 : }
573 2 : return;
574 7 : } else if (id == Pid::VISIBLE && separator()) {
575 0 : separator()->undoChangeProperty(Pid::VISIBLE, v.toBool(), ps);
576 : }
577 :
578 7 : TextBase::undoChangeProperty(id, v, ps);
579 : }
580 :
581 : //---------------------------------------------------------
582 : // reset
583 : //---------------------------------------------------------
584 :
585 0 : void Lyrics::reset()
586 : {
587 0 : undoResetProperty(Pid::CENTER_BETWEEN_STAVES);
588 0 : TextBase::reset();
589 0 : }
590 :
591 : //---------------------------------------------------------
592 : // removeInvalidSegments
593 : //
594 : // Remove lyric-final melisma lines and reset the alignment of the lyric
595 : //---------------------------------------------------------
596 :
597 2 : void Lyrics::removeInvalidSegments()
598 : {
599 2 : m_needRemoveInvalidSegments = false;
600 2 : if (m_separator && isMelisma() && m_ticks < m_separator->startCR()->ticks()) {
601 0 : setTicks(Fraction(0, 1));
602 0 : m_separator->setTicks(Fraction(0, 1));
603 0 : m_separator->removeUnmanaged();
604 0 : m_separator = nullptr;
605 0 : setPosition(propertyDefault(Pid::POSITION).value<AlignH>());
606 0 : if (m_syllabic == LyricsSyllabic::BEGIN || m_syllabic == LyricsSyllabic::SINGLE) {
607 0 : undoChangeProperty(Pid::SYLLABIC, int(LyricsSyllabic::SINGLE));
608 : } else {
609 0 : undoChangeProperty(Pid::SYLLABIC, int(LyricsSyllabic::END));
610 : }
611 : }
612 2 : }
613 : }
|