MuseScore/libmscore/figuredbass.cpp
Dmitri Ovodok 7d02bef86b Fix figured bass editing
This fixes mainly two problems:
1) Input box was too high compared to the resulting figured bass
   position. Now it is approximately at the same Y position.
2) Editing of existing figured bass worked incorrectly and could
   produce crashes (for example, on trying to add new lines to
   existing figured bass item).
2018-11-28 17:08:07 +02:00

1800 lines
70 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//=============================================================================
// 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 "figuredbass.h"
#include "score.h"
#include "note.h"
#include "measure.h"
#include "system.h"
#include "segment.h"
#include "chord.h"
#include "rest.h"
#include "score.h"
#include "sym.h"
#include "xml.h"
// trying to do without it
//#include <QQmlEngine>
namespace Ms {
//---------------------------------------------------------
// figuredBassTextStyle
//---------------------------------------------------------
static const std::vector<StyledProperty> figuredBassTextStyle {
{ Sid::figuredBassFontFace, Pid::FONT_FACE },
{ Sid::figuredBassFontSize, Pid::FONT_SIZE },
{ Sid::figuredBassFontStyle, Pid::FONT_STYLE },
};
static constexpr qreal FB_CONTLINE_HEIGHT = 0.875; // the % of font EM to raise the cont. line at
// (0 = top of font; 1 = bottom of font)
static constexpr qreal FB_CONTLINE_LEFT_PADDING = 0.1875; // (3/16sp) the blank space at the left of a cont. line (in sp)
static constexpr qreal FB_CONTLINE_OVERLAP = 0.125; // (1/8sp) the overlap of an extended cont. line (in sp)
static constexpr qreal FB_CONTLINE_THICKNESS = 0.09375; // (3/32sp) the thickness of a cont. line (in sp)
// the array of configured fonts
static QList<FiguredBassFont> g_FBFonts;
//---------------------------------------------------------
// F I G U R E D B A S S I T E M
//---------------------------------------------------------
// used for indexed access to parenthesis chars
// (these is no normAccidToChar[], as accidentals may use mult. chars in normalized display):
const QChar FiguredBassItem::normParenthToChar[int(FiguredBassItem::Parenthesis::NUMOF)] =
{ 0, '(', ')', '[', ']'};
FiguredBassItem::FiguredBassItem(Score* s, int l)
: Element(s), ord(l)
{
_prefix = _suffix = Modifier::NONE;
_digit = FBIDigitNone;
parenth[0] = parenth[1] = parenth[2] = parenth[3] = parenth[4] = Parenthesis::NONE;
_contLine = ContLine::NONE;
}
FiguredBassItem::FiguredBassItem(const FiguredBassItem& item)
: Element(item)
{
ord = item.ord;
_prefix = item._prefix;
_digit = item._digit;
_suffix = item._suffix;
parenth[0] = item.parenth[0];
parenth[1] = item.parenth[1];
parenth[2] = item.parenth[2];
parenth[3] = item.parenth[3];
parenth[4] = item.parenth[4];
_contLine = item._contLine;
textWidth = item.textWidth;
_displayText= item._displayText;
}
FiguredBassItem::~FiguredBassItem()
{
}
//---------------------------------------------------------
// FiguredBassItem parse()
//
// converts a string into a property-based representation, if possible;
// return true on success | false if the string is non-conformant
//---------------------------------------------------------
bool FiguredBassItem::parse(QString& str)
{
int retVal;
parseParenthesis(str, 0);
retVal = parsePrefixSuffix(str, true); // prefix
if(retVal == -1)
return false;
parseParenthesis(str, 1);
retVal = parseDigit(str); // digit
if(retVal == -1)
return false;
parseParenthesis(str, 2);
retVal = parsePrefixSuffix(str, false); // suffix
if(retVal == -1)
return false;
parseParenthesis(str, 3);
// check for a possible cont. line symbol(s)
_contLine = ContLine::NONE; // contLine
if(str[0] == '-' || str[0] == '_') { // 1 symbol: simple continuation
_contLine = ContLine::SIMPLE;
str.remove(0, 1);
}
while(str[0] == '-' || str[0] == '_') { // more than 1 symbol: extended continuation
_contLine = ContLine::EXTENDED;
str.remove(0, 1);
}
parseParenthesis(str, 4);
// remove useless parentheses, moving external parentheses toward central digit element
if(_prefix == Modifier::NONE && parenth[1] == Parenthesis::NONE) {
parenth[1] = parenth[0];
parenth[0] = Parenthesis::NONE;
}
if(_digit == FBIDigitNone && parenth[2] == Parenthesis::NONE) {
parenth[2] = parenth[1];
parenth[1] = Parenthesis::NONE;
}
if(_contLine == ContLine::NONE && parenth[3] == Parenthesis::NONE) {
parenth[3] = parenth[4];
parenth[4] = Parenthesis::NONE;
}
if(_suffix == Modifier::NONE && parenth[2] == Parenthesis::NONE) {
parenth[2] = parenth[3];
parenth[3] = Parenthesis::NONE;
}
// some checks:
// if some extra input, str is not conformant
if(str.size())
return false;
// can't have BOTH prefix and suffix
// prefix, digit, suffix and cont.line cannot be ALL empty
// suffix cannot combine with empty digit
if( (_prefix != Modifier::NONE && _suffix != Modifier::NONE)
|| (_prefix == Modifier::NONE && _digit == FBIDigitNone && _suffix == Modifier::NONE && _contLine == ContLine::NONE)
|| ( (_suffix == Modifier::CROSS || _suffix == Modifier::BACKSLASH || _suffix == Modifier::SLASH)
&& _digit == FBIDigitNone) )
return false;
return true;
}
//---------------------------------------------------------
// FiguredBassItem parsePrefixSuffix()
//
// scans str to extract prefix or suffix properties. Stops at the first char which cannot fit.
// Fitting chars are removed from str. DOES NOT generate any display text
//
// returns the number of QChar's read from str or -1 if prefix / suffix has an illegal format
// (no prefix / suffix at all IS legal)
//---------------------------------------------------------
int FiguredBassItem::parsePrefixSuffix(QString& str, bool bPrefix)
{
Modifier * dest = bPrefix ? &_prefix : &_suffix;
bool done = false;
int size = str.size();
str = str.trimmed();
*dest = Modifier::NONE;
while(str.size()) {
switch(str.at(0).unicode())
{
case 'b':
if(*dest != Modifier::NONE) {
if(*dest == Modifier::FLAT) // FLAT may double a previous FLAT
*dest = Modifier::DOUBLEFLAT;
else
return -1; // but no other combination is acceptable
}
else
*dest = Modifier::FLAT;
break;
case 'h':
if(*dest != Modifier::NONE) // cannot combine with any other accidental
return -1;
*dest = Modifier::NATURAL;
break;
case '#':
if(*dest != Modifier::NONE) {
if(*dest == Modifier::SHARP) // SHARP may double a preivous SHARP
*dest = Modifier::DOUBLESHARP;
else
return -1; // but no other combination is acceptable
}
else
*dest = Modifier::SHARP;
break;
case '+':
// accept '+' as both a prefix and a suffix for harmony notation
if(*dest != Modifier::NONE) // cannot combine with any other accidental
return -1;
*dest = Modifier::CROSS;
break;
// '\\' and '/' go into the suffix
case '\\':
if(_suffix != Modifier::NONE) // cannot combine with any other accidental
return -1;
_suffix = Modifier::BACKSLASH;
break;
case '/':
if(_suffix != Modifier::NONE) // cannot combine with any other accidental
return -1;
_suffix = Modifier::SLASH;
break;
default: // any other char: no longer in prefix/suffix
done = true;
break;
}
if(done)
break;
str.remove(0,1); // 'eat' the char and continue
}
return size - str.size(); // return how many chars we had read into prefix/suffix
}
//---------------------------------------------------------
// FiguredBassItem parseDigit()
//
// scans str to extract digit properties. Stops at the first char which cannot belong to digit part.
// Fitting chars are removed from str. DOES NOT generate any display text
//
// returns the number of QChar's read from str or -1 if no legal digit can be constructed
// (no digit at all IS legal)
//---------------------------------------------------------
int FiguredBassItem::parseDigit(QString& str)
{
int size = str.size();
str = str.trimmed();
_digit = FBIDigitNone;
while(str.size()) {
// any digit acceptable
if(str[0] >= '0' && str[0] <= '9') {
if (_digit == FBIDigitNone)
_digit = 0;
_digit = _digit*10 + (str[0].unicode() - '0');
str.remove(0, 1);
}
// anything else: no longer in digit part
else
break;
}
return size - str.size();
}
//---------------------------------------------------------
// FiguredBassItem parseParenthesis()
//
// scans str to extract a (possible) parenthesis, stores its code into parenth[parenthIdx]
// and removes it from str. Only looks at first str char.
//
// returns the number of QChar's read from str (actually 0 or 1).
//---------------------------------------------------------
int FiguredBassItem::parseParenthesis(QString& str, int parenthIdx)
{
int c = str[0].unicode();
Parenthesis code = Parenthesis::NONE;
switch(c)
{
case '(':
code = Parenthesis::ROUNDOPEN;
break;
case ')':
code = Parenthesis::ROUNDCLOSED;
break;
case '[':
code =Parenthesis::SQUAREDOPEN;
break;
case ']':
code = Parenthesis::SQUAREDCLOSED;
break;
default:
break;
}
parenth[parenthIdx] = code;
if(code != Parenthesis::NONE) {
str.remove(0, 1);
return 1;
}
return 0;
}
//---------------------------------------------------------
// FiguredBassItem normalizedText()
//
// returns a string with the normalized text, i.e. the text displayed while editing;
// this is a standard textual representation of the item properties
//---------------------------------------------------------
QString FiguredBassItem::normalizedText() const
{
QString str = QString();
if(parenth[0] != Parenthesis::NONE)
str.append(normParenthToChar[int(parenth[0])]);
if(_prefix != Modifier::NONE) {
switch(_prefix)
{
case Modifier::FLAT:
str.append('b');
break;
case Modifier::NATURAL:
str.append('h');
break;
case Modifier::SHARP:
str.append('#');
break;
case Modifier::CROSS:
str.append('+');
break;
case Modifier::DOUBLEFLAT:
str.append("bb");
break;
case Modifier::DOUBLESHARP:
str.append("##");
break;
default:
break;
}
}
if(parenth[1] != Parenthesis::NONE)
str.append(normParenthToChar[int(parenth[1])]);
// digit
if(_digit != FBIDigitNone)
str.append(QString::number(_digit));
if(parenth[2] != Parenthesis::NONE)
str.append(normParenthToChar[int(parenth[2])]);
// suffix
if(_suffix != Modifier::NONE) {
switch(_suffix)
{
case Modifier::FLAT:
str.append('b');
break;
case Modifier::NATURAL:
str.append('h');
break;
case Modifier::SHARP:
str.append('#');
break;
case Modifier::CROSS:
str.append('+');
break;
case Modifier::BACKSLASH:
str.append('\\');
break;
case Modifier::SLASH:
str.append('/');
break;
case Modifier::DOUBLEFLAT:
str.append("bb");
break;
case Modifier::DOUBLESHARP:
str.append("##");
break;
default:
break;
}
}
if(parenth[3] != Parenthesis::NONE)
str.append(normParenthToChar[int(parenth[3])]);
if(_contLine > ContLine::NONE) {
str.append('_');
if (_contLine > ContLine::SIMPLE)
str.append('_');
}
if(parenth[4] != Parenthesis::NONE)
str.append(normParenthToChar[int(parenth[4])]);
return str;
}
//---------------------------------------------------------
// FiguredBassItem write()
//---------------------------------------------------------
void FiguredBassItem::write(XmlWriter& xml) const
{
xml.stag("FiguredBassItem", this);
xml.tagE(QString("brackets b0=\"%1\" b1=\"%2\" b2=\"%3\" b3=\"%4\" b4=\"%5\"")
.arg(int(parenth[0])) .arg(int(parenth[1])) .arg(int(parenth[2])) .arg(int(parenth[3])) .arg(int(parenth[4])) );
if (_prefix != Modifier::NONE)
xml.tag(QString("prefix"), int(_prefix));
if (_digit != FBIDigitNone)
xml.tag(QString("digit"), _digit);
if (_suffix != Modifier::NONE)
xml.tag(QString("suffix"), int(_suffix));
if (_contLine != ContLine::NONE)
xml.tag("continuationLine", int(_contLine));
xml.etag();
}
//---------------------------------------------------------
// FiguredBassItem read()
//---------------------------------------------------------
void FiguredBassItem::read(XmlReader& e)
{
while (e.readNextStartElement()) {
const QStringRef& tag(e.name());
if (tag == "brackets") {
parenth[0] = (Parenthesis)e.intAttribute("b0");
parenth[1] = (Parenthesis)e.intAttribute("b1");
parenth[2] = (Parenthesis)e.intAttribute("b2");
parenth[3] = (Parenthesis)e.intAttribute("b3");
parenth[4] = (Parenthesis)e.intAttribute("b4");
e.readNext();
}
else if (tag == "prefix")
_prefix = (Modifier)(e.readInt());
else if (tag == "digit")
_digit = e.readInt();
else if (tag == "suffix")
_suffix = (Modifier)(e.readInt());
else if(tag == "continuationLine")
_contLine = (ContLine)(e.readInt());
else if (!Element::readProperties(e))
e.unknown();
}
}
//---------------------------------------------------------
// FiguredBassItem layout()
// creates the display text (set as element text) and computes
// the horiz. offset needed to align the right part as well as the vert. offset
//---------------------------------------------------------
void FiguredBassItem::layout()
{
qreal h, w, x, x1, x2, y;
// construct font metrics
int fontIdx = 0;
QFont f(g_FBFonts.at(fontIdx).family);
// font size in pixels, scaled according to spatium()
// (use the same font selection as used in draw() below)
qreal m = score()->styleD(Sid::figuredBassFontSize) * spatium() / SPATIUM20;
f.setPointSizeF(m);
QFontMetricsF fm(f, MScore::paintDevice());
QString str;
x = symWidth(SymId::noteheadBlack) * .5;
x1 = x2 = 0.0;
// create display text
int font = 0;
int style = score()->styleI(Sid::figuredBassStyle);
if (parenth[0] != Parenthesis::NONE)
str.append(g_FBFonts.at(font).displayParenthesis[int(parenth[0])]);
// prefix
if (_prefix != Modifier::NONE) {
// if no digit, the string created so far 'hangs' to the left of the note
if(_digit == FBIDigitNone)
x1 = fm.width(str);
str.append(g_FBFonts.at(font).displayAccidental[int(_prefix)]);
// if no digit, the string from here onward 'hangs' to the right of the note
if(_digit == FBIDigitNone)
x2 = fm.width(str);
}
if(parenth[1] != Parenthesis::NONE)
str.append(g_FBFonts.at(font).displayParenthesis[int(parenth[1])]);
// digit
if(_digit != FBIDigitNone) {
// if some digit, the string created so far 'hangs' to the left of the note
x1 = fm.width(str);
// if suffix is a combining shape, combine it with digit (multi-digit numbers cannot be combined)
// unless there is a parenthesis in between
if( (_digit < 10)
&& (_suffix == Modifier::CROSS || _suffix == Modifier::BACKSLASH || _suffix == Modifier::SLASH)
&& parenth[2] == Parenthesis::NONE)
str.append(g_FBFonts.at(font).displayDigit[style][_digit][int(_suffix)-(int(Modifier::CROSS)-1)]);
// if several digits or no shape combination, convert _digit to font styled chars
else {
QString digits = QString();
int digit = _digit;
while (true) {
digits.prepend(g_FBFonts.at(font).displayDigit[style][(digit % 10)][0]);
digit /= 10;
if (digit == 0)
break;
}
str.append(digits);
}
// if some digit, the string from here onward 'hangs' to the right of the note
x2 = fm.width(str);
}
if(parenth[2] != Parenthesis::NONE)
str.append(g_FBFonts.at(font).displayParenthesis[int(parenth[2])]);
// suffix
// append only if non-combining shape or cannot combine (no digit or parenthesis in between)
if( _suffix != Modifier::NONE
&& ( (_suffix != Modifier::CROSS && _suffix != Modifier::BACKSLASH && _suffix != Modifier::SLASH)
|| _digit == FBIDigitNone
|| parenth[2] != Parenthesis::NONE) )
str.append(g_FBFonts.at(font).displayAccidental[int(_suffix)]);
if(parenth[3] != Parenthesis::NONE)
str.append(g_FBFonts.at(font).displayParenthesis[int(parenth[3])]);
setDisplayText(str); // this text will be displayed
if (str.size()) // if some text
x = x - (x1+x2) * 0.5; // position the text so that [x1<-->x2] is centered below the note
else // if no text (but possibly a line)
x = 0; // start at note left margin
// vertical position
h = fm.lineSpacing();
h *= score()->styleD(Sid::figuredBassLineHeight);
if (score()->styleI(Sid::figuredBassAlignment) == 0) // top alignment: stack down from first item
y = h * ord;
else // bottom alignment: stack up from last item
y = -h * (figuredBass()->numOfItems() - ord);
setPos(x, y);
// determine bbox from text width
// w = fm.width(str);
w = fm.boundingRect(str).width();
textWidth = w;
// if there is a cont.line, extend width to cover the whole FB element duration line
int lineLen;
if(_contLine != ContLine::NONE && (lineLen=figuredBass()->lineLength(0)) > w)
w = lineLen;
bbox().setRect(0, 0, w, h);
}
//---------------------------------------------------------
// FiguredBassItem draw()
//---------------------------------------------------------
void FiguredBassItem::draw(QPainter* painter) const
{
int font = 0;
qreal _spatium = spatium();
// set font from general style
QFont f(g_FBFonts.at(font).family);
#ifdef USE_GLYPHS
f.setHintingPreference(QFont::PreferVerticalHinting);
#endif
// (use the same font selection as used in layout() above)
qreal m = score()->styleD(Sid::figuredBassFontSize) * spatium() / SPATIUM20;
f.setPointSizeF(m * MScore::pixelRatio);
painter->setFont(f);
painter->setBrush(Qt::NoBrush);
QPen pen(figuredBass()->curColor(), FB_CONTLINE_THICKNESS * _spatium, Qt::SolidLine, Qt::RoundCap);
painter->setPen(pen);
painter->drawText(bbox(), Qt::TextDontClip | Qt::AlignLeft | Qt::AlignTop, displayText());
// continuation line
qreal lineEndX = 0.0;
if (_contLine != ContLine::NONE) {
qreal lineStartX = textWidth; // by default, line starts right after text
if (lineStartX > 0.0)
lineStartX += _spatium * FB_CONTLINE_LEFT_PADDING; // if some text, give some room after it
lineEndX = figuredBass()->printedLineLength(); // by default, line ends with item duration
if (lineEndX - lineStartX < 1.0) // if line length < 1 sp, ignore it
lineEndX = 0.0;
// if extended cont.line and no closing parenthesis: look at next FB element
if (_contLine > ContLine::SIMPLE && parenth[4] == Parenthesis::NONE) {
FiguredBass * nextFB;
// if there is a contiguous FB element
if ( (nextFB=figuredBass()->nextFiguredBass()) != 0) {
// retrieve the X position (in page coords) of a possible cont. line of nextFB
// on the same line of 'this'
QPointF pgPos = pagePos();
qreal nextContPageX = nextFB->additionalContLineX(pgPos.y());
// if an additional cont. line has been found, extend up to its initial X coord
if (nextContPageX > 0)
lineEndX = nextContPageX - pgPos.x() + _spatium*FB_CONTLINE_OVERLAP;
// with a little bit of overlap
else
lineEndX = figuredBass()->lineLength(0); // if none found, draw to the duration end
}
}
// if some line, draw it
if (lineEndX > 0.0) {
qreal h = bbox().height() * FB_CONTLINE_HEIGHT;
painter->drawLine(lineStartX, h, lineEndX - ipos().x(), h);
}
}
// closing cont.line parenthesis
if (parenth[4] != Parenthesis::NONE) {
int x = lineEndX > 0.0 ? lineEndX : textWidth;
painter->drawText(QRectF(x, 0, bbox().width(), bbox().height()), Qt::AlignLeft | Qt::AlignTop,
g_FBFonts.at(font).displayParenthesis[int(parenth[4])]);
}
}
//---------------------------------------------------------
// PROPERTY METHODS
//---------------------------------------------------------
QVariant FiguredBassItem::getProperty(Pid propertyId) const
{
switch(propertyId) {
case Pid::FBPREFIX:
return int(_prefix);
case Pid::FBDIGIT:
return _digit;
case Pid::FBSUFFIX:
return int(_suffix);
case Pid::FBCONTINUATIONLINE:
return int(_contLine);
case Pid::FBPARENTHESIS1:
return int(parenth[0]);
case Pid::FBPARENTHESIS2:
return int(parenth[1]);
case Pid::FBPARENTHESIS3:
return int(parenth[2]);
case Pid::FBPARENTHESIS4:
return int(parenth[3]);
case Pid::FBPARENTHESIS5:
return int(parenth[4]);
default:
return Element::getProperty(propertyId);
}
}
bool FiguredBassItem::setProperty(Pid propertyId, const QVariant& v)
{
score()->addRefresh(canvasBoundingRect());
int val = v.toInt();
switch(propertyId) {
case Pid::FBPREFIX:
if(val < int(Modifier::NONE) || val >= int(Modifier::NUMOF))
return false;
_prefix = (Modifier)val;
break;
case Pid::FBDIGIT:
if(val < 1 || val > 9)
return false;
_digit = val;
break;
case Pid::FBSUFFIX:
if(val < int(Modifier::NONE) || val >= int(Modifier::NUMOF))
return false;
_suffix = (Modifier)val;
break;
case Pid::FBCONTINUATIONLINE:
_contLine = (ContLine)val;
break;
case Pid::FBPARENTHESIS1:
if(val < int(Parenthesis::NONE) || val >= int(Parenthesis::NUMOF))
return false;
parenth[0] = (Parenthesis)val;
break;
case Pid::FBPARENTHESIS2:
if(val < int(Parenthesis::NONE) || val >= int(Parenthesis::NUMOF))
return false;
parenth[1] = (Parenthesis)val;
break;
case Pid::FBPARENTHESIS3:
if(val < int(Parenthesis::NONE) || val >= int(Parenthesis::NUMOF))
return false;
parenth[2] = (Parenthesis)val;
break;
case Pid::FBPARENTHESIS4:
if(val < int(Parenthesis::NONE) || val >= int(Parenthesis::NUMOF))
return false;
parenth[3] = (Parenthesis)val;
break;
case Pid::FBPARENTHESIS5:
if(val < int(Parenthesis::NONE) || val >= int(Parenthesis::NUMOF))
return false;
parenth[4] = (Parenthesis)val;
break;
default:
return Element::setProperty(propertyId, v);
}
score()->setLayoutAll();
return true;
}
QVariant FiguredBassItem::propertyDefault(Pid id) const
{
switch(id) {
case Pid::FBPREFIX:
case Pid::FBSUFFIX:
return int(Modifier::NONE);
case Pid::FBDIGIT:
return FBIDigitNone;
case Pid::FBCONTINUATIONLINE:
return false;
default:
return Element::propertyDefault(id);
}
}
//---------------------------------------------------------
// UNDOABLE PROPERTY SETTERS
//---------------------------------------------------------
void FiguredBassItem::undoSetPrefix(Modifier pref)
{
if(pref <= Modifier::CROSS) {
undoChangeProperty(Pid::FBPREFIX, (int)pref);
// if setting some prefix and there is a suffix already, clear suffix
if(pref != Modifier::NONE && _suffix != Modifier::NONE)
undoChangeProperty(Pid::FBSUFFIX, int(Modifier::NONE));
layout(); // re-generate displayText
}
}
void FiguredBassItem::undoSetDigit(int digit)
{
if (digit >= 0 && digit <= 9) {
undoChangeProperty(Pid::FBDIGIT, digit);
layout(); // re-generate displayText
}
}
void FiguredBassItem::undoSetSuffix(Modifier suff)
{
undoChangeProperty(Pid::FBSUFFIX, int(suff));
// if setting some suffix and there is a prefix already, clear prefix
if(suff != Modifier::NONE && _prefix != Modifier::NONE)
undoChangeProperty(Pid::FBPREFIX, int(Modifier::NONE));
layout(); // re-generate displayText
}
void FiguredBassItem::undoSetContLine(ContLine val)
{
undoChangeProperty(Pid::FBCONTINUATIONLINE, int(val));
layout(); // re-generate displayText
}
void FiguredBassItem::undoSetParenth1(Parenthesis par)
{
undoChangeProperty(Pid::FBPARENTHESIS1, int(par));
layout(); // re-generate displayText
}
void FiguredBassItem::undoSetParenth2(Parenthesis par)
{
undoChangeProperty(Pid::FBPARENTHESIS2, int(par));
layout(); // re-generate displayText
}
void FiguredBassItem::undoSetParenth3(Parenthesis par)
{
undoChangeProperty(Pid::FBPARENTHESIS3, int(par));
layout(); // re-generate displayText
}
void FiguredBassItem::undoSetParenth4(Parenthesis par)
{
undoChangeProperty(Pid::FBPARENTHESIS4, int(par));
layout(); // re-generate displayText
}
void FiguredBassItem::undoSetParenth5(Parenthesis par)
{
undoChangeProperty(Pid::FBPARENTHESIS5, int(par));
layout(); // re-generate displayText
}
//---------------------------------------------------------
//
// MusicXML I/O
//
//---------------------------------------------------------
//---------------------------------------------------------
// Convert MusicXML prefix/suffix to Modifier
//---------------------------------------------------------
FiguredBassItem::Modifier FiguredBassItem::MusicXML2Modifier(const QString prefix) const
{
if (prefix == "sharp")
return Modifier::SHARP;
else if (prefix == "flat")
return Modifier::FLAT;
else if (prefix == "natural")
return Modifier::NATURAL;
else if (prefix == "double-sharp")
return Modifier::DOUBLESHARP;
else if (prefix == "flat-flat")
return Modifier::DOUBLEFLAT;
else if (prefix == "sharp-sharp")
return Modifier::DOUBLESHARP;
else if (prefix == "cross")
return Modifier::CROSS;
else if (prefix == "backslash")
return Modifier::BACKSLASH;
else if (prefix == "slash")
return Modifier::SLASH;
else
return Modifier::NONE;
}
//---------------------------------------------------------
// Convert Modifier to MusicXML prefix/suffix
//---------------------------------------------------------
QString FiguredBassItem::Modifier2MusicXML(FiguredBassItem::Modifier prefix) const
{
switch (prefix) {
case Modifier::NONE: return "";
case Modifier::DOUBLEFLAT: return "flat-flat";
case Modifier::FLAT: return "flat";
case Modifier::NATURAL: return "natural";
case Modifier::SHARP: return "sharp";
case Modifier::DOUBLESHARP: return "double-sharp";
case Modifier::CROSS: return "cross";
case Modifier::BACKSLASH: return "backslash";
case Modifier::SLASH: return "slash";
case Modifier::NUMOF: return ""; // prevent gcc "FBINumOfAccid not handled in switch" warning
}
return "";
}
//---------------------------------------------------------
// Read MusicXML
//
// Set the FiguredBassItem state based on the MusicXML <figure> node de.
// In MusicXML, parentheses is set to "yes" or "no" for the figured-bass
// node instead of for each individual <figure> node.
//---------------------------------------------------------
#if 0
void FiguredBassItem::readMusicXML(XmlReader& e, bool paren)
{
// read the <figure> node de
while (e.readNextStartElement()) {
const QStringRef& tag(e.name());
if (tag == "figure-number") {
// MusicXML spec states figure-number is a number
// MuseScore can only handle single digit
int iVal = e.readInt();
if (1 <= iVal && iVal <= 9)
_digit = iVal;
}
else if (tag == "prefix")
_prefix = MusicXML2Modifier(e.readElementText());
else if (tag == "suffix")
_suffix = MusicXML2Modifier(e.readElementText());
else
e.unknown();
}
// set parentheses
if (paren) {
// parenthesis open
if (_prefix != Modifier::NONE)
parenth[0] = Parenthesis::ROUNDOPEN; // before prefix
else if (_digit != FBIDigitNone)
parenth[1] = Parenthesis::ROUNDOPEN; // before digit
else if (_suffix != Modifier::NONE)
parenth[2] = Parenthesis::ROUNDOPEN; // before suffix
// parenthesis close
if (_suffix != Modifier::NONE)
parenth[3] = Parenthesis::ROUNDCLOSED; // after suffix
else if (_digit != FBIDigitNone)
parenth[2] = Parenthesis::ROUNDCLOSED; // after digit
else if (_prefix != Modifier::NONE)
parenth[1] = Parenthesis::ROUNDCLOSED; // after prefix
}
}
#endif
//---------------------------------------------------------
// Write MusicXML
//
// Writes the portion within the <figure> tag.
//
// NOTE: Both MuseScore and MusicXML provide two ways of altering the (temporal) length of a
// figured bass object: extension lines and duration. The convention is that an EXTENSION is
// used if the figure lasts LONGER than the note (i.e., it "extends" to the following notes),
// whereas DURATION is used if the figure lasts SHORTER than the note (e.g., when notating a
// figure change under a note). However, MuseScore does not restrict durations in this way,
// allowing them to act as extensions themselves. As a result, a few more branches are
// required in the decision tree to handle everything correctly.
//---------------------------------------------------------
void FiguredBassItem::writeMusicXML(XmlWriter& xml, bool isOriginalFigure, int crEndTick, int fbEndTick) const
{
xml.stag("figure");
// The first figure of each group is the "original" figure. Practically, it is one inserted manually
// by the user, rather than automatically by the "duration" extend method.
if (isOriginalFigure) {
QString strPrefix = Modifier2MusicXML(_prefix);
if (strPrefix != "")
xml.tag("prefix", strPrefix);
if (_digit != FBIDigitNone)
xml.tag("figure-number", _digit);
QString strSuffix = Modifier2MusicXML(_suffix);
if (strSuffix != "")
xml.tag("suffix", strSuffix);
// Check if the figure ends before or at the same time as the current note. Otherwise, the figure
// extends to the next note, and so carries an extension type "start" by definition.
if (fbEndTick <= crEndTick) {
if (_contLine == ContLine::SIMPLE)
xml.tagE("extend type=\"stop\" ");
else if (_contLine == ContLine::EXTENDED) {
bool hasFigure = (strPrefix != "" || _digit != FBIDigitNone || strSuffix != "");
if (hasFigure)
xml.tagE("extend type=\"start\" ");
else
xml.tagE("extend type=\"continue\" ");
}
}
else
xml.tagE("extend type=\"start\" ");
}
// If the figure is not "original", it must have been created using the "duration" feature of figured bass.
// In other words, the original figure belongs to a previous note rather than the current note.
else {
if (crEndTick < fbEndTick)
xml.tagE("extend type=\"continue\" ");
else
xml.tagE("extend type=\"stop\" ");
}
xml.etag();
}
//---------------------------------------------------------
// startsWithParenthesis
//---------------------------------------------------------
bool FiguredBassItem::startsWithParenthesis() const
{
if (_prefix != Modifier::NONE)
return (parenth[0] != Parenthesis::NONE);
if (_digit != FBIDigitNone)
return (parenth[1] != Parenthesis::NONE);
if (_suffix != Modifier::NONE)
return (parenth[2] != Parenthesis::NONE);
return false;
}
//---------------------------------------------------------
// F I G U R E D B A S S
//---------------------------------------------------------
FiguredBass::FiguredBass(Score* s)
: TextBase(s, ElementFlag::MOVABLE | ElementFlag::ON_STAFF)
{
initElementStyle(&figuredBassTextStyle);
setOnNote(true);
#if 0 // TODO
TextStyle st(
g_FBFonts[0].family,
score()->styleD(Sid::figuredBassFontSize),
false,
false,
false,
Align::LEFT | Align::TOP,
QPointF(0, score()->styleD(Sid::figuredBassYOffset)),
OffsetType::SPATIUM);
st.setSizeIsSpatiumDependent(true);
setElementStyle(st);
#endif
setTicks(0);
qDeleteAll(items);
items.clear();
}
FiguredBass::FiguredBass(const FiguredBass& fb)
: TextBase(fb)
{
setOnNote(fb.onNote());
setTicks(fb.ticks());
for (auto i : fb.items) { // deep copy is needed
FiguredBassItem* fbi = new FiguredBassItem(*i);
fbi->setParent(this);
items.push_back(fbi);
}
// items = fb.items;
}
FiguredBass::~FiguredBass()
{
for (FiguredBassItem* item : items)
delete item;
}
//---------------------------------------------------------
// write
//---------------------------------------------------------
void FiguredBass::write(XmlWriter& xml) const
{
if (!xml.canWrite(this))
return;
xml.stag(this);
if(!onNote())
xml.tag("onNote", onNote());
if (ticks() > 0)
xml.tag("ticks", ticks());
// if unparseable items, write full text data
if (items.size() < 1)
TextBase::writeProperties(xml, true);
else {
// if (textStyleType() != StyledPropertyListIdx::FIGURED_BASS)
// // if all items parsed and not unstiled, we simply have a special style: write it
// xml.tag("style", textStyle().name());
for(FiguredBassItem* item : items)
item->write(xml);
Element::writeProperties(xml);
}
xml.etag();
}
//---------------------------------------------------------
// read
//---------------------------------------------------------
void FiguredBass::read(XmlReader& e)
{
QString normalizedText;
int idx = 0;
while (e.readNextStartElement()) {
const QStringRef& tag(e.name());
if (tag == "ticks")
setTicks(e.readInt());
else if (tag == "onNote")
setOnNote(e.readInt() != 0l);
else if (tag == "FiguredBassItem") {
FiguredBassItem * pItem = new FiguredBassItem(score(), idx++);
pItem->setTrack(track());
pItem->setParent(this);
pItem->read(e);
items.push_back(pItem);
// add item normalized text
if(!normalizedText.isEmpty())
normalizedText.append('\n');
normalizedText.append(pItem->normalizedText());
}
// else if (tag == "style")
// setStyledPropertyListIdx(e.readElementText());
else if (!TextBase::readProperties(e))
e.unknown();
}
// if items could be parsed set normalized text
if (items.size() > 0)
setXmlText(normalizedText); // this is the text to show while editing
}
//---------------------------------------------------------
// layout
//---------------------------------------------------------
void FiguredBass::layout()
{
// if 'our' style, force 'our' style data from FiguredBass parameters
#if 0
if (textStyleType() == StyledPropertyListIdx::FIGURED_BASS) {
TextStyle st(g_FBFonts[0].family, score()->styleD(Sid::figuredBassFontSize),
false, false, false, Align::LEFT | Align::TOP, QPointF(0, yOff),
OffsetType::SPATIUM);
st.setSizeIsSpatiumDependent(true);
setTextStyle(st);
}
#endif
// VERTICAL POSITION:
const qreal y = score()->styleD(Sid::figuredBassYOffset) * spatium();
setPos(QPointF(0.0, y));
// BOUNDING BOX and individual item layout (if required)
TextBase::layout1(); // prepare structs and data expected by Text methods
// if element could be parsed into items, layout each element
// Items list will be empty in edit mode (see FiguredBass::startEdit).
// TODO: consider disabling specific layout in case text style is changed (tid() != Tid::FIGURED_BASS).
if (items.size() > 0) {
layoutLines();
bbox().setRect(0, 0, _lineLengths.at(0), 0);
// layout each item and enlarge bbox to include items bboxes
for (FiguredBassItem* item : items) {
item->layout();
addbbox(item->bbox().translated(item->pos()));
}
}
}
//---------------------------------------------------------
// layoutLines
//
// lays out the duration indicator line(s), filling the _lineLengths array
// and the length of printed lines (used by continuation lines)
//---------------------------------------------------------
void FiguredBass::layoutLines()
{
if (_ticks <= 0 || !segment()) {
_lineLengths.resize(1); // be sure to always have
_lineLengths[0] = 0; // at least 1 item in array
return;
}
ChordRest* lastCR = nullptr; // the last ChordRest of this
Segment * nextSegm = nullptr; // the Segment beyond this' segment
int nextTick = segment()->tick() + _ticks; // the tick beyond this' duration
// locate the measure containing the last tick of this; it is either:
// the same measure containing nextTick, if nextTick is not the first tick of a measure
// (and line should stop right before it)
// or the previous measure, if nextTick is the first tick of a measure
// (and line should stop before any measure terminal segment (bar, clef, ...) )
Measure* m = score()->tick2measure(nextTick-1);
if (m) {
// locate the first segment (of ANY type) right after this' last tick
for (nextSegm = m->first(SegmentType::All); nextSegm; nextSegm = nextSegm->next()) {
if (nextSegm->tick() >= nextTick)
break;
}
// locate the last ChordRest of this
if (nextSegm)
lastCR = nextSegm->prev1()->nextChordRest(track(), true);
}
if (!m || !nextSegm) {
qDebug("FiguredBass layout: no segment found for tick %d", nextTick);
_lineLengths.resize(1); // be sure to always have
_lineLengths[0] = 0; // at least 1 item in array
return;
}
// get length of printed lines from horiz. page position of lastCR
// (enter a bit 'into' the ChordRest for clarity)
_printedLineLength = lastCR ? lastCR->pageX() - pageX() + 1.5 * spatium() : 3 * spatium();
// get duration indicator line(s) from page position of nextSegm
const QList<System*>& systems = score()->systems();
System* s1 = segment()->measure()->system();
System* s2 = nextSegm->measure()->system();
int sysIdx1 = systems.indexOf(s1);
int sysIdx2 = systems.indexOf(s2);
if (sysIdx2 < sysIdx1) {
sysIdx2 = sysIdx1;
nextSegm = segment()->next1();
// TODO
// During layout of figured bass next systems' numbers may be still
// undefined (then sysIdx2 == -1) or change in the future.
// A layoutSystem() approach similar to that for spanners should
// probably be implemented.
}
int i, len ,segIdx;
for (i = sysIdx1, segIdx = 0; i <= sysIdx2; ++i, ++segIdx) {
len = 0;
if (sysIdx1 == sysIdx2 || i == sysIdx1) {
// single line
len = nextSegm->pageX() - pageX() - 4; // stop 4 raster units before next segm
}
else if (i == sysIdx1) {
// initial line
qreal w = s1->staff(staffIdx())->bbox().right();
qreal x = s1->pageX() + w;
len = x - pageX();
}
else if (i > 0 && i != sysIdx2) {
// middle line
qDebug("FiguredBass: duration indicator middle line not implemented");
}
else if (i == sysIdx2) {
// end line
qDebug("FiguredBass: duration indicator end line not implemented");
}
// store length item, reusing array items if already present
if (_lineLengths.size() <= segIdx)
_lineLengths.append(len);
else
_lineLengths[segIdx] = len;
}
// if more array items than needed, truncate array
if (_lineLengths.size() > segIdx)
_lineLengths.resize(segIdx);
}
//---------------------------------------------------------
// draw
//---------------------------------------------------------
void FiguredBass::draw(QPainter* painter) const
{
// if not printing, draw duration line(s)
if (!score()->printing() && score()->showUnprintable()) {
for (qreal len : _lineLengths) {
if (len > 0) {
painter->setPen(QPen(Qt::lightGray, 1));
painter->drawLine(0.0, -2, len, -2); // -2: 2 rast. un. above digits
}
}
}
// if in edit mode or with custom style, use standard text drawing
// if (editMode() || subStyle() != ElementStyle::FIGURED_BASS)
// if (tid() != Tid::FIGURED_BASS)
// TextBase::draw(painter);
// else
{ // not edit mode:
if (items.size() < 1) // if not parseable into f.b. items
TextBase::draw(painter); // draw as standard text
else
for (FiguredBassItem* item : items) { // if parseable into f.b. items
painter->translate(item->pos()); // draw each item in its proper position
item->draw(painter);
painter->translate(-item->pos());
}
}
/* DEBUG
QString str = QString();
str.setNum(_ticks);
painter->drawText(0, (_onNote ? 40 : 30), str);
*/
}
//---------------------------------------------------------
// startEdit / edit / endEdit
//---------------------------------------------------------
void FiguredBass::startEdit(EditData& ed)
{
qDeleteAll(items);
items.clear();
layout(); // re-layout without F.B.-specific formatting.
TextBase::startEdit(ed);
}
void FiguredBass::endEdit(EditData& ed)
{
int idx;
TextBase::endEdit(ed);
// as the standard text editor keeps inserting spurious HTML formatting and styles
// retrieve and work only on the plain text
QString txt = plainText();
if (txt.isEmpty()) { // if no text, nothing to do
setXmlText(txt); // clear the stored text: the empty f.b. element will be deleted
return;
}
// split text into lines and create an item for each line
QStringList list = txt.split('\n', QString::SkipEmptyParts);
qDeleteAll(items);
items.clear();
QString normalizedText = QString();
idx = 0;
foreach(QString str, list) {
FiguredBassItem* pItem = new FiguredBassItem(score(), idx++);
if(!pItem->parse(str)) { // if any item fails parsing
qDeleteAll(items);
items.clear(); // clear item list
layout();
return;
}
pItem->setTrack(track());
pItem->setParent(this);
items.push_back(pItem);
// add item normalized text
if(!normalizedText.isEmpty())
normalizedText.append('\n');
normalizedText.append(pItem->normalizedText());
}
// if all items parsed and text is styled, replaced entered text with normalized text
if (items.size()) {
setXmlText(normalizedText);
layout();
}
}
//---------------------------------------------------------
// setSelected /setVisible
//
// forward flags to items
//---------------------------------------------------------
void FiguredBass::setSelected(bool flag)
{
Element::setSelected(flag);
for(FiguredBassItem* item : items) {
item->setSelected(flag);
}
}
void FiguredBass::setVisible(bool flag)
{
Element::setVisible(flag);
for(FiguredBassItem* item : items) {
item->setVisible(flag);
}
}
//---------------------------------------------------------
// nextFiguredBass
//
// returns the next *contiguous* FiguredBass element if it exists,
// i.e. the FiguredBass element which starts where 'this' ends
// returns 0 if none
//---------------------------------------------------------
FiguredBass* FiguredBass::nextFiguredBass() const
{
if (_ticks <= 0) // if _ticks unset, no clear idea of when 'this' ends
return 0;
Segment * nextSegm; // the Segment beyond this' segment
int nextTick = segment()->tick() + _ticks; // the tick beyond this' duration
// locate the ChordRest segment right after this' end
nextSegm = score()->tick2segment(nextTick, true, SegmentType::ChordRest);
if (nextSegm == 0)
return 0;
// scan segment annotations for an existing FB element in the this' staff
for (Element* e : nextSegm->annotations())
if (e->type() == ElementType::FIGURED_BASS && e->track() == track())
return toFiguredBass(e);
return 0;
}
//---------------------------------------------------------
// additionalContLineX
//
// if there is a continuation line, without other text elements, at pagePosY, returns its X coord (in page coords)
// returns 0 if no cont.line there or if there are text elements before the cont.line
//
// In practice, returns the X coord of a cont. line which can be the continuation of a previous cont. line
//
// Note: pagePosY is the Y coord of the FiguredBassItem containing the line, not of the line itself,
// as line position might depend on styles.
//---------------------------------------------------------
qreal FiguredBass::additionalContLineX(qreal pagePosY) const
{
QPointF pgPos = pagePos();
for (FiguredBassItem* fbi : items)
// if item has cont.line but nothing before it
// and item Y coord near enough to pagePosY
if(fbi->contLine() != FiguredBassItem::ContLine::NONE
&& fbi->digit() == FBIDigitNone
&& fbi->prefix() == FiguredBassItem::Modifier::NONE
&& fbi->suffix() == FiguredBassItem::Modifier::NONE
&& fbi->parenth4() == FiguredBassItem::Parenthesis::NONE
&& qAbs(pgPos.y() + fbi->ipos().y() - pagePosY) < 0.05)
return pgPos.x() + fbi->ipos().x();
return 0.0; // no suitable line
}
//---------------------------------------------------------
// PROPERTY METHODS
//---------------------------------------------------------
QVariant FiguredBass::getProperty(Pid propertyId) const
{
return TextBase::getProperty(propertyId);
}
bool FiguredBass::setProperty(Pid propertyId, const QVariant& v)
{
score()->addRefresh(canvasBoundingRect());
return TextBase::setProperty(propertyId, v);
}
QVariant FiguredBass::propertyDefault(Pid id) const
{
return TextBase::propertyDefault(id);
}
//---------------------------------------------------------
// TEMPORARY HACK!!!
//---------------------------------------------------------
/*
FiguredBassItem * FiguredBass::addItem()
{
int line = items.size();
FiguredBassItem* fib = new FiguredBassItem(score(), line);
// tell QML not to garbage collect this item
QQmlEngine::setObjectOwnership(fib, QQmlEngine::CppOwnership);
items.push_back(fib);
return fib;
}
*/
//---------------------------------------------------------
// STATIC FUNCTION
// adding a new FiguredBass to a Segment;
// the main purpose of this function is to ensure that ONLY ONE F.b. element exists for each Segment/staff;
// it either re-uses an existing FiguredBass or creates a new one if none is found;
// returns the FiguredBass and sets pNew to true if it has been newly created.
//
// Sets an initial duration of the element up to the next ChordRest of the same staff.
//
// As the F.b. very concept requires the base chord to have ONLY ONE note,
// FiguredBass elements are created and looked for only in the first track of the staff.
//---------------------------------------------------------
FiguredBass * FiguredBass::addFiguredBassToSegment(Segment * seg, int track, int extTicks, bool * pNew)
{
int endTick; // where this FB is initially assumed to end
int staff = track / VOICES; // convert track to staff
track = staff * VOICES; // first track for this staff
// scan segment annotations for an existing FB element in the same staff
FiguredBass* fb = 0;
for (Element* e : seg->annotations()) {
if (e->type() == ElementType::FIGURED_BASS && (e->track() / VOICES) == staff) {
// an FB already exists in segment: re-use it
fb = toFiguredBass(e);
*pNew = false;
endTick = seg->tick() + fb->ticks();
break;
}
}
if (fb == 0) { // no FB at segment: create new
fb = new FiguredBass(seg->score());
fb->setTrack(track);
fb->setParent(seg);
// locate next SegChordRest in the same staff to estimate presumed duration of element
endTick = INT_MAX;
Segment * nextSegm;
for (int iVoice = 0; iVoice < VOICES; iVoice++) {
nextSegm = seg->nextCR(track + iVoice);
if(nextSegm && nextSegm->tick() < endTick)
endTick = nextSegm->tick();
}
if(endTick == INT_MAX) { // no next segment: set up to score end
Measure * meas = seg->score()->lastMeasure();
endTick = meas->tick() + meas->ticks();
}
fb->setTicks(endTick - seg->tick());
// set onNote status
fb->setOnNote(false); // assume not onNote
for (int i = track; i < track + VOICES; i++) // if segment has chord in staff, set onNote
if (seg->element(i) && seg->element(i)->type() == ElementType::CHORD) {
fb->setOnNote(true);
break;
}
*pNew = true;
}
// if we are extending a previous FB
if(extTicks > 0) {
// locate previous FB for same staff
Segment * prevSegm;
FiguredBass* prevFB = 0;
for(prevSegm = seg->prev1(SegmentType::ChordRest); prevSegm; prevSegm = prevSegm->prev1(SegmentType::ChordRest)) {
for (Element* e : prevSegm->annotations()) {
if (e->type() == ElementType::FIGURED_BASS && (e->track() ) == track) {
prevFB = toFiguredBass(e); // previous FB found
break;
}
}
if(prevFB) {
// if previous FB did not stop more than extTicks before this FB...
int delta = seg->tick() - prevFB->segment()->tick();
if(prevFB->ticks() + extTicks >= delta)
prevFB->setTicks(delta); // update prev FB ticks to last up to this FB
break;
}
}
}
return fb;
}
//---------------------------------------------------------
// STATIC FUNCTIONS FOR FONT CONFIGURATION MANAGEMENT
//---------------------------------------------------------
bool FiguredBassFont::read(XmlReader& e)
{
while (e.readNextStartElement()) {
const QStringRef& tag(e.name());
if (tag == "family")
family = e.readElementText();
else if (tag == "displayName")
displayName = e.readElementText();
else if (tag == "defaultPitch")
defPitch = e.readDouble();
else if (tag == "defaultLineHeight")
defLineHeight = e.readDouble();
else if (tag == "parenthesisRoundOpen")
displayParenthesis[1] = e.readElementText()[0];
else if (tag == "parenthesisRoundClosed")
displayParenthesis[2] = e.readElementText()[0];
else if (tag == "parenthesisSquareOpen")
displayParenthesis[3] = e.readElementText()[0];
else if (tag == "parenthesisSquareClosed")
displayParenthesis[4] = e.readElementText()[0];
else if (tag == "doubleflat")
displayAccidental[int(FiguredBassItem::Modifier::DOUBLEFLAT)]= e.readElementText()[0];
else if (tag == "flat")
displayAccidental[int(FiguredBassItem::Modifier::FLAT)] = e.readElementText()[0];
else if (tag == "natural")
displayAccidental[int(FiguredBassItem::Modifier::NATURAL)] = e.readElementText()[0];
else if (tag == "sharp")
displayAccidental[int(FiguredBassItem::Modifier::SHARP)] = e.readElementText()[0];
else if (tag == "doublesharp")
displayAccidental[int(FiguredBassItem::Modifier::DOUBLESHARP)]= e.readElementText()[0];
else if (tag == "cross")
displayAccidental[int(FiguredBassItem::Modifier::CROSS)] = e.readElementText()[0];
else if (tag == "backslash")
displayAccidental[int(FiguredBassItem::Modifier::BACKSLASH)] = e.readElementText()[0];
else if (tag == "slash")
displayAccidental[int(FiguredBassItem::Modifier::SLASH)] = e.readElementText()[0];
else if (tag == "digit") {
int digit = e.intAttribute("value");
if (digit < 0 || digit > 9)
return false;
while (e.readNextStartElement()) {
const QStringRef& t(e.name());
if (t == "simple")
displayDigit[int(FiguredBassItem::Style::MODERN)] [digit][int(FiguredBassItem::Combination::SIMPLE)] = e.readElementText()[0];
else if (t == "crossed")
displayDigit[int(FiguredBassItem::Style::MODERN)] [digit][int(FiguredBassItem::Combination::CROSSED)] = e.readElementText()[0];
else if (t == "backslashed")
displayDigit[int(FiguredBassItem::Style::MODERN)] [digit][int(FiguredBassItem::Combination::BACKSLASHED)] = e.readElementText()[0];
else if (t == "slashed")
displayDigit[int(FiguredBassItem::Style::MODERN)] [digit][int(FiguredBassItem::Combination::SLASHED)] = e.readElementText()[0];
else if (t == "simpleHistoric")
displayDigit[int(FiguredBassItem::Style::HISTORIC)][digit][int(FiguredBassItem::Combination::SIMPLE)] = e.readElementText()[0];
else if (t == "crossedHistoric")
displayDigit[int(FiguredBassItem::Style::HISTORIC)][digit][int(FiguredBassItem::Combination::CROSSED)] = e.readElementText()[0];
else if (t == "backslashedHistoric")
displayDigit[int(FiguredBassItem::Style::HISTORIC)][digit][int(FiguredBassItem::Combination::BACKSLASHED)] = e.readElementText()[0];
else if (t == "slashedHistoric")
displayDigit[int(FiguredBassItem::Style::HISTORIC)][digit][int(FiguredBassItem::Combination::SLASHED)] = e.readElementText()[0];
else {
e.unknown();
return false;
}
}
}
else {
e.unknown();
return false;
}
}
displayParenthesis[0] = displayAccidental[int(FiguredBassItem::Modifier::NONE)] = ' ';
return true;
}
//---------------------------------------------------------
// Read Configuration File
//
// reads a configuration and appends read data to g_FBFonts
// resets everything and reads the built-in config file if fileName is null or empty
//---------------------------------------------------------
bool FiguredBass::readConfigFile(const QString& fileName)
{
QString path;
if (fileName == 0 || fileName.isEmpty()) { // defaults to built-in xml
#ifdef Q_OS_IOS
{
extern QString resourcePath();
QString rpath = resourcePath();
path = rpath + QString("/fonts_figuredbass.xml");
}
#else
path = ":/fonts/fonts_figuredbass.xml";
#endif
g_FBFonts.clear();
}
else
path = fileName;
QFile fi(path);
if (!fi.open(QIODevice::ReadOnly)) {
MScore::lastError = QObject::tr("Cannot open figured bass description:\n%1\n%2").arg(fi.fileName()).arg(fi.errorString());
qDebug("FiguredBass::read failed: <%s>", qPrintable(path));
return false;
}
XmlReader e(&fi);
while (e.readNextStartElement()) {
if (e.name() == "museScore") {
// QString version = e.attribute(QString("version"));
// QStringList sl = version.split('.');
// int _mscVersion = sl[0].toInt() * 100 + sl[1].toInt();
while (e.readNextStartElement()) {
if (e.name() == "font") {
FiguredBassFont f;
if (f.read(e))
g_FBFonts.append(f);
else
return false;
}
else
e.unknown();
}
return true;
}
}
return false;
}
//---------------------------------------------------------
// Get Font Names
//
// returns a list of display names for the fonts configured to work with Figured Bass;
// the index of a name in the list can be used to retrieve the font data with fontData()
//---------------------------------------------------------
QList<QString> FiguredBass::fontNames()
{
QList<QString> names;
foreach(const FiguredBassFont& f, g_FBFonts)
names.append(f.displayName);
return names;
}
//---------------------------------------------------------
// Get Font Data
//
// retrieves data about a Figured Bass font.
// returns: true if idx is valid | false if it is not
// any of the pointer parameter can be null, if that datum is not needed
//---------------------------------------------------------
bool FiguredBass::fontData(int nIdx, QString * pFamily, QString * pDisplayName,
qreal * pSize, qreal * pLineHeight)
{
if(nIdx >= 0 && nIdx < g_FBFonts.size()) {
FiguredBassFont f = g_FBFonts.at(nIdx);
if(pFamily) *pFamily = f.family;
if(pDisplayName) *pDisplayName = f.displayName;
if(pSize) *pSize = f.defPitch;
if(pLineHeight) *pLineHeight = f.defLineHeight;
return true;
}
return false;
}
//---------------------------------------------------------
//
// MusicXML I/O
//
//---------------------------------------------------------
//---------------------------------------------------------
// Read MusicXML
//
// Set the FiguredBass state based on the MusicXML <figured-bass> node de.
// Note that onNote and ticks must be set by the MusicXML importer,
// as the required context is not present in the items DOM tree.
// Exception: if a <duration> element is present, tick can be set.
// Return true if valid, non-empty figure(s) are found
//---------------------------------------------------------
#if 0
bool FiguredBass::readMusicXML(XmlReader& e, int divisions)
{
bool parentheses = e.attribute("parentheses") == "yes";
QString normalizedText;
int idx = 0;
while (e.readNextStartElement()) {
const QStringRef& tag(e.name());
if (tag == "duration") {
QString val(e.readElementText());
bool ok = true;
int duration = val.toInt(&ok);
if (ok) {
duration *= MScore::division;
duration /= divisions;
setTicks(duration);
}
else
qDebug("MusicXml-Import: bad duration value: <%s>",
qPrintable(val));
}
else if (tag == "figure") {
FiguredBassItem * pItem = new FiguredBassItem(score(), idx++);
pItem->setTrack(track());
pItem->setParent(this);
pItem->readMusicXML(e, parentheses);
items.append(*pItem);
// add item normalized text
if (!normalizedText.empty())
normalizedText.append('\n');
normalizedText.append(pItem->normalizedText());
}
else {
e.unknown();
return false;
}
}
setText(normalizedText); // this is the text to show while editing
bool res = !normalizedText.empty();
return res;
}
#endif
//---------------------------------------------------------
// hasParentheses
//
// return true if any FiguredBassItem starts with a parenthesis
//---------------------------------------------------------
bool FiguredBass::hasParentheses() const
{
for(FiguredBassItem* item : items)
if (item->startsWithParenthesis())
return true;
return false;
}
//---------------------------------------------------------
// Write MusicXML
//---------------------------------------------------------
void FiguredBass::writeMusicXML(XmlWriter& xml, bool isOriginalFigure, int crEndTick, int fbEndTick, bool writeDuration, int divisions) const
{
QString stag = "figured-bass";
if (hasParentheses())
stag += " parentheses=\"yes\"";
xml.stag(stag);
for(FiguredBassItem* item : items)
item->writeMusicXML(xml, isOriginalFigure, crEndTick, fbEndTick);
if (writeDuration)
xml.tag("duration", ticks() / divisions);
xml.etag();
}
//---------------------------------------------------------
//
// METHODS BELONGING TO OTHER CLASSES
//
// Work In Progress: kept here until the FiguredBass framework is reasonably set up;
// To be finally moved to their respective class implementation files.
//
//---------------------------------------------------------
//---------------------------------------------------------
// Score::addFiguredBass
// called from Keyboard Accelerator & menus
//---------------------------------------------------------
FiguredBass* Score::addFiguredBass()
{
Element* el = selection().element();
if (el == 0 || (el->type() != ElementType::NOTE && el->type() != ElementType::FIGURED_BASS)) {
MScore::setError(NO_NOTE_FIGUREDBASS_SELECTED);
return 0;
}
FiguredBass * fb;
bool bNew;
if (el->isNote()) {
ChordRest * cr = toNote(el)->chord();
fb = FiguredBass::addFiguredBassToSegment(cr->segment(), cr->staffIdx() * VOICES, 0, &bNew);
}
else if (el->isFiguredBass()) {
fb = toFiguredBass(el);
bNew = false;
}
else
return 0;
if(fb == 0)
return 0;
if(bNew)
undoAddElement(fb);
select(fb, SelectType::SINGLE, 0);
return fb;
}
}