474 lines
20 KiB
C++
474 lines
20 KiB
C++
//=============================================================================
|
|
// MuseScore
|
|
// Music Composition & Notation
|
|
//
|
|
// Copyright (C) 2002-2018 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"
|
|
#include "undo.h"
|
|
#include "textedit.h"
|
|
#include "measure.h"
|
|
|
|
namespace Ms {
|
|
|
|
//---------------------------------------------------------
|
|
// 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;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// LyricsLine
|
|
//---------------------------------------------------------
|
|
|
|
LyricsLine::LyricsLine(Score* s)
|
|
: SLine(s, ElementFlag::NOT_SELECTABLE)
|
|
{
|
|
setGenerated(true); // no need to save it, as it can be re-generated
|
|
setDiagonal(false);
|
|
setLineWidth(score()->styleP(Sid::lyricsDashLineThickness));
|
|
setAnchor(Spanner::Anchor::SEGMENT);
|
|
_nextLyrics = 0;
|
|
}
|
|
|
|
LyricsLine::LyricsLine(const LyricsLine& g)
|
|
: SLine(g)
|
|
{
|
|
_nextLyrics = 0;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// styleChanged
|
|
//---------------------------------------------------------
|
|
|
|
void LyricsLine::styleChanged()
|
|
{
|
|
setLineWidth(score()->styleP(Sid::lyricsDashLineThickness));
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// layout
|
|
//---------------------------------------------------------
|
|
|
|
void LyricsLine::layout()
|
|
{
|
|
bool tempMelismaTicks = (lyrics()->ticks() == Fraction::fromTicks(Lyrics::TEMP_MELISMA_TICKS));
|
|
if (isEndMelisma()) { // melisma
|
|
setLineWidth(score()->styleP(Sid::lyricsLineThickness));
|
|
// if lyrics has a temporary one-chord melisma, set to 0 ticks (just its own chord)
|
|
if (tempMelismaTicks)
|
|
lyrics()->setTicks(Fraction(0,1));
|
|
|
|
// 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();
|
|
Fraction lyricsStartTick = lyricsSegment->tick();
|
|
Fraction 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 = Fraction(-1,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(Pid::LYRIC_TICKS, 0);
|
|
setTicks(Fraction(0,1));
|
|
return;
|
|
}
|
|
}
|
|
lyrics()->undoChangeProperty(Pid::LYRIC_TICKS, ps->tick() - lyricsStartTick);
|
|
}
|
|
// Spanner::computeEndElement() will actually ignore this value and use the (earlier) lyrics()->endTick() instead
|
|
// still, for consistency with other lines, we should set the ticks for this to the computed (later) value
|
|
setTicks(s->tick() - lyricsStartTick);
|
|
}
|
|
else { // dash(es)
|
|
_nextLyrics = searchNextLyrics(lyrics()->segment(), staffIdx(), lyrics()->no(), lyrics()->placement());
|
|
setTick2(_nextLyrics ? _nextLyrics->segment()->tick() : tick());
|
|
}
|
|
if (ticks().isNotZero()) { // only do layout if some time span
|
|
// do layout with non-0 duration
|
|
if (tempMelismaTicks)
|
|
lyrics()->setTicks(Fraction::fromTicks(Lyrics::TEMP_MELISMA_TICKS));
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// layoutSystem
|
|
//---------------------------------------------------------
|
|
|
|
SpannerSegment* LyricsLine::layoutSystem(System* system)
|
|
{
|
|
Fraction stick = system->firstMeasure()->tick();
|
|
Fraction etick = system->lastMeasure()->endTick();
|
|
|
|
LyricsLineSegment* lineSegm = toLyricsLineSegment(getNextLayoutSystemSegment(system, [this]() { return createLineSegment(); }));
|
|
|
|
SpannerSegmentType sst;
|
|
if (tick() >= stick) {
|
|
layout();
|
|
if (ticks().isZero()) // only do layout if some time span
|
|
return nullptr;
|
|
SLine::layout();
|
|
//
|
|
// this is the first call to layoutSystem,
|
|
// processing the first line segment
|
|
//
|
|
computeStartElement();
|
|
computeEndElement();
|
|
sst = tick2() <= etick ? SpannerSegmentType::SINGLE : SpannerSegmentType::BEGIN;
|
|
}
|
|
else if (tick() < stick && tick2() > etick) {
|
|
sst = SpannerSegmentType::MIDDLE;
|
|
}
|
|
else {
|
|
//
|
|
// this is the last call to layoutSystem
|
|
// processing the last line segment
|
|
//
|
|
sst = SpannerSegmentType::END;
|
|
}
|
|
lineSegm->setSpannerSegmentType(sst);
|
|
|
|
switch (sst) {
|
|
case SpannerSegmentType::SINGLE: {
|
|
System* s;
|
|
QPointF p1 = linePos(Grip::START, &s);
|
|
QPointF p2 = linePos(Grip::END, &s);
|
|
qreal len = p2.x() - p1.x();
|
|
lineSegm->setPos(p1);
|
|
lineSegm->setPos2(QPointF(len, p2.y() - p1.y()));
|
|
}
|
|
break;
|
|
case SpannerSegmentType::BEGIN: {
|
|
System* s;
|
|
QPointF p1 = linePos(Grip::START, &s);
|
|
lineSegm->setPos(p1);
|
|
qreal x2 = system->bbox().right();
|
|
lineSegm->setPos2(QPointF(x2 - p1.x(), 0.0));
|
|
}
|
|
break;
|
|
case SpannerSegmentType::MIDDLE: {
|
|
Measure* firstMeasure = system->firstMeasure();
|
|
Segment* firstCRSeg = firstMeasure->first(SegmentType::ChordRest);
|
|
qreal x1 = (firstCRSeg ? firstCRSeg->pos().x() : 0) + firstMeasure->pos().x();
|
|
qreal x2 = system->bbox().right();
|
|
System* s;
|
|
QPointF p1 = linePos(Grip::START, &s);
|
|
lineSegm->setPos(QPointF(x1, p1.y()));
|
|
lineSegm->setPos2(QPointF(x2 - x1, 0.0));
|
|
}
|
|
break;
|
|
case SpannerSegmentType::END: {
|
|
qreal offset = 0.0;
|
|
System* s;
|
|
QPointF p2 = linePos(Grip::END, &s);
|
|
Measure* firstMeas = system->firstMeasure();
|
|
Segment* firstCRSeg = firstMeas->first(SegmentType::ChordRest);
|
|
if (anchor() == Anchor::SEGMENT || anchor() == Anchor::MEASURE) {
|
|
// start line just after previous element (eg, key signature)
|
|
firstCRSeg = firstCRSeg->prev();
|
|
Element* e = firstCRSeg ? firstCRSeg->element(staffIdx() * VOICES) : nullptr;
|
|
if (e)
|
|
offset = e->width();
|
|
}
|
|
qreal x1 = (firstCRSeg ? firstCRSeg->pos().x() : 0) + firstMeas->pos().x() + offset;
|
|
qreal len = p2.x() - x1;
|
|
lineSegm->setPos(QPointF(p2.x() - len, p2.y()));
|
|
lineSegm->setPos2(QPointF(len, 0.0));
|
|
}
|
|
break;
|
|
}
|
|
lineSegm->layout();
|
|
// if temp melisma extend the first line segment to be
|
|
// after the lyrics syllable (otherwise the melisma segment
|
|
// will be too short).
|
|
const bool tempMelismaTicks = (lyrics()->ticks() == Fraction::fromTicks(Lyrics::TEMP_MELISMA_TICKS));
|
|
if (tempMelismaTicks && spannerSegments().size() > 0 && spannerSegments().front() == lineSegm)
|
|
lineSegm->rxpos2() += lyrics()->width();
|
|
// avoid backwards melisma
|
|
if (lineSegm->pos2().x() < 0)
|
|
lineSegm->rxpos2() = 0;
|
|
return lineSegm;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// createLineSegment
|
|
//---------------------------------------------------------
|
|
|
|
LineSegment* LyricsLine::createLineSegment()
|
|
{
|
|
LyricsLineSegment* seg = new LyricsLineSegment(this, 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(Pid propertyId, const QVariant& v)
|
|
{
|
|
switch (propertyId) {
|
|
case Pid::SPANNER_TICKS:
|
|
{
|
|
// if parent lyrics has a melisma, change its length too
|
|
if (parent() && parent()->type() == ElementType::LYRICS
|
|
&& isEndMelisma()) {
|
|
Fraction newTicks = toLyrics(parent())->ticks() + v.value<Fraction>() - ticks();
|
|
parent()->undoChangeProperty(Pid::LYRIC_TICKS, newTicks);
|
|
}
|
|
setTicks(v.value<Fraction>());
|
|
}
|
|
break;
|
|
default:
|
|
if (!SLine::setProperty(propertyId, v))
|
|
return false;
|
|
break;
|
|
}
|
|
score()->setLayoutAll();
|
|
return true;
|
|
}
|
|
|
|
//=========================================================
|
|
// LyricsLineSegment
|
|
//=========================================================
|
|
|
|
LyricsLineSegment::LyricsLineSegment(Spanner* sp, Score* s)
|
|
: LineSegment(sp, s, ElementFlag::ON_STAFF | ElementFlag::NOT_SELECTABLE)
|
|
{
|
|
setGenerated(true);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// layout
|
|
//---------------------------------------------------------
|
|
|
|
void LyricsLineSegment::layout()
|
|
{
|
|
ryoffset() = 0.0;
|
|
|
|
bool endOfSystem = false;
|
|
bool isEndMelisma = lyricsLine()->isEndMelisma();
|
|
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() <= Fraction(0,1)) { // 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() && isSingleEndType()) {
|
|
lyr = nextLyr = lyricsLine()->nextLyrics();
|
|
sys = lyr->segment()->system();
|
|
endOfSystem = (sys != system());
|
|
// if next lyrics is on a different system, this line segment is at the end of its system:
|
|
// do not adjust for next lyrics position
|
|
if (sys && !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() - score()->styleP(Sid::lyricsDashPad);
|
|
// 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 && isSingleBeginType()) {
|
|
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();
|
|
offsetX += score()->styleP(isEndMelisma ? Sid::lyricsMelismaPad : Sid::lyricsDashPad);
|
|
|
|
// delta from curr.pos. | add initial padding
|
|
rxpos() += offsetX;
|
|
rxpos2() -= offsetX;
|
|
}
|
|
|
|
// VERTICAL POSITION: at the base line of the syllable text
|
|
if (!isEndType()) {
|
|
rypos() = lyr->ipos().y();
|
|
ryoffset() = lyr->offset().y();
|
|
}
|
|
else {
|
|
// use Y position of *next* syllable if there is one on same system
|
|
Lyrics* nextLyr1 = searchNextLyrics(lyr->segment(), lyr->staffIdx(), lyr->no(), lyr->placement());
|
|
if (nextLyr1 && nextLyr1->segment()->system() == system()) {
|
|
rypos() = nextLyr1->ipos().y();
|
|
ryoffset() = nextLyr1->offset().y();
|
|
}
|
|
else {
|
|
rypos() = lyr->ipos().y();
|
|
ryoffset() = lyr->offset().y();
|
|
}
|
|
}
|
|
|
|
// MELISMA vs. DASHES
|
|
if (isEndMelisma) { // melisma
|
|
_numOfDashes = 1;
|
|
rypos() -= lyricsLine()->lineWidth() * .5; // let the line 'sit on' the base line
|
|
qreal offsetX = score()->styleP(Sid::minNoteDistance) * mag();
|
|
// if final segment, extend slightly after the chord, otherwise shorten it
|
|
rxpos2() += (isBeginType() || isEndType()) ? -offsetX : +offsetX;
|
|
}
|
|
else { // dash(es)
|
|
// set conventional dash Y pos
|
|
rypos() -= MScore::pixelRatio * lyr->fontMetrics().xHeight() * score()->styleD(Sid::lyricsDashYposRatio);
|
|
_dashLength = score()->styleP(Sid::lyricsDashMaxLength) * mag(); // and dash length
|
|
qreal len = pos2().x();
|
|
qreal minDashLen = score()->styleS(Sid::lyricsDashMinLength).val() * sp;
|
|
qreal maxDashDist = score()->styleS(Sid::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(Sid::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 * 1.5)) { // 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 + 1; // 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 = lyricsLine()->lineWidth() * .5;
|
|
setbbox(r.adjusted(-lw, -lw, lw, lw));
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// draw
|
|
//---------------------------------------------------------
|
|
|
|
void LyricsLineSegment::draw(QPainter* painter) const
|
|
{
|
|
if (_numOfDashes < 1) // nothing to draw
|
|
return;
|
|
|
|
QPen pen(lyricsLine()->lyrics()->curColor());
|
|
pen.setWidthF(lyricsLine()->lineWidth());
|
|
pen.setCapStyle(Qt::FlatCap);
|
|
painter->setPen(pen);
|
|
if (lyricsLine()->isEndMelisma()) // melisma
|
|
painter->drawLine(QPointF(), pos2());
|
|
else { // dash(es)
|
|
qreal step = pos2().x() / _numOfDashes;
|
|
qreal x = step * .5 - _dashLength * .5;
|
|
for (int i = 0; i < _numOfDashes; i++, x += step)
|
|
painter->drawLine(QPointF(x, 0.0), QPointF(x + _dashLength, 0.0));
|
|
}
|
|
}
|
|
|
|
}
|
|
|