1 //=============================================================================
2 //  MuseScore
3 //  Music Composition & Notation
4 //
5 //  Copyright (C) 2002-2011 Werner Schweer
6 //
7 //  This program is free software; you can redistribute it and/or modify
8 //  it under the terms of the GNU General Public License version 2
9 //  as published by the Free Software Foundation and appearing in
10 //  the file LICENCE.GPL
11 //=============================================================================
12 
13 #include "figuredbass.h"
14 #include "score.h"
15 #include "note.h"
16 #include "measure.h"
17 #include "system.h"
18 #include "segment.h"
19 #include "chord.h"
20 #include "rest.h"
21 #include "score.h"
22 #include "sym.h"
23 #include "xml.h"
24 
25 // trying to do without it
26 //#include <QQmlEngine>
27 
28 namespace Ms {
29 
30 //---------------------------------------------------------
31 //   figuredBassStyle
32 //---------------------------------------------------------
33 
34 static const ElementStyle figuredBassStyle {
35       { Sid::figuredBassMinDistance,             Pid::MIN_DISTANCE           },
36       };
37 
38 //---------------------------------------------------------
39 //   figuredBassTextStyle
40 //---------------------------------------------------------
41 
42 static const ElementStyle figuredBassTextStyle {
43       { Sid::figuredBassFontFace,                Pid::FONT_FACE              },
44       { Sid::figuredBassFontSize,                Pid::FONT_SIZE              },
45       { Sid::figuredBassFontStyle,               Pid::FONT_STYLE             },
46       };
47 
48 static constexpr qreal  FB_CONTLINE_HEIGHT            = 0.875;    // the % of font EM to raise the cont. line at
49                                                                   // (0 = top of font; 1 = bottom of font)
50 static constexpr qreal  FB_CONTLINE_LEFT_PADDING      = 0.1875;   // (3/16sp) the blank space at the left of a cont. line (in sp)
51 static constexpr qreal  FB_CONTLINE_OVERLAP           = 0.125;    // (1/8sp)  the overlap of an extended cont. line (in sp)
52 static constexpr qreal  FB_CONTLINE_THICKNESS         = 0.09375;  // (3/32sp) the thickness of a cont. line (in sp)
53 
54 // the array of configured fonts
55 static QList<FiguredBassFont> g_FBFonts;
56 
57 //---------------------------------------------------------
58 //   F I G U R E D   B A S S   I T E M
59 //---------------------------------------------------------
60 
61 // used for indexed access to parenthesis chars
62 // (these is no normAccidToChar[], as accidentals may use mult. chars in normalized display):
63 const QChar FiguredBassItem::normParenthToChar[int(FiguredBassItem::Parenthesis::NUMOF)] =
64 { 0, '(', ')', '[', ']'};
65 
66 
FiguredBassItem(Score * s,int l)67 FiguredBassItem::FiguredBassItem(Score* s, int l)
68       : Element(s), ord(l)
69       {
70       _prefix     = _suffix = Modifier::NONE;
71       _digit      = FBIDigitNone;
72       parenth[0]  = parenth[1] = parenth[2] = parenth[3] = parenth[4] = Parenthesis::NONE;
73       _contLine   = ContLine::NONE;
74       textWidth   = 0;
75       }
76 
FiguredBassItem(const FiguredBassItem & item)77 FiguredBassItem::FiguredBassItem(const FiguredBassItem& item)
78       : Element(item)
79       {
80       ord         = item.ord;
81       _prefix     = item._prefix;
82       _digit      = item._digit;
83       _suffix     = item._suffix;
84       parenth[0]  = item.parenth[0];
85       parenth[1]  = item.parenth[1];
86       parenth[2]  = item.parenth[2];
87       parenth[3]  = item.parenth[3];
88       parenth[4]  = item.parenth[4];
89       _contLine   = item._contLine;
90       textWidth   = item.textWidth;
91       _displayText= item._displayText;
92       }
93 
~FiguredBassItem()94 FiguredBassItem::~FiguredBassItem()
95       {
96       }
97 
98 //---------------------------------------------------------
99 //   FiguredBassItem parse()
100 //
101 // converts a string into a property-based representation, if possible;
102 // return true on success | false if the string is non-conformant
103 //---------------------------------------------------------
104 
parse(QString & str)105 bool FiguredBassItem::parse(QString& str)
106       {
107       int retVal;
108 
109       parseParenthesis(str, 0);
110       retVal = parsePrefixSuffix(str, true);          // prefix
111       if(retVal == -1)
112             return false;
113       parseParenthesis(str, 1);
114       retVal = parseDigit(str);                       // digit
115       if(retVal == -1)
116             return false;
117       parseParenthesis(str, 2);
118       retVal = parsePrefixSuffix(str, false);         // suffix
119       if(retVal == -1)
120             return false;
121       parseParenthesis(str, 3);
122       // check for a possible cont. line symbol(s)
123       _contLine = ContLine::NONE;                       // contLine
124       if(str[0] == '-' || str[0] == '_') {            // 1 symbol: simple continuation
125             _contLine = ContLine::SIMPLE;
126             str.remove(0, 1);
127       }
128       while(str[0] == '-' || str[0] == '_') {         // more than 1 symbol: extended continuation
129             _contLine = ContLine::EXTENDED;
130             str.remove(0, 1);
131       }
132       parseParenthesis(str, 4);
133 
134       // remove useless parentheses, moving external parentheses toward central digit element
135       if(_prefix == Modifier::NONE && parenth[1] == Parenthesis::NONE) {
136             parenth[1] = parenth[0];
137             parenth[0] = Parenthesis::NONE;
138             }
139       if(_digit == FBIDigitNone && parenth[2] == Parenthesis::NONE) {
140             parenth[2] = parenth[1];
141             parenth[1] = Parenthesis::NONE;
142             }
143       if(_contLine == ContLine::NONE && parenth[3] == Parenthesis::NONE) {
144             parenth[3] = parenth[4];
145             parenth[4] = Parenthesis::NONE;
146             }
147       if(_suffix == Modifier::NONE && parenth[2] == Parenthesis::NONE) {
148             parenth[2] = parenth[3];
149             parenth[3] = Parenthesis::NONE;
150             }
151 
152       // some checks:
153       // if some extra input, str is not conformant
154       if(str.size())
155             return false;
156       // can't have BOTH prefix and suffix
157       // prefix, digit, suffix and cont.line cannot be ALL empty
158       // suffix cannot combine with empty digit
159       if( (_prefix != Modifier::NONE && _suffix != Modifier::NONE)
160             || (_prefix == Modifier::NONE && _digit == FBIDigitNone && _suffix == Modifier::NONE && _contLine == ContLine::NONE)
161             || ( (_suffix == Modifier::CROSS || _suffix == Modifier::BACKSLASH || _suffix == Modifier::SLASH)
162                   && _digit == FBIDigitNone) )
163             return false;
164       return true;
165 }
166 
167 //---------------------------------------------------------
168 //   FiguredBassItem parsePrefixSuffix()
169 //
170 //    scans str to extract prefix or suffix properties. Stops at the first char which cannot fit.
171 //    Fitting chars are removed from str. DOES NOT generate any display text
172 //
173 // returns the number of QChar's read from str or -1 if prefix / suffix has an illegal format
174 // (no prefix / suffix at all IS legal)
175 //---------------------------------------------------------
176 
parsePrefixSuffix(QString & str,bool bPrefix)177 int FiguredBassItem::parsePrefixSuffix(QString& str, bool bPrefix)
178       {
179       Modifier *  dest  = bPrefix ? &_prefix : &_suffix;
180       bool        done  = false;
181       int         size  = str.size();
182       str = str.trimmed();
183 
184       *dest       = Modifier::NONE;
185 
186       while(str.size()) {
187             switch(str.at(0).unicode())
188             {
189             case 'b':
190                   if(*dest != Modifier::NONE) {
191                         if(*dest == Modifier::FLAT)     // FLAT may double a previous FLAT
192                               *dest = Modifier::DOUBLEFLAT;
193                         else
194                               return -1;              // but no other combination is acceptable
195                         }
196                   else
197                         *dest = Modifier::FLAT;
198                   break;
199             case 'h':
200                   if(*dest != Modifier::NONE)           // cannot combine with any other accidental
201                         return -1;
202                   *dest = Modifier::NATURAL;
203                   break;
204             case '#':
205                   if(*dest != Modifier::NONE) {
206                         if(*dest == Modifier::SHARP)    // SHARP may double a previous SHARP
207                               *dest = Modifier::DOUBLESHARP;
208                         else
209                               return -1;              // but no other combination is acceptable
210                         }
211                   else
212                         *dest = Modifier::SHARP;
213                   break;
214             case '+':
215                   // accept '+' as both a prefix and a suffix for harmony notation
216                   if(*dest != Modifier::NONE)           // cannot combine with any other accidental
217                         return -1;
218                   *dest = Modifier::CROSS;
219                   break;
220             // '\\' and '/' go into the suffix
221             case '\\':
222                   if(_suffix != Modifier::NONE)         // cannot combine with any other accidental
223                         return -1;
224                   _suffix = Modifier::BACKSLASH;
225                   break;
226             case '/':
227                   if(_suffix != Modifier::NONE)         // cannot combine with any other accidental
228                         return -1;
229                   _suffix = Modifier::SLASH;
230                   break;
231             default:                                 // any other char: no longer in prefix/suffix
232                   done = true;
233                   break;
234             }
235             if(done)
236                   break;
237             str.remove(0,1);                         // 'eat' the char and continue
238             }
239 
240       return size - str.size();                      // return how many chars we had read into prefix/suffix
241       }
242 
243 //---------------------------------------------------------
244 //   FiguredBassItem parseDigit()
245 //
246 //    scans str to extract digit properties. Stops at the first char which cannot belong to digit part.
247 //    Fitting chars are removed from str. DOES NOT generate any display text
248 //
249 // returns the number of QChar's read from str or -1 if no legal digit can be constructed
250 // (no digit at all IS legal)
251 //---------------------------------------------------------
252 
parseDigit(QString & str)253 int FiguredBassItem::parseDigit(QString& str)
254       {
255       int  size   = str.size();
256       str         = str.trimmed();
257 
258       _digit = FBIDigitNone;
259 
260       while(str.size()) {
261             // any digit acceptable
262             if(str[0] >= '0' && str[0] <= '9') {
263                   if (_digit == FBIDigitNone)
264                         _digit = 0;
265                   _digit = _digit*10 + (str[0].unicode() - '0');
266                   str.remove(0, 1);
267                   }
268             // anything else: no longer in digit part
269             else
270                   break;
271             }
272 
273       return size  - str.size();
274       }
275 
276 //---------------------------------------------------------
277 //   FiguredBassItem parseParenthesis()
278 //
279 //    scans str to extract a (possible) parenthesis, stores its code into parenth[parenthIdx]
280 //    and removes it from str. Only looks at first str char.
281 //
282 // returns the number of QChar's read from str (actually 0 or 1).
283 //---------------------------------------------------------
284 
parseParenthesis(QString & str,int parenthIdx)285 int FiguredBassItem::parseParenthesis(QString& str, int parenthIdx)
286       {
287       int c = str[0].unicode();
288       Parenthesis code = Parenthesis::NONE;
289       switch(c)
290       {
291       case '(':
292             code = Parenthesis::ROUNDOPEN;
293             break;
294       case ')':
295             code = Parenthesis::ROUNDCLOSED;
296             break;
297       case '[':
298             code =Parenthesis::SQUAREDOPEN;
299             break;
300       case ']':
301             code = Parenthesis::SQUAREDCLOSED;
302             break;
303       default:
304             break;
305             }
306       parenth[parenthIdx] = code;
307       if(code != Parenthesis::NONE) {
308             str.remove(0, 1);
309             return 1;
310             }
311       return 0;
312       }
313 
314 //---------------------------------------------------------
315 //   FiguredBassItem normalizedText()
316 //
317 // returns a string with the normalized text, i.e. the text displayed while editing;
318 // this is a standard textual representation of the item properties
319 //---------------------------------------------------------
320 
normalizedText() const321 QString FiguredBassItem::normalizedText() const
322       {
323       QString str = QString();
324       if(parenth[0] != Parenthesis::NONE)
325             str.append(normParenthToChar[int(parenth[0])]);
326 
327       if(_prefix != Modifier::NONE) {
328             switch(_prefix)
329             {
330             case Modifier::FLAT:
331                   str.append('b');
332                   break;
333             case Modifier::NATURAL:
334                   str.append('h');
335                   break;
336             case Modifier::SHARP:
337                   str.append('#');
338                   break;
339             case Modifier::CROSS:
340                   str.append('+');
341                   break;
342             case Modifier::DOUBLEFLAT:
343                   str.append("bb");
344                   break;
345             case Modifier::DOUBLESHARP:
346                   str.append("##");
347                   break;
348             default:
349                   break;
350             }
351             }
352 
353       if(parenth[1] != Parenthesis::NONE)
354             str.append(normParenthToChar[int(parenth[1])]);
355 
356       // digit
357       if(_digit != FBIDigitNone)
358             str.append(QString::number(_digit));
359 
360       if(parenth[2] != Parenthesis::NONE)
361             str.append(normParenthToChar[int(parenth[2])]);
362 
363       // suffix
364       if(_suffix != Modifier::NONE) {
365             switch(_suffix)
366             {
367             case Modifier::FLAT:
368                   str.append('b');
369                   break;
370             case Modifier::NATURAL:
371                   str.append('h');
372                   break;
373             case Modifier::SHARP:
374                   str.append('#');
375                   break;
376             case Modifier::CROSS:
377                   str.append('+');
378                   break;
379             case Modifier::BACKSLASH:
380                   str.append('\\');
381                   break;
382             case Modifier::SLASH:
383                   str.append('/');
384                   break;
385             case Modifier::DOUBLEFLAT:
386                   str.append("bb");
387                   break;
388             case Modifier::DOUBLESHARP:
389                   str.append("##");
390                   break;
391             default:
392                   break;
393             }
394             }
395 
396       if(parenth[3] != Parenthesis::NONE)
397             str.append(normParenthToChar[int(parenth[3])]);
398       if(_contLine > ContLine::NONE) {
399             str.append('_');
400             if (_contLine > ContLine::SIMPLE)
401                   str.append('_');
402             }
403       if(parenth[4] != Parenthesis::NONE)
404             str.append(normParenthToChar[int(parenth[4])]);
405 
406       return str;
407       }
408 
409 //---------------------------------------------------------
410 //   FiguredBassItem write()
411 //---------------------------------------------------------
412 
write(XmlWriter & xml) const413 void FiguredBassItem::write(XmlWriter& xml) const
414       {
415       xml.stag("FiguredBassItem", this);
416       xml.tagE(QString("brackets b0=\"%1\" b1=\"%2\" b2=\"%3\" b3=\"%4\" b4=\"%5\"")
417                     .arg(int(parenth[0])) .arg(int(parenth[1])) .arg(int(parenth[2])) .arg(int(parenth[3])) .arg(int(parenth[4])) );
418       if (_prefix != Modifier::NONE)
419             xml.tag(QString("prefix"), int(_prefix));
420       if (_digit != FBIDigitNone)
421             xml.tag(QString("digit"), _digit);
422       if (_suffix != Modifier::NONE)
423             xml.tag(QString("suffix"), int(_suffix));
424       if (_contLine != ContLine::NONE)
425             xml.tag("continuationLine", int(_contLine));
426       xml.etag();
427       }
428 
429 //---------------------------------------------------------
430 //   FiguredBassItem read()
431 //---------------------------------------------------------
432 
read(XmlReader & e)433 void FiguredBassItem::read(XmlReader& e)
434       {
435       while (e.readNextStartElement()) {
436             const QStringRef& tag(e.name());
437 
438             if (tag == "brackets") {
439                   parenth[0] = (Parenthesis)e.intAttribute("b0");
440                   parenth[1] = (Parenthesis)e.intAttribute("b1");
441                   parenth[2] = (Parenthesis)e.intAttribute("b2");
442                   parenth[3] = (Parenthesis)e.intAttribute("b3");
443                   parenth[4] = (Parenthesis)e.intAttribute("b4");
444                   e.readNext();
445                   }
446             else if (tag == "prefix")
447                   _prefix = (Modifier)(e.readInt());
448             else if (tag == "digit")
449                   _digit = e.readInt();
450             else if (tag == "suffix")
451                   _suffix = (Modifier)(e.readInt());
452             else if(tag == "continuationLine")
453                   _contLine = (ContLine)(e.readInt());
454             else if (!Element::readProperties(e))
455                   e.unknown();
456             }
457       }
458 
459 //---------------------------------------------------------
460 //   FiguredBassItem layout()
461 //    creates the display text (set as element text) and computes
462 //    the horiz. offset needed to align the right part as well as the vert. offset
463 //---------------------------------------------------------
464 
layout()465 void FiguredBassItem::layout()
466       {
467       qreal             h, w, x, x1, x2, y;
468 
469       // construct font metrics
470       int   fontIdx = 0;
471       QFont f(g_FBFonts.at(fontIdx).family);
472 
473       // font size in pixels, scaled according to spatium()
474       // (use the same font selection as used in draw() below)
475       qreal m = score()->styleD(Sid::figuredBassFontSize) * spatium() / SPATIUM20;
476       f.setPointSizeF(m);
477       QFontMetricsF fm(f, MScore::paintDevice());
478 
479       QString str;
480       x  = symWidth(SymId::noteheadBlack) * .5;
481       x1 = x2 = 0.0;
482 
483       // create display text
484       int font = 0;
485       int style = score()->styleI(Sid::figuredBassStyle);
486 
487       if (parenth[0] != Parenthesis::NONE)
488             str.append(g_FBFonts.at(font).displayParenthesis[int(parenth[0])]);
489 
490       // prefix
491       if (_prefix != Modifier::NONE) {
492             // if no digit, the string created so far 'hangs' to the left of the note
493             if(_digit == FBIDigitNone)
494                   x1 = fm.width(str);
495             str.append(g_FBFonts.at(font).displayAccidental[int(_prefix)]);
496             // if no digit, the string from here onward 'hangs' to the right of the note
497             if(_digit == FBIDigitNone)
498                   x2 = fm.width(str);
499             }
500 
501       if(parenth[1] != Parenthesis::NONE)
502             str.append(g_FBFonts.at(font).displayParenthesis[int(parenth[1])]);
503 
504       // digit
505       if(_digit != FBIDigitNone) {
506             // if some digit, the string created so far 'hangs' to the left of the note
507             x1 = fm.width(str);
508             // if suffix is a combining shape, combine it with digit (multi-digit numbers cannot be combined)
509             // unless there is a parenthesis in between
510             if( (_digit < 10)
511                         && (_suffix == Modifier::CROSS || _suffix == Modifier::BACKSLASH || _suffix == Modifier::SLASH)
512                         && parenth[2] == Parenthesis::NONE)
513                   str.append(g_FBFonts.at(font).displayDigit[style][_digit][int(_suffix)-(int(Modifier::CROSS)-1)]);
514             // if several digits or no shape combination, convert _digit to font styled chars
515             else {
516                   QString digits    = QString();
517                   int digit         = _digit;
518                   while (true) {
519                         digits.prepend(g_FBFonts.at(font).displayDigit[style][(digit % 10)][0]);
520                         digit /= 10;
521                         if (digit == 0)
522                               break;
523                         }
524                   str.append(digits);
525                   }
526             // if some digit, the string from here onward 'hangs' to the right of the note
527             x2 = fm.width(str);
528             }
529 
530       if(parenth[2] != Parenthesis::NONE)
531             str.append(g_FBFonts.at(font).displayParenthesis[int(parenth[2])]);
532 
533       // suffix
534       // append only if non-combining shape or cannot combine (no digit or parenthesis in between)
535       if( _suffix != Modifier::NONE
536                   && ( (_suffix != Modifier::CROSS && _suffix != Modifier::BACKSLASH && _suffix != Modifier::SLASH)
537                         || _digit == FBIDigitNone
538                         || parenth[2] != Parenthesis::NONE) )
539             str.append(g_FBFonts.at(font).displayAccidental[int(_suffix)]);
540 
541       if(parenth[3] != Parenthesis::NONE)
542             str.append(g_FBFonts.at(font).displayParenthesis[int(parenth[3])]);
543 
544       setDisplayText(str);                // this text will be displayed
545 
546       if (str.size())                     // if some text
547             x = x - (x1+x2) * 0.5;        // position the text so that [x1<-->x2] is centered below the note
548       else                                // if no text (but possibly a line)
549             x = 0;                        // start at note left margin
550       // vertical position
551       h = fm.lineSpacing();
552       h *= score()->styleD(Sid::figuredBassLineHeight);
553       if (score()->styleI(Sid::figuredBassAlignment) == 0)          // top alignment: stack down from first item
554             y = h * ord;
555       else                                                        // bottom alignment: stack up from last item
556             y = -h * (figuredBass()->numOfItems() - ord);
557       setPos(x, y);
558       // determine bbox from text width
559 //      w = fm.width(str);
560       w = fm.boundingRect(str).width();
561       textWidth = w;
562       // if there is a cont.line, extend width to cover the whole FB element duration line
563       int lineLen;
564       if(_contLine != ContLine::NONE && (lineLen=figuredBass()->lineLength(0)) > w)
565             w = lineLen;
566       bbox().setRect(0, 0, w, h);
567       }
568 
569 //---------------------------------------------------------
570 //   FiguredBassItem draw()
571 //---------------------------------------------------------
572 
draw(QPainter * painter) const573 void FiguredBassItem::draw(QPainter* painter) const
574       {
575       int   font = 0;
576       qreal _spatium = spatium();
577       // set font from general style
578       QFont f(g_FBFonts.at(font).family);
579 #ifdef USE_GLYPHS
580       f.setHintingPreference(QFont::PreferVerticalHinting);
581 #endif
582       // (use the same font selection as used in layout() above)
583       qreal m = score()->styleD(Sid::figuredBassFontSize) * spatium() / SPATIUM20;
584       f.setPointSizeF(m * MScore::pixelRatio);
585 
586       painter->setFont(f);
587       painter->setBrush(Qt::NoBrush);
588       QPen pen(figuredBass()->curColor(), FB_CONTLINE_THICKNESS * _spatium, Qt::SolidLine, Qt::RoundCap);
589       painter->setPen(pen);
590       painter->drawText(bbox(), Qt::TextDontClip | Qt::AlignLeft | Qt::AlignTop, displayText());
591 
592       // continuation line
593       qreal lineEndX = 0.0;
594       if (_contLine != ContLine::NONE) {
595             qreal lineStartX  = textWidth;                       // by default, line starts right after text
596             if (lineStartX > 0.0)
597                   lineStartX += _spatium * FB_CONTLINE_LEFT_PADDING;    // if some text, give some room after it
598             lineEndX = figuredBass()->printedLineLength();        // by default, line ends with item duration
599             if (lineEndX - lineStartX < 1.0)                       // if line length < 1 sp, ignore it
600                   lineEndX = 0.0;
601 
602             // if extended cont.line and no closing parenthesis: look at next FB element
603             if (_contLine > ContLine::SIMPLE && parenth[4] == Parenthesis::NONE) {
604                   FiguredBass * nextFB;
605                   // if there is a contiguous FB element
606                   if ( (nextFB=figuredBass()->nextFiguredBass()) != 0) {
607                         // retrieve the X position (in page coords) of a possible cont. line of nextFB
608                         // on the same line of 'this'
609                         QPointF pgPos = pagePos();
610                         qreal nextContPageX = nextFB->additionalContLineX(pgPos.y());
611                         // if an additional cont. line has been found, extend up to its initial X coord
612                         if (nextContPageX > 0)
613                               lineEndX = nextContPageX - pgPos.x() + _spatium*FB_CONTLINE_OVERLAP;
614                                                                               // with a little bit of overlap
615                         else
616                               lineEndX = figuredBass()->lineLength(0);        // if none found, draw to the duration end
617                         }
618                   }
619             // if some line, draw it
620             if (lineEndX > 0.0) {
621                   qreal h = bbox().height() * FB_CONTLINE_HEIGHT;
622                   painter->drawLine(lineStartX, h, lineEndX - ipos().x(), h);
623                   }
624             }
625 
626       // closing cont.line parenthesis
627       if (parenth[4] != Parenthesis::NONE) {
628             int x = lineEndX > 0.0 ? lineEndX : textWidth;
629             painter->drawText(QRectF(x, 0, bbox().width(), bbox().height()), Qt::AlignLeft | Qt::AlignTop,
630                   g_FBFonts.at(font).displayParenthesis[int(parenth[4])]);
631             }
632       }
633 
634 //---------------------------------------------------------
635 //   PROPERTY METHODS
636 //---------------------------------------------------------
637 
getProperty(Pid propertyId) const638 QVariant FiguredBassItem::getProperty(Pid propertyId) const
639       {
640       switch(propertyId) {
641             case Pid::FBPREFIX:
642                   return int(_prefix);
643             case Pid::FBDIGIT:
644                   return _digit;
645             case Pid::FBSUFFIX:
646                   return int(_suffix);
647             case Pid::FBCONTINUATIONLINE:
648                   return int(_contLine);
649             case Pid::FBPARENTHESIS1:
650                   return int(parenth[0]);
651             case Pid::FBPARENTHESIS2:
652                   return int(parenth[1]);
653             case Pid::FBPARENTHESIS3:
654                   return int(parenth[2]);
655             case Pid::FBPARENTHESIS4:
656                   return int(parenth[3]);
657             case Pid::FBPARENTHESIS5:
658                   return int(parenth[4]);
659             default:
660                   return Element::getProperty(propertyId);
661             }
662       }
663 
setProperty(Pid propertyId,const QVariant & v)664 bool FiguredBassItem::setProperty(Pid propertyId, const QVariant& v)
665       {
666       score()->addRefresh(canvasBoundingRect());
667       int   val = v.toInt();
668       switch(propertyId) {
669             case Pid::FBPREFIX:
670                   if(val < int(Modifier::NONE) || val >= int(Modifier::NUMOF))
671                         return false;
672                   _prefix = (Modifier)val;
673                   break;
674             case Pid::FBDIGIT:
675                   if(val < 1 || val > 9)
676                         return false;
677                   _digit = val;
678                   break;
679             case Pid::FBSUFFIX:
680                   if(val < int(Modifier::NONE) || val >= int(Modifier::NUMOF))
681                         return false;
682                   _suffix = (Modifier)val;
683                   break;
684             case Pid::FBCONTINUATIONLINE:
685                   _contLine = (ContLine)val;
686                   break;
687             case Pid::FBPARENTHESIS1:
688                   if(val < int(Parenthesis::NONE) || val >= int(Parenthesis::NUMOF))
689                         return false;
690                   parenth[0] = (Parenthesis)val;
691                   break;
692             case Pid::FBPARENTHESIS2:
693                   if(val < int(Parenthesis::NONE) || val >= int(Parenthesis::NUMOF))
694                         return false;
695                   parenth[1] = (Parenthesis)val;
696                   break;
697             case Pid::FBPARENTHESIS3:
698                   if(val < int(Parenthesis::NONE) || val >= int(Parenthesis::NUMOF))
699                         return false;
700                   parenth[2] = (Parenthesis)val;
701                   break;
702             case Pid::FBPARENTHESIS4:
703                   if(val < int(Parenthesis::NONE) || val >= int(Parenthesis::NUMOF))
704                         return false;
705                   parenth[3] = (Parenthesis)val;
706                   break;
707             case Pid::FBPARENTHESIS5:
708                   if(val < int(Parenthesis::NONE) || val >= int(Parenthesis::NUMOF))
709                         return false;
710                   parenth[4] = (Parenthesis)val;
711                   break;
712             default:
713                   return Element::setProperty(propertyId, v);
714             }
715       triggerLayoutAll();
716       return true;
717       }
718 
propertyDefault(Pid id) const719 QVariant FiguredBassItem::propertyDefault(Pid id) const
720       {
721       switch(id) {
722             case Pid::FBPREFIX:
723             case Pid::FBSUFFIX:
724                   return int(Modifier::NONE);
725             case Pid::FBDIGIT:
726                   return FBIDigitNone;
727             case Pid::FBCONTINUATIONLINE:
728                   return false;
729             default:
730                   return Element::propertyDefault(id);
731             }
732       }
733 
734 //---------------------------------------------------------
735 //   UNDOABLE PROPERTY SETTERS
736 //---------------------------------------------------------
737 
undoSetPrefix(Modifier pref)738 void FiguredBassItem::undoSetPrefix(Modifier pref)
739       {
740       if(pref <= Modifier::CROSS) {
741             undoChangeProperty(Pid::FBPREFIX, (int)pref);
742             // if setting some prefix and there is a suffix already, clear suffix
743             if(pref != Modifier::NONE && _suffix != Modifier::NONE)
744                   undoChangeProperty(Pid::FBSUFFIX, int(Modifier::NONE));
745             layout();                     // re-generate displayText
746             }
747       }
748 
undoSetDigit(int digit)749 void FiguredBassItem::undoSetDigit(int digit)
750       {
751       if (digit >= 0 && digit <= 9) {
752             undoChangeProperty(Pid::FBDIGIT, digit);
753             layout();                     // re-generate displayText
754             }
755       }
756 
undoSetSuffix(Modifier suff)757 void FiguredBassItem::undoSetSuffix(Modifier suff)
758       {
759       undoChangeProperty(Pid::FBSUFFIX, int(suff));
760       // if setting some suffix and there is a prefix already, clear prefix
761       if(suff != Modifier::NONE && _prefix != Modifier::NONE)
762             undoChangeProperty(Pid::FBPREFIX, int(Modifier::NONE));
763       layout();                     // re-generate displayText
764       }
765 
undoSetContLine(ContLine val)766 void FiguredBassItem::undoSetContLine(ContLine val)
767       {
768       undoChangeProperty(Pid::FBCONTINUATIONLINE, int(val));
769       layout();                     // re-generate displayText
770       }
771 
undoSetParenth1(Parenthesis par)772 void FiguredBassItem::undoSetParenth1(Parenthesis par)
773       {
774       undoChangeProperty(Pid::FBPARENTHESIS1, int(par));
775       layout();                     // re-generate displayText
776       }
undoSetParenth2(Parenthesis par)777 void FiguredBassItem::undoSetParenth2(Parenthesis par)
778       {
779       undoChangeProperty(Pid::FBPARENTHESIS2, int(par));
780       layout();                     // re-generate displayText
781       }
undoSetParenth3(Parenthesis par)782 void FiguredBassItem::undoSetParenth3(Parenthesis par)
783       {
784       undoChangeProperty(Pid::FBPARENTHESIS3, int(par));
785       layout();                     // re-generate displayText
786       }
undoSetParenth4(Parenthesis par)787 void FiguredBassItem::undoSetParenth4(Parenthesis par)
788       {
789       undoChangeProperty(Pid::FBPARENTHESIS4, int(par));
790       layout();                     // re-generate displayText
791       }
undoSetParenth5(Parenthesis par)792 void FiguredBassItem::undoSetParenth5(Parenthesis par)
793       {
794       undoChangeProperty(Pid::FBPARENTHESIS5, int(par));
795       layout();                     // re-generate displayText
796       }
797 
798 //---------------------------------------------------------
799 //
800 //    MusicXML I/O
801 //
802 //---------------------------------------------------------
803 
804 //---------------------------------------------------------
805 //   Convert MusicXML prefix/suffix to Modifier
806 //---------------------------------------------------------
807 
MusicXML2Modifier(const QString prefix) const808 FiguredBassItem::Modifier FiguredBassItem::MusicXML2Modifier(const QString prefix) const
809       {
810       if (prefix == "sharp")
811             return Modifier::SHARP;
812       else if (prefix == "flat")
813             return Modifier::FLAT;
814       else if (prefix == "natural")
815             return Modifier::NATURAL;
816       else if (prefix == "double-sharp")
817             return Modifier::DOUBLESHARP;
818       else if (prefix == "flat-flat")
819             return Modifier::DOUBLEFLAT;
820       else if (prefix == "sharp-sharp")
821             return Modifier::DOUBLESHARP;
822       else if (prefix == "cross")
823             return Modifier::CROSS;
824       else if (prefix == "backslash")
825             return Modifier::BACKSLASH;
826       else if (prefix == "slash")
827             return Modifier::SLASH;
828       else
829             return Modifier::NONE;
830       }
831 
832 //---------------------------------------------------------
833 //   Convert Modifier to MusicXML prefix/suffix
834 //---------------------------------------------------------
835 
Modifier2MusicXML(FiguredBassItem::Modifier prefix) const836 QString FiguredBassItem::Modifier2MusicXML(FiguredBassItem::Modifier prefix) const
837       {
838       switch (prefix) {
839             case Modifier::NONE:        return "";
840             case Modifier::DOUBLEFLAT:  return "flat-flat";
841             case Modifier::FLAT:        return "flat";
842             case Modifier::NATURAL:     return "natural";
843             case Modifier::SHARP:       return "sharp";
844             case Modifier::DOUBLESHARP: return "double-sharp";
845             case Modifier::CROSS:       return "cross";
846             case Modifier::BACKSLASH:   return "backslash";
847             case Modifier::SLASH:       return "slash";
848             case Modifier::NUMOF:       return ""; // prevent gcc "‘FBINumOfAccid’ not handled in switch" warning
849             }
850       return "";
851       }
852 
853 //---------------------------------------------------------
854 //   Read MusicXML
855 //
856 // Set the FiguredBassItem state based on the MusicXML <figure> node de.
857 // In MusicXML, parentheses is set to "yes" or "no" for the figured-bass
858 // node instead of for each individual <figure> node.
859 //---------------------------------------------------------
860 
861 #if 0
862 void FiguredBassItem::readMusicXML(XmlReader& e, bool paren)
863       {
864       // read the <figure> node de
865       while (e.readNextStartElement()) {
866             const QStringRef& tag(e.name());
867             if (tag == "figure-number") {
868                   // MusicXML spec states figure-number is a number
869                   // MuseScore can only handle single digit
870                   int iVal = e.readInt();
871                   if (1 <= iVal && iVal <= 9)
872                         _digit = iVal;
873                   }
874             else if (tag == "prefix")
875                   _prefix = MusicXML2Modifier(e.readElementText());
876             else if (tag == "suffix")
877                   _suffix = MusicXML2Modifier(e.readElementText());
878             else
879                   e.unknown();
880             }
881       // set parentheses
882       if (paren) {
883             // parenthesis open
884             if (_prefix != Modifier::NONE)
885                   parenth[0] = Parenthesis::ROUNDOPEN; // before prefix
886             else if (_digit != FBIDigitNone)
887                   parenth[1] = Parenthesis::ROUNDOPEN; // before digit
888             else if (_suffix != Modifier::NONE)
889                   parenth[2] = Parenthesis::ROUNDOPEN; // before suffix
890             // parenthesis close
891             if (_suffix != Modifier::NONE)
892                   parenth[3] = Parenthesis::ROUNDCLOSED; // after suffix
893             else if (_digit != FBIDigitNone)
894                   parenth[2] = Parenthesis::ROUNDCLOSED; // after digit
895             else if (_prefix != Modifier::NONE)
896                   parenth[1] = Parenthesis::ROUNDCLOSED; // after prefix
897             }
898       }
899 #endif
900 
901 //---------------------------------------------------------
902 //   Write MusicXML
903 //
904 // Writes the portion within the <figure> tag.
905 //
906 // NOTE: Both MuseScore and MusicXML provide two ways of altering the (temporal) length of a
907 // figured bass object: extension lines and duration. The convention is that an EXTENSION is
908 // used if the figure lasts LONGER than the note (i.e., it "extends" to the following notes),
909 // whereas DURATION is used if the figure lasts SHORTER than the note (e.g., when notating a
910 // figure change under a note). However, MuseScore does not restrict durations in this way,
911 // allowing them to act as extensions themselves. As a result, a few more branches are
912 // required in the decision tree to handle everything correctly.
913 //---------------------------------------------------------
914 
writeMusicXML(XmlWriter & xml,bool isOriginalFigure,int crEndTick,int fbEndTick) const915 void FiguredBassItem::writeMusicXML(XmlWriter& xml, bool isOriginalFigure, int crEndTick, int fbEndTick) const
916       {
917       xml.stag("figure");
918 
919       // The first figure of each group is the "original" figure. Practically, it is one inserted manually
920       // by the user, rather than automatically by the "duration" extend method.
921       if (isOriginalFigure) {
922             QString strPrefix = Modifier2MusicXML(_prefix);
923             if (strPrefix != "")
924                   xml.tag("prefix", strPrefix);
925             if (_digit != FBIDigitNone)
926                   xml.tag("figure-number", _digit);
927             QString strSuffix = Modifier2MusicXML(_suffix);
928             if (strSuffix != "")
929                   xml.tag("suffix", strSuffix);
930 
931             // Check if the figure ends before or at the same time as the current note. Otherwise, the figure
932             // extends to the next note, and so carries an extension type "start" by definition.
933             if (fbEndTick <= crEndTick) {
934                   if (_contLine == ContLine::SIMPLE)
935                         xml.tagE("extend type=\"stop\" ");
936                   else if (_contLine == ContLine::EXTENDED) {
937                         bool hasFigure = (strPrefix != "" || _digit != FBIDigitNone || strSuffix != "");
938                         if (hasFigure)
939                               xml.tagE("extend type=\"start\" ");
940                         else
941                               xml.tagE("extend type=\"continue\" ");
942                         }
943                   }
944             else
945                   xml.tagE("extend type=\"start\" ");
946             }
947       // If the figure is not "original", it must have been created using the "duration" feature of figured bass.
948       // In other words, the original figure belongs to a previous note rather than the current note.
949       else {
950             if (crEndTick < fbEndTick)
951                   xml.tagE("extend type=\"continue\" ");
952             else
953                   xml.tagE("extend type=\"stop\" ");
954             }
955       xml.etag();
956       }
957 
958 //---------------------------------------------------------
959 //   startsWithParenthesis
960 //---------------------------------------------------------
961 
startsWithParenthesis() const962 bool FiguredBassItem::startsWithParenthesis() const
963       {
964       if (_prefix != Modifier::NONE)
965             return (parenth[0] != Parenthesis::NONE);
966       if (_digit != FBIDigitNone)
967             return (parenth[1] != Parenthesis::NONE);
968       if (_suffix != Modifier::NONE)
969             return (parenth[2] != Parenthesis::NONE);
970       return false;
971       }
972 
973 //---------------------------------------------------------
974 //   F I G U R E D   B A S S
975 //---------------------------------------------------------
976 
FiguredBass(Score * s)977 FiguredBass::FiguredBass(Score* s)
978    : TextBase(s, ElementFlag::MOVABLE | ElementFlag::ON_STAFF)
979       {
980       initElementStyle(&figuredBassStyle);
981       // figured bass inherits from TextBase for layout purposes
982       // but there is no specific text style to use as a basis for styled properties
983       // (this is true for historical reasons due to the special layout of figured bass)
984       // override the styled property definitions
985       for (const StyledProperty& p : *textStyle(tid()))
986             setPropertyFlags(p.pid, PropertyFlags::NOSTYLE);
987       for (const StyledProperty& p : figuredBassTextStyle) {
988             setPropertyFlags(p.pid, PropertyFlags::STYLED);
989             setProperty(p.pid, styleValue(p.pid, p.sid));
990             }
991       setOnNote(true);
992 #if 0  // TODO
993       TextStyle st(
994          g_FBFonts[0].family,
995          score()->styleD(Sid::figuredBassFontSize),
996          false,
997          false,
998          false,
999          Align::LEFT | Align::TOP,
1000          QPointF(0, score()->styleD(Sid::figuredBassYOffset)),
1001          OffsetType::SPATIUM);
1002       st.setSizeIsSpatiumDependent(true);
1003       setElementStyle(st);
1004 #endif
1005       setTicks(Fraction(0,1));
1006       qDeleteAll(items);
1007       items.clear();
1008       }
1009 
FiguredBass(const FiguredBass & fb)1010 FiguredBass::FiguredBass(const FiguredBass& fb)
1011    : TextBase(fb)
1012       {
1013       setOnNote(fb.onNote());
1014       setTicks(fb.ticks());
1015       for (auto i : fb.items) {     // deep copy is needed
1016             FiguredBassItem* fbi = new FiguredBassItem(*i);
1017             fbi->setParent(this);
1018             items.push_back(fbi);
1019             }
1020 //      items = fb.items;
1021       }
1022 
~FiguredBass()1023 FiguredBass::~FiguredBass()
1024       {
1025       for (FiguredBassItem* item : items)
1026             delete item;
1027       }
1028 
1029 //---------------------------------------------------------
1030 //   getPropertyStyle
1031 //---------------------------------------------------------
1032 
getPropertyStyle(Pid id) const1033 Sid FiguredBass::getPropertyStyle(Pid id) const
1034       {
1035       // do not use TextBase::getPropertyStyle
1036       // as most text style properties do not apply
1037       for (const StyledProperty& p : figuredBassTextStyle) {
1038             if (p.pid == id)
1039                   return p.sid;
1040             }
1041       return Element::getPropertyStyle(id);
1042       }
1043 
1044 //---------------------------------------------------------
1045 //   write
1046 //---------------------------------------------------------
1047 
write(XmlWriter & xml) const1048 void FiguredBass::write(XmlWriter& xml) const
1049       {
1050       if (!xml.canWrite(this))
1051             return;
1052       xml.stag(this);
1053       if (!onNote())
1054             xml.tag("onNote", onNote());
1055       if (ticks().isNotZero())
1056             xml.tag("ticks", ticks());
1057       // if unparseable items, write full text data
1058       if (items.size() < 1)
1059             TextBase::writeProperties(xml, true);
1060       else {
1061 //            if (textStyleType() != StyledPropertyListIdx::FIGURED_BASS)
1062 //                  // if all items parsed and not unstiled, we simply have a special style: write it
1063 //                  xml.tag("style", textStyle().name());
1064             for (FiguredBassItem* item : items)
1065                   item->write(xml);
1066             for (const StyledProperty& spp : *_elementStyle)
1067                   writeProperty(xml, spp.pid);
1068             Element::writeProperties(xml);
1069             }
1070       xml.etag();
1071       }
1072 
1073 //---------------------------------------------------------
1074 //   read
1075 //---------------------------------------------------------
1076 
read(XmlReader & e)1077 void FiguredBass::read(XmlReader& e)
1078       {
1079       QString normalizedText;
1080       int idx = 0;
1081       while (e.readNextStartElement()) {
1082             const QStringRef& tag(e.name());
1083             if (tag == "ticks")
1084                   setTicks(e.readFraction());
1085             else if (tag == "onNote")
1086                   setOnNote(e.readInt() != 0l);
1087             else if (tag == "FiguredBassItem") {
1088                   FiguredBassItem * pItem = new FiguredBassItem(score(), idx++);
1089                   pItem->setTrack(track());
1090                   pItem->setParent(this);
1091                   pItem->read(e);
1092                   items.push_back(pItem);
1093                   // add item normalized text
1094                   if(!normalizedText.isEmpty())
1095                         normalizedText.append('\n');
1096                   normalizedText.append(pItem->normalizedText());
1097                   }
1098 //            else if (tag == "style")
1099 //                  setStyledPropertyListIdx(e.readElementText());
1100             else if (!TextBase::readProperties(e))
1101                   e.unknown();
1102             }
1103       // if items could be parsed set normalized text
1104       if (items.size() > 0)
1105             setXmlText(normalizedText);      // this is the text to show while editing
1106       }
1107 
1108 //---------------------------------------------------------
1109 //   layout
1110 //---------------------------------------------------------
1111 
layout()1112 void FiguredBass::layout()
1113       {
1114       // if 'our' style, force 'our' style data from FiguredBass parameters
1115 #if 0
1116       if (textStyleType() == StyledPropertyListIdx::FIGURED_BASS) {
1117             TextStyle st(g_FBFonts[0].family, score()->styleD(Sid::figuredBassFontSize),
1118                         false, false, false, Align::LEFT | Align::TOP, QPointF(0, yOff),
1119                         OffsetType::SPATIUM);
1120             st.setSizeIsSpatiumDependent(true);
1121             setTextStyle(st);
1122             }
1123 #endif
1124 
1125       // VERTICAL POSITION:
1126       const qreal y = score()->styleD(Sid::figuredBassYOffset) * spatium();
1127       setPos(QPointF(0.0, y));
1128 
1129       // BOUNDING BOX and individual item layout (if required)
1130       TextBase::layout1(); // prepare structs and data expected by Text methods
1131       // if element could be parsed into items, layout each element
1132       // Items list will be empty in edit mode (see FiguredBass::startEdit).
1133       // TODO: consider disabling specific layout in case text style is changed (tid() != Tid::FIGURED_BASS).
1134       if (items.size() > 0) {
1135             layoutLines();
1136             bbox().setRect(0, 0, _lineLengths.at(0), 0);
1137             // layout each item and enlarge bbox to include items bboxes
1138             for (FiguredBassItem* item : items) {
1139                   item->layout();
1140                   addbbox(item->bbox().translated(item->pos()));
1141                   }
1142             }
1143       }
1144 
1145 //---------------------------------------------------------
1146 //   layoutLines
1147 //
1148 //    lays out the duration indicator line(s), filling the _lineLengths array
1149 //    and the length of printed lines (used by continuation lines)
1150 //---------------------------------------------------------
1151 
layoutLines()1152 void FiguredBass::layoutLines()
1153       {
1154       if (_ticks <= Fraction(0,1) || !segment()) {
1155             _lineLengths.resize(1);                         // be sure to always have
1156             _lineLengths[0] = 0;                            // at least 1 item in array
1157             return;
1158             }
1159 
1160       ChordRest* lastCR  = nullptr;                       // the last ChordRest of this
1161       Segment*  nextSegm = nullptr;                       // the Segment beyond this' segment
1162       Fraction  nextTick = segment()->tick() + _ticks;    // the tick beyond this' duration
1163 
1164       // locate the measure containing the last tick of this; it is either:
1165       // the same measure containing nextTick, if nextTick is not the first tick of a measure
1166       //    (and line should stop right before it)
1167       // or the previous measure, if nextTick is the first tick of a measure
1168       //    (and line should stop before any measure terminal segment (bar, clef, ...) )
1169 
1170       Measure* m = score()->tick2measure(nextTick-Fraction::fromTicks(1));
1171       if (m) {
1172             // locate the first segment (of ANY type) right after this' last tick
1173             for (nextSegm = m->first(SegmentType::All); nextSegm; nextSegm = nextSegm->next()) {
1174                   if (nextSegm->tick() >= nextTick)
1175                         break;
1176                   }
1177             // locate the last ChordRest of this
1178             if (nextSegm) {
1179                   int startTrack = trackZeroVoice(track());
1180                   int endTrack = startTrack + VOICES;
1181                   for (const Segment* seg = nextSegm->prev1(); seg; seg = seg->prev1()) {
1182                         for (int t = startTrack; t < endTrack; ++t) {
1183                               Element* el = seg->element(t);
1184                               if (el && el->isChordRest()) {
1185                                     lastCR = toChordRest(el);
1186                                     break;
1187                                     }
1188                               }
1189                         if (lastCR)
1190                               break;
1191                         }
1192                   }
1193             }
1194       if (!m || !nextSegm) {
1195             qDebug("FiguredBass layout: no segment found for tick %d", nextTick.ticks());
1196             _lineLengths.resize(1);                         // be sure to always have
1197             _lineLengths[0] = 0;                            // at least 1 item in array
1198             return;
1199             }
1200 
1201       // get length of printed lines from horiz. page position of lastCR
1202       // (enter a bit 'into' the ChordRest for clarity)
1203       _printedLineLength = lastCR ? lastCR->pageX() - pageX() + 1.5 * spatium() : 3 * spatium();
1204 
1205       // get duration indicator line(s) from page position of nextSegm
1206       const QList<System*>& systems = score()->systems();
1207       System* s1  = segment()->measure()->system();
1208       System* s2  = nextSegm->measure()->system();
1209       int sysIdx1 = systems.indexOf(s1);
1210       int sysIdx2 = systems.indexOf(s2);
1211 
1212       if (sysIdx2 < sysIdx1) {
1213             sysIdx2 = sysIdx1;
1214             nextSegm = segment()->next1();
1215             // TODO
1216             // During layout of figured bass next systems' numbers may be still
1217             // undefined (then sysIdx2 == -1) or change in the future.
1218             // A layoutSystem() approach similar to that for spanners should
1219             // probably be implemented.
1220             }
1221 
1222       int i, len ,segIdx;
1223       for (i = sysIdx1, segIdx = 0; i <= sysIdx2; ++i, ++segIdx) {
1224             len = 0;
1225             if (sysIdx1 == sysIdx2 || i == sysIdx1) {
1226                   // single line
1227                   len = nextSegm->pageX() - pageX() - 4;         // stop 4 raster units before next segm
1228                   }
1229             else if (i == sysIdx1) {
1230                   // initial line
1231                   qreal w   = s1->staff(staffIdx())->bbox().right();
1232                   qreal x   = s1->pageX() + w;
1233                   len = x - pageX();
1234                   }
1235             else if (i > 0 && i != sysIdx2) {
1236                   // middle line
1237 qDebug("FiguredBass: duration indicator middle line not implemented");
1238                   }
1239             else if (i == sysIdx2) {
1240                   // end line
1241 qDebug("FiguredBass: duration indicator end line not implemented");
1242                   }
1243             // store length item, reusing array items if already present
1244             if (_lineLengths.size() <= segIdx)
1245                   _lineLengths.append(len);
1246             else
1247                   _lineLengths[segIdx] = len;
1248             }
1249       // if more array items than needed, truncate array
1250       if (_lineLengths.size() > segIdx)
1251             _lineLengths.resize(segIdx);
1252       }
1253 
1254 //---------------------------------------------------------
1255 //   draw
1256 //---------------------------------------------------------
1257 
draw(QPainter * painter) const1258 void FiguredBass::draw(QPainter* painter) const
1259       {
1260       // if not printing, draw duration line(s)
1261       if (!score()->printing() && score()->showUnprintable()) {
1262             for (qreal len : _lineLengths) {
1263                   if (len > 0) {
1264                         painter->setPen(QPen(Qt::lightGray, 3));
1265                         painter->drawLine(0.0, -2, len, -2);      // -2: 2 rast. un. above digits
1266                         }
1267                   }
1268             }
1269       // if in edit mode or with custom style, use standard text drawing
1270 //      if (editMode() || subStyle() != ElementStyle::FIGURED_BASS)
1271 //      if (tid() != Tid::FIGURED_BASS)
1272 //            TextBase::draw(painter);
1273 //      else
1274             {                                                // not edit mode:
1275             if (items.size() < 1)                           // if not parseable into f.b. items
1276                   TextBase::draw(painter);                      // draw as standard text
1277             else
1278                   for (FiguredBassItem* item : items) {     // if parseable into f.b. items
1279                         painter->translate(item->pos());    // draw each item in its proper position
1280                         item->draw(painter);
1281                         painter->translate(-item->pos());
1282                         }
1283             }
1284 /* DEBUG
1285       QString str = QString();
1286       str.setNum(_ticks);
1287       painter->drawText(0, (_onNote ? 40 : 30), str);
1288 */
1289       }
1290 
1291 //---------------------------------------------------------
1292 //   startEdit / edit / endEdit
1293 //---------------------------------------------------------
1294 
startEdit(EditData & ed)1295 void FiguredBass::startEdit(EditData& ed)
1296       {
1297       qDeleteAll(items);
1298       items.clear();
1299       layout1(); // re-layout without F.B.-specific formatting.
1300       TextBase::startEdit(ed);
1301       }
1302 
endEdit(EditData & ed)1303 void FiguredBass::endEdit(EditData& ed)
1304       {
1305       int idx;
1306 
1307       TextBase::endEdit(ed);
1308       // as the standard text editor keeps inserting spurious HTML formatting and styles
1309       // retrieve and work only on the plain text
1310       const QString txt = plainText();
1311       if (txt.isEmpty()) // if no text, nothing to do
1312             return;
1313 
1314       // split text into lines and create an item for each line
1315       QStringList list = txt.split('\n', QString::SkipEmptyParts);
1316       qDeleteAll(items);
1317       items.clear();
1318       QString normalizedText = QString();
1319       idx = 0;
1320       for (QString str : qAsConst(list)) {
1321             FiguredBassItem* pItem = new FiguredBassItem(score(), idx++);
1322             if(!pItem->parse(str)) {            // if any item fails parsing
1323                   qDeleteAll(items);
1324                   items.clear();                // clear item list
1325                   score()->startCmd();
1326                   triggerLayout();
1327                   score()->endCmd();
1328                   delete pItem;
1329                   return;
1330                   }
1331             pItem->setTrack(track());
1332             pItem->setParent(this);
1333             items.push_back(pItem);
1334 
1335             // add item normalized text
1336             if(!normalizedText.isEmpty())
1337                   normalizedText.append('\n');
1338             normalizedText.append(pItem->normalizedText());
1339             }
1340       // if all items parsed and text is styled, replaced entered text with normalized text
1341       if (items.size())
1342             setXmlText(normalizedText);
1343 
1344       score()->startCmd();
1345       triggerLayout();
1346       score()->endCmd();
1347       }
1348 
1349 //---------------------------------------------------------
1350 //   setSelected /setVisible
1351 //
1352 //    forward flags to items
1353 //---------------------------------------------------------
1354 
setSelected(bool flag)1355 void FiguredBass::setSelected(bool flag)
1356       {
1357       Element::setSelected(flag);
1358       for(FiguredBassItem* item : items) {
1359             item->setSelected(flag);
1360             }
1361       }
1362 
setVisible(bool flag)1363 void FiguredBass::setVisible(bool flag)
1364       {
1365       Element::setVisible(flag);
1366       for(FiguredBassItem* item : items) {
1367             item->setVisible(flag);
1368             }
1369       }
1370 
1371 //---------------------------------------------------------
1372 //   nextFiguredBass
1373 //
1374 //    returns the next *contiguous* FiguredBass element if it exists,
1375 //    i.e. the FiguredBass element which starts where 'this' ends
1376 //    returns 0 if none
1377 //---------------------------------------------------------
1378 
nextFiguredBass() const1379 FiguredBass* FiguredBass::nextFiguredBass() const
1380       {
1381       if (_ticks <= Fraction(0,1))                                      // if _ticks unset, no clear idea of when 'this' ends
1382             return 0;
1383       Segment* nextSegm;                                 // the Segment beyond this' segment
1384       Fraction nextTick = segment()->tick() + _ticks;    // the tick beyond this' duration
1385 
1386       // locate the ChordRest segment right after this' end
1387       nextSegm = score()->tick2segment(nextTick, true, SegmentType::ChordRest);
1388       if (nextSegm == 0)
1389             return 0;
1390 
1391       // scan segment annotations for an existing FB element in the this' staff
1392       for (Element* e : nextSegm->annotations())
1393             if (e->type() == ElementType::FIGURED_BASS && e->track() == track())
1394                   return toFiguredBass(e);
1395 
1396       return 0;
1397       }
1398 
1399 //---------------------------------------------------------
1400 //   additionalContLineX
1401 //
1402 //    if there is a continuation line, without other text elements, at pagePosY, returns its X coord (in page coords)
1403 //    returns 0 if no cont.line there or if there are text elements before the cont.line
1404 //
1405 //    In practice, returns the X coord of a cont. line which can be the continuation of a previous cont. line
1406 //
1407 //    Note: pagePosY is the Y coord of the FiguredBassItem containing the line, not of the line itself,
1408 //    as line position might depend on styles.
1409 //---------------------------------------------------------
1410 
additionalContLineX(qreal pagePosY) const1411 qreal FiguredBass::additionalContLineX(qreal pagePosY) const
1412 {
1413       QPointF pgPos = pagePos();
1414       for (FiguredBassItem* fbi : items)
1415             // if item has cont.line but nothing before it
1416             // and item Y coord near enough to pagePosY
1417             if(fbi->contLine() != FiguredBassItem::ContLine::NONE
1418                   && fbi->digit() == FBIDigitNone
1419                      && fbi->prefix() == FiguredBassItem::Modifier::NONE
1420                         && fbi->suffix() == FiguredBassItem::Modifier::NONE
1421                            && fbi->parenth4() == FiguredBassItem::Parenthesis::NONE
1422                               && qAbs(pgPos.y() + fbi->ipos().y() - pagePosY) < 0.05)
1423                   return pgPos.x() + fbi->ipos().x();
1424 
1425       return 0.0;                               // no suitable line
1426 }
1427 
1428 //---------------------------------------------------------
1429 //   PROPERTY METHODS
1430 //---------------------------------------------------------
1431 
getProperty(Pid propertyId) const1432 QVariant FiguredBass::getProperty(Pid propertyId) const
1433       {
1434       return TextBase::getProperty(propertyId);
1435       }
1436 
setProperty(Pid propertyId,const QVariant & v)1437 bool FiguredBass::setProperty(Pid propertyId, const QVariant& v)
1438       {
1439       score()->addRefresh(canvasBoundingRect());
1440       return TextBase::setProperty(propertyId, v);
1441       }
1442 
propertyDefault(Pid id) const1443 QVariant FiguredBass::propertyDefault(Pid id) const
1444       {
1445       return TextBase::propertyDefault(id);
1446       }
1447 
1448 //---------------------------------------------------------
1449 //   TEMPORARY HACK!!!
1450 //---------------------------------------------------------
1451 /*
1452 FiguredBassItem * FiguredBass::addItem()
1453       {
1454       int line = items.size();
1455       FiguredBassItem* fib = new FiguredBassItem(score(), line);
1456       // tell QML not to garbage collect this item
1457       QQmlEngine::setObjectOwnership(fib, QQmlEngine::CppOwnership);
1458       items.push_back(fib);
1459       return fib;
1460       }
1461 */
1462 //---------------------------------------------------------
1463 //   STATIC FUNCTION
1464 //    adding a new FiguredBass to a Segment;
1465 //    the main purpose of this function is to ensure that ONLY ONE F.b. element exists for each Segment/staff;
1466 //    it either re-uses an existing FiguredBass or creates a new one if none is found;
1467 //    returns the FiguredBass and sets pNew to true if it has been newly created.
1468 //
1469 //    Sets an initial duration of the element up to the next ChordRest of the same staff.
1470 //
1471 //    As the F.b. very concept requires the base chord to have ONLY ONE note,
1472 //    FiguredBass elements are created and looked for only in the first track of the staff.
1473 //---------------------------------------------------------
1474 
addFiguredBassToSegment(Segment * seg,int track,const Fraction & extTicks,bool * pNew)1475 FiguredBass* FiguredBass::addFiguredBassToSegment(Segment * seg, int track, const Fraction& extTicks, bool * pNew)
1476       {
1477       Fraction endTick;                      // where this FB is initially assumed to end
1478       int  staff = track / VOICES;       // convert track to staff
1479       track = staff * VOICES;                   // first track for this staff
1480 
1481       // scan segment annotations for an existing FB element in the same staff
1482       FiguredBass* fb = 0;
1483       for (Element* e : seg->annotations()) {
1484             if (e->type() == ElementType::FIGURED_BASS && (e->track() / VOICES) == staff) {
1485                   // an FB already exists in segment: re-use it
1486                   fb = toFiguredBass(e);
1487                   *pNew = false;
1488                   endTick = seg->tick() + fb->ticks();
1489                   break;
1490                   }
1491             }
1492       if (fb == 0) {                          // no FB at segment: create new
1493             fb = new FiguredBass(seg->score());
1494             fb->setTrack(track);
1495             fb->setParent(seg);
1496 
1497             // locate next SegChordRest in the same staff to estimate presumed duration of element
1498             endTick = Fraction(INT_MAX,1);
1499             Segment *   nextSegm;
1500             for (int iVoice = 0; iVoice < VOICES; iVoice++) {
1501                   nextSegm = seg->nextCR(track + iVoice);
1502                   if(nextSegm && nextSegm->tick() < endTick)
1503                         endTick = nextSegm->tick();
1504                   }
1505             if(endTick == Fraction(INT_MAX,1)) {            // no next segment: set up to score end
1506                   Measure * meas = seg->score()->lastMeasure();
1507                   endTick = meas->tick() + meas->ticks();
1508                   }
1509             fb->setTicks(endTick - seg->tick());
1510 
1511             // set onNote status
1512             fb->setOnNote(false);               // assume not onNote
1513             for (int i = track; i < track + VOICES; i++)         // if segment has chord in staff, set onNote
1514                   if (seg->element(i) && seg->element(i)->type() == ElementType::CHORD) {
1515                         fb->setOnNote(true);
1516                         break;
1517                   }
1518             *pNew = true;
1519             }
1520 
1521       // if we are extending a previous FB
1522       if (extTicks > Fraction(0,1)) {
1523             // locate previous FB for same staff
1524             Segment *         prevSegm;
1525             FiguredBass*      prevFB = 0;
1526             for(prevSegm = seg->prev1(SegmentType::ChordRest); prevSegm; prevSegm = prevSegm->prev1(SegmentType::ChordRest)) {
1527                   for (Element* e : prevSegm->annotations()) {
1528                         if (e->type() == ElementType::FIGURED_BASS && (e->track() ) == track) {
1529                               prevFB = toFiguredBass(e);   // previous FB found
1530                               break;
1531                               }
1532                         }
1533                   if(prevFB) {
1534                         // if previous FB did not stop more than extTicks before this FB...
1535                         Fraction delta = seg->tick() - prevFB->segment()->tick();
1536                         if (prevFB->ticks() + extTicks >= delta)
1537                               prevFB->setTicks(delta);      // update prev FB ticks to last up to this FB
1538                         break;
1539                         }
1540                   }
1541             }
1542       return fb;
1543       }
1544 
1545 //---------------------------------------------------------
1546 //   STATIC FUNCTIONS FOR FONT CONFIGURATION MANAGEMENT
1547 //---------------------------------------------------------
1548 
read(XmlReader & e)1549 bool FiguredBassFont::read(XmlReader& e)
1550       {
1551       while (e.readNextStartElement()) {
1552             const QStringRef& tag(e.name());
1553 
1554             if (tag == "family")
1555                   family = e.readElementText();
1556             else if (tag == "displayName")
1557                   displayName = e.readElementText();
1558             else if (tag == "defaultPitch")
1559                   defPitch = e.readDouble();
1560             else if (tag == "defaultLineHeight")
1561                   defLineHeight = e.readDouble();
1562             else if (tag == "parenthesisRoundOpen")
1563                   displayParenthesis[1] = e.readElementText()[0];
1564             else if (tag == "parenthesisRoundClosed")
1565                   displayParenthesis[2] = e.readElementText()[0];
1566             else if (tag == "parenthesisSquareOpen")
1567                   displayParenthesis[3] = e.readElementText()[0];
1568             else if (tag == "parenthesisSquareClosed")
1569                   displayParenthesis[4] = e.readElementText()[0];
1570             else if (tag == "doubleflat")
1571                   displayAccidental[int(FiguredBassItem::Modifier::DOUBLEFLAT)]= e.readElementText()[0];
1572             else if (tag == "flat")
1573                   displayAccidental[int(FiguredBassItem::Modifier::FLAT)]      = e.readElementText()[0];
1574             else if (tag == "natural")
1575                   displayAccidental[int(FiguredBassItem::Modifier::NATURAL)]   = e.readElementText()[0];
1576             else if (tag == "sharp")
1577                   displayAccidental[int(FiguredBassItem::Modifier::SHARP)]     = e.readElementText()[0];
1578             else if (tag == "doublesharp")
1579                   displayAccidental[int(FiguredBassItem::Modifier::DOUBLESHARP)]= e.readElementText()[0];
1580             else if (tag == "cross")
1581                   displayAccidental[int(FiguredBassItem::Modifier::CROSS)]     = e.readElementText()[0];
1582             else if (tag == "backslash")
1583                   displayAccidental[int(FiguredBassItem::Modifier::BACKSLASH)] = e.readElementText()[0];
1584             else if (tag == "slash")
1585                   displayAccidental[int(FiguredBassItem::Modifier::SLASH)]     = e.readElementText()[0];
1586             else if (tag == "digit") {
1587                   int digit = e.intAttribute("value");
1588                   if (digit < 0 || digit > 9)
1589                         return false;
1590                   while (e.readNextStartElement()) {
1591                         const QStringRef& t(e.name());
1592                         if (t == "simple")
1593                               displayDigit[int(FiguredBassItem::Style::MODERN)]  [digit][int(FiguredBassItem::Combination::SIMPLE)]      = e.readElementText()[0];
1594                         else if (t == "crossed")
1595                               displayDigit[int(FiguredBassItem::Style::MODERN)]  [digit][int(FiguredBassItem::Combination::CROSSED)]     = e.readElementText()[0];
1596                         else if (t == "backslashed")
1597                               displayDigit[int(FiguredBassItem::Style::MODERN)]  [digit][int(FiguredBassItem::Combination::BACKSLASHED)] = e.readElementText()[0];
1598                         else if (t == "slashed")
1599                               displayDigit[int(FiguredBassItem::Style::MODERN)]  [digit][int(FiguredBassItem::Combination::SLASHED)]     = e.readElementText()[0];
1600                         else if (t == "simpleHistoric")
1601                               displayDigit[int(FiguredBassItem::Style::HISTORIC)][digit][int(FiguredBassItem::Combination::SIMPLE)]      = e.readElementText()[0];
1602                         else if (t == "crossedHistoric")
1603                               displayDigit[int(FiguredBassItem::Style::HISTORIC)][digit][int(FiguredBassItem::Combination::CROSSED)]     = e.readElementText()[0];
1604                         else if (t == "backslashedHistoric")
1605                               displayDigit[int(FiguredBassItem::Style::HISTORIC)][digit][int(FiguredBassItem::Combination::BACKSLASHED)] = e.readElementText()[0];
1606                         else if (t == "slashedHistoric")
1607                               displayDigit[int(FiguredBassItem::Style::HISTORIC)][digit][int(FiguredBassItem::Combination::SLASHED)]     = e.readElementText()[0];
1608                         else {
1609                               e.unknown();
1610                               return false;
1611                               }
1612                         }
1613                   }
1614             else {
1615                   e.unknown();
1616                   return false;
1617                   }
1618             }
1619       displayParenthesis[0] = displayAccidental[int(FiguredBassItem::Modifier::NONE)] = ' ';
1620       return true;
1621       }
1622 
1623 //---------------------------------------------------------
1624 //   Read Configuration File
1625 //
1626 //    reads a configuration and appends read data to g_FBFonts
1627 //    resets everything and reads the built-in config file if fileName is null or empty
1628 //---------------------------------------------------------
1629 
readConfigFile(const QString & fileName)1630 bool FiguredBass::readConfigFile(const QString& fileName)
1631       {
1632       QString     path;
1633 
1634       if (fileName == 0 || fileName.isEmpty()) {       // defaults to built-in xml
1635 #ifdef Q_OS_IOS
1636             {
1637             extern QString resourcePath();
1638             QString rpath = resourcePath();
1639             path = rpath + QString("/fonts_figuredbass.xml");
1640             }
1641 #else
1642             path = ":/fonts/fonts_figuredbass.xml";
1643 #endif
1644             g_FBFonts.clear();
1645             }
1646       else
1647             path = fileName;
1648 
1649       QFile fi(path);
1650       if (!fi.open(QIODevice::ReadOnly)) {
1651             MScore::lastError = QObject::tr("Cannot open figured bass description:\n%1\n%2").arg(fi.fileName(), fi.errorString());
1652             qDebug("FiguredBass::read failed: <%s>", qPrintable(path));
1653             return false;
1654             }
1655       XmlReader e(&fi);
1656       while (e.readNextStartElement()) {
1657             if (e.name() == "museScore") {
1658                   // QString version = e.attribute(QString("version"));
1659                   // QStringList sl = version.split('.');
1660                   // int _mscVersion = sl[0].toInt() * 100 + sl[1].toInt();
1661 
1662                   while (e.readNextStartElement()) {
1663                         if (e.name() == "font") {
1664                               FiguredBassFont f;
1665                               if (f.read(e))
1666                                     g_FBFonts.append(f);
1667                               else
1668                                     return false;
1669                               }
1670                         else
1671                               e.unknown();
1672                         }
1673                   return true;
1674                   }
1675             }
1676       return false;
1677       }
1678 
1679 //---------------------------------------------------------
1680 //   Get Font Names
1681 //
1682 //    returns a list of display names for the fonts  configured to work with Figured Bass;
1683 //    the index of a name in the list can be used to retrieve the font data with fontData()
1684 //---------------------------------------------------------
1685 
fontNames()1686 QList<QString> FiguredBass::fontNames()
1687       {
1688       QList<QString> names;
1689       foreach(const FiguredBassFont& f, g_FBFonts)
1690             names.append(f.displayName);
1691       return names;
1692       }
1693 
1694 //---------------------------------------------------------
1695 //   Get Font Data
1696 //
1697 //    retrieves data about a Figured Bass font.
1698 //    returns: true if idx is valid | false if it is not
1699 // any of the pointer parameter can be null, if that datum is not needed
1700 //---------------------------------------------------------
1701 
fontData(int nIdx,QString * pFamily,QString * pDisplayName,qreal * pSize,qreal * pLineHeight)1702 bool FiguredBass::fontData(int nIdx, QString * pFamily, QString * pDisplayName,
1703             qreal * pSize, qreal * pLineHeight)
1704       {
1705       if(nIdx >= 0 && nIdx < g_FBFonts.size()) {
1706             FiguredBassFont f = g_FBFonts.at(nIdx);
1707             if(pFamily)       *pFamily          = f.family;
1708             if(pDisplayName)  *pDisplayName     = f.displayName;
1709             if(pSize)         *pSize            = f.defPitch;
1710             if(pLineHeight)   *pLineHeight      = f.defLineHeight;
1711             return true;
1712       }
1713       return false;
1714       }
1715 
1716 //---------------------------------------------------------
1717 //   hasParentheses
1718 //
1719 //   return true if any FiguredBassItem starts with a parenthesis
1720 //---------------------------------------------------------
1721 
hasParentheses() const1722 bool FiguredBass::hasParentheses() const
1723       {
1724       for (FiguredBassItem* item : items)
1725             if (item->startsWithParenthesis())
1726                   return true;
1727       return false;
1728       }
1729 
1730 //---------------------------------------------------------
1731 //   Write MusicXML
1732 //---------------------------------------------------------
1733 
writeMusicXML(XmlWriter & xml,bool isOriginalFigure,int crEndTick,int fbEndTick,bool writeDuration,int divisions) const1734 void FiguredBass::writeMusicXML(XmlWriter& xml, bool isOriginalFigure, int crEndTick, int fbEndTick, bool writeDuration, int divisions) const
1735       {
1736       QString stag = "figured-bass";
1737       if (hasParentheses())
1738             stag += " parentheses=\"yes\"";
1739       xml.stag(stag);
1740       for(FiguredBassItem* item : items)
1741             item->writeMusicXML(xml, isOriginalFigure, crEndTick, fbEndTick);
1742       if (writeDuration)
1743             xml.tag("duration", ticks().ticks() / divisions);
1744       xml.etag();
1745       }
1746 
1747 //---------------------------------------------------------
1748 //
1749 // METHODS BELONGING TO OTHER CLASSES
1750 //
1751 //    Work In Progress: kept here until the FiguredBass framework is reasonably set up;
1752 //    To be finally moved to their respective class implementation files.
1753 //
1754 //---------------------------------------------------------
1755 
1756 //---------------------------------------------------------
1757 //   Score::addFiguredBass
1758 //    called from Keyboard Accelerator & menus
1759 //---------------------------------------------------------
1760 
1761 
addFiguredBass()1762 FiguredBass* Score::addFiguredBass()
1763       {
1764       Element* el = selection().element();
1765       if (!el || (!(el->isNote()) && !(el->isRest()) && !(el->isFiguredBass()))) {
1766             MScore::setError(NO_NOTE_FIGUREDBASS_SELECTED);
1767             return 0;
1768             }
1769 
1770       FiguredBass * fb;
1771       bool bNew;
1772       if (el->isNote()) {
1773             ChordRest * cr = toNote(el)->chord();
1774             fb = FiguredBass::addFiguredBassToSegment(cr->segment(), cr->staffIdx() * VOICES, Fraction(0,1), &bNew);
1775             }
1776       else if (el->isRest()) {
1777             ChordRest* cr = toRest(el);
1778             fb = FiguredBass::addFiguredBassToSegment(cr->segment(), cr->staffIdx() * VOICES, Fraction(0, 1), &bNew);
1779             }
1780       else if (el->isFiguredBass()) {
1781             fb = toFiguredBass(el);
1782             bNew = false;
1783             }
1784       else
1785             return 0;
1786 
1787       if(fb == 0)
1788             return 0;
1789 
1790       if(bNew)
1791             undoAddElement(fb);
1792       select(fb, SelectType::SINGLE, 0);
1793       return fb;
1794       }
1795 
1796 }
1797 
1798