1008 lines
37 KiB
C++
1008 lines
37 KiB
C++
//=============================================================================
|
|
// MuseScore
|
|
// Music Composition & Notation
|
|
//
|
|
// Copyright (C) 2002-2011 Werner Schweer
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License version 2
|
|
// as published by the Free Software Foundation and appearing in
|
|
// the file LICENCE.GPL
|
|
//=============================================================================
|
|
|
|
#include "lyrics.h"
|
|
|
|
#include "chord.h"
|
|
#include "score.h"
|
|
#include "sym.h"
|
|
#include "system.h"
|
|
#include "xml.h"
|
|
#include "staff.h"
|
|
#include "segment.h"
|
|
|
|
namespace Ms {
|
|
|
|
// some useful values:
|
|
static const qreal HALF = 0.5;
|
|
static const qreal TWICE = 2.0;
|
|
|
|
//---------------------------------------------------------
|
|
// searchNextLyrics
|
|
//---------------------------------------------------------
|
|
|
|
static Lyrics* searchNextLyrics(Segment* s, int staffIdx, int verse, Placement p)
|
|
{
|
|
Lyrics* l = 0;
|
|
while ((s = s->next1(SegmentType::ChordRest))) {
|
|
int strack = staffIdx * VOICES;
|
|
int etrack = strack + VOICES;
|
|
// search through all tracks of current staff looking for a lyric in specified verse
|
|
for (int track = strack; track < etrack; ++track) {
|
|
ChordRest* cr = toChordRest(s->element(track));
|
|
if (cr) {
|
|
// cr with lyrics found, but does it have a syllable in specified verse?
|
|
l = cr->lyrics(verse, p);
|
|
if (l)
|
|
break;
|
|
}
|
|
}
|
|
if (l)
|
|
break;
|
|
}
|
|
return l;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// Lyrics
|
|
//---------------------------------------------------------
|
|
|
|
Lyrics::Lyrics(Score* s)
|
|
: TextBase(s)
|
|
{
|
|
init(SubStyle::LYRIC1);
|
|
_no = 0;
|
|
_ticks = 0;
|
|
_syllabic = Syllabic::SINGLE;
|
|
_separator = 0;
|
|
placementStyle = PropertyFlags::STYLED;
|
|
setPlacement(Placement(s->styleI(StyleIdx::lyricsPlacement)));
|
|
}
|
|
|
|
Lyrics::Lyrics(const Lyrics& l)
|
|
: TextBase(l)
|
|
{
|
|
_no = l._no;
|
|
_ticks = l._ticks;
|
|
_syllabic = l._syllabic;
|
|
_separator = 0;
|
|
}
|
|
|
|
Lyrics::~Lyrics()
|
|
{
|
|
if (_separator)
|
|
remove(_separator);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// scanElements
|
|
//---------------------------------------------------------
|
|
|
|
void Lyrics::scanElements(void* data, void (*func)(void*, Element*), 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 manged.
|
|
if (_separator)
|
|
_separator->scanElements(data, func, all); */
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// write
|
|
//---------------------------------------------------------
|
|
|
|
void Lyrics::write(XmlWriter& xml) const
|
|
{
|
|
if (!xml.canWrite(this))
|
|
return;
|
|
xml.stag("Lyrics");
|
|
writeProperty(xml, P_ID::VERSE);
|
|
if (_syllabic != Syllabic::SINGLE) {
|
|
static const char* sl[] = {
|
|
"single", "begin", "end", "middle"
|
|
};
|
|
xml.tag("syllabic", sl[int(_syllabic)]);
|
|
}
|
|
writeProperty(xml, P_ID::LYRIC_TICKS);
|
|
|
|
TextBase::writeProperties(xml);
|
|
xml.etag();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// read
|
|
//---------------------------------------------------------
|
|
|
|
void Lyrics::read(XmlReader& e)
|
|
{
|
|
int iEndTick = 0; // used for backward compatibility
|
|
Text* _verseNumber = 0;
|
|
|
|
while (e.readNextStartElement()) {
|
|
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
|
|
qDebug("bad syllabic property");
|
|
}
|
|
else if (tag == "endTick") { // obsolete
|
|
// store <endTick> tag value until a <ticks> tag has been read
|
|
// which positions this lyrics element in the score
|
|
iEndTick = e.readInt();
|
|
}
|
|
else if (tag == "ticks")
|
|
_ticks = e.readInt();
|
|
else if (tag == "Number") { // obsolete
|
|
_verseNumber = new Text(score());
|
|
_verseNumber->read(e);
|
|
_verseNumber->setParent(this);
|
|
}
|
|
else if (tag == "placement") {
|
|
placementStyle = PropertyFlags::UNSTYLED;
|
|
TextBase::readProperties(e);
|
|
}
|
|
else if (!TextBase::readProperties(e))
|
|
e.unknown();
|
|
}
|
|
// if any endTick, make it relative to current tick
|
|
if (iEndTick) {
|
|
_ticks = iEndTick - e.tick();
|
|
// qDebug("Lyrics::endTick: %d ticks %d", iEndTick, _ticks);
|
|
}
|
|
if (_verseNumber) {
|
|
// TODO: add text to main text
|
|
}
|
|
|
|
delete _verseNumber;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// add
|
|
//---------------------------------------------------------
|
|
|
|
void Lyrics::add(Element* el)
|
|
{
|
|
// el->setParent(this);
|
|
// if (el->type() == ElementType::LINE)
|
|
// _separator.append((Line*)el); // ignore! Internally managed
|
|
// ;
|
|
// else
|
|
qDebug("Lyrics::add: unknown element %s", el->name());
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// remove
|
|
//---------------------------------------------------------
|
|
|
|
void Lyrics::remove(Element* el)
|
|
{
|
|
if (el->isLyricsLine()) {
|
|
// only if separator still exists and is the right one
|
|
if (_separator != nullptr && el == _separator) {
|
|
// Lyrics::remove() and LyricsLine::removeUnmanaged() call each other;
|
|
// be sure each finds a clean context
|
|
LyricsLine* separ = _separator;
|
|
_separator = 0;
|
|
separ->setParent(0);
|
|
separ->removeUnmanaged();
|
|
delete separ;
|
|
}
|
|
}
|
|
else
|
|
qDebug("Lyrics::remove: unknown element %s", el->name());
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// isMelisma
|
|
//---------------------------------------------------------
|
|
|
|
bool Lyrics::isMelisma() const
|
|
{
|
|
// entered as melisma using underscore?
|
|
if (_ticks > 0)
|
|
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();
|
|
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
|
|
//---------------------------------------------------------
|
|
|
|
void Lyrics::layout()
|
|
{
|
|
layout1();
|
|
adjustReadPos();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// layout1
|
|
//---------------------------------------------------------
|
|
|
|
#if defined (USE_FONT_DASH_METRIC)
|
|
static QString g_fontFamily = QString();
|
|
static qreal g_fontSize = -1;
|
|
static qreal g_cachedDashY;
|
|
static qreal g_cachedDashLength;
|
|
#if defined(USE_FONT_DASH_TICKNESS)
|
|
static qreal g_cachedDashThickness;
|
|
#endif
|
|
#endif
|
|
|
|
void Lyrics::layout1()
|
|
{
|
|
setPos(QPointF());
|
|
TextBase::layout1();
|
|
if (!parent()) // palette & clone trick
|
|
return;
|
|
|
|
qreal lh = lineSpacing() * score()->styleD(StyleIdx::lyricsLineHeight);
|
|
qreal y = 0;
|
|
|
|
if (placeBelow())
|
|
// y = lh * (_no+1) + score()->styleP(StyleIdx::lyricsPosBelow) + staff()->height();
|
|
y = lh * _no + score()->styleP(StyleIdx::lyricsPosBelow) + staff()->height();
|
|
else {
|
|
// we are counting _no from bottom to top for verses above
|
|
y = -lh * _no + score()->styleP(StyleIdx::lyricsPosAbove);
|
|
}
|
|
|
|
qreal x = 0.0;
|
|
|
|
//
|
|
// parse leading verse number and/or punctuation, so we can factor it into layout separately
|
|
//
|
|
bool hasNumber = false; // _verseNumber;
|
|
qreal centerAdjust = 0.0;
|
|
qreal leftAdjust = 0.0;
|
|
QString s = plainText();
|
|
|
|
// 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]+)");
|
|
|
|
if (score()->styleB(StyleIdx::lyricsAlignVerseNumber)) {
|
|
QRegularExpression punctuationPattern("(^[\\d\\W]*)([^\\d\\W].*?)([\\d\\W]*$)", QRegularExpression::UseUnicodePropertiesOption);
|
|
QRegularExpressionMatch punctuationMatch = punctuationPattern.match(s);
|
|
if (punctuationMatch.hasMatch()) {
|
|
#if 0 // TODO::ws
|
|
// leading and trailing punctuation
|
|
QString lp = punctuationMatch.captured(1);
|
|
QString tp = punctuationMatch.captured(3);
|
|
// actual lyric
|
|
//QString actualLyric = punctuationMatch.captured(2);
|
|
Text leading(*this);
|
|
leading.setPlainText(lp);
|
|
leading.layout1();
|
|
Text trailing(*this);
|
|
trailing.setPlainText(tp);
|
|
trailing.layout1();
|
|
leftAdjust = leading.width();
|
|
centerAdjust = leading.width() - trailing.width();
|
|
if (!lp.isEmpty() && lp[0].isDigit())
|
|
hasNumber = true;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
ChordRest* cr = chordRest();
|
|
Align ta = align();
|
|
if (ta & Align::HCENTER) {
|
|
//
|
|
// center under notehead, not origin
|
|
// however, lyrics that are melismas or have verse numbers will be forced to left alignment
|
|
// TODO: provide a way to disable the automatic left alignment
|
|
//
|
|
qreal nominalWidth = symWidth(SymId::noteheadBlack);
|
|
if (!isMelisma() && !hasNumber) // center under notehead
|
|
x += nominalWidth * .5 - cr->x() - centerAdjust * 0.5;
|
|
else // force left alignment
|
|
x += width() * .5 - cr->x() - leftAdjust;
|
|
}
|
|
else if (!(ta & Align::RIGHT)) {
|
|
// even for left aligned syllables, ignore leading verse numbers and/or punctuation
|
|
x -= leftAdjust;
|
|
}
|
|
|
|
rxpos() += x;
|
|
rypos() += y;
|
|
|
|
if (_ticks > 0 || _syllabic == Syllabic::BEGIN || _syllabic == Syllabic::MIDDLE) {
|
|
if (_separator == nullptr) {
|
|
_separator = new LyricsLine(score());
|
|
_separator->setTick(cr->tick());
|
|
score()->addUnmanagedSpanner(_separator);
|
|
}
|
|
_separator->setParent(this);
|
|
_separator->setTick(cr->tick());
|
|
_separator->setTrack(track());
|
|
_separator->setTrack2(track());
|
|
#if defined(USE_FONT_DASH_METRIC)
|
|
// if font parameters different from font cached values, compute new dash values from font metrics
|
|
if (textStyle().family() != g_fontFamily && textStyle().size() != g_fontSize) {
|
|
QFontMetricsF fm = textStyle().fontMetrics(spatium());
|
|
QRectF r = fm.tightBoundingRect("\u2013"); // U+2013 EN DASH
|
|
g_cachedDashY = _dashY = r.y() + (r.height() * HALF);
|
|
g_cachedDashLength = _dashLength = r.width();
|
|
#if defined(USE_FONT_DASH_TICKNESS)
|
|
g_cachedDashThickness = _dashThickness = r.height();
|
|
#endif
|
|
g_fontFamily = textStyle().family();
|
|
g_fontSize = textStyle().size();
|
|
}
|
|
// if same font, use cached values
|
|
else {
|
|
_dashY = g_cachedDashY;
|
|
_dashLength = g_cachedDashLength;
|
|
#if defined(USE_FONT_DASH_TICKNESS)
|
|
_dashThickness = g_cachedDashThickness;
|
|
#endif
|
|
}
|
|
#endif
|
|
}
|
|
else {
|
|
if (_separator) {
|
|
_separator->removeUnmanaged();
|
|
delete _separator;
|
|
_separator = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// paste
|
|
//---------------------------------------------------------
|
|
|
|
void Lyrics::paste(EditData& ed)
|
|
{
|
|
MuseScoreView* scoreview = ed.view;
|
|
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
|
|
QClipboard::Mode mode = QClipboard::Clipboard;
|
|
#else
|
|
QClipboard::Mode mode = QClipboard::Selection;
|
|
#endif
|
|
QString txt = QApplication::clipboard()->text(mode);
|
|
QString regex = QString("[^\\S") + QChar(0xa0) + QChar(0x202F) + "]+";
|
|
QStringList sl = txt.split(QRegExp(regex), QString::SkipEmptyParts);
|
|
if (sl.empty())
|
|
return;
|
|
|
|
QStringList hyph = sl[0].split("-");
|
|
bool minus = false;
|
|
bool underscore = false;
|
|
if(hyph.length() > 1) {
|
|
insertText(ed, hyph[0]);
|
|
hyph.removeFirst();
|
|
sl[0] = hyph.join("-");
|
|
minus = true;
|
|
}
|
|
else if (sl.length() > 1 && sl[1] == "-") {
|
|
insertText(ed, sl[0]);
|
|
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("_");
|
|
insertText(ed, sl[0].left(p));
|
|
sl[0] = sl[0].mid(p + 1);
|
|
if (sl[0].isEmpty())
|
|
sl.removeFirst();
|
|
underscore = true;
|
|
}
|
|
else if (sl.length() > 1 && sl[1] == "_") {
|
|
insertText(ed, sl[0]);
|
|
sl.removeFirst();
|
|
sl.removeFirst();
|
|
underscore = true;
|
|
}
|
|
else {
|
|
insertText(ed, sl[0]);
|
|
sl.removeFirst();
|
|
}
|
|
|
|
layout();
|
|
score()->setLayoutAll();
|
|
score()->update();
|
|
txt = sl.join(" ");
|
|
|
|
QApplication::clipboard()->setText(txt, mode);
|
|
if (minus)
|
|
scoreview->lyricsMinus();
|
|
else if (underscore)
|
|
scoreview->lyricsUnderscore();
|
|
else
|
|
scoreview->lyricsTab(false, false, true);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// endTick
|
|
//---------------------------------------------------------
|
|
|
|
int Lyrics::endTick() const
|
|
{
|
|
return segment()->tick() + ticks();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// acceptDrop
|
|
//---------------------------------------------------------
|
|
|
|
bool Lyrics::acceptDrop(EditData& data) const
|
|
{
|
|
return data.element->isText() || TextBase::acceptDrop(data);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// drop
|
|
//---------------------------------------------------------
|
|
|
|
Element* Lyrics::drop(EditData& data)
|
|
{
|
|
ElementType type = data.element->type();
|
|
if (type == ElementType::SYMBOL || type == ElementType::FSYMBOL) {
|
|
TextBase::drop(data);
|
|
return 0;
|
|
}
|
|
if (!data.element->isText()) {
|
|
delete data.element;
|
|
return 0;
|
|
}
|
|
Text* e = toText(data.element);
|
|
e->setParent(this);
|
|
score()->undoAddElement(e);
|
|
return e;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// setNo
|
|
//---------------------------------------------------------
|
|
|
|
void Lyrics::setNo(int n)
|
|
{
|
|
_no = n;
|
|
// adjust beween LYRICS1 and LYRICS2 only; keep other styles as they are
|
|
// (_no is 0-based, so odd _no means even line and viceversa)
|
|
if (type() == ElementType::LYRICS) {
|
|
if ((_no & 1) && subStyle() == SubStyle::LYRIC1)
|
|
initSubStyle(SubStyle::LYRIC2);
|
|
if (!(_no & 1) && subStyle() == SubStyle::LYRIC2)
|
|
initSubStyle(SubStyle::LYRIC1);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// endEdit
|
|
//---------------------------------------------------------
|
|
|
|
void Lyrics::endEdit(EditData& ed)
|
|
{
|
|
TextBase::endEdit(ed);
|
|
score()->setLayoutAll();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// removeFromScore
|
|
//---------------------------------------------------------
|
|
|
|
void Lyrics::removeFromScore()
|
|
{
|
|
if (_separator) {
|
|
_separator->removeUnmanaged();
|
|
delete _separator;
|
|
_separator = nullptr;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// getProperty
|
|
//---------------------------------------------------------
|
|
|
|
QVariant Lyrics::getProperty(P_ID propertyId) const
|
|
{
|
|
switch (propertyId) {
|
|
case P_ID::SYLLABIC:
|
|
return int(_syllabic);
|
|
case P_ID::LYRIC_TICKS:
|
|
return _ticks;
|
|
case P_ID::VERSE:
|
|
return _no;
|
|
default:
|
|
return TextBase::getProperty(propertyId);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// setProperty
|
|
//---------------------------------------------------------
|
|
|
|
bool Lyrics::setProperty(P_ID propertyId, const QVariant& v)
|
|
{
|
|
switch (propertyId) {
|
|
case P_ID::PLACEMENT:
|
|
placementStyle = PropertyFlags::UNSTYLED;
|
|
setPlacement(Placement(v.toInt()));
|
|
break;
|
|
case P_ID::SYLLABIC:
|
|
_syllabic = Syllabic(v.toInt());
|
|
break;
|
|
case P_ID::LYRIC_TICKS:
|
|
_ticks = v.toInt();
|
|
break;
|
|
case P_ID::VERSE:
|
|
_no = v.toInt();
|
|
break;
|
|
default:
|
|
if (!TextBase::setProperty(propertyId, v))
|
|
return false;
|
|
break;
|
|
}
|
|
score()->setLayout(tick());
|
|
return true;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// propertyDefault
|
|
//---------------------------------------------------------
|
|
|
|
QVariant Lyrics::propertyDefault(P_ID id) const
|
|
{
|
|
switch (id) {
|
|
case P_ID::SUB_STYLE:
|
|
return int(SubStyle::LYRIC1);
|
|
case P_ID::PLACEMENT:
|
|
return score()->styleI(StyleIdx::lyricsPlacement);
|
|
case P_ID::SYLLABIC:
|
|
return int(Syllabic::SINGLE);
|
|
case P_ID::LYRIC_TICKS:
|
|
case P_ID::VERSE:
|
|
return 0;
|
|
default: return TextBase::propertyDefault(id);
|
|
}
|
|
}
|
|
|
|
//=========================================================
|
|
// LyricsLine
|
|
//=========================================================
|
|
|
|
LyricsLine::LyricsLine(Score* s)
|
|
: SLine(s)
|
|
{
|
|
setFlags(0);
|
|
|
|
setGenerated(true); // no need to save it, as it can be re-generated
|
|
setDiagonal(false);
|
|
setLineWidth(Spatium(Lyrics::LYRICS_DASH_DEFAULT_LINE_THICKNESS));
|
|
setAnchor(Spanner::Anchor::SEGMENT);
|
|
_nextLyrics = 0;
|
|
}
|
|
|
|
LyricsLine::LyricsLine(const LyricsLine& g)
|
|
: SLine(g)
|
|
{
|
|
_nextLyrics = 0;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// layout
|
|
//---------------------------------------------------------
|
|
|
|
void LyricsLine::layout()
|
|
{
|
|
bool tempMelismaTicks = (lyrics()->ticks() == Lyrics::TEMP_MELISMA_TICKS);
|
|
if (lyrics()->ticks()) { // melisma
|
|
setLineWidth(score()->styleS(StyleIdx::lyricsLineThickness));
|
|
// if lyrics has a temporary one-chord melisma, set to 0 ticks (just its own chord)
|
|
if (tempMelismaTicks)
|
|
lyrics()->setTicks(0);
|
|
// Lyrics::_ticks points to the beginning of the last spanned segment,
|
|
// but the line shall include it:
|
|
// include the duration of this last segment in the melisma duration
|
|
Segment* lyricsSegment = lyrics()->segment();
|
|
int lyricsStartTick = lyricsSegment->tick();
|
|
int lyricsEndTick = lyrics()->endTick();
|
|
int lyricsTrack = lyrics()->track();
|
|
// find segment with tick >= endTick
|
|
Segment* s = lyricsSegment;
|
|
while (s && s->tick() < lyricsEndTick)
|
|
s = s->nextCR(lyricsTrack, true);
|
|
if (!s) {
|
|
// user probably deleted measures at end of score, leaving this melisma too long
|
|
// set s to last segment and reset lyricsEndTick to trigger FIXUP code below
|
|
s = score()->lastSegment();
|
|
lyricsEndTick = -1;
|
|
}
|
|
Element* se = s->element(lyricsTrack);
|
|
// everything is OK if we have reached a chord at right tick on right track
|
|
if (s->tick() == lyricsEndTick && se && se->type() == ElementType::CHORD) {
|
|
// advance to next CR, or last segment if no next CR
|
|
s = s->nextCR(lyricsTrack, true);
|
|
if (!s)
|
|
s = score()->lastSegment();
|
|
}
|
|
else {
|
|
// FIXUP - lyrics tick count not valid
|
|
// this happens if edits to score have removed the original end segment
|
|
// so let's fix it here
|
|
// s is already pointing to segment past endTick (or to last segment)
|
|
// we should shorten the lyrics tick count to make this work
|
|
Segment* ns = s;
|
|
Segment* ps = s->prev1(SegmentType::ChordRest);
|
|
while (ps && ps != lyricsSegment) {
|
|
Element* pe = ps->element(lyricsTrack);
|
|
// we're looking for an actual chord on this track
|
|
if (pe && pe->type() == ElementType::CHORD)
|
|
break;
|
|
s = ps;
|
|
ps = ps->prev1(SegmentType::ChordRest);
|
|
}
|
|
if (!ps || ps == lyricsSegment) {
|
|
// no valid previous CR, so try to lengthen melisma instead
|
|
ps = ns;
|
|
s = ps->nextCR(lyricsTrack, true);
|
|
Element* e = s ? s->element(lyricsTrack) : nullptr;
|
|
// check to make sure we have a chord
|
|
if (!e || e->type() != ElementType::CHORD) {
|
|
// nothing to do but set ticks to 0
|
|
// this will result in melisma being deleted later
|
|
lyrics()->undoChangeProperty(P_ID::LYRIC_TICKS, 0);
|
|
setTicks(0);
|
|
return;
|
|
}
|
|
}
|
|
lyrics()->undoChangeProperty(P_ID::LYRIC_TICKS, ps->tick() - lyricsStartTick);
|
|
}
|
|
setTicks(s->tick() - lyricsStartTick);
|
|
}
|
|
else { // dash(es)
|
|
#if defined(USE_FONT_DASH_TICKNESS)
|
|
setLineWidth(Spatium(lyrics()->dashThickness() / spatium()));
|
|
#endif
|
|
_nextLyrics = searchNextLyrics(lyrics()->segment(), staffIdx(), lyrics()->no(), lyrics()->placement());
|
|
setTick2(_nextLyrics ? _nextLyrics->segment()->tick() : tick());
|
|
}
|
|
if (ticks()) { // only do layout if some time span
|
|
// do layout with non-0 duration
|
|
if (tempMelismaTicks)
|
|
lyrics()->setTicks(Lyrics::TEMP_MELISMA_TICKS);
|
|
SLine::layout();
|
|
// if temp melisma and there is a first line segment,
|
|
// extend it to be after the lyrics syllable (otherwise
|
|
// the melisma segment will be often covered by the syllable itself)
|
|
if (tempMelismaTicks && segments.size() > 0)
|
|
segmentAt(0)->rxpos2() += lyrics()->width();
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// createLineSegment
|
|
//---------------------------------------------------------
|
|
|
|
LineSegment* LyricsLine::createLineSegment()
|
|
{
|
|
LyricsLineSegment* seg = new LyricsLineSegment(score());
|
|
seg->setTrack(track());
|
|
seg->setColor(color());
|
|
return seg;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// removeUnmanaged
|
|
// same as Spanner::removeUnmanaged(), but in addition, remove from hosting Lyrics
|
|
//---------------------------------------------------------
|
|
|
|
void LyricsLine::removeUnmanaged()
|
|
{
|
|
Spanner::removeUnmanaged();
|
|
if (lyrics())
|
|
lyrics()->remove(this);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// setProperty
|
|
//---------------------------------------------------------
|
|
|
|
bool LyricsLine::setProperty(P_ID propertyId, const QVariant& v)
|
|
{
|
|
switch (propertyId) {
|
|
case P_ID::SPANNER_TICKS:
|
|
{
|
|
// if parent lyrics has a melisma, change its length too
|
|
if (parent() && parent()->type() == ElementType::LYRICS
|
|
&& toLyrics(parent())->ticks() > 0) {
|
|
int newTicks = toLyrics(parent())->ticks() + v.toInt() - ticks();
|
|
parent()->undoChangeProperty(P_ID::LYRIC_TICKS, newTicks);
|
|
}
|
|
setTicks(v.toInt());
|
|
}
|
|
break;
|
|
default:
|
|
if (!SLine::setProperty(propertyId, v))
|
|
return false;
|
|
break;
|
|
}
|
|
score()->setLayoutAll();
|
|
return true;
|
|
}
|
|
|
|
//=========================================================
|
|
// LyricsLineSegment
|
|
//=========================================================
|
|
|
|
LyricsLineSegment::LyricsLineSegment(Score* s)
|
|
: LineSegment(s)
|
|
{
|
|
setFlags(ElementFlag::SEGMENT | ElementFlag::ON_STAFF);
|
|
clearFlags(ElementFlag::SELECTABLE | ElementFlag::MOVABLE);
|
|
setGenerated(true);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// layout
|
|
//---------------------------------------------------------
|
|
|
|
void LyricsLineSegment::layout()
|
|
{
|
|
bool endOfSystem = false;
|
|
bool isEndMelisma = lyricsLine()->lyrics()->ticks() > 0;
|
|
Lyrics* lyr = 0;
|
|
Lyrics* nextLyr = 0;
|
|
qreal fromX = 0;
|
|
qreal toX = 0; // start and end point of intra-lyrics room
|
|
qreal sp = spatium();
|
|
System* sys;
|
|
|
|
if (lyricsLine()->ticks() <= 0) { // if no span,
|
|
_numOfDashes = 0; // nothing to draw
|
|
return; // and do nothing
|
|
}
|
|
|
|
// HORIZONTAL POSITION
|
|
// A) if line precedes a syllable, advance line end to right before the next syllable text
|
|
// if not a melisma and there is a next syllable;
|
|
if (!isEndMelisma && lyricsLine()->nextLyrics() != nullptr
|
|
&& (spannerSegmentType() == SpannerSegmentType::END
|
|
|| spannerSegmentType() == SpannerSegmentType::SINGLE)) {
|
|
lyr = nextLyr = lyricsLine()->nextLyrics();
|
|
sys = lyr->segment()->system();
|
|
endOfSystem = (sys != system());
|
|
// if next lyrics is on a different sytem, this line segment is at the end of its system:
|
|
// do not adjust for next lyrics position
|
|
if (!endOfSystem) {
|
|
qreal lyrX = lyr->bbox().x();
|
|
qreal lyrXp = lyr->pagePos().x();
|
|
qreal sysXp = sys->pagePos().x();
|
|
toX = lyrXp - sysXp + lyrX; // syst.rel. X pos.
|
|
qreal offsetX = toX - pos().x() - pos2().x() - Lyrics::LYRICS_DASH_DEFAULT_PAD * sp;
|
|
// delta from current end pos.| ending padding
|
|
rxpos2() += offsetX;
|
|
}
|
|
}
|
|
// B) if line follows a syllable, advance line start to after the syllable text
|
|
lyr = lyricsLine()->lyrics();
|
|
sys = lyr->segment()->system();
|
|
if (sys && (spannerSegmentType() == SpannerSegmentType::BEGIN || spannerSegmentType() == SpannerSegmentType::SINGLE)) {
|
|
qreal lyrX = lyr->bbox().x();
|
|
qreal lyrXp = lyr->pagePos().x();
|
|
qreal lyrW = lyr->bbox().width();
|
|
qreal sysXp = sys->pagePos().x();
|
|
fromX = lyrXp - sysXp + lyrX + lyrW;
|
|
// syst.rel. X pos. | lyr.advance
|
|
qreal offsetX = fromX - pos().x() + (isEndMelisma ? Lyrics::MELISMA_DEFAULT_PAD : Lyrics::LYRICS_DASH_DEFAULT_PAD) * sp;
|
|
// delta from curr.pos. | add initial padding
|
|
rxpos() += offsetX;
|
|
rxpos2() -= offsetX;
|
|
}
|
|
|
|
// VERTICAL POSITION: at the base line of the syllable text
|
|
if (spannerSegmentType() != SpannerSegmentType::END)
|
|
rypos() = lyr->y();
|
|
else {
|
|
// use Y position of *next* syllable if there is one on same system
|
|
Lyrics* nextLyr = searchNextLyrics(lyr->segment(), lyr->staffIdx(), lyr->no(), lyr->placement());
|
|
if (nextLyr && nextLyr->segment()->system() == system())
|
|
rypos() = nextLyr->y();
|
|
else
|
|
rypos() = lyr->y();
|
|
}
|
|
|
|
// MELISMA vs. DASHES
|
|
if (isEndMelisma) { // melisma
|
|
_numOfDashes = 1;
|
|
rypos() -= lyricsLine()->lineWidth().val() * sp * HALF; // let the line 'sit on' the base line
|
|
qreal offsetX = score()->styleP(StyleIdx::minNoteDistance) * mag();
|
|
// if final segment, extend slightly after the chord, otherwise shorten it
|
|
rxpos2() +=
|
|
(spannerSegmentType() == SpannerSegmentType::BEGIN ||
|
|
spannerSegmentType() == SpannerSegmentType::MIDDLE)
|
|
? -offsetX : +offsetX;
|
|
|
|
}
|
|
else { // dash(es)
|
|
#if defined(USE_FONT_DASH_METRIC)
|
|
rypos() += lyr->dashY();
|
|
_dashLength = lyr->dashLength();
|
|
#else
|
|
// set conventional dash Y pos
|
|
rypos() -= MScore::pixelRatio * lyr->fontMetrics().xHeight() * Lyrics::LYRICS_DASH_Y_POS_RATIO;
|
|
_dashLength = score()->styleP(StyleIdx::lyricsDashMaxLength) * mag(); // and dash length
|
|
#endif
|
|
qreal len = pos2().x();
|
|
qreal minDashLen = score()->styleS(StyleIdx::lyricsDashMinLength).val() * sp;
|
|
qreal maxDashDist = score()->styleS(StyleIdx::lyricsDashMaxDistance).val() * sp;
|
|
if (len < minDashLen) { // if no room for a dash
|
|
// if at end of system or dash is forced
|
|
if (endOfSystem || score()->styleB(StyleIdx::lyricsDashForce)) {
|
|
rxpos2() = minDashLen; // draw minimal dash
|
|
_numOfDashes = 1;
|
|
_dashLength = minDashLen;
|
|
}
|
|
else // if within system or dash not forced
|
|
_numOfDashes = 0; // draw no dash
|
|
}
|
|
else if (len < (maxDashDist * TWICE)) { // if no room for two dashes
|
|
_numOfDashes = 1; // draw one dash
|
|
if (_dashLength > len) // if no room for a full dash
|
|
_dashLength = len; // shorten it
|
|
}
|
|
else
|
|
_numOfDashes = len / (maxDashDist); // draw several dashes
|
|
|
|
// adjust next lyrics horiz. position if too little a space forced to skip the dash
|
|
if (_numOfDashes == 0 && nextLyr != nullptr && len > 0)
|
|
nextLyr->rxpos() -= (toX - fromX);
|
|
}
|
|
|
|
// set bounding box
|
|
QRectF r = QRectF(0.0, 0.0, pos2().x(), pos2().y()).normalized();
|
|
qreal lw = spatium() * lyricsLine()->lineWidth().val() * HALF;
|
|
setbbox(r.adjusted(-lw, -lw, lw, lw));
|
|
adjustReadPos();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// draw
|
|
//---------------------------------------------------------
|
|
|
|
void LyricsLineSegment::draw(QPainter* painter) const
|
|
{
|
|
if (_numOfDashes < 1) // nothing to draw
|
|
return;
|
|
qreal _spatium = spatium();
|
|
|
|
QPen pen(lyricsLine()->lyrics()->curColor());
|
|
pen.setWidthF(lyricsLine()->lineWidth().val() * _spatium);
|
|
pen.setCapStyle(Qt::FlatCap);
|
|
painter->setPen(pen);
|
|
if (lyricsLine()->lyrics()->ticks() > 0) // melisma
|
|
painter->drawLine(QPointF(), pos2());
|
|
else { // dash(es)
|
|
qreal step = pos2().x() / (_numOfDashes+1);
|
|
qreal x = step - _dashLength * HALF;
|
|
for (int i = 0; i < _numOfDashes; i++, x += step)
|
|
painter->drawLine(QPointF(x, 0.0), QPointF(x + _dashLength, 0.0));
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// propertyStyle
|
|
//---------------------------------------------------------
|
|
|
|
PropertyFlags& Lyrics::propertyFlags(P_ID id)
|
|
{
|
|
switch (id) {
|
|
case P_ID::PLACEMENT:
|
|
return ScoreElement::propertyFlags(id); // return PropertyFlags::NOSTYLE;
|
|
|
|
default:
|
|
return TextBase::propertyFlags(id);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// getPropertyStyle
|
|
//---------------------------------------------------------
|
|
|
|
StyleIdx Lyrics::getPropertyStyle(P_ID id) const
|
|
{
|
|
switch (id) {
|
|
case P_ID::PLACEMENT:
|
|
return StyleIdx::lyricsPlacement;
|
|
case P_ID::FONT_FACE:
|
|
return isEven() ? StyleIdx::lyricsEvenFontFace : StyleIdx::lyricsOddFontFace;
|
|
case P_ID::FONT_SIZE:
|
|
return isEven() ? StyleIdx::lyricsEvenFontSize : StyleIdx::lyricsOddFontSize;
|
|
case P_ID::FONT_BOLD:
|
|
return isEven() ? StyleIdx::lyricsEvenFontBold : StyleIdx::lyricsOddFontBold;
|
|
case P_ID::FONT_ITALIC:
|
|
return isEven() ? StyleIdx::lyricsEvenFontItalic : StyleIdx::lyricsOddFontItalic;
|
|
case P_ID::FONT_UNDERLINE:
|
|
return isEven() ? StyleIdx::lyricsEvenFontUnderline : StyleIdx::lyricsOddFontUnderline;
|
|
case P_ID::ALIGN:
|
|
return isEven() ? StyleIdx::lyricsEvenAlign : StyleIdx::lyricsOddAlign;
|
|
default:
|
|
break;
|
|
}
|
|
return StyleIdx::NOSTYLE;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// styleChanged
|
|
// reset all styled values to actual style
|
|
//---------------------------------------------------------
|
|
|
|
void Lyrics::styleChanged()
|
|
{
|
|
if (placementStyle == PropertyFlags::STYLED)
|
|
setPlacement(Placement(score()->styleI(StyleIdx::lyricsPlacement)));
|
|
TextBase::styleChanged();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// reset
|
|
//---------------------------------------------------------
|
|
|
|
void Lyrics::reset()
|
|
{
|
|
undoResetProperty(P_ID::PLACEMENT);
|
|
TextBase::reset();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// resetProperty
|
|
//---------------------------------------------------------
|
|
|
|
void Lyrics::resetProperty(P_ID id)
|
|
{
|
|
switch (id) {
|
|
case P_ID::PLACEMENT:
|
|
setProperty(id, propertyDefault(id));
|
|
placementStyle = PropertyFlags::STYLED;
|
|
break;
|
|
|
|
default:
|
|
return TextBase::resetProperty(id);
|
|
}
|
|
}
|
|
|
|
}
|
|
|