1 //=============================================================================
2 //  MuseScore
3 //  Music Composition & Notation
4 //
5 //  Copyright (C) 2011-2014 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 "text.h"
14 #include "textedit.h"
15 #include "jump.h"
16 #include "marker.h"
17 #include "score.h"
18 #include "segment.h"
19 #include "measure.h"
20 #include "system.h"
21 #include "box.h"
22 #include "page.h"
23 #include "textframe.h"
24 #include "sym.h"
25 #include "xml.h"
26 #include "undo.h"
27 #include "mscore.h"
28 
29 namespace Ms {
30 
31 #ifdef Q_OS_MAC
32 #define CONTROL_MODIFIER Qt::AltModifier
33 #else
34 #define CONTROL_MODIFIER Qt::ControlModifier
35 #endif
36 
37 static const qreal subScriptSize     = 0.6;
38 static const qreal subScriptOffset   = 0.5;       // of x-height
39 static const qreal superScriptOffset = -.9;      // of x-height
40 
41 //static const qreal tempotextOffset = 0.4; // of x-height // 80% of 50% = 2 spatiums
42 
43 //---------------------------------------------------------
44 //   accessibleChar
45 /// Return the name of common symbols and punctuation, or return the
46 /// character itself if the name is unknown. For screen readers.
47 //---------------------------------------------------------
48 
accessibleChar(QChar chr)49 static QString accessibleChar(QChar chr)
50       {
51       if (chr == " ") return QObject::tr("space");
52       if (chr == "-") return QObject::tr("dash");
53       if (chr == "=") return QObject::tr("equals");
54       if (chr == ",") return QObject::tr("comma");
55       if (chr == ".") return QObject::tr("period");
56       if (chr == ":") return QObject::tr("colon");
57       if (chr == ";") return QObject::tr("semicolon");
58       if (chr == "(") return QObject::tr("left parenthesis");
59       if (chr == ")") return QObject::tr("right parenthesis");
60       if (chr == "[") return QObject::tr("left bracket");
61       if (chr == "]") return QObject::tr("right bracket");
62       return chr;
63       }
64 
65 //---------------------------------------------------------
66 //   accessibleCharacter
67 /// Given a string, if it has one character return its name, otherwise return
68 /// the string. Useful to force screen readers to speak punctuation when it
69 /// is alone but not when it forms part of a sentence.
70 //---------------------------------------------------------
71 
accessibleCharacter(QString str)72 static QString accessibleCharacter(QString str)
73       {
74       if (str.length() == 1)
75             return accessibleChar(str.at(0));
76       return str;
77       }
78 
79 //---------------------------------------------------------
80 //   isSorted
81 /// return true if (r1,c1) is at or before (r2,c2)
82 //---------------------------------------------------------
83 
isSorted(int r1,int c1,int r2,int c2)84 static bool isSorted(int r1, int c1, int r2, int c2)
85       {
86       if (r1 < r2)
87             return true;
88       if ((r1 == r2) && (c1 <= c2))
89             return true;
90       return false;
91       }
92 
93 //---------------------------------------------------------
94 //   swap
95 /// swap (r1,c1) with (r2,c2)
96 //---------------------------------------------------------
97 
swap(int & r1,int & c1,int & r2,int & c2)98 static void swap(int& r1, int& c1, int& r2, int& c2)
99       {
100       qSwap(r1, r2);
101       qSwap(c1, c2);
102       }
103 
104 //---------------------------------------------------------
105 //   sort
106 /// swap (r1,c1) with (r2,c2) if they are not sorted
107 //---------------------------------------------------------
108 
sort(int & r1,int & c1,int & r2,int & c2)109 static void sort(int& r1, int& c1, int& r2, int& c2)
110       {
111       if (!isSorted(r1, c1, r2, c2))
112             swap(r1, c1, r2, c2);
113       }
114 
115 //---------------------------------------------------------
116 //   operator==
117 //---------------------------------------------------------
118 
operator ==(const CharFormat & cf) const119 bool CharFormat::operator==(const CharFormat& cf) const
120       {
121       return cf.style()     == style()
122          && cf.preedit()    == preedit()
123          && cf.valign()     == valign()
124          && cf.fontSize()   == fontSize()
125          && cf.fontFamily() == fontFamily();
126       }
127 
128 //---------------------------------------------------------
129 //   clearSelection
130 //---------------------------------------------------------
131 
clearSelection()132 void TextCursor::clearSelection()
133       {
134       _selectLine   = _row;
135       _selectColumn = _column;
136       }
137 
138 //---------------------------------------------------------
139 //   init
140 //---------------------------------------------------------
141 
init()142 void TextCursor::init()
143       {
144       _format.setFontFamily(_text->family());
145       _format.setFontSize(_text->size());
146       _format.setStyle(_text->fontStyle());
147       _format.setPreedit(false);
148       _format.setValign(VerticalAlignment::AlignNormal);
149       }
150 
151 //---------------------------------------------------------
152 //   columns
153 //---------------------------------------------------------
154 
columns() const155 int TextCursor::columns() const
156       {
157       return _text->textBlock(_row).columns();
158       }
159 
160 //---------------------------------------------------------
161 //   currentCharacter
162 //---------------------------------------------------------
163 
currentCharacter() const164 QChar TextCursor::currentCharacter() const
165       {
166       const TextBlock& t = _text->_layout[row()];
167       QString s = t.text(column(), 1);
168       if (s.isEmpty())
169             return QChar();
170       return s[0];
171       }
172 
173 //---------------------------------------------------------
174 //   currentWord
175 //---------------------------------------------------------
176 
currentWord() const177 QString TextCursor::currentWord() const
178       {
179       const TextBlock& t = _text->_layout[row()];
180       QString s = t.text(column(), -1);
181       return s.remove(QRegularExpression(" .*"));
182       }
183 
184 //---------------------------------------------------------
185 //   currentLine
186 //---------------------------------------------------------
187 
currentLine() const188 QString TextCursor::currentLine() const
189       {
190       const TextBlock& t = _text->_layout[row()];
191       return t.text(0, -1);
192       }
193 
194 //---------------------------------------------------------
195 //   updateCursorFormat
196 //---------------------------------------------------------
197 
updateCursorFormat()198 void TextCursor::updateCursorFormat()
199       {
200       TextBlock* block = &_text->_layout[_row];
201       int col = hasSelection() ? selectColumn() : column();
202       const CharFormat* format = block->formatAt(col);
203       if (!format || format->fontFamily() == "ScoreText")
204             init();
205       else
206             setFormat(*format);
207       }
208 
209 //---------------------------------------------------------
210 //   cursorRect
211 //---------------------------------------------------------
212 
cursorRect() const213 QRectF TextCursor::cursorRect() const
214       {
215       const TextBlock& tline       = curLine();
216       const TextFragment* fragment = tline.fragment(column());
217 
218       QFont _font  = fragment ? fragment->font(_text) : _text->font();
219       qreal ascent = QFontMetricsF(_font, MScore::paintDevice()).ascent();
220       qreal h = ascent;
221       qreal x = tline.xpos(column(), _text);
222       qreal y = tline.y() - ascent * .9;
223       return QRectF(x, y, 4.0, h);
224       }
225 
226 //---------------------------------------------------------
227 //   curLine
228 //    return the current text line in edit mode
229 //---------------------------------------------------------
230 
curLine() const231 TextBlock& TextCursor::curLine() const
232       {
233       Q_ASSERT(!_text->_layout.empty());
234       return _text->_layout[_row];
235       }
236 
237 //---------------------------------------------------------
238 //   changeSelectionFormat
239 //---------------------------------------------------------
240 
changeSelectionFormat(FormatId id,QVariant val)241 void TextCursor::changeSelectionFormat(FormatId id, QVariant val)
242       {
243       if (!hasSelection())
244             return;
245       int r1 = selectLine();
246       int r2 = row();
247       int c1 = selectColumn();
248       int c2 = column();
249       sort(r1, c1, r2, c2);
250       int rows = _text->rows();
251       for (int row = 0; row < rows; ++row) {
252             TextBlock& t = _text->_layout[row];
253             if (row < r1)
254                   continue;
255             if (row > r2)
256                   break;
257             if (row == r1 && r1 == r2)
258                   t.changeFormat(id, val, c1, c2 - c1);
259             else if (row == r1)
260                   t.changeFormat(id, val, c1, t.columns() - c1);
261             else if (row == r2)
262                   t.changeFormat(id, val, 0, c2);
263             else
264                   t.changeFormat(id, val, 0, t.columns());
265             }
266       _text->layout1();
267       }
268 
269 //---------------------------------------------------------
270 //   setFormat
271 //---------------------------------------------------------
272 
setFormat(FormatId id,QVariant val)273 void TextCursor::setFormat(FormatId id, QVariant val)
274       {
275       changeSelectionFormat(id, val);
276       format()->setFormat(id, val);
277       text()->setTextInvalid();
278       }
279 
280 //---------------------------------------------------------
281 //   movePosition
282 //---------------------------------------------------------
283 
movePosition(QTextCursor::MoveOperation op,QTextCursor::MoveMode mode,int count)284 bool TextCursor::movePosition(QTextCursor::MoveOperation op, QTextCursor::MoveMode mode, int count)
285       {
286       QString accMsg;
287       int oldRow = _row;
288       int oldCol = _column;
289 
290       QString oldSelection;
291       if (hasSelection())
292             oldSelection = selectedText();
293 
294       for (int i = 0; i < count; i++) {
295             switch (op) {
296                   case QTextCursor::Left:
297                         if (hasSelection() && mode == QTextCursor::MoveAnchor) {
298                               int r1 = _selectLine;
299                               int r2 = _row;
300                               int c1 = _selectColumn;
301                               int c2 = _column;
302                               sort(r1, c1, r2, c2);
303                               clearSelection();
304                               _row    = r1;
305                               _column = c1;
306                               }
307                         else if (_column == 0) {
308                               if (_row == 0)
309                                     return false;
310                               --_row;
311                               _column = curLine().columns();
312                               }
313                         else
314                               --_column;
315 #if !defined(Q_OS_MAC)
316                         if (mode == QTextCursor::MoveAnchor)
317                               accMsg += accessibleCurrentCharacter();
318 #endif
319                         break;
320 
321                   case QTextCursor::Right:
322                         if (hasSelection() && mode == QTextCursor::MoveAnchor) {
323                               int r1 = _selectLine;
324                               int r2 = _row;
325                               int c1 = _selectColumn;
326                               int c2 = _column;
327                               sort(r1, c1, r2, c2);
328                               clearSelection();
329                               _row    = r2;
330                               _column = c2;
331                               }
332                         else if (column() >= curLine().columns()) {
333                               if (_row >= _text->rows() - 1)
334                                     return false;
335                               ++_row;
336                               _column = 0;
337                               }
338                         else
339                               ++_column;
340 #if !defined(Q_OS_MAC)
341                         if (mode == QTextCursor::MoveAnchor)
342                               accMsg += accessibleCurrentCharacter();
343 #endif
344                         break;
345 
346                   case QTextCursor::Up:
347                         if (_row == 0)
348                               return false;
349                         --_row;
350                         if (_column > curLine().columns())
351                               _column = curLine().columns();
352 #if !defined(Q_OS_MAC)
353                         if (mode == QTextCursor::MoveAnchor)
354                               accMsg += accessibleCharacter(currentLine()) + "\n";
355 #endif
356                         break;
357 
358                   case QTextCursor::Down:
359                         if (_row >= _text->rows() - 1)
360                               return false;
361                         ++_row;
362                         if (_column > curLine().columns())
363                               _column = curLine().columns();
364 #if !defined(Q_OS_MAC)
365                         if (mode == QTextCursor::MoveAnchor)
366                               accMsg += accessibleCharacter(currentLine()) + "\n";
367 #endif
368                         break;
369 
370                   case QTextCursor::Start:
371                         _row    = 0;
372                         _column = 0;
373 #if !defined(Q_OS_MAC)
374                         if (mode == QTextCursor::MoveAnchor)
375                               accMsg = accessibleCharacter(currentLine());
376 #endif
377                         break;
378 
379                   case QTextCursor::End:
380                         _row    = _text->rows() - 1;
381                         _column = curLine().columns();
382 #if !defined(Q_OS_MAC)
383                         if (mode == QTextCursor::MoveAnchor)
384                               accMsg = accessibleCharacter(currentLine());
385 #endif
386                         break;
387 
388                   case QTextCursor::StartOfLine:
389                         _column = 0;
390 #if !defined(Q_OS_MAC)
391                         if (mode == QTextCursor::MoveAnchor)
392                               accMsg = accessibleCurrentCharacter();
393 #endif
394                         break;
395 
396                   case QTextCursor::EndOfLine:
397                         _column = curLine().columns();
398 #if !defined(Q_OS_MAC)
399                         if (mode == QTextCursor::MoveAnchor)
400                               accMsg = accessibleCurrentCharacter();
401 #endif
402                         break;
403 
404                   case QTextCursor::WordLeft:
405                         if (_column > 0) {
406                               --_column;
407                               while (_column > 0 && currentCharacter().isSpace())
408                                     --_column;
409                               while (_column > 0 && !currentCharacter().isSpace())
410                                     --_column;
411                               if (currentCharacter().isSpace())
412                                     ++_column;
413                               }
414 #if !defined(Q_OS_MAC)
415                         if (mode == QTextCursor::MoveAnchor)
416                               accMsg += accessibleCharacter(currentWord()) + " ";
417 #endif
418                         break;
419 
420                   case QTextCursor::NextWord: {
421                         int cols =  columns();
422                         if (_column < cols) {
423                               ++_column;
424                               while (_column < cols && !currentCharacter().isSpace())
425                                     ++_column;
426                               while (_column < cols && currentCharacter().isSpace())
427                                     ++_column;
428                               }
429                         }
430 #if !defined(Q_OS_MAC)
431                         if (mode == QTextCursor::MoveAnchor)
432                               accMsg += accessibleCharacter(currentWord()) + " ";
433 #endif
434                         break;
435 
436                   default:
437                         qDebug("Text::movePosition: not implemented");
438                         return false;
439                   }
440             if (mode == QTextCursor::MoveAnchor)
441                   clearSelection();
442             }
443       accessibileMessage(accMsg, oldRow, oldCol, oldSelection, mode);
444       _text->score()->setAccessibleMessage(accMsg);
445       updateCursorFormat();
446       _text->score()->addRefresh(_text->canvasBoundingRect());
447       return true;
448       }
449 
450 
451 
452 //---------------------------------------------------------
453 //   doubleClickSelect
454 //---------------------------------------------------------
455 
doubleClickSelect()456 void TextCursor::doubleClickSelect()
457       {
458       clearSelection();
459 
460       // if clicked on a space, select surrounding spaces
461       // otherwise select surround non-spaces
462       const bool selectSpaces = currentCharacter().isSpace();
463 
464       //handle double-clicking inside a word
465       int startPosition = _column;
466 
467       while (_column > 0 && currentCharacter().isSpace() == selectSpaces)
468             --_column;
469 
470       if (currentCharacter().isSpace() != selectSpaces)
471             ++_column;
472 
473       _selectColumn = _column;
474 
475       _column = startPosition;
476       while (_column  < curLine().columns() && currentCharacter().isSpace() == selectSpaces)
477             ++_column;
478 
479       updateCursorFormat();
480       _text->score()->addRefresh(_text->canvasBoundingRect());
481       }
482 
483 //---------------------------------------------------------
484 //   set
485 //---------------------------------------------------------
486 
set(const QPointF & p,QTextCursor::MoveMode mode)487 bool TextCursor::set(const QPointF& p, QTextCursor::MoveMode mode)
488       {
489       QPointF pt  = p - _text->canvasPos();
490       if (!_text->bbox().contains(pt))
491             return false;
492       int oldRow    = _row;
493       int oldColumn = _column;
494 
495 //      if (_text->_layout.empty())
496 //            _text->_layout.append(TextBlock());
497       _row = 0;
498       for (int row = 0; row < _text->rows(); ++row) {
499             const TextBlock& l = _text->_layout.at(row);
500             if (l.y() > pt.y()) {
501                   _row = row;
502                   break;
503                   }
504             }
505       _column = curLine().column(pt.x(), _text);
506 
507       if (oldRow != _row || oldColumn != _column) {
508             _text->score()->setUpdateAll();
509             if (mode == QTextCursor::MoveAnchor)
510                   clearSelection();
511             if (hasSelection())
512                   QApplication::clipboard()->setText(selectedText(), QClipboard::Selection);
513             }
514       updateCursorFormat();
515       return true;
516       }
517 
518 //---------------------------------------------------------
519 //   selectedText
520 //    return current selection
521 //---------------------------------------------------------
522 
selectedText() const523 QString TextCursor::selectedText() const
524       {
525       int r1 = selectLine();
526       int r2 = _row;
527       int c1 = selectColumn();
528       int c2 = column();
529       sort(r1, c1, r2, c2);
530       return extractText(r1, c1, r2, c2);
531       }
532 
533 //---------------------------------------------------------
534 //   extractText
535 //    return text between (r1,c1) and (r2,c2).
536 //---------------------------------------------------------
537 
extractText(int r1,int c1,int r2,int c2) const538 QString TextCursor::extractText(int r1, int c1, int r2, int c2) const
539       {
540       Q_ASSERT(isSorted(r1, c1, r2, c2));
541       const QList<TextBlock>& tb = _text->_layout;
542 
543       if (r1 == r2)
544             return tb.at(r1).text(c1, c2 - c1);
545 
546       QString str = tb.at(r1).text(c1, -1) + "\n";
547 
548       for (int r = r1 + 1; r < r2; ++r)
549             str += tb.at(r).text(0, -1) + "\n";
550 
551       str += tb.at(r2).text(0, c2);
552       return str;
553       }
554 
555 //---------------------------------------------------------
556 //   accessibleCurrentCharacter
557 /// Return current character or its name in the case of a symbol. For screen readers.
558 //---------------------------------------------------------
559 
accessibleCurrentCharacter() const560 QString TextCursor::accessibleCurrentCharacter() const
561       {
562       if (_column < curLine().columns())
563             return accessibleChar(currentCharacter());
564       if (_row < _text->rows() - 1)
565             return QObject::tr("line feed");
566       return QObject::tr("blank"); // end of text
567       }
568 
569 //---------------------------------------------------------
570 //   accessibileMessage
571 /// Set the accMsg string to describe the result of having moved the cursor
572 /// from P(oldRow,oldCol) to the current position. For use by screen readers.
573 ///
574 /// The default message simply contains the characters that the cursor
575 /// skipped over when moving between the old and current positions. This
576 /// mimics the behavior of VoiceOver on macOS, but other screen readers do
577 /// things a bit differently. (Many report the character or word to the right
578 /// of the cursor regardless of the direction that the cursor moved, but that
579 /// behavior is not implemented here.) You can override the default message
580 /// by setting accMsg to a different value before this function is called.
581 ///
582 /// If the cursor's movement caused a change in selection then the message
583 /// will say which characters were selected and which were deselected (both
584 /// can happen in a single move operation). All screen readers do this, so
585 /// this message cannot be overridden.
586 //---------------------------------------------------------
587 
accessibileMessage(QString & accMsg,int oldRow,int oldCol,QString oldSelection,QTextCursor::MoveMode mode) const588 void TextCursor::accessibileMessage(QString& accMsg, int oldRow, int oldCol, QString oldSelection, QTextCursor::MoveMode mode) const
589       {
590       int r1 = oldRow;
591       int c1 = oldCol;
592       int r2 = _row;
593       int c2 = _column;
594 
595       const bool movedForwards = isSorted(r1, c1, r2, c2);
596 
597       // ensure P1 before P2
598       if (!movedForwards)
599             swap(r1, c1, r2, c2);
600 
601       if (mode == QTextCursor::MoveAnchor) {
602             if (accMsg.isEmpty()) {
603                   // Provide a default message based on skipped characters.
604                   accMsg = accessibleCharacter(extractText(r1, c1, r2, c2));
605                   }
606 
607             if (!oldSelection.isEmpty()) {
608                   // Cursor's movement has cancelled a previous selection.
609                   oldSelection = QObject::tr("%1 unselected").arg(oldSelection);
610 
611                   if (accMsg.isEmpty()) // no characters were skipped
612                         accMsg = oldSelection;
613                   else
614                         accMsg = QObject::tr("%1, %2").arg(accMsg, oldSelection);
615                   }
616 
617             return;
618             }
619 
620       // Skipped characters were added and/or removed from selection.
621       const int rs = _selectLine;
622       const int cs = _selectColumn;
623 
624       bool selectForwards;
625       bool anchorOutsideRange;
626 
627       if (isSorted(rs, cs, r1, c1)) {
628             // Selection anchor is before range of skipped characters.
629             anchorOutsideRange = true;
630             selectForwards = true; // forward movement selects characters
631             }
632       else if (isSorted(r2, c2, rs, cs)) {
633             // Selection anchor is after range of skipped characters.
634             anchorOutsideRange = true;
635             selectForwards = false; // forward movement deselects characters
636             }
637       else {
638             // Selection anchor is within the range of skipped characters
639             // so some characters have been selected and others deselected.
640             anchorOutsideRange = false;
641             selectForwards = false;
642       }
643 
644       if (anchorOutsideRange) {
645             // Entire range of skipped characters was selected or deselected.
646             accMsg = accessibleCharacter(extractText(r1, c1, r2, c2));
647 
648             if (movedForwards == selectForwards)
649                   accMsg = QObject::tr("%1 selected").arg(accMsg);
650             else
651                   accMsg = QObject::tr("%1 unselected").arg(accMsg);
652 
653             return;
654             }
655 
656       // cursor skipped over the selection anchor
657       QString str1 = accessibleCharacter(extractText(r1, c1, rs, cs));
658       QString str2 = accessibleCharacter(extractText(rs, cs, r2, c2));
659 
660       if (movedForwards) {
661             str1 = QObject::tr("%1 unselected").arg(str1);
662             str2 = QObject::tr("%1 selected").arg(str2);
663             }
664       else {
665             str1 = QObject::tr("%1 selected").arg(str1);
666             str2 = QObject::tr("%1 unselected").arg(str2);
667             }
668 
669       accMsg =  QObject::tr("%1, %2").arg(str1, str2);
670       }
671 
672 //---------------------------------------------------------
673 //   TextFragment
674 //---------------------------------------------------------
675 
TextFragment()676 TextFragment::TextFragment()
677       {
678       }
679 
TextFragment(const QString & s)680 TextFragment::TextFragment(const QString& s)
681       {
682       text = s;
683       }
684 
TextFragment(TextCursor * cursor,const QString & s)685 TextFragment::TextFragment(TextCursor* cursor, const QString& s)
686       {
687       format = *cursor->format();
688       text = s;
689       }
690 
691 //---------------------------------------------------------
692 //   split
693 //---------------------------------------------------------
694 
split(int column)695 TextFragment TextFragment::split(int column)
696       {
697       int idx = 0;
698       int col = 0;
699       TextFragment f;
700       f.format = format;
701 
702       for (const QChar& c : qAsConst(text)) {
703             if (col == column) {
704                   if (idx) {
705                         if (idx < text.size()) {
706                               f.text = text.mid(idx);
707                               text   = text.left(idx);
708                               }
709                         }
710                   return f;
711                   }
712             ++idx;
713             if (c.isHighSurrogate())
714                   continue;
715             ++col;
716             }
717       return f;
718       }
719 
720 
721 //---------------------------------------------------------
722 //   columns
723 //---------------------------------------------------------
724 
columns() const725 int TextFragment::columns() const
726       {
727       int col = 0;
728       for (const QChar& c : qAsConst(text)) {
729             if (c.isHighSurrogate())
730                   continue;
731             ++col;
732             }
733       return col;
734       }
735 
736 //---------------------------------------------------------
737 //   operator ==
738 //---------------------------------------------------------
739 
operator ==(const TextFragment & f) const740 bool TextFragment::operator ==(const TextFragment& f) const
741       {
742       return format == f.format && text == f.text;
743       }
744 
745 //---------------------------------------------------------
746 //   draw
747 //---------------------------------------------------------
748 
draw(QPainter * p,const TextBase * t) const749 void TextFragment::draw(QPainter* p, const TextBase* t) const
750       {
751       QFont f(font(t));
752       f.setPointSizeF(f.pointSizeF() * MScore::pixelRatio);
753 #ifndef Q_OS_MACOS
754       TextBase::drawTextWorkaround(p, f, pos, text);
755 #else
756       p->setFont(f);
757       p->drawText(pos, text);
758 #endif
759       }
760 
761 //---------------------------------------------------------
762 //   drawTextWorkaround
763 //---------------------------------------------------------
764 
drawTextWorkaround(QPainter * p,QFont & f,const QPointF pos,const QString text)765 void TextBase::drawTextWorkaround(QPainter* p, QFont& f, const QPointF pos, const QString text)
766       {
767       qreal mm = p->worldTransform().m11();
768       if (!(MScore::pdfPrinting) && (mm < 1.0) && f.bold() && !(f.underline())) {
769             // workaround for https://musescore.org/en/node/284218
770             // and https://musescore.org/en/node/281601
771             // only needed for certain artificially emboldened fonts
772             // see https://musescore.org/en/node/281601#comment-900261
773             // in Qt 5.12.x this workaround should be no more necessary if
774             // env variable QT_MAX_CACHED_GLYPH_SIZE is set to 1.
775             // The workaround works badly if the text is at the same time
776             // bold and underlined.
777             p->save();
778             qreal dx = p->worldTransform().dx();
779             qreal dy = p->worldTransform().dy();
780             // diagonal elements will now be changed to 1.0
781             p->setMatrix(QMatrix(1.0, 0.0, 0.0, 1.0, dx, dy));
782             // correction factor for bold text drawing, due to the change of the diagonal elements
783             qreal factor = 1.0 / mm;
784             QFont fnew(f, p->device());
785             fnew.setPointSizeF(f.pointSizeF() / factor);
786             QRawFont fRaw = QRawFont::fromFont(fnew);
787             QTextLayout textLayout(text, f, p->device());
788             textLayout.beginLayout();
789             while (true) {
790                   QTextLine line = textLayout.createLine();
791                   if (!line.isValid())
792                         break;
793                   }
794             textLayout.endLayout();
795             // glyphruns with correct positions, but potentially wrong glyphs
796             // (see bug https://musescore.org/en/node/117191 regarding positions and DPI)
797             QList<QGlyphRun> glyphruns = textLayout.glyphRuns();
798             qreal offset = 0;
799             // glyphrun drawing has an offset equal to the max ascent of the text fragment
800             for (int i = 0; i < glyphruns.length(); i++) {
801                   qreal value = glyphruns.at(i).rawFont().ascent() / factor;
802                   if (value > offset)
803                         offset = value;
804                   }
805             for (int i = 0; i < glyphruns.length(); i++) {
806                   QVector<QPointF> positions1 = glyphruns.at(i).positions();
807                   QVector<QPointF> positions2;
808                   // calculate the new positions for the scaled geometry
809                   for (int j = 0; j < positions1.length(); j++) {
810                         QPointF newPoint = positions1.at(j) / factor;
811                         positions2.append(newPoint);
812                         }
813                   QGlyphRun glyphrun2 = glyphruns.at(i);
814                   glyphrun2.setPositions(positions2);
815                   // change the glyphs with the correct glyphs
816                   // and account for glyph substitution
817                   if (glyphrun2.rawFont().familyName() != fnew.family()) {
818                         QFont f2(fnew);
819                         f2.setFamily(glyphrun2.rawFont().familyName());
820                         glyphrun2.setRawFont(QRawFont::fromFont(f2));
821                         }
822                   else
823                         glyphrun2.setRawFont(fRaw);
824                   p->drawGlyphRun(QPointF(pos.x() / factor, pos.y() / factor - offset),glyphrun2);
825                   positions2.clear();
826                   }
827             // Restore the QPainter to its former state
828             p->setMatrix(QMatrix(mm, 0.0, 0.0, mm, dx, dy));
829             p->restore();
830             }
831       else {
832             p->setFont(f);
833             p->drawText(pos, text);
834             }
835       }
836 
837 //---------------------------------------------------------
838 //   font
839 //---------------------------------------------------------
840 
font(const TextBase * t) const841 QFont TextFragment::font(const TextBase* t) const
842       {
843       QFont font;
844 
845       qreal m = format.fontSize();
846 
847       if (t->sizeIsSpatiumDependent())
848             m *= t->spatium() / SPATIUM20;
849       if (format.valign() != VerticalAlignment::AlignNormal)
850             m *= subScriptSize;
851       font.setUnderline(format.underline() || format.preedit());
852 
853       QString family;
854       if (format.fontFamily() == "ScoreText") {
855             family = t->score()->styleSt(Sid::MusicalTextFont);
856 
857             // check if all symbols are available
858             font.setFamily(family);
859             QFontMetricsF fm(font);
860 
861             bool fail = false;
862             for (int i = 0; i < text.size(); ++i) {
863                   QChar c = text[i];
864                   if (c.isHighSurrogate()) {
865                         if (i+1 == text.size())
866                               qFatal("bad string");
867                         QChar c2 = text[i+1];
868                         ++i;
869                         uint v = QChar::surrogateToUcs4(c, c2);
870                         if (!fm.inFontUcs4(v)) {
871                               fail = true;
872                               break;
873                               }
874                         }
875                   else {
876                         if (!fm.inFont(c)) {
877                               fail = true;
878                               break;
879                               }
880                         }
881                   }
882             if (fail)
883                   family = ScoreFont::fallbackTextFont();
884             }
885       else
886             family = format.fontFamily();
887 
888       font.setFamily(family);
889       font.setBold(format.bold());
890       font.setItalic(format.italic());
891       Q_ASSERT(m > 0.0);
892 
893       font.setPointSizeF(m * t->mag());
894       return font;
895       }
896 
897 //---------------------------------------------------------
898 //   draw
899 //---------------------------------------------------------
900 
draw(QPainter * p,const TextBase * t) const901 void TextBlock::draw(QPainter* p, const TextBase* t) const
902       {
903       p->translate(0.0, _y);
904       for (const TextFragment& f : _fragments)
905             f.draw(p, t);
906       p->translate(0.0, -_y);
907       }
908 
909 //---------------------------------------------------------
910 //   layout
911 //---------------------------------------------------------
912 
layout(TextBase * t)913 void TextBlock::layout(TextBase* t)
914       {
915       _bbox        = QRectF();
916       qreal x      = 0.0;
917       _lineSpacing = 0.0;
918       qreal lm     = 0.0;
919 
920       qreal layoutWidth = 0;
921       Element* e = t->parent();
922       if (e && t->layoutToParentWidth()) {
923             layoutWidth = e->width();
924             switch(e->type()) {
925                   case ElementType::HBOX:
926                   case ElementType::VBOX:
927                   case ElementType::TBOX: {
928                         Box* b = toBox(e);
929                         layoutWidth -= ((b->leftMargin() + b->rightMargin()) * DPMM);
930                         lm = b->leftMargin() * DPMM;
931                         }
932                         break;
933                   case ElementType::PAGE: {
934                         Page* p = toPage(e);
935                         layoutWidth -= (p->lm() + p->rm());
936                         lm = p->lm();
937                         }
938                         break;
939                   case ElementType::MEASURE: {
940                         Measure* m = toMeasure(e);
941                         layoutWidth = m->bbox().width();
942                         }
943                         break;
944                   default:
945                         break;
946                   }
947             }
948 
949       if (_fragments.empty()) {
950             QFontMetricsF fm = t->fontMetrics();
951             _bbox.setRect(0.0, -fm.ascent(), 1.0, fm.descent());
952             _lineSpacing = fm.lineSpacing();
953             }
954       else if (_fragments.size() == 1 && _fragments.at(0).text.isEmpty()) {
955             auto fi = _fragments.begin();
956             TextFragment& f = *fi;
957             f.pos.setX(x);
958             QFontMetricsF fm(f.font(t), MScore::paintDevice());
959             if (f.format.valign() != VerticalAlignment::AlignNormal) {
960                   qreal voffset = fm.xHeight() / subScriptSize;   // use original height
961                   if (f.format.valign() == VerticalAlignment::AlignSubScript)
962                         voffset *= subScriptOffset;
963                   else
964                         voffset *= superScriptOffset;
965                   f.pos.setY(voffset);
966                   }
967             else {
968                   f.pos.setY(0.0);
969                   }
970 
971             QRectF temp(0.0, -fm.ascent(), 1.0, fm.descent());
972             _bbox |= temp;
973             _lineSpacing = qMax(_lineSpacing, fm.lineSpacing());
974             }
975       else {
976             const auto fiLast = --_fragments.end();
977             for (auto fi = _fragments.begin(); fi != _fragments.end(); ++fi) {
978                   TextFragment& f = *fi;
979                   f.pos.setX(x);
980                   QFontMetricsF fm(f.font(t), MScore::paintDevice());
981                   if (f.format.valign() != VerticalAlignment::AlignNormal) {
982                         qreal voffset = fm.xHeight() / subScriptSize;   // use original height
983                         if (f.format.valign() == VerticalAlignment::AlignSubScript)
984                               voffset *= subScriptOffset;
985                         else
986                               voffset *= superScriptOffset;
987                         f.pos.setY(voffset);
988                         }
989                   else {
990                         f.pos.setY(0.0);
991                         }
992 
993                   // Optimization: don't calculate character position
994                   // for the next fragment if there is no next fragment
995                   if (fi != fiLast) {
996                         const qreal w  = fm.width(f.text);
997                         x += w;
998                         }
999 
1000                   _bbox   |= fm.tightBoundingRect(f.text).translated(f.pos);
1001                   _lineSpacing = qMax(_lineSpacing, fm.lineSpacing());
1002                   }
1003             }
1004 
1005       // Apply style/custom line spacing
1006       _lineSpacing *= t->textLineSpacing();
1007 
1008       qreal rx;
1009       if (t->align() & Align::RIGHT)
1010             rx = layoutWidth-_bbox.right();
1011       else if (t->align() & Align::HCENTER)
1012             rx = (layoutWidth - (_bbox.left() + _bbox.right())) * .5;
1013       else  // Align::LEFT
1014             rx = -_bbox.left();
1015       rx += lm;
1016       for (TextFragment& f : _fragments)
1017             f.pos.rx() += rx;
1018       _bbox.translate(rx, 0.0);
1019       }
1020 
1021 //---------------------------------------------------------
1022 //   fragmentsWithoutEmpty
1023 //---------------------------------------------------------
1024 
fragmentsWithoutEmpty()1025 QList<TextFragment>* TextBlock::fragmentsWithoutEmpty()
1026       {
1027       QList<TextFragment>* list = new QList<TextFragment>();
1028       for (const auto &x :qAsConst(_fragments)) {
1029             if (x.text.isEmpty()) {
1030                   continue;
1031                   }
1032             else {
1033                   list->append(x);
1034                   }
1035             }
1036       return list;
1037       }
1038 
1039 //---------------------------------------------------------
1040 //   xpos
1041 //---------------------------------------------------------
1042 
xpos(int column,const TextBase * t) const1043 qreal TextBlock::xpos(int column, const TextBase* t) const
1044       {
1045       int col = 0;
1046       for (const TextFragment& f : _fragments) {
1047             if (column == col)
1048                   return f.pos.x();
1049             QFontMetricsF fm(f.font(t), MScore::paintDevice());
1050             int idx = 0;
1051             for (const QChar& c : qAsConst(f.text)) {
1052                   ++idx;
1053                   if (c.isHighSurrogate())
1054                         continue;
1055                   ++col;
1056                   if (column == col)
1057                         return f.pos.x() + fm.width(f.text.left(idx));
1058                   }
1059             }
1060       return _bbox.x();
1061       }
1062 
1063 //---------------------------------------------------------
1064 //   fragment
1065 //---------------------------------------------------------
1066 
fragment(int column) const1067 const TextFragment* TextBlock::fragment(int column) const
1068       {
1069       if (_fragments.empty())
1070             return 0;
1071       int col = 0;
1072       auto f = _fragments.begin();
1073       for (; f != _fragments.end(); ++f) {
1074             for (const QChar& c : qAsConst(f->text)) {
1075                   if (c.isHighSurrogate())
1076                         continue;
1077                   if (column == col)
1078                         return &*f;
1079                   ++col;
1080                   }
1081             }
1082       if (column == col)
1083             return &*(f-1);
1084       return 0;
1085       }
1086 
1087 //---------------------------------------------------------
1088 //   formatAt
1089 //---------------------------------------------------------
1090 
formatAt(int column) const1091 const CharFormat* TextBlock::formatAt(int column) const
1092       {
1093       const TextFragment* f = fragment(column);
1094       if (f)
1095             return &(f->format);
1096       return 0;
1097       }
1098 
1099 //---------------------------------------------------------
1100 //   boundingRect
1101 //---------------------------------------------------------
1102 
boundingRect(int col1,int col2,const TextBase * t) const1103 QRectF TextBlock::boundingRect(int col1, int col2, const TextBase* t) const
1104       {
1105       qreal x1 = xpos(col1, t);
1106       qreal x2 = xpos(col2, t);
1107       return QRectF(x1, _bbox.y(), x2-x1, _bbox.height());
1108       }
1109 
1110 //---------------------------------------------------------
1111 //   columns
1112 //---------------------------------------------------------
1113 
columns() const1114 int TextBlock::columns() const
1115       {
1116       int col = 0;
1117       for (const TextFragment& f : _fragments) {
1118             for (const QChar& c : qAsConst(f.text)) {
1119                   if (!c.isHighSurrogate())
1120                         ++col;
1121                   }
1122             }
1123       return col;
1124       }
1125 
1126 //---------------------------------------------------------
1127 //   column
1128 //    Return nearest column for position x. X is in
1129 //    Text coordinate system
1130 //---------------------------------------------------------
1131 
column(qreal x,TextBase * t) const1132 int TextBlock::column(qreal x, TextBase* t) const
1133       {
1134       int col = 0;
1135       for (const TextFragment& f : _fragments) {
1136             int idx = 0;
1137             if (x <= f.pos.x())
1138                   return col;
1139             qreal px = 0.0;
1140             for (const QChar& c : qAsConst(f.text)) {
1141                   ++idx;
1142                   if (c.isHighSurrogate())
1143                         continue;
1144                   QFontMetricsF fm(f.font(t), MScore::paintDevice());
1145                   qreal xo = fm.width(f.text.left(idx));
1146                   if (x <= f.pos.x() + px + (xo-px)*.5)
1147                         return col;
1148                   ++col;
1149                   px = xo;
1150                   }
1151             }
1152       return col;
1153       }
1154 
1155 //---------------------------------------------------------
1156 //   insert
1157 //---------------------------------------------------------
1158 
insert(TextCursor * cursor,const QString & s)1159 void TextBlock::insert(TextCursor* cursor, const QString& s)
1160       {
1161       int rcol, ridx;
1162       removeEmptyFragment(); // since we are going to write text, we don't need an empty fragment to hold format info. if such exists, delete it
1163       auto i = fragment(cursor->column(), &rcol, &ridx);
1164       if (i != _fragments.end()) {
1165             if (!(i->format == *cursor->format())) {
1166                   if (rcol == 0)
1167                         _fragments.insert(i, TextFragment(cursor, s));
1168                   else {
1169                         TextFragment f2 = i->split(rcol);
1170                         i = _fragments.insert(i+1, TextFragment(cursor, s));
1171                         _fragments.insert(i+1, f2);
1172                         }
1173                   }
1174             else
1175                   i->text.insert(ridx, s);
1176             }
1177       else {
1178             if (!_fragments.empty() && _fragments.back().format == *cursor->format())
1179                   _fragments.back().text.append(s);
1180             else
1181                   _fragments.append(TextFragment(cursor, s));
1182             }
1183       }
1184 
1185 //---------------------------------------------------------
1186 //
1187 //   insertEmptyFragmentIfNeeded
1188 //   used to insert an empty TextFragment in TextBlocks that have none
1189 //   that way, the formatting information (most importantly the font size) of the line is preserved
1190 //
1191 //---------------------------------------------------------
1192 
insertEmptyFragmentIfNeeded(TextCursor * cursor)1193 void TextBlock::insertEmptyFragmentIfNeeded(TextCursor* cursor)
1194       {
1195       if (_fragments.size() == 0 || _fragments.at(0).text.isEmpty()) {
1196             _fragments.insert(0, TextFragment(cursor, ""));
1197             }
1198       }
1199 
1200 //---------------------------------------------------------
1201 //   removeEmptyFragment
1202 //---------------------------------------------------------
1203 
removeEmptyFragment()1204 void TextBlock::removeEmptyFragment()
1205       {
1206       if (_fragments.size() > 0 && _fragments.at(0).text.isEmpty())
1207             _fragments.removeAt(0);
1208       }
1209 
1210 //---------------------------------------------------------
1211 //   fragment
1212 //    inputs:
1213 //      column is the column relative to the start of the TextBlock.
1214 //    outputs:
1215 //      rcol will be the column relative to the start of the TextFragment that the input column is in.
1216 //      ridx will be the QChar index into TextFragment's text QString relative to the start of that TextFragment.
1217 //
1218 //---------------------------------------------------------
1219 
fragment(int column,int * rcol,int * ridx)1220 QList<TextFragment>::iterator TextBlock::fragment(int column, int* rcol, int* ridx)
1221       {
1222       int col = 0;
1223       for (auto i = _fragments.begin(); i != _fragments.end(); ++i) {
1224             *rcol = 0;
1225             *ridx = 0;
1226             for (const QChar& c : qAsConst(i->text)) {
1227                   if (col == column)
1228                         return i;
1229                   ++*ridx;
1230                   if (c.isHighSurrogate())
1231                         continue;
1232                   ++col;
1233                   ++*rcol;
1234                   }
1235             }
1236       return _fragments.end();
1237       }
1238 
1239 //---------------------------------------------------------
1240 //   remove
1241 //---------------------------------------------------------
1242 
remove(int column,TextCursor * cursor)1243 QString TextBlock::remove(int column, TextCursor* cursor)
1244       {
1245       int col = 0;
1246       QString s;
1247       for (auto i = _fragments.begin(); i != _fragments.end(); ++i) {
1248             int idx  = 0;
1249             int rcol = 0;
1250             for (const QChar& c : qAsConst(i->text)) {
1251                   if (col == column) {
1252                         if (c.isSurrogate()) {
1253                               s = i->text.mid(idx, 2);
1254                               i->text.remove(idx, 2);
1255                               }
1256                         else {
1257                               s = i->text.mid(idx, 1);
1258                               i->text.remove(idx, 1);
1259                               }
1260                         if (i->text.isEmpty())
1261                               _fragments.erase(i);
1262                         simplify();
1263                         insertEmptyFragmentIfNeeded(cursor); // without this, cursorRect can't calculate the y position of the cursor correctly
1264                         return s;
1265                         }
1266                   ++idx;
1267                   if (c.isHighSurrogate())
1268                         continue;
1269                   ++col;
1270                   ++rcol;
1271                   }
1272             }
1273       insertEmptyFragmentIfNeeded(cursor); // without this, cursorRect can't calculate the y position of the cursor correctly
1274       return s;
1275 //      qDebug("TextBlock::remove: column %d not found", column);
1276       }
1277 
1278 //---------------------------------------------------------
1279 //   simplify
1280 //---------------------------------------------------------
1281 
simplify()1282 void TextBlock::simplify()
1283       {
1284       if (_fragments.size() < 2)
1285             return;
1286       auto i = _fragments.begin();
1287       TextFragment* f = &*i;
1288       ++i;
1289       for (; i != _fragments.end(); ++i) {
1290             while (i != _fragments.end() && (i->format == f->format)) {
1291                   f->text.append(i->text);
1292                   i = _fragments.erase(i);
1293                   }
1294             if (i == _fragments.end())
1295                   break;
1296             f = &*i;
1297             }
1298       }
1299 
1300 //---------------------------------------------------------
1301 //   remove
1302 //---------------------------------------------------------
1303 
remove(int start,int n,TextCursor * cursor)1304 QString TextBlock::remove(int start, int n, TextCursor* cursor)
1305       {
1306       if (n == 0)
1307             return QString();
1308       int col = 0;
1309       QString s;
1310       for (auto i = _fragments.begin(); i != _fragments.end();) {
1311             int rcol = 0;
1312             bool inc = true;
1313             for( int idx = 0; idx < i->text.length(); ) {
1314                   QChar c = i->text[idx];
1315                   if (col == start) {
1316                         if (c.isHighSurrogate()) {
1317                               s += c;
1318                               i->text.remove(idx, 1);
1319                               c = i->text[idx];
1320                               }
1321                         s += c;
1322                         i->text.remove(idx, 1);
1323                         if (i->text.isEmpty() && (_fragments.size() > 1)) {
1324                               i = _fragments.erase(i);
1325                               inc = false;
1326                               }
1327                         --n;
1328                         if (n == 0) {
1329                               insertEmptyFragmentIfNeeded(cursor); // without this, cursorRect can't calculate the y position of the cursor correctly
1330                               return s;
1331                               }
1332                         continue;
1333                         }
1334                   ++idx;
1335                   if (c.isHighSurrogate())
1336                         continue;
1337                   ++col;
1338                   ++rcol;
1339                   }
1340             if (inc)
1341                   ++i;
1342             }
1343       insertEmptyFragmentIfNeeded(cursor); // without this, cursorRect can't calculate the y position of the cursor correctly
1344       return s;
1345       }
1346 
1347 //---------------------------------------------------------
1348 //   changeFormat
1349 //---------------------------------------------------------
1350 
changeFormat(FormatId id,QVariant data,int start,int n)1351 void TextBlock::changeFormat(FormatId id, QVariant data, int start, int n)
1352       {
1353       int col = 0;
1354       for (auto i = _fragments.begin(); i != _fragments.end(); ++i) {
1355             int columns = i->columns();
1356             if (start + n <= col)
1357                   break;
1358             if (start >= col + columns) {
1359                   col += i->columns();
1360                   continue;
1361                   }
1362             int endCol = col + columns;
1363 
1364             if ((start <= col) && (start < endCol) && ((start+n) < endCol)) {
1365                   // left
1366                   TextFragment f = i->split(start + n - col);
1367                   i->changeFormat(id, data);
1368                   i = _fragments.insert(i+1, f);
1369                   }
1370             else if (start > col && ((start+n) < endCol)) {
1371                   // middle
1372                   TextFragment lf = i->split(start+n - col);
1373                   TextFragment mf = i->split(start - col);
1374                   mf.changeFormat(id, data);
1375                   i = _fragments.insert(i+1, mf);
1376                   i = _fragments.insert(i+1, lf);
1377                   }
1378             else if (start > col) {
1379                   // right
1380                   TextFragment f = i->split(start - col);
1381                   f.changeFormat(id, data);
1382                   i = _fragments.insert(i+1, f);
1383                   }
1384             else {
1385                   // complete fragment
1386                   i->changeFormat(id, data);
1387                   }
1388             col = endCol;
1389             }
1390       }
1391 
1392 //---------------------------------------------------------
1393 //   setFormat
1394 //---------------------------------------------------------
1395 
setFormat(FormatId id,QVariant data)1396 void CharFormat::setFormat(FormatId id, QVariant data)
1397       {
1398       switch (id) {
1399             case FormatId::Bold:
1400                   setBold(data.toBool());
1401                   break;
1402             case FormatId::Italic:
1403                   setItalic(data.toBool());
1404                   break;
1405             case FormatId::Underline:
1406                   setUnderline(data.toBool());
1407                   break;
1408             case FormatId::Valign:
1409                   _valign = static_cast<VerticalAlignment>(data.toInt());
1410                   break;
1411             case FormatId::FontSize:
1412                   _fontSize = data.toDouble();
1413                   break;
1414             case FormatId::FontFamily:
1415                   _fontFamily = data.toString();
1416                   break;
1417             }
1418       }
1419 
1420 //---------------------------------------------------------
1421 //   changeFormat
1422 //---------------------------------------------------------
1423 
changeFormat(FormatId id,QVariant data)1424 void TextFragment::changeFormat(FormatId id, QVariant data)
1425       {
1426       format.setFormat(id, data);
1427       }
1428 
1429 //---------------------------------------------------------
1430 //   split
1431 //---------------------------------------------------------
1432 
split(int column,Ms::TextCursor * cursor)1433 TextBlock TextBlock::split(int column, Ms::TextCursor* cursor)
1434       {
1435       TextBlock tl;
1436 
1437       int col = 0;
1438       for (auto i = _fragments.begin(); i != _fragments.end(); ++i) {
1439             int idx = 0;
1440             for (const QChar& c : qAsConst(i->text)) {
1441                   if (col == column) {
1442                         if (idx) {
1443                               if (idx < i->text.size()) {
1444                                     TextFragment tf(i->text.mid(idx));
1445                                     tf.format = i->format;
1446                                     tl._fragments.append(tf);
1447                                     i->text = i->text.left(idx);
1448                                     ++i;
1449                                     }
1450                               }
1451                         for (; i != _fragments.end(); i = _fragments.erase(i))
1452                               tl._fragments.append(*i);
1453                         if (_fragments.size() == 0)
1454                               insertEmptyFragmentIfNeeded(cursor);
1455                         return tl;
1456                         }
1457                   ++idx;
1458                   if (c.isHighSurrogate())
1459                         continue;
1460                   ++col;
1461                   }
1462             }
1463       TextFragment tf("");
1464       if (_fragments.size() > 0)
1465             tf.format = _fragments.last().format;
1466       else if (_fragments.size() == 0)
1467             insertEmptyFragmentIfNeeded(cursor);
1468       tl._fragments.append(tf);
1469       return tl;
1470       }
1471 
1472 //---------------------------------------------------------
1473 //   text
1474 //    extract text, symbols are marked with <sym>xxx</sym>
1475 //---------------------------------------------------------
1476 
text(int col1,int len) const1477 QString TextBlock::text(int col1, int len) const
1478       {
1479       QString s;
1480       int col = 0;
1481       for (const auto &f : _fragments) {
1482             if (f.text.isEmpty())
1483                   continue;
1484             for (const QChar& c : qAsConst(f.text)) {
1485                   if (col >= col1 && (len < 0 || ((col-col1) < len)))
1486                         s += XmlWriter::xmlString(c.unicode());
1487                   if (!c.isHighSurrogate())
1488                         ++col;
1489                   }
1490             }
1491       return s;
1492       }
1493 
1494 //---------------------------------------------------------
1495 //   Text
1496 //---------------------------------------------------------
1497 
TextBase(Score * s,Tid tid,ElementFlags f)1498 TextBase::TextBase(Score* s, Tid tid, ElementFlags f)
1499    : Element(s, f | ElementFlag::MOVABLE)
1500       {
1501       _tid                    = tid;
1502       _family                 = "Edwin";
1503       _size                   = 10.0;
1504       _textLineSpacing        = 1.0;
1505       _fontStyle              = FontStyle::Normal;
1506       _bgColor                = QColor(255, 255, 255, 0);
1507       _frameColor             = QColor(0, 0, 0, 255);
1508       _align                  = Align::LEFT;
1509       _frameType              = FrameType::NO_FRAME;
1510       _frameWidth             = Spatium(0.1);
1511       _paddingWidth           = Spatium(0.2);
1512       _frameRound             = 0;
1513       }
1514 
TextBase(Score * s,ElementFlags f)1515 TextBase::TextBase(Score* s, ElementFlags f)
1516    : TextBase(s, Tid::DEFAULT, f)
1517       {
1518       }
1519 
TextBase(const TextBase & st)1520 TextBase::TextBase(const TextBase& st)
1521    : Element(st)
1522       {
1523       _text                        = st._text;
1524       _layout                      = st._layout;
1525       textInvalid                  = st.textInvalid;
1526       layoutInvalid                = st.layoutInvalid;
1527       frame                        = st.frame;
1528       _layoutToParentWidth         = st._layoutToParentWidth;
1529       hexState                     = -1;
1530 
1531       _tid                         = st._tid;
1532       _family                      = st._family;
1533       _size                        = st._size;
1534       _textLineSpacing             = st._textLineSpacing;
1535       _fontStyle                   = st._fontStyle;
1536       _bgColor                     = st._bgColor;
1537       _frameColor                  = st._frameColor;
1538       _align                       = st._align;
1539       _frameType                   = st._frameType;
1540       _frameWidth                  = st._frameWidth;
1541       _paddingWidth                = st._paddingWidth;
1542       _frameRound                  = st._frameRound;
1543 
1544       size_t n = _elementStyle->size() + TEXT_STYLE_SIZE;
1545       delete[] _propertyFlagsList;
1546       _propertyFlagsList = new PropertyFlags[n];
1547       for (size_t i = 0; i < n; ++i)
1548             _propertyFlagsList[i] = st._propertyFlagsList[i];
1549       _links = 0;
1550       }
1551 
1552 //---------------------------------------------------------
1553 //   drawSelection
1554 //---------------------------------------------------------
1555 
drawSelection(QPainter * p,const QRectF & r) const1556 void TextBase::drawSelection(QPainter* p, const QRectF& r) const
1557       {
1558       QBrush bg(QColor("steelblue"));
1559       p->setCompositionMode(QPainter::CompositionMode_HardLight);
1560       p->setBrush(bg);
1561       p->setPen(Qt::NoPen);
1562       p->drawRect(r);
1563       p->setCompositionMode(QPainter::CompositionMode_SourceOver);
1564       p->setPen(textColor());
1565       }
1566 
1567 //---------------------------------------------------------
1568 //   textColor
1569 //---------------------------------------------------------
1570 
textColor() const1571 QColor TextBase::textColor() const
1572       {
1573       return curColor();
1574       }
1575 
1576 //---------------------------------------------------------
1577 //   insert
1578 //    insert character
1579 //---------------------------------------------------------
1580 
insert(TextCursor * cursor,uint code)1581 void TextBase::insert(TextCursor* cursor, uint code)
1582       {
1583       if (cursor->row() >= rows())
1584             _layout.append(TextBlock());
1585       if (code == '\t')
1586             code = ' ';
1587 
1588       QString s;
1589       if (QChar::requiresSurrogates(code))
1590             s = QString(QChar(QChar::highSurrogate(code))).append(QChar(QChar::lowSurrogate(code)));
1591       else
1592             s = QString(code);
1593       _layout[cursor->row()].insert(cursor, s);
1594 
1595       cursor->setColumn(cursor->column() + 1);
1596       cursor->clearSelection();
1597       }
1598 
1599 //---------------------------------------------------------
1600 //   parseStringProperty
1601 //---------------------------------------------------------
1602 
parseStringProperty(const QString & s)1603 static QString parseStringProperty(const QString& s)
1604       {
1605       QString rs;
1606       for (const QChar& c : s) {
1607             if (c == '"')
1608                   break;
1609             rs += c;
1610             }
1611       return rs;
1612       }
1613 
1614 //---------------------------------------------------------
1615 //   parseNumProperty
1616 //---------------------------------------------------------
1617 
parseNumProperty(const QString & s)1618 static qreal parseNumProperty(const QString& s)
1619       {
1620       return parseStringProperty(s).toDouble();
1621       }
1622 
1623 //---------------------------------------------------------
1624 //   createLayout
1625 //    create layout from text
1626 //---------------------------------------------------------
1627 
createLayout()1628 void TextBase::createLayout()
1629       {
1630       _layout.clear();  // deletes the text fragments so we lose all formatting information
1631       TextCursor cursor(this);
1632       cursor.init();
1633 
1634       int state = 0;
1635       QString token;
1636       QString sym;
1637       bool symState = false;
1638       for (int i = 0; i < _text.length(); i++) {
1639             const QChar& c = _text[i];
1640             if (state == 0) {
1641                   if (c == '<') {
1642                         state = 1;
1643                         token.clear();
1644                         }
1645                   else if (c == '&') {
1646                         state = 2;
1647                         token.clear();
1648                         }
1649                   else if (c == '\n') {
1650                         if (rows() <= cursor.row())
1651                               _layout.append(TextBlock());
1652                         if(_layout[cursor.row()].fragments().size() == 0)
1653                               _layout[cursor.row()].insertEmptyFragmentIfNeeded(&cursor); // used to preserve the Font size of the line (font info is held in TextFragments, see PR #5881)
1654                         _layout[cursor.row()].setEol(true);
1655                         cursor.setRow(cursor.row() + 1);
1656                         cursor.setColumn(0);
1657                         if (rows() <= cursor.row())
1658                               _layout.append(TextBlock());
1659                         }
1660                   else {
1661                         if (symState)
1662                               sym += c;
1663                         else {
1664                               if (c.isHighSurrogate()) {
1665                                     i++;
1666                                     Q_ASSERT(i < _text.length());
1667                                     insert(&cursor, QChar::surrogateToUcs4(c, _text[i]));
1668                                     }
1669                               else
1670                                     insert(&cursor, c.unicode());
1671                               }
1672                         }
1673                   }
1674             else if (state == 1) {
1675                   if (c == '>') {
1676                         bool unstyleFontStyle = false;
1677                         state = 0;
1678                         if (token == "b") {
1679                               cursor.format()->setBold(true);
1680                               unstyleFontStyle = true;
1681                               }
1682                         else if (token == "/b")
1683                               cursor.format()->setBold(false);
1684                         else if (token == "i") {
1685                               cursor.format()->setItalic(true);
1686                               unstyleFontStyle = true;
1687                               }
1688                         else if (token == "/i")
1689                               cursor.format()->setItalic(false);
1690                         else if (token == "u") {
1691                               cursor.format()->setUnderline(true);
1692                               unstyleFontStyle = true;
1693                               }
1694                         else if (token == "/u")
1695                               cursor.format()->setUnderline(false);
1696                         else if (token == "sub")
1697                               cursor.format()->setValign(VerticalAlignment::AlignSubScript);
1698                         else if (token == "/sub")
1699                               cursor.format()->setValign(VerticalAlignment::AlignNormal);
1700                         else if (token == "sup")
1701                               cursor.format()->setValign(VerticalAlignment::AlignSuperScript);
1702                         else if (token == "/sup")
1703                               cursor.format()->setValign(VerticalAlignment::AlignNormal);
1704                         else if (token == "sym") {
1705                               symState = true;
1706                               sym.clear();
1707                               }
1708                         else if (token == "/sym") {
1709                               symState = false;
1710                               SymId id = Sym::name2id(sym);
1711                               if (id != SymId::noSym) {
1712                                     CharFormat fmt = *cursor.format();  // save format
1713                                     // uint code = score()->scoreFont()->sym(id).code();
1714                                     uint code = ScoreFont::fallbackFont()->sym(id).code();
1715                                     cursor.format()->setFontFamily("ScoreText");
1716                                     cursor.format()->setBold(false);
1717                                     cursor.format()->setItalic(false);
1718                                     insert(&cursor, code);
1719                                     cursor.setFormat(fmt);  // restore format
1720                                     }
1721                               else {
1722                                     qDebug("unknown symbol <%s>", qPrintable(sym));
1723                                     }
1724                               }
1725                         else if (token.startsWith("font ")) {
1726                               token = token.mid(5);
1727                               if (token.startsWith("size=\"")) {
1728                                     cursor.format()->setFontSize(parseNumProperty(token.mid(6)));
1729                                     setPropertyFlags(Pid::FONT_SIZE, PropertyFlags::UNSTYLED);
1730                                     }
1731                               else if (token.startsWith("face=\"")) {
1732                                     QString face = parseStringProperty(token.mid(6));
1733                                     face = unEscape(face);
1734                                     cursor.format()->setFontFamily(face);
1735                                     setPropertyFlags(Pid::FONT_FACE, PropertyFlags::UNSTYLED);
1736                                     }
1737                               else
1738                                     qDebug("cannot parse html property <%s> in text <%s>",
1739                                        qPrintable(token), qPrintable(_text));
1740                               }
1741                         if (unstyleFontStyle)
1742                               setPropertyFlags(Pid::FONT_STYLE, PropertyFlags::UNSTYLED);
1743                         }
1744                   else
1745                         token += c;
1746                   }
1747             else if (state == 2) {
1748                   if (c == ';') {
1749                         state = 0;
1750                         if (token == "lt")
1751                               insert(&cursor, '<');
1752                         else if (token == "gt")
1753                               insert(&cursor, '>');
1754                         else if (token == "amp")
1755                               insert(&cursor, '&');
1756                         else if (token == "quot")
1757                               insert(&cursor, '"');
1758                         else {
1759                               // TODO insert(&cursor, Sym::name2id(token));
1760                               }
1761                         }
1762                   else
1763                         token += c;
1764                   }
1765             }
1766       if (_layout.empty())
1767             _layout.append(TextBlock());
1768       layoutInvalid = false;
1769       }
1770 
1771 //---------------------------------------------------------
1772 //   layout
1773 //---------------------------------------------------------
1774 
layout()1775 void TextBase::layout()
1776       {
1777       setPos(QPointF());
1778       if (!parent())
1779             setOffset(0.0, 0.0);
1780 //      else if (isStyled(Pid::OFFSET))                                   // TODO: should be set already
1781 //            setOffset(propertyDefault(Pid::OFFSET).toPointF());
1782       if (placeBelow())
1783             rypos() = staff() ? staff()->height() : 0.0;
1784       layout1();
1785       }
1786 
1787 //---------------------------------------------------------
1788 //   layout1
1789 //---------------------------------------------------------
1790 
layout1()1791 void TextBase::layout1()
1792       {
1793       if (layoutInvalid)
1794             createLayout();
1795       if (_layout.empty())
1796             _layout.append(TextBlock());
1797       QRectF bb;
1798       qreal y = 0;
1799       for (int i = 0; i < rows(); ++i) {
1800             TextBlock* t = &_layout[i];
1801             t->layout(this);
1802             const QRectF* r = &t->boundingRect();
1803 
1804             if (r->height() == 0)
1805                   r = &_layout[i-i].boundingRect();
1806             y += t->lineSpacing();
1807             t->setY(y);
1808             bb |= r->translated(0.0, y);
1809             }
1810       qreal yoff = 0;
1811       qreal h    = 0;
1812       if (parent()) {
1813             if (layoutToParentWidth()) {
1814                   if (parent()->isTBox()) {
1815                         // hack: vertical alignment is always TOP
1816                         _align = Align(((char)_align) & ((char)Align::HMASK)) | Align::TOP;
1817                         }
1818                   else if (parent()->isBox()) {
1819                         // consider inner margins of frame
1820                         Box* b = toBox(parent());
1821                         yoff = b->topMargin()  * DPMM;
1822 
1823                         if (b->height() < bb.bottom())
1824                               h = b->height() / 2 + bb.height();
1825                         else
1826                               h  = b->height() - yoff - b->bottomMargin() * DPMM;
1827                         }
1828                   else if (parent()->isPage()) {
1829                         Page* p = toPage(parent());
1830                         h = p->height() - p->tm() - p->bm();
1831                         yoff = p->tm();
1832                         }
1833                   else if (parent()->isMeasure())
1834                         ;
1835                   else
1836                         h  = parent()->height();
1837                   }
1838             }
1839       else
1840             setPos(QPointF());
1841 
1842       if (align() & Align::BOTTOM)
1843             yoff += h - bb.bottom();
1844       else if (align() & Align::VCENTER) {
1845             yoff +=  (h - (bb.top() + bb.bottom())) * .5;
1846             }
1847       else if (align() & Align::BASELINE)
1848             yoff += h * .5 - _layout.front().lineSpacing();
1849       else
1850             yoff += -bb.top();
1851 
1852       for (TextBlock& t : _layout)
1853             t.setY(t.y() + yoff);
1854 
1855       bb.translate(0.0, yoff);
1856 
1857       setbbox(bb);
1858       if (hasFrame())
1859             layoutFrame();
1860       score()->addRefresh(canvasBoundingRect());
1861       }
1862 
1863 //---------------------------------------------------------
1864 //   layoutFrame
1865 //---------------------------------------------------------
1866 
layoutFrame()1867 void TextBase::layoutFrame()
1868       {
1869 //      if (empty()) {    // or bbox.width() <= 1.0
1870       if (bbox().width() <= 1.0 || bbox().height() < 1.0) {    // or bbox.width() <= 1.0
1871             // this does not work for Harmony:
1872             QFontMetricsF fm = QFontMetricsF(font(), MScore::paintDevice());
1873             qreal ch = fm.ascent();
1874             qreal cw = fm.width('n');
1875             frame = QRectF(0.0, -ch, cw, ch);
1876             }
1877       else
1878             frame = bbox();
1879 
1880       if (square()) {
1881 #if 0
1882             // "real" square
1883             if (frame.width() > frame.height()) {
1884                   qreal w = frame.width() - frame.height();
1885                   frame.adjust(0.0, -w * .5, 0.0, w * .5);
1886                   }
1887             else {
1888                   qreal w = frame.height() - frame.width();
1889                   frame.adjust(-w * .5, 0.0, w * .5, 0.0);
1890                   }
1891 #else
1892             // make sure width >= height
1893             if (frame.height() > frame.width()) {
1894                   qreal w = frame.height() - frame.width();
1895                   frame.adjust(-w * .5, 0.0, w * .5, 0.0);
1896                   }
1897 #endif
1898             }
1899       else if (circle()) {
1900             if (frame.width() > frame.height()) {
1901                   frame.setY(frame.y() + (frame.width() - frame.height()) * -.5);
1902                   frame.setHeight(frame.width());
1903                   }
1904             else {
1905                   frame.setX(frame.x() + (frame.height() - frame.width()) * -.5);
1906                   frame.setWidth(frame.height());
1907                   }
1908             }
1909       qreal _spatium = spatium();
1910       qreal w = (paddingWidth() + frameWidth() * .5f).val() * _spatium;
1911       frame.adjust(-w, -w, w, w);
1912       w = frameWidth().val() * _spatium;
1913       setbbox(frame.adjusted(-w, -w, w, w));
1914       }
1915 
1916 //---------------------------------------------------------
1917 //   lineSpacing
1918 //---------------------------------------------------------
1919 
lineSpacing() const1920 qreal TextBase::lineSpacing() const
1921       {
1922       return fontMetrics().lineSpacing() * MScore::pixelRatio;
1923       }
1924 
1925 //---------------------------------------------------------
1926 //   lineHeight
1927 //---------------------------------------------------------
1928 
lineHeight() const1929 qreal TextBase::lineHeight() const
1930       {
1931       return fontMetrics().height();
1932       }
1933 
1934 //---------------------------------------------------------
1935 //   baseLine
1936 //---------------------------------------------------------
1937 
baseLine() const1938 qreal TextBase::baseLine() const
1939       {
1940       return fontMetrics().ascent();
1941       }
1942 
1943 //---------------------------------------------------------
1944 //   XmlNesting
1945 //---------------------------------------------------------
1946 
1947 class XmlNesting : public QStack<QString> {
1948       QString* _s;
1949 
1950    public:
XmlNesting(QString * s)1951       XmlNesting(QString* s) { _s = s; }
pushToken(const QString & t)1952       void pushToken(const QString& t) {
1953             *_s += "<";
1954             *_s += t;
1955             *_s += ">";
1956             push(t);
1957             }
pushB()1958       void pushB() { pushToken("b"); }
pushI()1959       void pushI() { pushToken("i"); }
pushU()1960       void pushU() { pushToken("u"); }
1961 
popToken()1962       QString popToken() {
1963             QString s = pop();
1964             *_s += "</";
1965             *_s += s;
1966             *_s += ">";
1967             return s;
1968             }
popToken(const char * t)1969       void popToken(const char* t) {
1970             QStringList ps;
1971             for (;;) {
1972                   QString s = popToken();
1973                   if (s == t)
1974                         break;
1975                   ps += s;
1976                   }
1977             for (const QString& s : qAsConst(ps))
1978                   pushToken(s);
1979             }
popB()1980       void popB() { popToken("b"); }
popI()1981       void popI() { popToken("i"); }
popU()1982       void popU() { popToken("u"); }
1983       };
1984 
1985 //---------------------------------------------------------
1986 //   genText
1987 //---------------------------------------------------------
1988 
genText() const1989 void TextBase::genText() const
1990       {
1991       _text.clear();
1992       bool bold_      = false;
1993       bool italic_    = false;
1994       bool underline_ = false;
1995 
1996       for (const TextBlock& block : _layout) {
1997             for (const TextFragment& f : block.fragments()) {
1998                   if (!f.format.bold() && bold())
1999                         bold_ = true;
2000                   if (!f.format.italic() && italic())
2001                         italic_ = true;
2002                   if (!f.format.underline() && underline())
2003                         underline_ = true;
2004                   }
2005             }
2006       CharFormat fmt;
2007       fmt.setFontFamily(family());
2008       fmt.setFontSize(size());
2009       fmt.setStyle(fontStyle());
2010       fmt.setPreedit(false);
2011       fmt.setValign(VerticalAlignment::AlignNormal);
2012 
2013       XmlNesting xmlNesting(&_text);
2014       if (bold_)
2015             xmlNesting.pushB();
2016       if (italic_)
2017             xmlNesting.pushI();
2018       if (underline_)
2019             xmlNesting.pushU();
2020 
2021       for (const TextBlock& block : _layout) {
2022             for (const TextFragment& f : block.fragments()) {
2023                   // don't skip, empty text fragments hold information for empty lines
2024 //                  if (f.text.isEmpty())                     // skip empty fragments, not to
2025 //                        continue;                           // insert extra HTML formatting
2026                   const CharFormat& format = f.format;
2027                   if (fmt.bold() != format.bold()) {
2028                         if (format.bold())
2029                               xmlNesting.pushB();
2030                         else
2031                               xmlNesting.popB();
2032                         }
2033                   if (fmt.italic() != format.italic()) {
2034                         if (format.italic())
2035                               xmlNesting.pushI();
2036                         else
2037                               xmlNesting.popI();
2038                         }
2039                   if (fmt.underline() != format.underline()) {
2040                         if (format.underline())
2041                               xmlNesting.pushU();
2042                         else
2043                               xmlNesting.popU();
2044                         }
2045 
2046                   if (format.fontSize() != fmt.fontSize())
2047                         _text += QString("<font size=\"%1\"/>").arg(format.fontSize());
2048                   if (format.fontFamily() != fmt.fontFamily())
2049                         _text += QString("<font face=\"%1\"/>").arg(TextBase::escape(format.fontFamily()));
2050 
2051                   VerticalAlignment va = format.valign();
2052                   VerticalAlignment cva = fmt.valign();
2053                   if (cva != va) {
2054                         switch (va) {
2055                               case VerticalAlignment::AlignNormal:
2056                                     xmlNesting.popToken(cva == VerticalAlignment::AlignSuperScript ? "sup" : "sub");
2057                                     break;
2058                               case VerticalAlignment::AlignSuperScript:
2059                                     xmlNesting.pushToken("sup");
2060                                     break;
2061                               case VerticalAlignment::AlignSubScript:
2062                                     xmlNesting.pushToken("sub");
2063                                     break;
2064                               }
2065                         }
2066                   _text += XmlWriter::xmlString(f.text);
2067                   fmt = format;
2068                   }
2069             if (block.eol())
2070                   _text += QChar::LineFeed;
2071             }
2072       while (!xmlNesting.empty())
2073             xmlNesting.popToken();
2074       textInvalid = false;
2075       }
2076 
2077 //---------------------------------------------------------
2078 //   selectAll
2079 //---------------------------------------------------------
2080 
selectAll(TextCursor * _cursor)2081 void TextBase::selectAll(TextCursor* _cursor)
2082       {
2083       _cursor->setSelectLine(0);
2084       _cursor->setSelectColumn(0);
2085       _cursor->setRow(rows() - 1);
2086       _cursor->setColumn(_cursor->curLine().columns());
2087       }
2088 
2089 //---------------------------------------------------------
2090 //   multiClickSelect
2091 //    for double and triple clicks
2092 //---------------------------------------------------------
2093 
multiClickSelect(EditData & editData,MultiClick clicks)2094 void TextBase::multiClickSelect(EditData& editData, MultiClick clicks)
2095       {
2096       switch (clicks) {
2097             case MultiClick::Double:
2098                   cursor(editData)->doubleClickSelect();
2099                   break;
2100             case MultiClick::Triple:
2101                   selectAll(cursor(editData));
2102                   break;
2103             }
2104       }
2105 
2106 //---------------------------------------------------------
2107 //   write
2108 //---------------------------------------------------------
2109 
write(XmlWriter & xml) const2110 void TextBase::write(XmlWriter& xml) const
2111       {
2112       if (!xml.canWrite(this))
2113             return;
2114       xml.stag(this);
2115       writeProperties(xml, true, true);
2116       xml.etag();
2117       }
2118 
2119 //---------------------------------------------------------
2120 //   read
2121 //---------------------------------------------------------
2122 
read(XmlReader & e)2123 void TextBase::read(XmlReader& e)
2124       {
2125       while (e.readNextStartElement()) {
2126             if (!readProperties(e))
2127                   e.unknown();
2128             }
2129       }
2130 
2131 //---------------------------------------------------------
2132 //   writeProperties
2133 //---------------------------------------------------------
2134 
writeProperties(XmlWriter & xml,bool writeText,bool) const2135 void TextBase::writeProperties(XmlWriter& xml, bool writeText, bool /*writeStyle*/) const
2136       {
2137       Element::writeProperties(xml);
2138       writeProperty(xml, Pid::SUB_STYLE);
2139 
2140       for (const StyledProperty& spp : *_elementStyle) {
2141             if (!isStyled(spp.pid))
2142                   writeProperty(xml, spp.pid);
2143             }
2144       for (const StyledProperty& spp : *textStyle(tid())) {
2145             if (!isStyled(spp.pid))
2146                   writeProperty(xml, spp.pid);
2147             }
2148       if (writeText)
2149             xml.writeXml("text", xmlText());
2150       }
2151 
2152 static constexpr std::array<Pid, 18> pids { {
2153       Pid::SUB_STYLE,
2154       Pid::FONT_FACE,
2155       Pid::FONT_SIZE,
2156       Pid::TEXT_LINE_SPACING,
2157       Pid::FONT_STYLE,
2158       Pid::COLOR,
2159       Pid::FRAME_TYPE,
2160       Pid::FRAME_WIDTH,
2161       Pid::FRAME_PADDING,
2162       Pid::FRAME_ROUND,
2163       Pid::FRAME_FG_COLOR,
2164       Pid::FRAME_BG_COLOR,
2165       Pid::ALIGN,
2166       } };
2167 
2168 //---------------------------------------------------------
2169 //   readProperties
2170 //---------------------------------------------------------
2171 
readProperties(XmlReader & e)2172 bool TextBase::readProperties(XmlReader& e)
2173       {
2174       const QStringRef& tag(e.name());
2175       for (Pid i :pids) {
2176             if (readProperty(tag, e, i))
2177                   return true;
2178             }
2179       if (tag == "text")
2180             setXmlText(e.readXml());
2181       else if (tag == "bold") {
2182             bool val = e.readInt();
2183             if (val)
2184                   _fontStyle = _fontStyle + FontStyle::Bold;
2185             else
2186                   _fontStyle = _fontStyle - FontStyle::Bold;
2187             if (isStyled(Pid::FONT_STYLE))
2188                   setPropertyFlags(Pid::FONT_STYLE, PropertyFlags::UNSTYLED);
2189             }
2190       else if (tag == "italic") {
2191             bool val = e.readInt();
2192             if (val)
2193                   _fontStyle = _fontStyle + FontStyle::Italic;
2194             else
2195                   _fontStyle = _fontStyle - FontStyle::Italic;
2196             if (isStyled(Pid::FONT_STYLE))
2197                   setPropertyFlags(Pid::FONT_STYLE, PropertyFlags::UNSTYLED);
2198             }
2199       else if (tag == "underline") {
2200             bool val = e.readInt();
2201             if (val)
2202                   _fontStyle = _fontStyle + FontStyle::Underline;
2203             else
2204                   _fontStyle = _fontStyle - FontStyle::Underline;
2205             if (isStyled(Pid::FONT_STYLE))
2206                   setPropertyFlags(Pid::FONT_STYLE, PropertyFlags::UNSTYLED);
2207             }
2208       else if (!Element::readProperties(e))
2209             return false;
2210       return true;
2211       }
2212 
2213 //---------------------------------------------------------
2214 //   propertyId
2215 //---------------------------------------------------------
2216 
propertyId(const QStringRef & name) const2217 Pid TextBase::propertyId(const QStringRef& name) const
2218       {
2219       if (name == "text")
2220             return Pid::TEXT;
2221       for (Pid pid : pids) {
2222             if (propertyName(pid) == name)
2223                   return pid;
2224             }
2225       return Element::propertyId(name);
2226       }
2227 
2228 //---------------------------------------------------------
2229 //   pageRectangle
2230 //---------------------------------------------------------
2231 
pageRectangle() const2232 QRectF TextBase::pageRectangle() const
2233       {
2234       if (parent() && (parent()->isHBox() || parent()->isVBox() || parent()->isTBox())) {
2235             Box* box = toBox(parent());
2236             QRectF r = box->abbox();
2237             qreal x = r.x() + box->leftMargin() * DPMM;
2238             qreal y = r.y() + box->topMargin() * DPMM;
2239             qreal h = r.height() - (box->topMargin() + box->bottomMargin()) * DPMM;
2240             qreal w = r.width()  - (box->leftMargin() + box->rightMargin()) * DPMM;
2241 
2242             // QSizeF ps = _doc->pageSize();
2243             // return QRectF(x, y, ps.width(), ps.height());
2244 
2245             return QRectF(x, y, w, h);
2246             }
2247       if (parent() && parent()->isPage()) {
2248             Page* box  = toPage(parent());
2249             QRectF r = box->abbox();
2250             qreal x = r.x() + box->lm();
2251             qreal y = r.y() + box->tm();
2252             qreal h = r.height() - box->tm() - box->bm();
2253             qreal w = r.width()  - box->lm() - box->rm();
2254             return QRectF(x, y, w, h);
2255             }
2256       return abbox();
2257       }
2258 
2259 //---------------------------------------------------------
2260 //   dragTo
2261 //---------------------------------------------------------
2262 
dragTo(EditData & ed)2263 void TextBase::dragTo(EditData& ed)
2264       {
2265       TextEditData* ted = static_cast<TextEditData*>(ed.getData(this));
2266       TextCursor* _cursor = &ted->cursor;
2267       _cursor->set(ed.pos, QTextCursor::KeepAnchor);
2268       score()->setUpdateAll();
2269       score()->update();
2270       }
2271 
2272 //---------------------------------------------------------
2273 //   dragAnchorLines
2274 //---------------------------------------------------------
2275 
dragAnchorLines() const2276 QVector<QLineF> TextBase::dragAnchorLines() const
2277       {
2278       QVector<QLineF> result(genericDragAnchorLines());
2279 
2280       if (layoutToParentWidth() && !result.empty()) {
2281             QLineF& line = result[0];
2282             line.setP2(line.p2() + bbox().topLeft());
2283             }
2284 
2285       return result;
2286       }
2287 
2288 //---------------------------------------------------------
2289 //   mousePress
2290 //    set text cursor
2291 //---------------------------------------------------------
2292 
mousePress(EditData & ed)2293 bool TextBase::mousePress(EditData& ed)
2294       {
2295       bool shift = ed.modifiers & Qt::ShiftModifier;
2296       TextEditData* ted = static_cast<TextEditData*>(ed.getData(this));
2297       if (!ted->cursor.set(ed.startMove, shift ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor))
2298             return false;
2299       if (ed.buttons == Qt::MidButton)
2300             paste(ed);
2301       score()->setUpdateAll();
2302       return true;
2303       }
2304 
2305 //---------------------------------------------------------
2306 //   layoutEdit
2307 //---------------------------------------------------------
2308 
layoutEdit()2309 void TextBase::layoutEdit()
2310       {
2311       layout();
2312       if (parent() && parent()->type() == ElementType::TBOX) {
2313             TBox* tbox = toTBox(parent());
2314             tbox->layout();
2315             System* system = tbox->system();
2316             system->setHeight(tbox->height());
2317             triggerLayout();
2318             }
2319       else {
2320             static const qreal w = 2.0; // 8.0 / view->matrix().m11();
2321             score()->addRefresh(canvasBoundingRect().adjusted(-w, -w, w, w));
2322             }
2323       }
2324 
2325 //---------------------------------------------------------
2326 //   acceptDrop
2327 //---------------------------------------------------------
2328 
acceptDrop(EditData & data) const2329 bool TextBase::acceptDrop(EditData& data) const
2330       {
2331       // do not accept the drop if this text element is not being edited
2332       ElementEditData* eed = data.getData(this);
2333       if (!eed || eed->type() != EditDataType::TextEditData)
2334             return false;
2335       ElementType type = data.dropElement->type();
2336       return type == ElementType::SYMBOL || type == ElementType::FSYMBOL;
2337       }
2338 
2339 //---------------------------------------------------------
2340 //   setXmlText
2341 //---------------------------------------------------------
2342 
setXmlText(const QString & s)2343 void TextBase::setXmlText(const QString& s)
2344       {
2345       _text = s;
2346       layoutInvalid = true;
2347       textInvalid   = false;
2348       }
2349 
2350 //---------------------------------------------------------
2351 //   plainText
2352 //    return plain text with symbols
2353 //---------------------------------------------------------
2354 
plainText() const2355 QString TextBase::plainText() const
2356       {
2357       QString s;
2358 
2359       const TextBase* text = this;
2360       std::unique_ptr<TextBase> tmpText;
2361       if (layoutInvalid) {
2362             // Create temporary text object to avoid side effects
2363             // of createLayout() call.
2364             tmpText.reset(toTextBase(this->clone()));
2365             tmpText->createLayout();
2366             text = tmpText.get();
2367             }
2368 
2369       for (const TextBlock& block : text->_layout) {
2370             for (const TextFragment& f : block.fragments())
2371                   s += f.text;
2372             if (block.eol())
2373                   s += QChar::LineFeed;
2374             }
2375       return s;
2376       }
2377 
2378 //---------------------------------------------------------
2379 //   xmlText
2380 //---------------------------------------------------------
2381 
xmlText() const2382 QString TextBase::xmlText() const
2383       {
2384 #if 1
2385       // this is way too expensive
2386       // what side effects has genText() ?
2387       // this method is const by design
2388 
2389       const TextBase* text = this;
2390       std::unique_ptr<TextBase> tmpText;
2391       if (textInvalid) {
2392             // Create temporary text object to avoid side effects
2393             // of genText() call.
2394             tmpText.reset(toTextBase(this->clone()));
2395             tmpText->genText();
2396             text = tmpText.get();
2397             }
2398       return text->_text;
2399 #else
2400       if (textInvalid)
2401             genText();
2402       return _text;
2403 #endif
2404       }
2405 
2406 //---------------------------------------------------------
2407 //   convertFromHtml
2408 //---------------------------------------------------------
2409 
convertFromHtml(const QString & ss) const2410 QString TextBase::convertFromHtml(const QString& ss) const
2411       {
2412       QTextDocument doc;
2413       doc.setHtml(ss);
2414 
2415       QString s;
2416       qreal size_ = size();
2417       QString family_ = family();
2418       for (auto b = doc.firstBlock(); b.isValid() ; b = b.next()) {
2419             if (!s.isEmpty())
2420                   s += "\n";
2421             for (auto it = b.begin(); !it.atEnd(); ++it) {
2422                   QTextFragment f = it.fragment();
2423                   if (f.isValid()) {
2424                         QTextCharFormat tf = f.charFormat();
2425                         QFont font = tf.font();
2426                         qreal htmlSize = font.pointSizeF();
2427                         // html font sizes may have spatium adjustments; need to undo this
2428                         if (sizeIsSpatiumDependent())
2429                               htmlSize *= SPATIUM20 / spatium();
2430                         if (fabs(size_ - htmlSize) > 0.1) {
2431                               size_ = htmlSize;
2432                               s += QString("<font size=\"%1\"/>").arg(size_);
2433                               }
2434                         if (family_ != font.family()) {
2435                               family_ = font.family();
2436                               s += QString("<font face=\"%1\"/>").arg(family_);
2437                               }
2438                         if (font.bold())
2439                               s += "<b>";
2440                         if (font.italic())
2441                               s += "<i>";
2442                         if (font.underline())
2443                               s += "<u>";
2444                         s += f.text().toHtmlEscaped();
2445                         if (font.underline())
2446                               s += "</u>";
2447                         if (font.italic())
2448                               s += "</i>";
2449                         if (font.bold())
2450                               s += "</b>";
2451                         }
2452                   }
2453             }
2454 
2455       if (score() && score()->mscVersion() <= 114) {
2456             s.replace(QChar(0xe10e), QString("<sym>accidentalNatural</sym>"));  //natural
2457             s.replace(QChar(0xe10c), QString("<sym>accidentalSharp</sym>"));    // sharp
2458             s.replace(QChar(0xe10d), QString("<sym>accidentalFlat</sym>"));     // flat
2459             s.replace(QChar(0xe104), QString("<sym>metNoteHalfUp</sym>")),      // note2_Sym
2460             s.replace(QChar(0xe105), QString("<sym>metNoteQuarterUp</sym>"));   // note4_Sym
2461             s.replace(QChar(0xe106), QString("<sym>metNote8thUp</sym>"));       // note8_Sym
2462             s.replace(QChar(0xe107), QString("<sym>metNote16thUp</sym>"));      // note16_Sym
2463             s.replace(QChar(0xe108), QString("<sym>metNote32ndUp</sym>"));      // note32_Sym
2464             s.replace(QChar(0xe109), QString("<sym>metNote64thUp</sym>"));      // note64_Sym
2465             s.replace(QChar(0xe10a), QString("<sym>metAugmentationDot</sym>")); // dot
2466             s.replace(QChar(0xe10b), QString("<sym>metAugmentationDot</sym><sym>space</sym><sym>metAugmentationDot</sym>"));    // dotdot
2467             s.replace(QChar(0xe167), QString("<sym>segno</sym>"));              // segno
2468             s.replace(QChar(0xe168), QString("<sym>coda</sym>"));               // coda
2469             s.replace(QChar(0xe169), QString("<sym>codaSquare</sym>"));         // varcoda
2470             }
2471       return s;
2472       }
2473 
2474 //---------------------------------------------------------
2475 //   convertToHtml
2476 //    convert from internal html format to Qt
2477 //---------------------------------------------------------
2478 
convertToHtml(const QString & s,const TextStyle &)2479 QString TextBase::convertToHtml(const QString& s, const TextStyle& /*st*/)
2480       {
2481 //TODO      qreal size     = st.size();
2482 //      QString family = st.family();
2483       qreal size     = 10;
2484       QString family = "arial";
2485       return QString("<html><body style=\"font-family:'%1'; font-size:%2pt;\">%3</body></html>").arg(family).arg(size).arg(s);
2486       }
2487 
2488 //---------------------------------------------------------
2489 //   tagEscape
2490 //---------------------------------------------------------
2491 
tagEscape(QString s)2492 QString TextBase::tagEscape(QString s)
2493       {
2494       QStringList tags = { "sym", "b", "i", "u", "sub", "sup" };
2495       for (const QString &tag : tags) {
2496             QString openTag = "<" + tag + ">";
2497             QString openProxy = "!!" + tag + "!!";
2498             QString closeTag = "</" + tag + ">";
2499             QString closeProxy = "!!/" + tag + "!!";
2500             s.replace(openTag, openProxy);
2501             s.replace(closeTag, closeProxy);
2502             }
2503       s = XmlWriter::xmlString(s);
2504       for (const QString &tag : tags) {
2505             QString openTag = "<" + tag + ">";
2506             QString openProxy = "!!" + tag + "!!";
2507             QString closeTag = "</" + tag + ">";
2508             QString closeProxy = "!!/" + tag + "!!";
2509             s.replace(openProxy, openTag);
2510             s.replace(closeProxy, closeTag);
2511             }
2512       return s;
2513       }
2514 
2515 //---------------------------------------------------------
2516 //   unEscape
2517 //---------------------------------------------------------
2518 
unEscape(QString s)2519 QString TextBase::unEscape(QString s)
2520       {
2521       s.replace("&lt;", "<");
2522       s.replace("&gt;", ">");
2523       s.replace("&amp;", "&");
2524       s.replace("&quot;", "\"");
2525       return s;
2526       }
2527 
2528 //---------------------------------------------------------
2529 //   escape
2530 //---------------------------------------------------------
2531 
escape(QString s)2532 QString TextBase::escape(QString s)
2533       {
2534       s.replace("<", "&lt;");
2535       s.replace(">", "&gt;");
2536       s.replace("&", "&amp;");
2537       s.replace("\"", "&quot;");
2538       return s;
2539       }
2540 
2541 //---------------------------------------------------------
2542 //   accessibleInfo
2543 //---------------------------------------------------------
2544 
accessibleInfo() const2545 QString TextBase::accessibleInfo() const
2546       {
2547       QString rez;
2548       switch (tid()) {
2549             case Tid::TITLE:
2550             case Tid::SUBTITLE:
2551             case Tid::COMPOSER:
2552             case Tid::POET:
2553             case Tid::TRANSLATOR:
2554             case Tid::MEASURE_NUMBER:
2555             case Tid::MMREST_RANGE:
2556                   rez = score() ? score()->getTextStyleUserName(tid()) : textStyleUserName(tid());
2557                   break;
2558             default:
2559                   rez = Element::accessibleInfo();
2560                   break;
2561             }
2562       QString s = plainText().simplified();
2563       if (s.length() > 20) {
2564             s.truncate(20);
2565             s += "…";
2566             }
2567       return  QString("%1: %2").arg(rez, s);
2568       }
2569 
2570 //---------------------------------------------------------
2571 //   screenReaderInfo
2572 //---------------------------------------------------------
2573 
screenReaderInfo() const2574 QString TextBase::screenReaderInfo() const
2575       {
2576       QString rez;
2577 
2578       switch (tid()) {
2579             case Tid::TITLE:
2580             case Tid::SUBTITLE:
2581             case Tid::COMPOSER:
2582             case Tid::POET:
2583             case Tid::TRANSLATOR:
2584             case Tid::MEASURE_NUMBER:
2585             case Tid::MMREST_RANGE:
2586                   rez = score() ? score()->getTextStyleUserName(tid()) : textStyleUserName(tid());
2587                   break;
2588             default:
2589                   rez = Element::accessibleInfo();
2590                   break;
2591             }
2592       QString s = plainText().simplified();
2593       return  QString("%1: %2").arg(rez, s);
2594       }
2595 
2596 //---------------------------------------------------------
2597 //   subtype
2598 //---------------------------------------------------------
2599 
subtype() const2600 int TextBase::subtype() const
2601       {
2602       return int(tid());
2603       }
2604 
2605 //---------------------------------------------------------
2606 //   subtypeName
2607 //---------------------------------------------------------
2608 
subtypeName() const2609 QString TextBase::subtypeName() const
2610       {
2611       return score() ? score()->getTextStyleUserName(tid()) : textStyleUserName(tid());
2612       }
2613 
2614 //---------------------------------------------------------
2615 //   fragmentList
2616 //---------------------------------------------------------
2617 
2618 /*
2619  Return the text as a single list of TextFragment
2620  Used by the MusicXML formatted export to avoid parsing the xml text format
2621  */
2622 
fragmentList() const2623 QList<TextFragment> TextBase::fragmentList() const
2624       {
2625       QList<TextFragment> res;
2626       for (const TextBlock& block : _layout) {
2627             for (const TextFragment& f : block.fragments()) {
2628                   /* TODO TBD
2629                   if (f.text.empty())                     // skip empty fragments, not to
2630                         continue;                           // insert extra HTML formatting
2631                    */
2632                   res.append(f);
2633                   if (block.eol()) {
2634                         // simply append a newline
2635                         res.last().text += "\n";
2636                         }
2637                   }
2638             }
2639       return res;
2640       }
2641 
2642 //---------------------------------------------------------
2643 //   validateText
2644 //    check if s is a valid musescore xml text string
2645 //    - simple bugs are automatically adjusted
2646 //   return true if text is valid or could be fixed
2647 //  (this is incomplete/experimental)
2648 //---------------------------------------------------------
2649 
validateText(QString & s)2650 bool TextBase::validateText(QString& s)
2651       {
2652       QString d;
2653       for (int i = 0; i < s.size(); ++i) {
2654             QChar c = s[i];
2655             if (c == '&') {
2656                   const char* ok[] { "amp;", "lt;", "gt;", "quot;" };
2657                   QString t = s.mid(i+1);
2658                   bool found = false;
2659                   for (auto k : ok) {
2660                         if (t.startsWith(k)) {
2661                               d.append(c);
2662                               d.append(k);
2663                               i += int(strlen(k));
2664                               found = true;
2665                               break;
2666                               }
2667                         }
2668                   if (!found)
2669                         d.append("&amp;");
2670                   }
2671             else if (c == '<') {
2672                   const char* ok[] { "b>", "/b>", "i>", "/i>", "u>", "/u", "font ", "/font>", "sym>", "/sym>" };
2673                   QString t = s.mid(i+1);
2674                   bool found = false;
2675                   for (auto k : ok) {
2676                         if (t.startsWith(k)) {
2677                               d.append(c);
2678                               d.append(k);
2679                               i += int(strlen(k));
2680                               found = true;
2681                               break;
2682                               }
2683                         }
2684                   if (!found)
2685                         d.append("&lt;");
2686                   }
2687             else
2688                   d.append(c);
2689             }
2690       QString ss = "<data>" + d + "</data>\n";
2691       XmlReader xml(ss);
2692       while (xml.readNextStartElement())
2693             ; // qDebug("  token %d <%s>", int(xml.tokenType()), qPrintable(xml.name().toString()));
2694       if (xml.error() == QXmlStreamReader::NoError) {
2695             s = d;
2696             return true;
2697             }
2698       qDebug("xml error at line %lld column %lld: %s",
2699             xml.lineNumber(),
2700             xml.columnNumber(),
2701             qPrintable(xml.errorString()));
2702       qDebug ("text: |%s|", qPrintable(ss));
2703       return false;
2704       }
2705 
2706 //---------------------------------------------------------
2707 //   font
2708 //---------------------------------------------------------
2709 
font() const2710 QFont TextBase::font() const
2711       {
2712       qreal m = _size;
2713       if (sizeIsSpatiumDependent())
2714             m *= spatium() / SPATIUM20;
2715       QFont f(_family, m, bold() ? QFont::Bold : QFont::Normal, italic());
2716       if (underline())
2717             f.setUnderline(underline());
2718 
2719       return f;
2720       }
2721 
2722 //---------------------------------------------------------
2723 //   fontMetrics
2724 //---------------------------------------------------------
2725 
fontMetrics() const2726 QFontMetricsF TextBase::fontMetrics() const
2727       {
2728       return QFontMetricsF(font());
2729       }
2730 
2731 //---------------------------------------------------------
2732 //   getProperty
2733 //---------------------------------------------------------
2734 
getProperty(Pid propertyId) const2735 QVariant TextBase::getProperty(Pid propertyId) const
2736       {
2737       switch (propertyId) {
2738             case Pid::SUB_STYLE:
2739                   return int(tid());
2740             case Pid::FONT_FACE:
2741                   return family();
2742             case Pid::FONT_SIZE:
2743                   return size();
2744             case Pid::FONT_STYLE:
2745                   return int(fontStyle());
2746             case Pid::TEXT_LINE_SPACING:
2747                 return textLineSpacing();
2748             case Pid::FRAME_TYPE:
2749                   return int(frameType());
2750             case Pid::FRAME_WIDTH:
2751                   return frameWidth();
2752             case Pid::FRAME_PADDING:
2753                   return paddingWidth();
2754             case Pid::FRAME_ROUND:
2755                   return frameRound();
2756             case Pid::FRAME_FG_COLOR:
2757                   return frameColor();
2758             case Pid::FRAME_BG_COLOR:
2759                   return bgColor();
2760             case Pid::ALIGN:
2761                   return QVariant::fromValue(align());
2762             case Pid::TEXT:
2763                   return xmlText();
2764             default:
2765                   return Element::getProperty(propertyId);
2766             }
2767       }
2768 
2769 //---------------------------------------------------------
2770 //   setProperty
2771 //---------------------------------------------------------
2772 
setProperty(Pid pid,const QVariant & v)2773 bool TextBase::setProperty(Pid pid, const QVariant& v)
2774       {
2775       if (textInvalid)
2776             genText();
2777       bool rv = true;
2778       switch (pid) {
2779             case Pid::SUB_STYLE:
2780                   initTid(Tid(v.toInt()));
2781                   break;
2782             case Pid::FONT_FACE:
2783                   setFamily(v.toString());
2784                   break;
2785             case Pid::FONT_SIZE:
2786                   setSize(v.toReal());
2787                   break;
2788             case Pid::FONT_STYLE:
2789                   setFontStyle(FontStyle(v.toInt()));
2790                   break;
2791             case Pid::TEXT_LINE_SPACING:
2792                   setTextLineSpacing(v.toReal());
2793                   break;
2794             case Pid::FRAME_TYPE:
2795                   setFrameType(FrameType(v.toInt()));
2796                   break;
2797             case Pid::FRAME_WIDTH:
2798                   setFrameWidth(v.value<Spatium>());
2799                   break;
2800             case Pid::FRAME_PADDING:
2801                   setPaddingWidth(v.value<Spatium>());
2802                   break;
2803             case Pid::FRAME_ROUND:
2804                   setFrameRound(v.toInt());
2805                   break;
2806             case Pid::FRAME_FG_COLOR:
2807                   setFrameColor(v.value<QColor>());
2808                   break;
2809             case Pid::FRAME_BG_COLOR:
2810                   setBgColor(v.value<QColor>());
2811                   break;
2812             case Pid::TEXT:
2813                   setXmlText(v.toString());
2814                   break;
2815             case Pid::ALIGN:
2816                   setAlign(v.value<Align>());
2817                   break;
2818             default:
2819                   rv = Element::setProperty(pid, v);
2820                   break;
2821             }
2822       layoutInvalid = true;
2823       triggerLayout();
2824       return rv;
2825       }
2826 
2827 //---------------------------------------------------------
2828 //   propertyDefault
2829 //---------------------------------------------------------
2830 
propertyDefault(Pid id) const2831 QVariant TextBase::propertyDefault(Pid id) const
2832       {
2833       if (id == Pid::Z)
2834             return Element::propertyDefault(id);
2835       if (composition()) {
2836             QVariant v = parent()->propertyDefault(id);
2837             if (v.isValid())
2838                   return v;
2839             }
2840       Sid sid = getPropertyStyle(id);
2841       if (sid != Sid::NOSTYLE)
2842             return styleValue(id, sid);
2843       QVariant v;
2844       switch (id) {
2845             case Pid::SUB_STYLE:
2846                   v = int(Tid::DEFAULT);
2847                   break;
2848             case Pid::TEXT:
2849                   v = QString();
2850                   break;
2851             default:
2852                   for (const StyledProperty& p : *textStyle(Tid::DEFAULT)) {
2853                         if (p.pid == id)
2854                               return styleValue(id, p.sid);
2855                         }
2856                   return Element::propertyDefault(id);
2857             }
2858       return v;
2859       }
2860 
2861 //---------------------------------------------------------
2862 //   getPropertyFlagsIdx
2863 //---------------------------------------------------------
2864 
getPropertyFlagsIdx(Pid id) const2865 int TextBase::getPropertyFlagsIdx(Pid id) const
2866       {
2867       int i = 0;
2868       for (const StyledProperty& p : *_elementStyle) {
2869             if (p.pid == id)
2870                   return i;
2871             ++i;
2872             }
2873       for (const StyledProperty& p : *textStyle(tid())) {
2874             if (p.pid == id)
2875                   return i;
2876             ++i;
2877             }
2878       return -1;
2879       }
2880 
2881 //---------------------------------------------------------
2882 //   offsetSid
2883 //---------------------------------------------------------
2884 
offsetSid() const2885 Sid TextBase::offsetSid() const
2886       {
2887       Tid defaultTid = Tid(propertyDefault(Pid::SUB_STYLE).toInt());
2888       if (tid() != defaultTid)
2889             return Sid::NOSTYLE;
2890       bool above = placeAbove();
2891       switch (tid()) {
2892             case Tid::DYNAMICS:
2893                   return above ? Sid::dynamicsPosAbove : Sid::dynamicsPosBelow;
2894             case Tid::LYRICS_ODD:
2895             case Tid::LYRICS_EVEN:
2896                   return above ? Sid::lyricsPosAbove : Sid::lyricsPosBelow;
2897             case Tid::REHEARSAL_MARK:
2898                   return above ? Sid::rehearsalMarkPosAbove : Sid::rehearsalMarkPosBelow;
2899             case Tid::STAFF:
2900                   return above ? Sid::staffTextPosAbove : Sid::staffTextPosBelow;
2901             case Tid::STICKING:
2902                   return above ? Sid::stickingPosAbove : Sid::stickingPosBelow;
2903             case Tid::SYSTEM:
2904                   return above ? Sid::systemTextPosAbove : Sid::systemTextPosBelow;
2905             case Tid::TEMPO:
2906                   return above ? Sid::tempoPosAbove : Sid::tempoPosBelow;
2907             case Tid::MEASURE_NUMBER:
2908                   return above ? Sid::measureNumberPosAbove : Sid::measureNumberPosBelow;
2909             case Tid::MMREST_RANGE:
2910                   return above ? Sid::mmRestRangePosAbove : Sid::mmRestRangePosBelow;
2911             default:
2912                   break;
2913             }
2914       return Sid::NOSTYLE;
2915       }
2916 
2917 //---------------------------------------------------------
2918 //   getPropertyStyle
2919 //---------------------------------------------------------
2920 
getPropertyStyle(Pid id) const2921 Sid TextBase::getPropertyStyle(Pid id) const
2922       {
2923       if (id == Pid::OFFSET) {
2924             Sid sid = offsetSid();
2925             if (sid != Sid::NOSTYLE)
2926                   return sid;
2927             }
2928       for (const StyledProperty& p : *_elementStyle) {
2929             if (p.pid == id)
2930                   return p.sid;
2931             }
2932       for (const StyledProperty& p : *textStyle(tid())) {
2933             if (p.pid == id)
2934                   return p.sid;
2935             }
2936       return Sid::NOSTYLE;
2937       }
2938 
2939 //---------------------------------------------------------
2940 //   styleChanged
2941 //---------------------------------------------------------
2942 
styleChanged()2943 void TextBase::styleChanged()
2944       {
2945       if (!styledProperties()) {
2946             qDebug("no styled properties");
2947             return;
2948             }
2949       int i = 0;
2950       for (const StyledProperty& spp : *_elementStyle) {
2951             PropertyFlags f = _propertyFlagsList[i];
2952             if (f == PropertyFlags::STYLED)
2953                   setProperty(spp.pid, styleValue(spp.pid, getPropertyStyle(spp.pid)));
2954             ++i;
2955             }
2956       for (const StyledProperty& spp : *textStyle(tid())) {
2957             PropertyFlags f = _propertyFlagsList[i];
2958             if (f == PropertyFlags::STYLED)
2959                   setProperty(spp.pid, styleValue(spp.pid, getPropertyStyle(spp.pid)));
2960             ++i;
2961             }
2962       }
2963 
2964 //---------------------------------------------------------
2965 //   initElementStyle
2966 //---------------------------------------------------------
2967 
initElementStyle(const ElementStyle * ss)2968 void TextBase::initElementStyle(const ElementStyle* ss)
2969       {
2970       _elementStyle = ss;
2971       size_t n      = ss->size() + TEXT_STYLE_SIZE;
2972 
2973       delete[] _propertyFlagsList;
2974       _propertyFlagsList = new PropertyFlags[n];
2975       for (size_t i = 0; i < n; ++i)
2976             _propertyFlagsList[i] = PropertyFlags::STYLED;
2977       for (const StyledProperty& p : *_elementStyle)
2978             setProperty(p.pid, styleValue(p.pid, p.sid));
2979       for (const StyledProperty& p : *textStyle(tid()))
2980             setProperty(p.pid, styleValue(p.pid, p.sid));
2981       }
2982 
2983 //---------------------------------------------------------
2984 //   initTid
2985 //---------------------------------------------------------
2986 
initTid(Tid tid,bool preserveDifferent)2987 void TextBase::initTid(Tid tid, bool preserveDifferent)
2988       {
2989       if (! preserveDifferent)
2990             initTid(tid);
2991       else {
2992             setTid(tid);
2993             for (const StyledProperty& p : *textStyle(tid)) {
2994                   if (getProperty(p.pid) == propertyDefault(p.pid))
2995                         setProperty(p.pid, styleValue(p.pid, p.sid));
2996                   }
2997             }
2998       }
2999 
initTid(Tid tid)3000 void TextBase::initTid(Tid tid)
3001       {
3002       setTid(tid);
3003       for (const StyledProperty& p : *textStyle(tid)) {
3004             setProperty(p.pid, styleValue(p.pid, p.sid));
3005             }
3006       }
3007 
3008 //---------------------------------------------------------
3009 //   editCut
3010 //---------------------------------------------------------
3011 
editCut(EditData & ed)3012 void TextBase::editCut(EditData& ed)
3013       {
3014       TextEditData* ted = static_cast<TextEditData*>(ed.getData(this));
3015       TextCursor* _cursor = &ted->cursor;
3016       QString s = _cursor->selectedText();
3017 
3018       if (!s.isEmpty()) {
3019             QApplication::clipboard()->setText(s, QClipboard::Clipboard);
3020             ed.curGrip = Grip::START;
3021             ed.key     = Qt::Key_Delete;
3022             ed.s       = QString();
3023             edit(ed);
3024             }
3025       }
3026 
3027 //---------------------------------------------------------
3028 //   editCopy
3029 //---------------------------------------------------------
3030 
editCopy(EditData & ed)3031 void TextBase::editCopy(EditData& ed)
3032       {
3033       //
3034       // store selection as plain text
3035       //
3036       TextEditData* ted = static_cast<TextEditData*>(ed.getData(this));
3037       TextCursor* _cursor = &ted->cursor;
3038       QString s = _cursor->selectedText();
3039       if (!s.isEmpty())
3040             QApplication::clipboard()->setText(s, QClipboard::Clipboard);
3041       }
3042 
3043 //---------------------------------------------------------
3044 //   cursor
3045 //---------------------------------------------------------
3046 
cursor(const EditData & ed)3047 TextCursor* TextBase::cursor(const EditData& ed)
3048       {
3049       TextEditData* ted = static_cast<TextEditData*>(ed.getData(this));
3050       Q_ASSERT(ted);
3051       return &ted->cursor;
3052       }
3053 
3054 //---------------------------------------------------------
3055 //   draw
3056 //---------------------------------------------------------
3057 
draw(QPainter * p) const3058 void TextBase::draw(QPainter* p) const
3059       {
3060       if (hasFrame()) {
3061             qreal baseSpatium = MScore::baseStyle().value(Sid::spatium).toDouble();
3062             if (frameWidth().val() != 0.0) {
3063                   QColor fColor = curColor(visible(), frameColor());
3064                   qreal frameWidthVal = frameWidth().val() * (sizeIsSpatiumDependent() ? spatium() : baseSpatium);
3065 
3066                   QPen pen(fColor, frameWidthVal, Qt::SolidLine,
3067                      Qt::SquareCap, Qt::MiterJoin);
3068                   p->setPen(pen);
3069                   }
3070             else
3071                   p->setPen(Qt::NoPen);
3072             QColor bg(bgColor());
3073             p->setBrush(bg.alpha() ? QBrush(bg) : Qt::NoBrush);
3074             if (circle())
3075                   p->drawEllipse(frame);
3076             else {
3077                   qreal frameRoundFactor = (sizeIsSpatiumDependent() ? (spatium()/baseSpatium) / 2 : 0.5f);
3078 
3079                   int r2 = frameRound() * frameRoundFactor;
3080                   if (r2 > 99)
3081                         r2 = 99;
3082                   p->drawRoundedRect(frame, frameRound() * frameRoundFactor, r2);
3083                   }
3084             }
3085       p->setBrush(Qt::NoBrush);
3086       p->setPen(textColor());
3087       for (const TextBlock& t : _layout)
3088             t.draw(p, this);
3089       }
3090 
3091 //---------------------------------------------------------
3092 //   drawEditMode
3093 //    draw edit mode decorations
3094 //---------------------------------------------------------
3095 
drawEditMode(QPainter * p,EditData & ed)3096 void TextBase::drawEditMode(QPainter* p, EditData& ed)
3097       {
3098       QPointF pos(canvasPos());
3099       p->translate(pos);
3100 
3101       TextEditData* ted = static_cast<TextEditData*>(ed.getData(this));
3102       if (!ted) {
3103             qDebug("ted not found");
3104             return;
3105             }
3106       TextCursor* _cursor = &ted->cursor;
3107 
3108       if (_cursor->hasSelection()) {
3109             p->setBrush(Qt::NoBrush);
3110             p->setPen(textColor());
3111             int r1 = _cursor->selectLine();
3112             int r2 = _cursor->row();
3113             int c1 = _cursor->selectColumn();
3114             int c2 = _cursor->column();
3115             sort(r1, c1, r2, c2);
3116             int row = 0;
3117             for (const TextBlock& t : qAsConst(_layout)) {
3118                   t.draw(p, this);
3119                   if (row >= r1 && row <= r2) {
3120                         QRectF br;
3121                         if (row == r1 && r1 == r2)
3122                               br = t.boundingRect(c1, c2, this);
3123                         else if (row == r1)
3124                               br = t.boundingRect(c1, t.columns(), this);
3125                         else if (row == r2)
3126                               br = t.boundingRect(0, c2, this);
3127                         else
3128                               br = t.boundingRect();
3129                         br.translate(0.0, t.y());
3130                         drawSelection(p, br);
3131                         }
3132                   ++row;
3133                   }
3134             }
3135       p->setBrush(curColor());
3136       QPen pen(curColor());
3137       pen.setJoinStyle(Qt::MiterJoin);
3138       p->setPen(pen);
3139 
3140       // Don't draw cursor if there is a selection
3141       if (!_cursor->hasSelection())
3142             p->drawRect(_cursor->cursorRect());
3143 
3144       QMatrix matrix = p->worldTransform().toAffine();
3145       p->translate(-pos);
3146       p->setPen(QPen(QBrush(Qt::lightGray), 4.0 / matrix.m11()));  // 4 pixel pen size
3147       p->setBrush(Qt::NoBrush);
3148 
3149       qreal m = spatium();
3150       QRectF r = canvasBoundingRect().adjusted(-m, -m, m, m);
3151 //      qDebug("%f %f %f %f\n", r.x(), r.y(), r.width(), r.height());
3152 
3153       p->drawRect(r);
3154       pen = QPen(MScore::defaultColor, 0.0);
3155       }
3156 
3157 //---------------------------------------------------------
3158 //   hasCustomFormatting
3159 //---------------------------------------------------------
3160 
hasCustomFormatting() const3161 bool TextBase::hasCustomFormatting() const
3162       {
3163       CharFormat fmt;
3164       fmt.setFontFamily(family());
3165       fmt.setFontSize(size());
3166       fmt.setStyle(fontStyle());
3167       fmt.setPreedit(false);
3168       fmt.setValign(VerticalAlignment::AlignNormal);
3169 
3170       for (const TextBlock& block : _layout) {
3171             for (const TextFragment& f : block.fragments()) {
3172                   if (f.text.isEmpty())                     // skip empty fragments, not to
3173                         continue;                           // insert extra HTML formatting
3174                   const CharFormat& format = f.format;
3175                   if (fmt.style() != format.style())
3176                         return true;
3177                   if (format.fontSize() != fmt.fontSize())
3178                         return true;
3179                   if (format.fontFamily() != fmt.fontFamily())
3180                         return true;
3181 
3182                   VerticalAlignment va = format.valign();
3183                   VerticalAlignment cva = fmt.valign();
3184                   if (cva != va)
3185                         return true;
3186                   }
3187             }
3188       return false;
3189       }
3190 
3191 //---------------------------------------------------------
3192 //   stripText
3193 //    remove some custom text formatting and return
3194 //    result as xml string
3195 //---------------------------------------------------------
3196 
stripText(bool removeStyle,bool removeSize,bool removeFace) const3197 QString TextBase::stripText(bool removeStyle, bool removeSize, bool removeFace) const
3198       {
3199       QString _txt;
3200       bool bold_      = false;
3201       bool italic_    = false;
3202       bool underline_ = false;
3203 
3204       for (const TextBlock& block : _layout) {
3205             for (const TextFragment& f : block.fragments()) {
3206                   if (!f.format.bold() && bold())
3207                         bold_ = true;
3208                   if (!f.format.italic() && italic())
3209                         italic_ = true;
3210                   if (!f.format.underline() && underline())
3211                         underline_ = true;
3212                   }
3213             }
3214       CharFormat fmt;
3215       fmt.setFontFamily(family());
3216       fmt.setFontSize(size());
3217       fmt.setStyle(fontStyle());
3218       fmt.setPreedit(false);
3219       fmt.setValign(VerticalAlignment::AlignNormal);
3220 
3221       XmlNesting xmlNesting(&_txt);
3222       if (!removeStyle) {
3223             if (bold_)
3224                   xmlNesting.pushB();
3225             if (italic_)
3226                   xmlNesting.pushI();
3227             if (underline_)
3228                   xmlNesting.pushU();
3229             }
3230 
3231       for (const TextBlock& block : _layout) {
3232             for (const TextFragment& f : block.fragments()) {
3233                   if (f.text.isEmpty())                     // skip empty fragments, not to
3234                         continue;                           // insert extra HTML formatting
3235                   const CharFormat& format = f.format;
3236                   if (!removeStyle) {
3237                         if (fmt.bold() != format.bold()) {
3238                               if (format.bold())
3239                                     xmlNesting.pushB();
3240                               else
3241                                     xmlNesting.popB();
3242                               }
3243                         if (fmt.italic() != format.italic()) {
3244                               if (format.italic())
3245                                     xmlNesting.pushI();
3246                               else
3247                                     xmlNesting.popI();
3248                               }
3249                         if (fmt.underline() != format.underline()) {
3250                               if (format.underline())
3251                                     xmlNesting.pushU();
3252                               else
3253                                     xmlNesting.popU();
3254                               }
3255                         }
3256 
3257                   if (!removeSize && (format.fontSize() != fmt.fontSize()))
3258                         _txt += QString("<font size=\"%1\"/>").arg(format.fontSize());
3259                   if (!removeFace && (format.fontFamily() != fmt.fontFamily()))
3260                         _txt += QString("<font face=\"%1\"/>").arg(TextBase::escape(format.fontFamily()));
3261 
3262                   VerticalAlignment va = format.valign();
3263                   VerticalAlignment cva = fmt.valign();
3264                   if (cva != va) {
3265                         switch (va) {
3266                               case VerticalAlignment::AlignNormal:
3267                                     xmlNesting.popToken(cva == VerticalAlignment::AlignSuperScript ? "sup" : "sub");
3268                                     break;
3269                               case VerticalAlignment::AlignSuperScript:
3270                                     xmlNesting.pushToken("sup");
3271                                     break;
3272                               case VerticalAlignment::AlignSubScript:
3273                                     xmlNesting.pushToken("sub");
3274                                     break;
3275                               }
3276                         }
3277                   _txt += XmlWriter::xmlString(f.text);
3278                   fmt = format;
3279                   }
3280             if (block.eol())
3281                   _txt += QChar::LineFeed;
3282             }
3283       while (!xmlNesting.empty())
3284             xmlNesting.popToken();
3285       return _txt;
3286       }
3287 
3288 //---------------------------------------------------------
3289 //   undoChangeProperty
3290 //---------------------------------------------------------
3291 
undoChangeProperty(Pid id,const QVariant & v,PropertyFlags ps)3292 void TextBase::undoChangeProperty(Pid id, const QVariant& v, PropertyFlags ps)
3293       {
3294       if (ps == PropertyFlags::STYLED && v == propertyDefault(id)) {
3295             // this is a reset
3296             // remove some custom formatting
3297             if (id == Pid::FONT_STYLE)
3298                   undoChangeProperty(Pid::TEXT, stripText(true, false, false), propertyFlags(id));
3299             else if (id == Pid::FONT_SIZE)
3300                   undoChangeProperty(Pid::TEXT, stripText(false, true, false), propertyFlags(id));
3301             else if (id == Pid::FONT_FACE)
3302                   undoChangeProperty(Pid::TEXT, stripText(false, false, true), propertyFlags(id));
3303             }
3304       Element::undoChangeProperty(id, v, ps);
3305       }
3306 
3307 }
3308