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("<", "<");
2522 s.replace(">", ">");
2523 s.replace("&", "&");
2524 s.replace(""", "\"");
2525 return s;
2526 }
2527
2528 //---------------------------------------------------------
2529 // escape
2530 //---------------------------------------------------------
2531
escape(QString s)2532 QString TextBase::escape(QString s)
2533 {
2534 s.replace("<", "<");
2535 s.replace(">", ">");
2536 s.replace("&", "&");
2537 s.replace("\"", """);
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("&");
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("<");
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