MuseScore/src/engraving/libmscore/lyrics.cpp

737 lines
22 KiB
C++

/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2021 MuseScore BVBA and others
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "lyrics.h"
#include <QRegularExpression>
#include "rw/xml.h"
#include "chord.h"
#include "masterscore.h"
#include "measure.h"
#include "mscoreview.h"
#include "score.h"
#include "segment.h"
#include "staff.h"
#include "system.h"
#include "textedit.h"
#include "undo.h"
#include "log.h"
using namespace mu;
using namespace mu::engraving;
namespace Ms {
//---------------------------------------------------------
// lyricsElementStyle
//---------------------------------------------------------
static const ElementStyle lyricsElementStyle {
{ Sid::lyricsPlacement, Pid::PLACEMENT },
};
//---------------------------------------------------------
// Lyrics
//---------------------------------------------------------
Lyrics::Lyrics(ChordRest* parent)
: TextBase(ElementType::LYRICS, parent, TextStyleType::LYRICS_ODD)
{
_even = false;
initElementStyle(&lyricsElementStyle);
_no = 0;
_ticks = Fraction(0, 1);
_syllabic = Syllabic::SINGLE;
_separator = 0;
}
Lyrics::Lyrics(const Lyrics& l)
: TextBase(l)
{
_even = l._even;
_no = l._no;
_ticks = l._ticks;
_syllabic = l._syllabic;
_separator = 0;
}
Lyrics::~Lyrics()
{
if (_separator) {
remove(_separator);
}
}
//---------------------------------------------------------
// write
//---------------------------------------------------------
void Lyrics::write(XmlWriter& xml) const
{
if (!xml.context()->canWrite(this)) {
return;
}
xml.startObject(this);
writeProperty(xml, Pid::VERSE);
if (_syllabic != Syllabic::SINGLE) {
static const char* sl[] = {
"single", "begin", "end", "middle"
};
xml.tag("syllabic", sl[int(_syllabic)]);
}
xml.tag("ticks", _ticks.ticks(), 0); // pre-3.1 compatibility: write integer ticks under <ticks> tag
writeProperty(xml, Pid::LYRIC_TICKS);
TextBase::writeProperties(xml);
xml.endObject();
}
//---------------------------------------------------------
// read
//---------------------------------------------------------
void Lyrics::read(XmlReader& e)
{
while (e.readNextStartElement()) {
if (!readProperties(e)) {
e.unknown();
}
}
if (!isStyled(Pid::OFFSET) && !e.context()->pasteMode()) {
// fix offset for pre-3.1 scores
// 3.0: y offset was meaningless if autoplace is set
QString version = mscoreVersion();
if (autoplace() && !version.isEmpty() && version < "3.1") {
PointF off = propertyDefault(Pid::OFFSET).value<PointF>();
ryoffset() = off.y();
}
}
}
//---------------------------------------------------------
// readProperties
//---------------------------------------------------------
bool Lyrics::readProperties(XmlReader& e)
{
const QStringRef& tag(e.name());
if (tag == "no") {
_no = e.readInt();
} else if (tag == "syllabic") {
QString val(e.readElementText());
if (val == "single") {
_syllabic = Syllabic::SINGLE;
} else if (val == "begin") {
_syllabic = Syllabic::BEGIN;
} else if (val == "end") {
_syllabic = Syllabic::END;
} else if (val == "middle") {
_syllabic = Syllabic::MIDDLE;
} else {
LOGD("bad syllabic property");
}
} else if (tag == "ticks") { // obsolete
_ticks = e.readFraction(); // will fall back to reading integer ticks on older scores
} else if (tag == "ticks_f") {
_ticks = e.readFraction();
} else if (readProperty(tag, e, Pid::PLACEMENT)) {
} else if (!TextBase::readProperties(e)) {
return false;
}
return true;
}
//---------------------------------------------------------
// add
//---------------------------------------------------------
void Lyrics::add(EngravingItem* el)
{
// el->setParent(this);
// if (el->type() == ElementType::LINE)
// _separator.append((Line*)el); // ignore! Internally managed
// ;
// else
LOGD("Lyrics::add: unknown element %s", el->typeName());
}
//---------------------------------------------------------
// remove
//---------------------------------------------------------
void Lyrics::remove(EngravingItem* el)
{
if (el->isLyricsLine()) {
// only if separator still exists and is the right one
if (_separator && el == _separator) {
// Lyrics::remove() and LyricsLine::removeUnmanaged() call each other;
// be sure each finds a clean context
LyricsLine* separ = _separator;
_separator = 0;
separ->resetExplicitParent();
separ->removeUnmanaged();
}
} else {
LOGD("Lyrics::remove: unknown element %s", el->typeName());
}
}
//---------------------------------------------------------
// isMelisma
//---------------------------------------------------------
bool Lyrics::isMelisma() const
{
// entered as melisma using underscore?
if (_ticks > Fraction(0, 1)) {
return true;
}
// hyphenated?
// if so, it is a melisma only if there is no lyric in same verse on next CR
if (_syllabic == Syllabic::BEGIN || _syllabic == Syllabic::MIDDLE) {
// find next CR on same track and check for existence of lyric in same verse
ChordRest* cr = chordRest();
if (cr) {
Segment* s = cr->segment()->next1();
ChordRest* ncr = s ? s->nextChordRest(cr->track()) : 0;
if (ncr && !ncr->lyrics(_no, placement())) {
return true;
}
}
}
// default - not a melisma
return false;
}
//---------------------------------------------------------
// layout
// - does not touch vertical position
//---------------------------------------------------------
void Lyrics::layout()
{
if (!explicitParent()) { // palette & clone trick
setPos(PointF());
TextBase::layout1();
return;
}
//
// parse leading verse number and/or punctuation, so we can factor it into layout separately
//
bool hasNumber = false; // _verseNumber;
// find:
// 1) string of numbers and non-word characters at start of syllable
// 2) at least one other character (indicating start of actual lyric)
// 3) string of non-word characters at end of syllable
//QRegularExpression leadingPattern("(^[\\d\\W]+)([^\\d\\W]+)");
const QString text = plainText();
QString leading;
QString trailing;
if (score()->styleB(Sid::lyricsAlignVerseNumber)) {
QRegularExpression punctuationPattern("(^[\\d\\W]*)([^\\d\\W].*?)([\\d\\W]*$)", QRegularExpression::UseUnicodePropertiesOption);
QRegularExpressionMatch punctuationMatch = punctuationPattern.match(text);
if (punctuationMatch.hasMatch()) {
// leading and trailing punctuation
leading = punctuationMatch.captured(1);
trailing = punctuationMatch.captured(3);
//QString actualLyric = punctuationMatch.captured(2);
if (!leading.isEmpty() && leading[0].isDigit()) {
hasNumber = true;
}
}
}
bool styleDidChange = false;
if (isEven() && !_even) {
initTextStyleType(TextStyleType::LYRICS_EVEN, /* preserveDifferent */ true);
_even = true;
styleDidChange = true;
}
if (!isEven() && _even) {
initTextStyleType(TextStyleType::LYRICS_ODD, /* preserveDifferent */ true);
_even = false;
styleDidChange = true;
}
if (styleDidChange) {
styleChanged();
}
if (isMelisma() || hasNumber) {
// use the melisma style alignment setting
if (isStyled(Pid::ALIGN)) {
setAlign(score()->styleV(Sid::lyricsMelismaAlign).value<Align>());
}
} else {
// use the text style alignment setting
if (isStyled(Pid::ALIGN)) {
setAlign(propertyDefault(Pid::ALIGN).value<Align>());
}
}
PointF o(propertyDefault(Pid::OFFSET).value<PointF>());
rxpos() = o.x();
qreal x = pos().x();
TextBase::layout1();
qreal centerAdjust = 0.0;
qreal leftAdjust = 0.0;
if (score()->styleB(Sid::lyricsAlignVerseNumber)) {
// Calculate leading and trailing parts widths. Lyrics
// should have text layout to be able to do it correctly.
Q_ASSERT(rows() != 0);
if (!leading.isEmpty() || !trailing.isEmpty()) {
// LOGD("create leading, trailing <%s> -- <%s><%s>", qPrintable(text), qPrintable(leading), qPrintable(trailing));
const TextBlock& tb = textBlock(0);
const qreal leadingWidth = tb.xpos(leading.length(), this) - tb.boundingRect().x();
const int trailingPos = text.length() - trailing.length();
const qreal trailingWidth = tb.boundingRect().right() - tb.xpos(trailingPos, this);
leftAdjust = leadingWidth;
centerAdjust = leadingWidth - trailingWidth;
}
}
ChordRest* cr = chordRest();
if (align() == AlignH::HCENTER) {
//
// center under notehead, not origin
// however, lyrics that are melismas or have verse numbers will be forced to left alignment
//
// center under note head
qreal nominalWidth = symWidth(SymId::noteheadBlack);
x += nominalWidth * .5 - cr->x() - centerAdjust * 0.5;
} else if (!(align() == AlignH::RIGHT)) {
// even for left aligned syllables, ignore leading verse numbers and/or punctuation
x -= leftAdjust;
}
rxpos() = x;
if (_ticks > Fraction(0, 1) || _syllabic == Syllabic::BEGIN || _syllabic == Syllabic::MIDDLE) {
if (!_separator) {
_separator = new LyricsLine(score()->dummy());
_separator->setTick(cr->tick());
score()->addUnmanagedSpanner(_separator);
}
_separator->setParent(this);
_separator->setTick(cr->tick());
// HACK separator should have non-zero length to get its layout
// always triggered. A proper ticks length will be set later on the
// separator layout.
_separator->setTicks(Fraction::fromTicks(1));
_separator->setTrack(track());
_separator->setTrack2(track());
_separator->setVisible(visible());
// bbox().setWidth(bbox().width()); // ??
} else {
if (_separator) {
_separator->removeUnmanaged();
delete _separator;
_separator = 0;
}
}
if (_ticks.isNotZero()) {
// set melisma end
ChordRest* ecr = score()->findCR(endTick(), track());
if (ecr) {
ecr->setMelismaEnd(true);
}
}
}
//---------------------------------------------------------
// scanElements
//---------------------------------------------------------
void Lyrics::scanElements(void* data, void (* func)(void*, EngravingItem*), bool /*all*/)
{
func(data, this);
/* DO NOT ADD EITHER THE LYRICSLINE OR THE SEGMENTS: segments are added through the system each belongs to;
LyricsLine is not needed, as it is internally managed.
if (_separator)
_separator->scanElements(data, func, all); */
}
//---------------------------------------------------------
// layout2
// compute vertical position
//---------------------------------------------------------
void Lyrics::layout2(int nAbove)
{
qreal lh = lineSpacing() * score()->styleD(Sid::lyricsLineHeight);
if (placeBelow()) {
qreal yo = segment()->measure()->system()->staff(staffIdx())->bbox().height();
rypos() = lh * (_no - nAbove) + yo - chordRest()->y();
rpos() += styleValue(Pid::OFFSET, Sid::lyricsPosBelow).value<PointF>();
} else {
rypos() = -lh * (nAbove - _no - 1) - chordRest()->y();
rpos() += styleValue(Pid::OFFSET, Sid::lyricsPosAbove).value<PointF>();
}
}
//---------------------------------------------------------
// paste
//---------------------------------------------------------
void Lyrics::paste(EditData& ed, const QString& txt)
{
MuseScoreView* scoreview = ed.view();
QString regex = QString("[^\\S") + QChar(0xa0) + QChar(0x202F) + "]+";
QStringList sl = txt.split(QRegularExpression(regex), Qt::SkipEmptyParts);
if (sl.empty()) {
return;
}
QStringList hyph = sl[0].split("-");
bool minus = false;
bool underscore = false;
score()->startCmd();
if (hyph.length() > 1) {
score()->undo(new InsertText(cursorFromEditData(ed), hyph[0]), &ed);
hyph.removeFirst();
sl[0] = hyph.join("-");
minus = true;
} else if (sl.length() > 1 && sl[1] == "-") {
score()->undo(new InsertText(cursorFromEditData(ed), sl[0]), &ed);
sl.removeFirst();
sl.removeFirst();
minus = true;
} else if (sl[0].startsWith("_")) {
sl[0].remove(0, 1);
if (sl[0].isEmpty()) {
sl.removeFirst();
}
underscore = true;
} else if (sl[0].contains("_")) {
int p = sl[0].indexOf("_");
score()->undo(new InsertText(cursorFromEditData(ed), sl[0]), &ed);
sl[0] = sl[0].mid(p + 1);
if (sl[0].isEmpty()) {
sl.removeFirst();
}
underscore = true;
} else if (sl.length() > 1 && sl[1] == "_") {
score()->undo(new InsertText(cursorFromEditData(ed), sl[0]), &ed);
sl.removeFirst();
sl.removeFirst();
underscore = true;
} else {
score()->undo(new InsertText(cursorFromEditData(ed), sl[0]), &ed);
sl.removeFirst();
}
score()->endCmd();
if (minus) {
scoreview->lyricsMinus();
} else if (underscore) {
scoreview->lyricsUnderscore();
} else {
scoreview->lyricsTab(false, false, true);
}
}
//---------------------------------------------------------
// endTick
//---------------------------------------------------------
Fraction Lyrics::endTick() const
{
return segment()->tick() + ticks();
}
//---------------------------------------------------------
// acceptDrop
//---------------------------------------------------------
bool Lyrics::acceptDrop(EditData& data) const
{
return data.dropElement->isText() || TextBase::acceptDrop(data);
}
//---------------------------------------------------------
// drop
//---------------------------------------------------------
EngravingItem* Lyrics::drop(EditData& data)
{
ElementType type = data.dropElement->type();
if (type == ElementType::SYMBOL || type == ElementType::FSYMBOL) {
TextBase::drop(data);
return 0;
}
if (!data.dropElement->isText()) {
delete data.dropElement;
data.dropElement = 0;
return 0;
}
Text* e = toText(data.dropElement);
e->setParent(this);
score()->undoAddElement(e);
return e;
}
bool Lyrics::isEditAllowed(EditData& ed) const
{
if (isTextNavigationKey(ed.key, ed.modifiers)) {
return false;
}
static const std::set<Qt::KeyboardModifiers> navigationModifiers {
Qt::NoModifier,
Qt::KeypadModifier,
Qt::ShiftModifier
};
if (navigationModifiers.find(ed.modifiers) != navigationModifiers.end()) {
static const std::set<int> navigationKeys {
Qt::Key_Underscore,
Qt::Key_Minus,
Qt::Key_Enter,
Qt::Key_Return,
Qt::Key_Up,
Qt::Key_Down
};
if (navigationKeys.find(ed.key) != navigationKeys.end()) {
return false;
}
}
if (ed.key == Qt::Key_Left) {
return cursor()->column() != 0 || cursor()->hasSelection();
}
if (ed.key == Qt::Key_Right) {
bool cursorInLastColumn = cursor()->column() == cursor()->curLine().columns();
return !cursorInLastColumn || cursor()->hasSelection();
}
return TextBase::isEditAllowed(ed);
}
//---------------------------------------------------------
// edit
//---------------------------------------------------------
bool Lyrics::edit(EditData& ed)
{
if (!isEditAllowed(ed)) {
return false;
}
return TextBase::edit(ed);
}
//---------------------------------------------------------
// endEdit
//---------------------------------------------------------
void Lyrics::endEdit(EditData& ed)
{
TextBase::endEdit(ed);
triggerLayoutAll();
}
//---------------------------------------------------------
// removeFromScore
//---------------------------------------------------------
void Lyrics::removeFromScore()
{
if (_ticks.isNotZero()) {
// clear melismaEnd flag from end cr
ChordRest* ecr = score()->findCR(endTick(), track());
if (ecr) {
ecr->setMelismaEnd(false);
}
}
if (_separator) {
_separator->removeUnmanaged();
delete _separator;
_separator = 0;
}
}
//---------------------------------------------------------
// getProperty
//---------------------------------------------------------
PropertyValue Lyrics::getProperty(Pid propertyId) const
{
switch (propertyId) {
case Pid::SYLLABIC:
return int(_syllabic);
case Pid::LYRIC_TICKS:
return _ticks;
case Pid::VERSE:
return _no;
default:
return TextBase::getProperty(propertyId);
}
}
//---------------------------------------------------------
// setProperty
//---------------------------------------------------------
bool Lyrics::setProperty(Pid propertyId, const PropertyValue& v)
{
switch (propertyId) {
case Pid::PLACEMENT:
setPlacement(v.value<PlacementV>());
break;
case Pid::SYLLABIC:
_syllabic = Syllabic(v.toInt());
break;
case Pid::LYRIC_TICKS:
if (_ticks.isNotZero()) {
// clear melismaEnd flag from previous end cr
// this might be premature, as there may be other melismas ending there
// but flag will be generated correctly on layout
// TODO: after inserting a measure,
// endTick info is wrong.
// Somehow we need to fix this.
// See https://musescore.org/en/node/285304 and https://musescore.org/en/node/311289
ChordRest* ecr = score()->findCR(endTick(), track());
if (ecr) {
ecr->setMelismaEnd(false);
}
}
_ticks = v.value<Fraction>();
break;
case Pid::VERSE:
_no = v.toInt();
break;
default:
if (!TextBase::setProperty(propertyId, v)) {
return false;
}
break;
}
triggerLayout();
return true;
}
//---------------------------------------------------------
// propertyDefault
//---------------------------------------------------------
PropertyValue Lyrics::propertyDefault(Pid id) const
{
switch (id) {
case Pid::TEXT_STYLE:
return isEven() ? TextStyleType::LYRICS_EVEN : TextStyleType::LYRICS_ODD;
case Pid::PLACEMENT:
return score()->styleV(Sid::lyricsPlacement);
case Pid::SYLLABIC:
return int(Syllabic::SINGLE);
case Pid::LYRIC_TICKS:
return Fraction(0, 1);
case Pid::VERSE:
return 0;
case Pid::ALIGN:
if (isMelisma()) {
return score()->styleV(Sid::lyricsMelismaAlign);
}
// fall through
default:
return TextBase::propertyDefault(id);
}
}
//---------------------------------------------------------
// forAllLyrics
//---------------------------------------------------------
void Score::forAllLyrics(std::function<void(Lyrics*)> f)
{
for (Segment* s = firstSegment(SegmentType::ChordRest); s; s = s->next1(SegmentType::ChordRest)) {
for (EngravingItem* e : s->elist()) {
if (e) {
for (Lyrics* l : toChordRest(e)->lyrics()) {
f(l);
}
}
}
}
}
//---------------------------------------------------------
// undoChangeProperty
//---------------------------------------------------------
void Lyrics::undoChangeProperty(Pid id, const PropertyValue& v, PropertyFlags ps)
{
if (id == Pid::VERSE && no() != v.toInt()) {
for (Lyrics* l : chordRest()->lyrics()) {
if (l->no() == v.toInt()) {
// verse already exists, swap
l->TextBase::undoChangeProperty(id, no(), ps);
PlacementV p = l->placement();
l->TextBase::undoChangeProperty(Pid::PLACEMENT, int(placement()), ps);
TextBase::undoChangeProperty(Pid::PLACEMENT, int(p), ps);
break;
}
}
TextBase::undoChangeProperty(id, v, ps);
return;
} else if (id == Pid::AUTOPLACE && v.toBool() != autoplace()) {
if (v.toBool()) {
// setting autoplace
// reset offset
undoResetProperty(Pid::OFFSET);
} else {
// unsetting autoplace
// rebase offset
PointF off = offset();
qreal y = pos().y() - propertyDefault(Pid::OFFSET).value<PointF>().y();
off.ry() = placeAbove() ? y : y - staff()->height();
undoChangeProperty(Pid::OFFSET, off, PropertyFlags::UNSTYLED);
}
TextBase::undoChangeProperty(id, v, ps);
return;
}
TextBase::undoChangeProperty(id, v, ps);
}
}