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