Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2021 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include <cmath>
24 : #include <stack>
25 :
26 : #include "draw/fontmetrics.h"
27 :
28 : #include "iengravingfont.h"
29 :
30 : #include "rendering/score/textlayout.h"
31 : #include "rendering/iscorerenderer.h"
32 :
33 : #include "style/textstyle.h"
34 :
35 : #include "rw/xmlreader.h"
36 : #include "rw/xmlwriter.h"
37 :
38 : #include "types/symnames.h"
39 : #include "types/typesconv.h"
40 :
41 : #ifndef ENGRAVING_NO_ACCESSIBILITY
42 : #include "accessibility/accessibleitem.h"
43 : #endif
44 :
45 : #include "anchors.h"
46 : #include "box.h"
47 : #include "harppedaldiagram.h"
48 : #include "instrumentname.h"
49 : #include "measure.h"
50 : #include "mscore.h"
51 : #include "page.h"
52 : #include "score.h"
53 :
54 : #include "../editing/textedit.h"
55 :
56 : #include "log.h"
57 :
58 : using namespace mu;
59 : using namespace muse::draw;
60 : using namespace mu::engraving;
61 : using namespace mu::engraving::rendering::score;
62 :
63 : namespace mu::engraving {
64 : static const char* FALLBACK_SYMBOL_FONT = "Bravura";
65 : static const char* FALLBACK_SYMBOLTEXT_FONT = "Bravura Text";
66 :
67 : //---------------------------------------------------------
68 : // isSorted
69 : /// return true if (r1,c1) is at or before (r2,c2)
70 : //---------------------------------------------------------
71 :
72 33461 : bool TextBase::isSorted(size_t r1, size_t c1, size_t r2, size_t c2)
73 : {
74 33461 : if (r1 < r2) {
75 72 : return true;
76 : }
77 :
78 33389 : if ((r1 == r2) && (c1 <= c2)) {
79 33389 : return true;
80 : }
81 :
82 0 : return false;
83 : }
84 :
85 : //---------------------------------------------------------
86 : // swap
87 : /// swap (r1,c1) with (r2,c2)
88 : //---------------------------------------------------------
89 :
90 0 : void TextBase::swap(size_t& r1, size_t& c1, size_t& r2, size_t& c2)
91 : {
92 0 : std::swap(r1, r2);
93 0 : std::swap(c1, c2);
94 0 : }
95 :
96 : //---------------------------------------------------------
97 : // sort
98 : /// swap (r1,c1) with (r2,c2) if they are not sorted
99 : //---------------------------------------------------------
100 :
101 33461 : void TextBase::sort(size_t& r1, size_t& c1, size_t& r2, size_t& c2)
102 : {
103 33461 : if (!isSorted(r1, c1, r2, c2)) {
104 0 : swap(r1, c1, r2, c2);
105 : }
106 33461 : }
107 :
108 : const String TextBase::UNDEFINED_FONT_FAMILY = String(u"Undefined");
109 : const double TextBase::UNDEFINED_FONT_SIZE = -1.0;
110 :
111 : //---------------------------------------------------------
112 : // operator==
113 : //---------------------------------------------------------
114 :
115 150934 : bool CharFormat::operator==(const CharFormat& cf) const
116 : {
117 150934 : return cf.style() == style()
118 150709 : && cf.valign() == valign()
119 150707 : && cf.fontSize() == fontSize()
120 452577 : && cf.fontFamily() == fontFamily();
121 : }
122 :
123 : //---------------------------------------------------------
124 : // operator=
125 : //---------------------------------------------------------
126 :
127 183272 : CharFormat& CharFormat::operator=(const CharFormat& cf)
128 : {
129 183272 : setStyle(cf.style());
130 183272 : setValign(cf.valign());
131 183272 : setFontSize(cf.fontSize());
132 183272 : setFontFamily(cf.fontFamily());
133 :
134 183272 : return *this;
135 : }
136 :
137 : //---------------------------------------------------------
138 : // clearSelection
139 : //---------------------------------------------------------
140 :
141 443068 : void TextCursor::clearSelection()
142 : {
143 443068 : m_selectLine = m_row;
144 443068 : m_selectColumn = m_column;
145 443068 : }
146 :
147 4 : void TextCursor::startEdit()
148 : {
149 4 : setRow(0);
150 4 : setColumn(0);
151 4 : clearSelection();
152 4 : m_editing = true;
153 4 : m_visible = true;
154 4 : }
155 :
156 4 : void TextCursor::endEdit()
157 : {
158 4 : setRow(0);
159 4 : setColumn(0);
160 4 : clearSelection();
161 4 : m_editing = false;
162 4 : m_visible = false;
163 4 : }
164 :
165 : //---------------------------------------------------------
166 : // init
167 : //---------------------------------------------------------
168 :
169 28330 : void TextCursor::init()
170 : {
171 28330 : PropertyValue family = m_text->propertyDefault(Pid::FONT_FACE);
172 28330 : m_format.setFontFamily(family.value<String>());
173 :
174 28330 : PropertyValue size = m_text->propertyDefault(Pid::FONT_SIZE);
175 28330 : m_format.setFontSize(size.toReal());
176 :
177 28330 : PropertyValue style = m_text->propertyDefault(Pid::FONT_STYLE);
178 28330 : m_format.setStyle(static_cast<FontStyle>(style.toInt()));
179 :
180 28330 : PropertyValue verticalAlign = m_text->propertyDefault(Pid::TEXT_SCRIPT_ALIGN);
181 28330 : m_format.setValign(static_cast<VerticalAlignment>(verticalAlign.toInt()));
182 28330 : }
183 :
184 0 : std::pair<size_t, size_t> TextCursor::positionToLocalCoord(int position) const
185 : {
186 0 : const TextBase::LayoutData* ldata = m_text->ldata();
187 0 : IF_ASSERT_FAILED(ldata) {
188 0 : return { muse::nidx, muse::nidx };
189 : }
190 :
191 0 : int currentPosition = 0;
192 0 : for (size_t i = 0; i < ldata->blocks.size(); ++i) {
193 0 : const TextBlock& t = ldata->blocks.at(i);
194 0 : for (size_t j = 0; j < t.columns(); ++j) {
195 0 : if (currentPosition == position) {
196 0 : return { i, j };
197 : }
198 :
199 0 : currentPosition++;
200 : }
201 : }
202 :
203 0 : return { muse::nidx, muse::nidx };
204 : }
205 :
206 1 : int TextCursor::currentPosition() const
207 : {
208 1 : return position(static_cast<int>(row()), static_cast<int>(column()));
209 : }
210 :
211 0 : TextCursor::Range TextCursor::selectionRange() const
212 : {
213 0 : int cursorPosition = currentPosition();
214 0 : int selectionPosition = position(static_cast<int>(selectLine()), static_cast<int>(selectColumn()));
215 :
216 0 : if (cursorPosition > selectionPosition) {
217 0 : return range(selectionPosition, cursorPosition);
218 : } else {
219 0 : return range(cursorPosition, selectionPosition);
220 : }
221 : }
222 :
223 : //---------------------------------------------------------
224 : // columns
225 : //---------------------------------------------------------
226 :
227 2 : size_t TextCursor::columns() const
228 : {
229 2 : return m_text->ldata()->textBlock(static_cast<int>(m_row)).columns();
230 : }
231 :
232 : //---------------------------------------------------------
233 : // currentCharacter
234 : //---------------------------------------------------------
235 :
236 18 : Char TextCursor::currentCharacter() const
237 : {
238 18 : const TextBase::LayoutData* ldata = m_text->ldata();
239 18 : IF_ASSERT_FAILED(ldata) {
240 0 : return Char();
241 : }
242 :
243 18 : const TextBlock& t = ldata->blocks.at(row());
244 18 : String s = t.text(static_cast<int>(column()), 1, m_text);
245 18 : s = TextBase::unEscape(s);
246 18 : if (s.isEmpty()) {
247 0 : return Char();
248 : }
249 18 : return s.at(0);
250 18 : }
251 :
252 : //---------------------------------------------------------
253 : // updateCursorFormat
254 : //---------------------------------------------------------
255 :
256 6 : void TextCursor::updateCursorFormat()
257 : {
258 6 : const TextBase::LayoutData* ldata = m_text->ldata();
259 6 : IF_ASSERT_FAILED(ldata) {
260 0 : return;
261 : }
262 6 : const TextBlock& block = ldata->blocks.at(m_row);
263 :
264 6 : size_t col = hasSelection() ? selectColumn() : column();
265 : // Get format at the LEFT of the cursor position
266 6 : const CharFormat* format = block.formatAt(std::max(static_cast<int>(col) - 1, 0));
267 6 : if (!format) {
268 0 : init();
269 : } else {
270 6 : setFormat(*format);
271 : }
272 : }
273 :
274 : //---------------------------------------------------------
275 : // cursorRect
276 : //---------------------------------------------------------
277 :
278 0 : RectF TextCursor::cursorRect() const
279 : {
280 0 : const TextBlock& tline = curLine();
281 0 : const TextFragment* fragment = tline.fragment(static_cast<int>(column()));
282 :
283 0 : Font _font = fragment ? fragment->font(m_text) : m_text->font();
284 0 : if (fragment) {
285 : // Ensure the cursor height matches that of the associated text font
286 0 : TextLayout::substituteMusicFont(_font, fragment->calculatedFontSize(m_text));
287 : }
288 0 : const FontMetrics fm(_font);
289 :
290 0 : double fontCapHeight = fm.capHeight();
291 0 : double fontAscent = fm.ascent();
292 :
293 : // Bravura Text returns small values for its cap heights
294 0 : double cursorCapHeight = fontCapHeight > 0 && _font.family().id() != u"Bravura Text"
295 0 : ? std::min(fontCapHeight, fontAscent) : fontAscent;
296 0 : double cursorDescent = cursorCapHeight * .3;
297 :
298 0 : double h = cursorCapHeight + 2 * cursorDescent; // symmetrical cursor
299 0 : double w = std::max(2.0, cursorCapHeight / 16.0);
300 0 : double x = tline.xpos(column(), m_text);
301 0 : double y = tline.y() + cursorDescent - h;
302 0 : return RectF(x - w / 2, y, w, h);
303 0 : }
304 :
305 0 : RectF TextCursor::cursorCanvasRect() const
306 : {
307 0 : return cursorRect().translated(m_text->canvasPos());
308 : }
309 :
310 : //---------------------------------------------------------
311 : // curLine
312 : // return the current text line in edit mode
313 : //---------------------------------------------------------
314 :
315 0 : const TextBlock& TextCursor::curLine() const
316 : {
317 0 : const TextBase::LayoutData* ldata = m_text->ldata();
318 0 : IF_ASSERT_FAILED(ldata) {
319 0 : static TextBlock dummy;
320 0 : return dummy;
321 : }
322 :
323 0 : return ldata->blocks.at(m_row);
324 : }
325 :
326 31771 : TextBlock& TextCursor::curLine()
327 : {
328 31771 : TextBase::LayoutData* ldata = m_text->mutldata();
329 31771 : return ldata->blocks[m_row];
330 : }
331 :
332 : //---------------------------------------------------------
333 : // changeSelectionFormat
334 : //---------------------------------------------------------
335 :
336 31758 : void TextCursor::changeSelectionFormat(FormatId id, const FormatValue& val)
337 : {
338 31758 : TextBase::LayoutData* ldata = m_text->mutldata();
339 :
340 31758 : size_t r1 = selectLine();
341 31758 : size_t r2 = row();
342 31758 : size_t c1 = selectColumn();
343 31758 : size_t c2 = column();
344 :
345 31758 : TextBase::sort(r1, c1, r2, c2);
346 :
347 63590 : for (size_t row = 0; row < ldata->blocks.size(); ++row) {
348 31832 : TextBlock& t = ldata->blocks[row];
349 31832 : if (row < r1) {
350 0 : continue;
351 : }
352 31832 : if (row > r2) {
353 0 : break;
354 : }
355 31832 : if (row == r1 && r1 == r2) {
356 31690 : t.changeFormat(id, val, static_cast<int>(c1), static_cast<int>(c2 - c1));
357 142 : } else if (row == r1) {
358 68 : t.changeFormat(id, val, static_cast<int>(c1), static_cast<int>(t.columns() - c1));
359 74 : } else if (row == r2) {
360 68 : t.changeFormat(id, val, 0, static_cast<int>(c2));
361 : } else {
362 6 : t.changeFormat(id, val, 0, static_cast<int>(t.columns()));
363 : }
364 : }
365 :
366 31758 : m_text->renderer()->layoutText1(m_text);
367 31758 : }
368 :
369 1705 : const CharFormat TextCursor::selectedFragmentsFormat() const
370 : {
371 1705 : if (!m_text || (!hasSelection() && editing())) {
372 2 : return m_format;
373 : }
374 :
375 1703 : const TextBase::LayoutData* ldata = m_text->ldata();
376 1703 : IF_ASSERT_FAILED(ldata) {
377 0 : return CharFormat();
378 : }
379 :
380 1703 : if (ldata->blocks.empty()) {
381 0 : return m_format;
382 : }
383 :
384 1703 : size_t startRow = hasSelection() ? m_selectLine : 0;
385 1703 : size_t endRow = hasSelection() ? m_row : ldata->blocks.size() - 1;
386 1703 : size_t selectionStartCol = hasSelection() ? m_selectColumn : 0;
387 1703 : size_t selectionEndCol = hasSelection() ? m_column : 1; // corrected below at `endColumn
388 :
389 1703 : TextBase::sort(startRow, selectionStartCol, endRow, selectionEndCol);
390 :
391 1703 : const TextFragment* tf = ldata->textBlock(static_cast<int>(startRow)).fragment(static_cast<int>(selectionStartCol));
392 1703 : CharFormat resultFormat = tf ? tf->format : CharFormat();
393 :
394 1703 : bool allBlocksEmpty = true;
395 3410 : for (size_t row = startRow; row <= endRow; ++row) {
396 1707 : const TextBlock& block = ldata->blocks.at(row);
397 :
398 1707 : if (block.fragments().empty()) {
399 0 : continue;
400 : }
401 1707 : allBlocksEmpty = false;
402 :
403 1707 : const size_t startColumn = (row == startRow) ? selectionStartCol : 0;
404 1707 : const size_t endColumn = (row == endRow && hasSelection()) ? selectionEndCol : block.columns();
405 :
406 1707 : size_t column = 0;
407 :
408 1707 : bool isSingleFragment = block.fragments().size() == 1;
409 :
410 3442 : for (const TextFragment& fragment : block.fragments()) {
411 1736 : const size_t fragCols = fragment.columns();
412 :
413 1736 : if (!isSingleFragment) {
414 38 : if (column + fragCols <= startColumn) {
415 1 : column += fragCols;
416 1 : continue;
417 : }
418 37 : if (column > 0 && column >= endColumn) {
419 1 : break;
420 : }
421 : }
422 :
423 : // proper bitwise 'and' to ensure Bold/Italic/Underline/Strike only true if true for all fragments
424 1734 : resultFormat.setStyle(static_cast<FontStyle>(static_cast<int>(resultFormat.style())
425 1734 : & static_cast<int>(fragment.format.style())));
426 :
427 1734 : if (resultFormat.fontFamily() == "ScoreText") {
428 10 : resultFormat.setFontFamily(fragment.format.fontFamily());
429 : }
430 1734 : if (fragment.format.fontFamily() != "ScoreText" && resultFormat.fontFamily() != fragment.format.fontFamily()) {
431 0 : resultFormat.setFontFamily(TextBase::UNDEFINED_FONT_FAMILY);
432 : }
433 :
434 1734 : if (resultFormat.fontSize() != fragment.format.fontSize()) {
435 0 : resultFormat.setFontSize(TextBase::UNDEFINED_FONT_SIZE);
436 : }
437 :
438 1734 : if (resultFormat.valign() != fragment.format.valign()) {
439 0 : resultFormat.setValign(VerticalAlignment::AlignUndefined);
440 : }
441 :
442 1734 : column += fragCols;
443 : }
444 : }
445 :
446 1703 : if (!allBlocksEmpty) {
447 1703 : return resultFormat;
448 : }
449 :
450 0 : CharFormat defaultFormat;
451 0 : defaultFormat.setStyle(FontStyle(m_text->propertyDefault(Pid::FONT_STYLE).value<int>()));
452 0 : defaultFormat.setFontFamily(m_text->propertyDefault(Pid::FONT_FACE).value<String>());
453 0 : defaultFormat.setFontSize(m_text->propertyDefault(Pid::FONT_SIZE).toDouble());
454 0 : defaultFormat.setValign(VerticalAlignment(m_text->propertyDefault(Pid::TEXT_SCRIPT_ALIGN).toInt()));
455 :
456 0 : return defaultFormat;
457 1703 : }
458 :
459 : //---------------------------------------------------------
460 : // PointF
461 : //---------------------------------------------------------
462 :
463 265462 : void TextCursor::setFormat(FormatId id, FormatValue val)
464 : {
465 265462 : if (!hasSelection()) {
466 265462 : if (!editing()) {
467 265454 : m_text->selectAll();
468 8 : } else if (format()->formatValue(id) == val) {
469 6 : return;
470 : }
471 : }
472 265456 : format()->setFormatValue(id, val);
473 265456 : if (hasSelection()) {
474 31758 : changeSelectionFormat(id, val);
475 31758 : text()->setTextInvalid();
476 : }
477 265456 : if (!editing()) {
478 265454 : clearSelection();
479 : }
480 : }
481 :
482 : //---------------------------------------------------------
483 : // movePosition
484 : //---------------------------------------------------------
485 :
486 6 : bool TextCursor::movePosition(TextCursor::MoveOperation op, TextCursor::MoveMode mode, int count)
487 : {
488 12 : for (int i = 0; i < count; i++) {
489 6 : switch (op) {
490 0 : case TextCursor::MoveOperation::Left:
491 0 : if (hasSelection() && mode == TextCursor::MoveMode::MoveAnchor) {
492 0 : size_t r1 = m_selectLine;
493 0 : size_t r2 = m_row;
494 0 : size_t c1 = m_selectColumn;
495 0 : size_t c2 = m_column;
496 :
497 0 : TextBase::sort(r1, c1, r2, c2);
498 0 : clearSelection();
499 0 : m_row = r1;
500 0 : m_column = c1;
501 0 : } else if (m_column == 0) {
502 0 : if (m_row == 0) {
503 0 : return false;
504 : }
505 0 : --m_row;
506 0 : m_column = curLine().columns();
507 : } else {
508 0 : --m_column;
509 : }
510 0 : break;
511 :
512 0 : case TextCursor::MoveOperation::Right:
513 0 : if (hasSelection() && mode == TextCursor::MoveMode::MoveAnchor) {
514 0 : size_t r1 = m_selectLine;
515 0 : size_t r2 = m_row;
516 0 : size_t c1 = m_selectColumn;
517 0 : size_t c2 = m_column;
518 :
519 0 : TextBase::sort(r1, c1, r2, c2);
520 0 : clearSelection();
521 0 : m_row = r2;
522 0 : m_column = c2;
523 0 : } else if (column() >= curLine().columns()) {
524 0 : if (m_row >= m_text->ldata()->rows() - 1) {
525 0 : return false;
526 : }
527 0 : ++m_row;
528 0 : m_column = 0;
529 : } else {
530 0 : ++m_column;
531 : }
532 0 : break;
533 :
534 0 : case TextCursor::MoveOperation::Up:
535 0 : if (m_row == 0) {
536 0 : return false;
537 : }
538 0 : --m_row;
539 0 : if (m_column > curLine().columns()) {
540 0 : m_column = curLine().columns();
541 : }
542 :
543 0 : break;
544 :
545 0 : case TextCursor::MoveOperation::Down:
546 0 : if (m_row >= m_text->ldata()->rows() - 1) {
547 0 : return false;
548 : }
549 0 : ++m_row;
550 0 : if (m_column > curLine().columns()) {
551 0 : m_column = curLine().columns();
552 : }
553 :
554 0 : break;
555 :
556 1 : case TextCursor::MoveOperation::Start:
557 1 : m_row = 0;
558 1 : m_column = 0;
559 :
560 1 : break;
561 :
562 2 : case TextCursor::MoveOperation::End:
563 2 : m_row = m_text->ldata()->rows() - 1;
564 2 : m_column = curLine().columns();
565 :
566 2 : break;
567 :
568 0 : case TextCursor::MoveOperation::StartOfLine:
569 0 : m_column = 0;
570 :
571 0 : break;
572 :
573 0 : case TextCursor::MoveOperation::EndOfLine:
574 0 : m_column = curLine().columns();
575 :
576 0 : break;
577 :
578 1 : case TextCursor::MoveOperation::WordLeft:
579 1 : if (m_column > 0) {
580 1 : --m_column;
581 1 : while (m_column > 0 && currentCharacter().isSpace()) {
582 0 : --m_column;
583 : }
584 5 : while (m_column > 0 && !currentCharacter().isSpace()) {
585 4 : --m_column;
586 : }
587 1 : if (currentCharacter().isSpace()) {
588 1 : ++m_column;
589 : }
590 : }
591 1 : break;
592 :
593 2 : case TextCursor::MoveOperation::NextWord: {
594 2 : size_t cols = columns();
595 2 : if (m_column < cols) {
596 2 : ++m_column;
597 10 : while (m_column < cols && !currentCharacter().isSpace()) {
598 8 : ++m_column;
599 : }
600 3 : while (m_column < cols && currentCharacter().isSpace()) {
601 1 : ++m_column;
602 : }
603 : }
604 : }
605 2 : break;
606 :
607 0 : default:
608 0 : LOGD("Text::movePosition: not implemented");
609 0 : return false;
610 : }
611 6 : if (mode == TextCursor::MoveMode::MoveAnchor) {
612 3 : clearSelection();
613 : }
614 : }
615 :
616 6 : updateCursorFormat();
617 6 : m_text->score()->addRefresh(m_text->canvasBoundingRect());
618 :
619 6 : return true;
620 : }
621 :
622 : //---------------------------------------------------------
623 : // doubleClickSelect
624 : //---------------------------------------------------------
625 :
626 0 : void TextCursor::selectWord()
627 : {
628 0 : clearSelection();
629 :
630 : // if clicked on a space, select surrounding spaces
631 : // otherwise select surround non-spaces
632 0 : const bool selectSpaces = currentCharacter().isSpace();
633 :
634 : //handle double-clicking inside a word
635 0 : size_t startPosition = m_column;
636 :
637 0 : while (m_column > 0 && currentCharacter().isSpace() == selectSpaces) {
638 0 : --m_column;
639 : }
640 :
641 0 : if (currentCharacter().isSpace() != selectSpaces) {
642 0 : ++m_column;
643 : }
644 :
645 0 : m_selectColumn = m_column;
646 :
647 0 : m_column = startPosition;
648 0 : while (m_column < curLine().columns() && currentCharacter().isSpace() == selectSpaces) {
649 0 : ++m_column;
650 : }
651 :
652 0 : updateCursorFormat();
653 0 : m_text->score()->addRefresh(m_text->canvasBoundingRect());
654 0 : }
655 :
656 : //---------------------------------------------------------
657 : // set
658 : //---------------------------------------------------------
659 :
660 0 : bool TextCursor::set(const PointF& p, TextCursor::MoveMode mode)
661 : {
662 0 : PointF pt = p - m_text->canvasPos();
663 0 : if (!m_text->ldata()->bbox().contains(pt)) {
664 0 : return false;
665 : }
666 :
667 : // if (_text->_layout.empty())
668 : // _text->_layout.append(TextBlock());
669 0 : m_row = 0;
670 :
671 0 : const TextBase::LayoutData* ldata = m_text->ldata();
672 0 : IF_ASSERT_FAILED(ldata) {
673 0 : return false;
674 : }
675 :
676 0 : for (size_t row = 0; row < ldata->blocks.size(); ++row) {
677 0 : const TextBlock& l = ldata->blocks.at(row);
678 0 : if (l.y() > pt.y()) {
679 0 : m_row = row;
680 0 : break;
681 : }
682 : }
683 0 : m_column = curLine().column(pt.x(), m_text);
684 :
685 0 : m_text->score()->setUpdateAll();
686 0 : if (mode == TextCursor::MoveMode::MoveAnchor) {
687 0 : clearSelection();
688 : }
689 0 : updateCursorFormat();
690 :
691 0 : return true;
692 : }
693 :
694 : //---------------------------------------------------------
695 : // selectedText
696 : // return current selection
697 : //---------------------------------------------------------
698 :
699 0 : String TextCursor::selectedText(bool withFormat) const
700 : {
701 0 : size_t r1 = selectLine();
702 0 : size_t r2 = m_row;
703 0 : size_t c1 = selectColumn();
704 0 : size_t c2 = column();
705 0 : TextBase::sort(r1, c1, r2, c2);
706 0 : return extractText(static_cast<int>(r1), static_cast<int>(c1), static_cast<int>(r2), static_cast<int>(c2), withFormat);
707 : }
708 :
709 : //---------------------------------------------------------
710 : // extractText
711 : // return text between (r1,c1) and (r2,c2).
712 : //---------------------------------------------------------
713 :
714 0 : String TextCursor::extractText(int r1, int c1, int r2, int c2, bool withFormat) const
715 : {
716 0 : const TextBase::LayoutData* ldata = m_text->ldata();
717 0 : IF_ASSERT_FAILED(ldata) {
718 0 : return String();
719 : }
720 :
721 0 : assert(TextBase::isSorted(r1, c1, r2, c2));
722 0 : const std::vector<TextBlock>& tb = ldata->blocks;
723 :
724 0 : if (r1 == r2) {
725 0 : return tb.at(r1).text(c1, c2 - c1, m_text, withFormat);
726 : }
727 :
728 0 : String str = tb.at(r1).text(c1, -1, m_text, withFormat) + u"\n";
729 :
730 0 : for (int r = r1 + 1; r < r2; ++r) {
731 0 : str += tb.at(r).text(0, -1, m_text, withFormat) + u"\n";
732 : }
733 :
734 0 : str += tb.at(r2).text(0, c2, m_text, withFormat);
735 0 : return str;
736 0 : }
737 :
738 0 : TextCursor::Range TextCursor::range(int start, int end) const
739 : {
740 0 : const TextBase::LayoutData* ldata = m_text->ldata();
741 0 : IF_ASSERT_FAILED(ldata) {
742 0 : return Range();
743 : }
744 :
745 0 : String result;
746 0 : int pos = 0;
747 0 : for (size_t i = 0; i < ldata->blocks.size(); ++i) {
748 0 : const TextBlock& t = ldata->blocks.at(i);
749 :
750 0 : for (size_t j = 0; j < t.columns(); ++j) {
751 0 : if (pos > end) {
752 0 : return { start, end, result };
753 : }
754 :
755 0 : if (start < pos) {
756 0 : result += t.text(static_cast<int>(j), 1, m_text);
757 : }
758 :
759 0 : pos++;
760 : }
761 : }
762 :
763 0 : return { start, end, result };
764 0 : }
765 :
766 1 : int TextCursor::position(int row, int column) const
767 : {
768 1 : const TextBase::LayoutData* ldata = m_text->ldata();
769 1 : IF_ASSERT_FAILED(ldata) {
770 0 : return 0;
771 : }
772 :
773 1 : int result = 0;
774 :
775 1 : for (int i = 0; i < row; ++i) {
776 0 : const TextBlock& t = ldata->blocks.at(i);
777 0 : result += static_cast<int>(t.columns());
778 : }
779 :
780 1 : result += column;
781 :
782 1 : return result;
783 : }
784 :
785 : //---------------------------------------------------------
786 : // TextFragment
787 : //---------------------------------------------------------
788 32584 : TextFragment::TextFragment(const String& s)
789 : {
790 32584 : text = s;
791 32584 : }
792 :
793 32431 : TextFragment::TextFragment(TextCursor* cursor, const String& s)
794 32431 : : TextFragment(s)
795 : {
796 32431 : format = *cursor->format();
797 32431 : }
798 :
799 50776 : TextFragment::TextFragment(const TextFragment& f)
800 : {
801 50776 : text = f.text;
802 50776 : format = f.format;
803 50776 : pos = f.pos;
804 50776 : }
805 :
806 552 : TextFragment& TextFragment::operator =(const TextFragment& f)
807 : {
808 552 : text = f.text;
809 552 : format = f.format;
810 552 : pos = f.pos;
811 552 : return *this;
812 : }
813 :
814 : //---------------------------------------------------------
815 : // split
816 : //---------------------------------------------------------
817 :
818 2 : TextFragment TextFragment::split(int column)
819 : {
820 2 : size_t idx = 0;
821 2 : int col = 0;
822 2 : TextFragment f;
823 2 : f.format = format;
824 :
825 6 : for (size_t i = 0; i < text.size(); ++i) {
826 4 : const Char& c = text.at(i);
827 4 : if (col == column) {
828 0 : if (idx) {
829 0 : if (idx < text.size()) {
830 0 : f.text = text.mid(idx);
831 0 : text = text.left(idx);
832 : }
833 : }
834 0 : return f;
835 : }
836 4 : ++idx;
837 4 : if (c.isHighSurrogate()) {
838 0 : continue;
839 : }
840 4 : ++col;
841 : }
842 2 : return f;
843 0 : }
844 :
845 : //---------------------------------------------------------
846 : // columns
847 : //---------------------------------------------------------
848 :
849 33929 : int TextFragment::columns() const
850 : {
851 33929 : int col = 0;
852 200582 : for (size_t i = 0; i < text.size(); ++i) {
853 166653 : if (text.at(i).isHighSurrogate()) {
854 96 : continue;
855 : }
856 166557 : ++col;
857 : }
858 33929 : return col;
859 : }
860 :
861 : //---------------------------------------------------------
862 : // operator ==
863 : //---------------------------------------------------------
864 :
865 0 : bool TextFragment::operator ==(const TextFragment& f) const
866 : {
867 0 : return format == f.format && text == f.text;
868 : }
869 :
870 : //---------------------------------------------------------
871 : // font
872 : //---------------------------------------------------------
873 :
874 189404 : Font TextFragment::font(const TextBase* t) const
875 : {
876 189404 : Font font;
877 :
878 189404 : double m = calculatedFontSize(t);
879 :
880 189404 : String family;
881 189404 : Font::Type fontType = Font::Type::Unknown;
882 189404 : if (format.fontFamily() == "ScoreText") {
883 7255 : if (t->hasSymbolScale()) {
884 3466 : std::string fontName = engravingFonts()->fontByName(t->style().styleSt(Sid::musicalSymbolFont).toStdString())->family();
885 3466 : family = String::fromStdString(fontName);
886 3466 : fontType = Font::Type::MusicSymbol;
887 :
888 3466 : m = StyleDef::DEFAULT_SMUFL_POINT_SIZE();
889 3466 : m *= t->getProperty(Pid::MUSICAL_SYMBOLS_SCALE).toDouble();
890 3466 : if (t->sizeIsSpatiumDependent()) {
891 3466 : m *= t->spatiumScaling();
892 : }
893 :
894 3466 : if (t->style().styleB(Sid::dynamicsOverrideFont)) {
895 0 : std::string fontName2 = engravingFonts()->fontByName(t->style().styleSt(Sid::dynamicsFont).toStdString())->family();
896 0 : family = String::fromStdString(fontName2);
897 0 : }
898 :
899 3466 : m *= t->mag();
900 7255 : } else if (t->hasSymbolSize()) {
901 3789 : family = t->style().styleSt(Sid::musicalTextFont);
902 3789 : fontType = Font::Type::MusicSymbolText;
903 3789 : m = t->getProperty(Pid::MUSIC_SYMBOL_SIZE).toDouble();
904 3789 : if (t->sizeIsSpatiumDependent()) {
905 3789 : m *= t->spatiumScaling();
906 : }
907 3789 : m *= t->mag();
908 : }
909 : // check if all symbols are available
910 7255 : font.setFamily(family, fontType);
911 7255 : font.setNoFontMerging(true);
912 7255 : FontMetrics fm(font);
913 :
914 7255 : resolveFallback(fontType, fm, family);
915 7255 : } else {
916 182149 : family = format.fontFamily();
917 182149 : fontType = Font::Type::Unknown;
918 182149 : font.setBold(format.bold());
919 182149 : font.setItalic(format.italic());
920 182149 : font.setUnderline(format.underline());
921 182149 : font.setStrike(format.strike());
922 : }
923 :
924 189404 : font.setFamily(family, fontType);
925 189404 : assert(m > 0.0);
926 :
927 189404 : font.setPointSizeF(m);
928 189404 : return font;
929 189404 : }
930 :
931 : //---------------------------------------------------------
932 : // calculatedFontSize
933 : //---------------------------------------------------------
934 :
935 189534 : double TextFragment::calculatedFontSize(const TextBase* t) const
936 : {
937 189534 : double size = format.fontSize();
938 :
939 189534 : if (t->sizeIsSpatiumDependent()) {
940 154525 : size *= t->spatiumScaling();
941 : }
942 189534 : if (format.valign() != VerticalAlignment::AlignNormal) {
943 8 : size *= SUBSCRIPT_SIZE;
944 : }
945 :
946 189534 : assert(size > 0.0);
947 189534 : return size * t->mag();
948 : }
949 :
950 7255 : void TextFragment::resolveFallback(muse::draw::Font::Type fontType, const muse::draw::FontMetrics& fm,
951 : String& family) const
952 : {
953 7255 : std::vector<char32_t> missingChars;
954 16994 : for (size_t i = 0; i < text.size(); ++i) {
955 9739 : const Char& c = text.at(i);
956 9739 : if (c.isHighSurrogate()) {
957 0 : if (i + 1 == text.size()) {
958 0 : ASSERT_X("bad string");
959 : }
960 0 : const Char& c2 = text.at(i + 1);
961 0 : ++i;
962 0 : char32_t v = Char::surrogateToUcs4(c, c2);
963 0 : if (!fm.inFont(v)) {
964 0 : missingChars.push_back(v);
965 : }
966 : } else {
967 9739 : if (!fm.inFont(c.unicode())) {
968 1331 : missingChars.push_back(c.unicode());
969 : }
970 : }
971 : }
972 :
973 7255 : static String fallbackSymbolFontFamily = String::fromUtf8(FALLBACK_SYMBOL_FONT);
974 7255 : static String fallbackSymbolTextFontFamily = String::fromUtf8(FALLBACK_SYMBOLTEXT_FONT);
975 7255 : static FontMetrics fallbackSymbolFM(Font(fallbackSymbolFontFamily, Font::Type::MusicSymbol));
976 7255 : static FontMetrics fallbackSymbolTextFM(Font(fallbackSymbolTextFontFamily, Font::Type::MusicSymbolText));
977 :
978 7255 : if (fontType == Font::Type::MusicSymbol) {
979 3466 : for (char32_t missingChar : missingChars) {
980 233 : if (fallbackSymbolFM.inFont(missingChar)) {
981 233 : family = fallbackSymbolFontFamily;
982 233 : return;
983 : }
984 : }
985 : } else {
986 3857 : for (char32_t missingChar : missingChars) {
987 1068 : if (fallbackSymbolTextFM.inFont(missingChar)) {
988 1000 : family = fallbackSymbolTextFontFamily;
989 1000 : return;
990 : }
991 : }
992 : }
993 7255 : }
994 :
995 : //---------------------------------------------------------
996 :
997 : // fragmentsWithoutEmpty
998 : //---------------------------------------------------------
999 :
1000 0 : std::list<TextFragment> TextBlock::fragmentsWithoutEmpty()
1001 : {
1002 0 : std::list<TextFragment> list;
1003 0 : for (const auto& x : m_fragments) {
1004 0 : if (!x.text.isEmpty()) {
1005 0 : list.push_back(x);
1006 : }
1007 : }
1008 :
1009 0 : return list;
1010 0 : }
1011 :
1012 : //---------------------------------------------------------
1013 : // xpos
1014 : //---------------------------------------------------------
1015 :
1016 630 : double TextBlock::xpos(size_t column, const TextBase* t) const
1017 : {
1018 630 : size_t col = 0;
1019 642 : for (const TextFragment& f : m_fragments) {
1020 640 : if (column == col) {
1021 628 : return f.pos.x();
1022 : }
1023 355 : FontMetrics fm(f.font(t));
1024 355 : size_t idx = 0;
1025 2938 : for (size_t i = 0; i < f.text.size(); ++i) {
1026 2926 : ++idx;
1027 2926 : if (f.text.at(i).isHighSurrogate()) {
1028 4 : continue;
1029 : }
1030 2922 : ++col;
1031 2922 : if (column == col) {
1032 343 : return f.pos.x() + fm.width(f.text.left(idx));
1033 : }
1034 : }
1035 355 : }
1036 2 : return m_shape.bbox().x();
1037 : }
1038 :
1039 : //---------------------------------------------------------
1040 : // fragment
1041 : //---------------------------------------------------------
1042 :
1043 1714 : const TextFragment* TextBlock::fragment(int column) const
1044 : {
1045 1714 : if (m_fragments.empty()) {
1046 1 : return nullptr;
1047 : }
1048 1713 : int col = 0;
1049 1713 : auto f = m_fragments.begin();
1050 1714 : for (; f != m_fragments.end(); ++f) {
1051 1739 : for (size_t i = 0; i < f->text.size(); ++i) {
1052 1738 : if (f->text.at(i).isHighSurrogate()) {
1053 0 : continue;
1054 : }
1055 1738 : if (column == col) {
1056 1713 : return &*f;
1057 : }
1058 25 : ++col;
1059 : }
1060 : }
1061 0 : if (column == col) {
1062 0 : return &*(std::prev(f));
1063 : }
1064 0 : return 0;
1065 : }
1066 :
1067 : //---------------------------------------------------------
1068 : // formatAt
1069 : //---------------------------------------------------------
1070 :
1071 11 : const CharFormat* TextBlock::formatAt(int column) const
1072 : {
1073 11 : const TextFragment* f = fragment(column);
1074 11 : if (f) {
1075 10 : return &(f->format);
1076 : }
1077 1 : return 0;
1078 : }
1079 :
1080 : //---------------------------------------------------------
1081 : // boundingRect
1082 : //---------------------------------------------------------
1083 :
1084 0 : RectF TextBlock::boundingRect(int col1, int col2, const TextBase* t) const
1085 : {
1086 0 : double x1 = xpos(col1, t);
1087 0 : double x2 = xpos(col2, t);
1088 0 : const RectF& bbox = m_shape.bbox();
1089 0 : return RectF(x1, bbox.y(), x2 - x1, bbox.height());
1090 : }
1091 :
1092 : //---------------------------------------------------------
1093 : // columns
1094 : //---------------------------------------------------------
1095 :
1096 33546 : size_t TextBlock::columns() const
1097 : {
1098 33546 : size_t col = 0;
1099 67150 : for (const TextFragment& f : m_fragments) {
1100 199454 : for (size_t i = 0; i < f.text.size(); ++i) {
1101 165850 : if (!f.text.at(i).isHighSurrogate()) {
1102 165754 : ++col;
1103 : }
1104 : }
1105 : }
1106 33546 : return col;
1107 : }
1108 :
1109 : //---------------------------------------------------------
1110 : // column
1111 : // Return nearest column for position x. X is in
1112 : // Text coordinate system
1113 : //---------------------------------------------------------
1114 :
1115 0 : int TextBlock::column(double x, TextBase* t) const
1116 : {
1117 0 : int col = 0;
1118 0 : for (const TextFragment& f : m_fragments) {
1119 0 : int idx = 0;
1120 0 : if (x <= f.pos.x()) {
1121 0 : return col;
1122 : }
1123 0 : double px = 0.0;
1124 0 : for (size_t i = 0; i < f.text.size(); ++i) {
1125 0 : ++idx;
1126 0 : if (f.text.at(i).isHighSurrogate()) {
1127 0 : continue;
1128 : }
1129 0 : FontMetrics fm(f.font(t));
1130 0 : double xo = fm.width(f.text.left(idx));
1131 0 : if (x <= f.pos.x() + px + (xo - px) * .5) {
1132 0 : return col;
1133 : }
1134 0 : ++col;
1135 0 : px = xo;
1136 0 : }
1137 : }
1138 0 : return static_cast<int>(this->columns());
1139 : }
1140 :
1141 : //---------------------------------------------------------
1142 : // insert
1143 : //---------------------------------------------------------
1144 :
1145 177603 : void TextBlock::insert(TextCursor* cursor, const String& s)
1146 : {
1147 : int rcol, ridx;
1148 177603 : removeEmptyFragment(); // since we are going to write text, we don't need an empty fragment to hold format info. if such exists, delete it
1149 177603 : auto i = fragment(static_cast<int>(cursor->column()), &rcol, &ridx);
1150 177603 : if (i != m_fragments.end()) {
1151 2 : if (!(i->format == *cursor->format())) {
1152 2 : if (rcol == 0) {
1153 2 : m_fragments.insert(i, TextFragment(cursor, s));
1154 : } else {
1155 0 : TextFragment f2 = i->split(rcol);
1156 0 : i = m_fragments.insert(std::next(i), TextFragment(cursor, s));
1157 0 : m_fragments.insert(std::next(i), f2);
1158 0 : }
1159 : } else {
1160 0 : i->text.insert(ridx, s);
1161 : }
1162 : } else {
1163 177601 : if (!m_fragments.empty() && m_fragments.back().format == *cursor->format()) {
1164 145478 : m_fragments.back().text.append(s);
1165 : } else {
1166 32123 : m_fragments.push_back(TextFragment(cursor, s));
1167 : }
1168 : }
1169 177603 : }
1170 :
1171 : //---------------------------------------------------------
1172 : //
1173 : // insertEmptyFragmentIfNeeded
1174 : // used to insert an empty TextFragment in TextBlocks that have none
1175 : // that way, the formatting information (most importantly the font size) of the line is preserved
1176 : //
1177 : //---------------------------------------------------------
1178 :
1179 306 : void TextBlock::insertEmptyFragmentIfNeeded(TextCursor* cursor)
1180 : {
1181 306 : if (m_fragments.size() == 0 || m_fragments.front().text.isEmpty()) {
1182 306 : m_fragments.insert(m_fragments.begin(), TextFragment(cursor, u""));
1183 : }
1184 306 : }
1185 :
1186 : //---------------------------------------------------------
1187 : // removeEmptyFragment
1188 : //---------------------------------------------------------
1189 :
1190 177603 : void TextBlock::removeEmptyFragment()
1191 : {
1192 177603 : if (m_fragments.size() > 0 && m_fragments.front().text.isEmpty()) {
1193 213 : m_fragments.pop_back();
1194 : }
1195 177603 : }
1196 :
1197 : //---------------------------------------------------------
1198 : // fragment
1199 : // inputs:
1200 : // column is the column relative to the start of the TextBlock.
1201 : // outputs:
1202 : // rcol will be the column relative to the start of the TextFragment that the input column is in.
1203 : // ridx will be the QChar index into TextFragment's text String relative to the start of that TextFragment.
1204 : //
1205 : //---------------------------------------------------------
1206 :
1207 177603 : std::list<TextFragment>::iterator TextBlock::fragment(int column, int* rcol, int* ridx)
1208 : {
1209 177603 : int col = 0;
1210 330061 : for (auto it = m_fragments.begin(); it != m_fragments.end(); ++it) {
1211 152460 : *rcol = 0;
1212 152460 : *ridx = 0;
1213 1332698 : for (size_t i = 0; i < it->text.size(); ++i) {
1214 1180240 : if (col == column) {
1215 2 : return it;
1216 : }
1217 1180238 : ++*ridx;
1218 1180238 : if (it->text.at(i).isHighSurrogate()) {
1219 936 : continue;
1220 : }
1221 1179302 : ++col;
1222 1179302 : ++*rcol;
1223 : }
1224 : }
1225 177601 : return m_fragments.end();
1226 : }
1227 :
1228 : //---------------------------------------------------------
1229 : // remove
1230 : //---------------------------------------------------------
1231 :
1232 0 : String TextBlock::remove(int column, TextCursor* cursor)
1233 : {
1234 0 : int col = 0;
1235 0 : String s;
1236 0 : for (auto it = m_fragments.begin(); it != m_fragments.end(); ++it) {
1237 0 : size_t idx = 0;
1238 :
1239 0 : for (size_t i = 0; i < it->text.size(); ++i) {
1240 0 : if (col == column) {
1241 0 : if (it->text.at(i).isSurrogate()) {
1242 0 : s = it->text.mid(idx, 2);
1243 0 : it->text.remove(idx, 2);
1244 : } else {
1245 0 : s = it->text.mid(idx, 1);
1246 0 : it->text.remove(idx, 1);
1247 : }
1248 0 : if (it->text.isEmpty()) {
1249 0 : m_fragments.erase(it);
1250 : }
1251 0 : simplify();
1252 0 : insertEmptyFragmentIfNeeded(cursor); // without this, cursorRect can't calculate the y position of the cursor correctly
1253 0 : return s;
1254 : }
1255 0 : ++idx;
1256 0 : if (it->text.at(i).isHighSurrogate()) {
1257 0 : continue;
1258 : }
1259 0 : ++col;
1260 : }
1261 : }
1262 0 : insertEmptyFragmentIfNeeded(cursor); // without this, cursorRect can't calculate the y position of the cursor correctly
1263 0 : return s;
1264 : // LOGD("TextBlock::remove: column %d not found", column);
1265 0 : }
1266 :
1267 : //---------------------------------------------------------
1268 : // simplify
1269 : //---------------------------------------------------------
1270 :
1271 0 : void TextBlock::simplify()
1272 : {
1273 0 : if (m_fragments.size() < 2) {
1274 0 : return;
1275 : }
1276 0 : auto i = m_fragments.begin();
1277 0 : TextFragment* f = &*i;
1278 0 : ++i;
1279 0 : for (; i != m_fragments.end(); ++i) {
1280 0 : while (i != m_fragments.end() && (i->format == f->format)) {
1281 0 : f->text.append(i->text);
1282 0 : i = m_fragments.erase(i);
1283 : }
1284 0 : if (i == m_fragments.end()) {
1285 0 : break;
1286 : }
1287 0 : f = &*i;
1288 : }
1289 : }
1290 :
1291 : //---------------------------------------------------------
1292 : // remove
1293 : //---------------------------------------------------------
1294 :
1295 0 : String TextBlock::remove(int start, int n, TextCursor* cursor)
1296 : {
1297 0 : if (n == 0) {
1298 0 : return String();
1299 : }
1300 0 : int col = 0;
1301 0 : String s;
1302 0 : for (auto i = m_fragments.begin(); i != m_fragments.end();) {
1303 0 : bool inc = true;
1304 0 : for (size_t idx = 0; idx < i->text.size();) {
1305 0 : Char c = i->text.at(idx);
1306 0 : if (col == start) {
1307 0 : if (c.isHighSurrogate()) {
1308 0 : s += c;
1309 0 : i->text.remove(idx, 1);
1310 0 : c = i->text.at(idx);
1311 : }
1312 0 : s += c;
1313 0 : i->text.remove(idx, 1);
1314 0 : if (i->text.isEmpty() && (m_fragments.size() > 1)) {
1315 0 : i = m_fragments.erase(i);
1316 0 : inc = false;
1317 : }
1318 0 : --n;
1319 0 : if (n == 0) {
1320 0 : insertEmptyFragmentIfNeeded(cursor); // without this, cursorRect can't calculate the y position of the cursor correctly
1321 0 : return s;
1322 : }
1323 0 : continue;
1324 : }
1325 0 : ++idx;
1326 0 : if (c.isHighSurrogate()) {
1327 0 : continue;
1328 : }
1329 0 : ++col;
1330 : }
1331 0 : if (inc) {
1332 0 : ++i;
1333 : }
1334 : }
1335 0 : insertEmptyFragmentIfNeeded(cursor); // without this, cursorRect can't calculate the y position of the cursor correctly
1336 0 : return s;
1337 0 : }
1338 :
1339 : //---------------------------------------------------------
1340 : // changeFormat
1341 : //---------------------------------------------------------
1342 :
1343 31832 : void TextBlock::changeFormat(FormatId id, const FormatValue& data, int start, int n)
1344 : {
1345 31832 : int col = 0;
1346 63695 : for (auto i = m_fragments.begin(); i != m_fragments.end(); ++i) {
1347 31867 : int columns = i->columns();
1348 31867 : if (start + n <= col) {
1349 4 : if (columns == 0) {
1350 : // still apply the format change. Otherwise we have deviating formats for e. g. empty lines
1351 : // otherwise we have Issue #19571
1352 4 : i->changeFormat(id, data);
1353 : }
1354 4 : break;
1355 : }
1356 31863 : if (start >= col + columns) {
1357 0 : col += i->columns();
1358 0 : continue;
1359 : }
1360 31863 : int endCol = col + columns;
1361 :
1362 31863 : if ((start <= col) && (start < endCol) && ((start + n) < endCol)) {
1363 : // left
1364 0 : TextFragment f = i->split(start + n - col);
1365 0 : i->changeFormat(id, data);
1366 0 : i = m_fragments.insert(std::next(i), f);
1367 31863 : } else if (start > col && ((start + n) < endCol)) {
1368 : // middle
1369 0 : TextFragment lf = i->split(start + n - col);
1370 0 : TextFragment mf = i->split(start - col);
1371 0 : mf.changeFormat(id, data);
1372 0 : i = m_fragments.insert(std::next(i), mf);
1373 0 : i = m_fragments.insert(std::next(i), lf);
1374 31863 : } else if (start > col) {
1375 : // right
1376 0 : TextFragment f = i->split(start - col);
1377 0 : f.changeFormat(id, data);
1378 0 : i = m_fragments.insert(std::next(i), f);
1379 0 : } else {
1380 31863 : if (id == FormatId::FontFamily && i->format.fontFamily() == "ScoreText") {
1381 : void(0);// do nothing, we need to leave that as is
1382 : } else {
1383 : // complete fragment
1384 31852 : i->changeFormat(id, data);
1385 : }
1386 : }
1387 31863 : col = endCol;
1388 : }
1389 31832 : }
1390 :
1391 : //---------------------------------------------------------
1392 : // formatValue
1393 : //---------------------------------------------------------
1394 8 : FormatValue CharFormat::formatValue(FormatId id) const
1395 : {
1396 8 : switch (id) {
1397 2 : case FormatId::Bold: return bold();
1398 2 : case FormatId::Italic: return italic();
1399 2 : case FormatId::Underline: return underline();
1400 2 : case FormatId::Strike: return strike();
1401 0 : case FormatId::Valign: return static_cast<int>(valign());
1402 0 : case FormatId::FontSize: return fontSize();
1403 0 : case FormatId::FontFamily: return fontFamily();
1404 : }
1405 :
1406 0 : return FormatValue();
1407 : }
1408 :
1409 : //---------------------------------------------------------
1410 : // setFormatValue
1411 : //---------------------------------------------------------
1412 :
1413 297312 : void CharFormat::setFormatValue(FormatId id, const FormatValue& val)
1414 : {
1415 297312 : switch (id) {
1416 49548 : case FormatId::Bold:
1417 49548 : setBold(std::get<bool>(val));
1418 49548 : break;
1419 49541 : case FormatId::Italic:
1420 49541 : setItalic(std::get<bool>(val));
1421 49541 : break;
1422 49540 : case FormatId::Underline:
1423 49540 : setUnderline(std::get<bool>(val));
1424 49540 : break;
1425 49526 : case FormatId::Strike:
1426 49526 : setStrike(std::get<bool>(val));
1427 49526 : break;
1428 0 : case FormatId::Valign:
1429 0 : m_valign = static_cast<VerticalAlignment>(std::get<int>(val));
1430 0 : break;
1431 49645 : case FormatId::FontSize:
1432 49645 : m_fontSize = std::get<double>(val);
1433 49645 : break;
1434 49512 : case FormatId::FontFamily:
1435 49512 : m_fontFamily = std::get<String>(val);
1436 49512 : break;
1437 : }
1438 297312 : }
1439 :
1440 : //---------------------------------------------------------
1441 : // changeFormat
1442 : //---------------------------------------------------------
1443 :
1444 31856 : void TextFragment::changeFormat(FormatId id, const FormatValue& data)
1445 : {
1446 31856 : format.setFormatValue(id, data);
1447 31856 : }
1448 :
1449 : //---------------------------------------------------------
1450 : // split
1451 : //---------------------------------------------------------
1452 :
1453 0 : TextBlock TextBlock::split(int column, TextCursor* cursor)
1454 : {
1455 0 : TextBlock tl;
1456 :
1457 0 : int col = 0;
1458 0 : for (auto it = m_fragments.begin(); it != m_fragments.end(); ++it) {
1459 0 : size_t idx = 0;
1460 0 : for (size_t i = 0; i < it->text.size(); ++i) {
1461 0 : if (col == column) {
1462 0 : if (idx) {
1463 0 : if (idx < it->text.size()) {
1464 0 : TextFragment tf(it->text.mid(idx));
1465 0 : tf.format = it->format;
1466 0 : tl.m_fragments.push_back(tf);
1467 0 : it->text = it->text.left(idx);
1468 0 : ++it;
1469 0 : }
1470 : }
1471 0 : for (; it != m_fragments.end(); it = m_fragments.erase(it)) {
1472 0 : tl.m_fragments.push_back(*it);
1473 : }
1474 :
1475 0 : if (m_fragments.size() == 0) {
1476 0 : insertEmptyFragmentIfNeeded(cursor);
1477 : }
1478 0 : return tl;
1479 : }
1480 0 : ++idx;
1481 0 : if (it->text.at(i).isHighSurrogate()) {
1482 0 : continue;
1483 : }
1484 0 : ++col;
1485 : }
1486 : }
1487 :
1488 0 : TextFragment tf(u"");
1489 0 : if (m_fragments.size() > 0) {
1490 0 : tf.format = m_fragments.back().format;
1491 0 : } else if (m_fragments.size() == 0) {
1492 0 : insertEmptyFragmentIfNeeded(cursor);
1493 : }
1494 :
1495 0 : tl.m_fragments.push_back(tf);
1496 0 : return tl;
1497 0 : }
1498 :
1499 66 : static String toSymbolXml(Char c, const TextBase* t)
1500 : {
1501 66 : static std::shared_ptr<IEngravingFontsProvider> provider = muse::modularity::globalIoc()->resolve<IEngravingFontsProvider>("engraving");
1502 :
1503 66 : SymId symId = t->score()->engravingFont()->fromCode(c.unicode());
1504 66 : if (symId == SymId::noSym) {
1505 0 : symId = provider->fallbackFont()->fromCode(c.unicode());
1506 : }
1507 66 : return u"<sym>" + String::fromAscii(SymNames::nameForSymId(symId).ascii()) + u"</sym>";
1508 : }
1509 :
1510 : //---------------------------------------------------------
1511 : // text
1512 : // extract text, symbols are marked with <sym>xxx</sym>
1513 : //---------------------------------------------------------
1514 :
1515 18 : String TextBlock::text(int col1, int len, const TextBase* t, bool withFormat) const
1516 : {
1517 18 : String s;
1518 18 : int col = 0;
1519 : double size;
1520 18 : String family;
1521 54 : for (const auto& f : m_fragments) {
1522 36 : if (f.text.isEmpty()) {
1523 0 : continue;
1524 : }
1525 36 : if (withFormat) {
1526 0 : s += TextBase::getHtmlStartTag(f.format.fontSize(), size, f.format.fontFamily(), family, f.format.style(), f.format.valign());
1527 : }
1528 :
1529 234 : for (size_t i = 0; i < f.text.size(); ++i) {
1530 198 : Char c = f.text.at(i);
1531 198 : if (col >= col1 && (len < 0 || ((col - col1) < len))) {
1532 18 : if (f.format.fontFamily() == "ScoreText" && withFormat) {
1533 0 : s += toSymbolXml(c, t);
1534 : } else {
1535 18 : s += String::toXmlEscaped(c.unicode());
1536 : }
1537 : }
1538 198 : if (!c.isHighSurrogate()) {
1539 198 : ++col;
1540 : }
1541 : }
1542 36 : if (withFormat) {
1543 0 : s += TextBase::getHtmlEndTag(f.format.style(), f.format.valign());
1544 : }
1545 : }
1546 18 : return s;
1547 18 : }
1548 :
1549 : //---------------------------------------------------------
1550 : // Text
1551 : //---------------------------------------------------------
1552 :
1553 28330 : TextBase::TextBase(const ElementType& type, EngravingObject* parent, TextStyleType tid, ElementFlags f)
1554 28330 : : EngravingItem(type, parent, f | ElementFlag::MOVABLE)
1555 : {
1556 28330 : m_textLineSpacing = 1.0;
1557 28330 : m_textStyleType = tid;
1558 28330 : m_bgColor = Color::transparent;
1559 28330 : m_frameColor = Color::BLACK;
1560 28330 : m_align = { AlignH::LEFT, AlignV::TOP };
1561 28330 : m_frameType = FrameType::NO_FRAME;
1562 28330 : m_frameWidth = 0.1_sp;
1563 28330 : m_paddingWidth = 0.2_sp;
1564 28330 : m_frameRound = 0_sp;
1565 :
1566 28330 : m_cursor = new TextCursor(this);
1567 28330 : m_cursor->init();
1568 28330 : }
1569 :
1570 100 : TextBase::TextBase(const ElementType& type, EngravingObject* parent, ElementFlags f)
1571 100 : : TextBase(type, parent, TextStyleType::DEFAULT, f)
1572 : {
1573 100 : }
1574 :
1575 2388 : TextBase::TextBase(const TextBase& st)
1576 2388 : : EngravingItem(st)
1577 : {
1578 2388 : m_cursor = new TextCursor(this);
1579 2388 : m_cursor->setFormat(*(st.cursor()->format()));
1580 2388 : m_text = st.m_text;
1581 2388 : m_textInvalid = st.m_textInvalid;
1582 2388 : m_layoutToParentWidth = st.m_layoutToParentWidth;
1583 2388 : m_hexState = -1;
1584 :
1585 2388 : m_textStyleType = st.m_textStyleType;
1586 2388 : m_textLineSpacing = st.m_textLineSpacing;
1587 2388 : m_bgColor = st.m_bgColor;
1588 2388 : m_frameColor = st.m_frameColor;
1589 2388 : m_align = st.m_align;
1590 2388 : m_frameType = st.m_frameType;
1591 2388 : m_frameWidth = st.m_frameWidth;
1592 2388 : m_paddingWidth = st.m_paddingWidth;
1593 2388 : m_frameRound = st.m_frameRound;
1594 2388 : m_position = st.m_position;
1595 2388 : m_symbolSize = st.m_symbolSize;
1596 2388 : m_symbolScale = st.m_symbolScale;
1597 :
1598 2388 : m_voiceAssignment = st.m_voiceAssignment;
1599 2388 : m_direction = st.m_direction;
1600 2388 : m_centerBetweenStaves = st.m_centerBetweenStaves;
1601 2388 : m_anchorToEndOfPrevious = st.m_anchorToEndOfPrevious;
1602 :
1603 2388 : size_t n = m_elementStyle->size() + TEXT_STYLE_SIZE;
1604 2388 : delete[] m_propertyFlagsList;
1605 2388 : m_propertyFlagsList = new PropertyFlags[n];
1606 45784 : for (size_t i = 0; i < n; ++i) {
1607 43396 : m_propertyFlagsList[i] = st.m_propertyFlagsList[i];
1608 : }
1609 2388 : m_links = 0;
1610 2388 : }
1611 :
1612 29311 : TextBase::~TextBase()
1613 : {
1614 29311 : delete m_cursor;
1615 29311 : }
1616 :
1617 : //---------------------------------------------------------
1618 : // textColor
1619 : //---------------------------------------------------------
1620 :
1621 0 : Color TextBase::textColor(const rendering::PaintOptions& opt) const
1622 : {
1623 0 : return curColor(opt);
1624 : }
1625 :
1626 : //---------------------------------------------------------
1627 : // insert
1628 : // insert character
1629 : //---------------------------------------------------------
1630 :
1631 177598 : void TextBase::insert(TextCursor* cursor, char32_t code, LayoutData* ldata) const
1632 : {
1633 177598 : if (cursor->row() >= ldata->blocks.size()) {
1634 30635 : ldata->blocks.push_back(TextBlock());
1635 : }
1636 177598 : if (code == '\t') {
1637 0 : code = ' ';
1638 : }
1639 :
1640 177598 : if (cursor->row() < ldata->blocks.size()) {
1641 177598 : ldata->blocks[cursor->row()].insert(cursor, String::fromUcs4(code));
1642 : }
1643 :
1644 177598 : cursor->setColumn(cursor->column() + 1);
1645 177598 : cursor->clearSelection();
1646 177598 : }
1647 :
1648 : //---------------------------------------------------------
1649 : // parseStringProperty
1650 : //---------------------------------------------------------
1651 :
1652 811 : static String parseStringProperty(const String& s)
1653 : {
1654 811 : String rs;
1655 4099 : for (size_t i = 0; i < s.size(); ++i) {
1656 4099 : Char c = s.at(i);
1657 4099 : if (c == '"') {
1658 811 : break;
1659 : }
1660 3288 : rs += c;
1661 : }
1662 811 : return rs;
1663 0 : }
1664 :
1665 : //---------------------------------------------------------
1666 : // parseNumProperty
1667 : //---------------------------------------------------------
1668 :
1669 567 : static double parseNumProperty(const String& s)
1670 : {
1671 567 : return parseStringProperty(s).toDouble();
1672 : }
1673 :
1674 : //---------------------------------------------------------
1675 : // createLayout
1676 : // create layout from text
1677 : //---------------------------------------------------------
1678 12944 : void TextBase::createBlocks()
1679 : {
1680 12944 : createBlocks(mutldata());
1681 12944 : }
1682 :
1683 37827 : void TextBase::createBlocks(LayoutData* ldata) const
1684 : {
1685 : // reset all previous formatting information
1686 37827 : ldata->blocks.clear();
1687 37827 : TextCursor cursor = *m_cursor;
1688 37827 : cursor.setRow(0);
1689 37827 : cursor.setColumn(0);
1690 :
1691 37827 : int state = 0;
1692 37827 : String token;
1693 37827 : String sym;
1694 37827 : bool symState = false;
1695 312856 : for (size_t i = 0; i < m_text.size(); i++) {
1696 275029 : const Char& c = m_text.at(i);
1697 275029 : if (state == 0) {
1698 224570 : if (c == '<') {
1699 8386 : state = 1;
1700 8386 : token.clear();
1701 216184 : } else if (c == '&') {
1702 72 : state = 2;
1703 72 : token.clear();
1704 216112 : } else if (c == '\n') {
1705 294 : if (ldata->rows() <= cursor.row()) {
1706 12 : ldata->blocks.push_back(TextBlock());
1707 : }
1708 :
1709 294 : if (cursor.row() < ldata->rows()) {
1710 294 : if (ldata->blocks.at(cursor.row()).fragments().size() == 0) {
1711 12 : ldata->blocks[cursor.row()].insertEmptyFragmentIfNeeded(&cursor); // used to preserve the Font size of the line (font info is held in TextFragments, see PR #5881)
1712 : }
1713 :
1714 294 : ldata->blocks[cursor.row()].setEol(true);
1715 : }
1716 :
1717 294 : cursor.setRow(cursor.row() + 1);
1718 294 : cursor.setColumn(0);
1719 294 : if (ldata->rows() <= cursor.row()) {
1720 294 : ldata->blocks.push_back(TextBlock());
1721 : }
1722 :
1723 294 : if (cursor.row() < ldata->rows()) {
1724 294 : if (ldata->blocks.at(cursor.row()).fragments().size() == 0) {
1725 294 : ldata->blocks[cursor.row()].insertEmptyFragmentIfNeeded(&cursor); // an empty fragment may be needed on either side of the newline
1726 : }
1727 : }
1728 : } else {
1729 215818 : if (symState) {
1730 41558 : sym += c;
1731 : } else {
1732 174260 : if (c.isHighSurrogate()) {
1733 161 : i++;
1734 161 : assert(i < m_text.size());
1735 161 : insert(&cursor, Char::surrogateToUcs4(c, m_text.at(i)), ldata);
1736 : } else {
1737 174099 : insert(&cursor, c.unicode(), ldata);
1738 : }
1739 : }
1740 : }
1741 50459 : } else if (state == 1) {
1742 50172 : if (c == '>') {
1743 8386 : state = 0;
1744 8386 : const_cast<TextBase*>(this)->prepareFormat(token, cursor);
1745 8386 : if (token == "sym") {
1746 3270 : symState = true;
1747 3270 : sym.clear();
1748 5116 : } else if (token == "/sym") {
1749 3266 : symState = false;
1750 3266 : SymId id = SymNames::symIdByName(sym);
1751 3266 : if (id != SymId::noSym) {
1752 3266 : CharFormat fmt = *cursor.format(); // save format
1753 :
1754 3266 : const std::shared_ptr<IEngravingFont> scoreFont = score()->engravingFont();
1755 3266 : char32_t code = static_cast<char32_t>(' ');
1756 3266 : if (id != SymId::space) {
1757 3239 : code = scoreFont->isValid(id) ? scoreFont->symCode(id) : score()->engravingFonts()->fallbackFont()->symCode(id);
1758 : }
1759 3266 : cursor.format()->setFontFamily(u"ScoreText");
1760 3266 : insert(&cursor, code, ldata);
1761 3266 : cursor.setFormat(fmt); // restore format
1762 3266 : } else {
1763 0 : LOGD("unknown symbol <%s>", muPrintable(sym));
1764 : }
1765 : }
1766 : } else {
1767 41786 : token += c;
1768 : }
1769 287 : } else if (state == 2) {
1770 287 : if (c == ';') {
1771 72 : state = 0;
1772 72 : if (token == "lt") {
1773 9 : insert(&cursor, '<', ldata);
1774 63 : } else if (token == "gt") {
1775 13 : insert(&cursor, '>', ldata);
1776 50 : } else if (token == "amp") {
1777 29 : insert(&cursor, '&', ldata);
1778 21 : } else if (token == "quot") {
1779 21 : insert(&cursor, '"', ldata);
1780 : } else {
1781 : // TODO insert(&cursor, SymNames::symIdByName(token));
1782 : }
1783 : } else {
1784 215 : token += c;
1785 : }
1786 : }
1787 : }
1788 37827 : if (ldata->blocks.empty()) {
1789 7180 : ldata->blocks.push_back(TextBlock());
1790 : }
1791 37827 : ldata->layoutInvalid = false;
1792 37827 : }
1793 :
1794 : //---------------------------------------------------------
1795 : // prepareFormat - used when reading from XML and when pasting from clipboard
1796 : //---------------------------------------------------------
1797 8386 : bool TextBase::prepareFormat(const String& token, CharFormat& format)
1798 : {
1799 8386 : if (token == "b") {
1800 367 : format.setBold(true);
1801 367 : return true;
1802 8019 : } else if (token == "/b") {
1803 367 : format.setBold(false);
1804 7652 : } else if (token == "i") {
1805 50 : format.setItalic(true);
1806 50 : return true;
1807 7602 : } else if (token == "/i") {
1808 50 : format.setItalic(false);
1809 7552 : } else if (token == "u") {
1810 7 : format.setUnderline(true);
1811 7 : return true;
1812 7545 : } else if (token == "/u") {
1813 7 : format.setUnderline(false);
1814 7538 : } else if (token == "s") {
1815 2 : format.setStrike(true);
1816 2 : return true;
1817 7536 : } else if (token == "/s") {
1818 2 : format.setStrike(false);
1819 7534 : } else if (token == "sub") {
1820 0 : format.setValign(VerticalAlignment::AlignSubScript);
1821 7534 : } else if (token == "/sub") {
1822 0 : format.setValign(VerticalAlignment::AlignNormal);
1823 7534 : } else if (token == "sup") {
1824 2 : format.setValign(VerticalAlignment::AlignSuperScript);
1825 7532 : } else if (token == "/sup") {
1826 2 : format.setValign(VerticalAlignment::AlignNormal);
1827 7530 : } else if (token.startsWith(u"font ")) {
1828 811 : String remainder = token.mid(5);
1829 811 : if (remainder.startsWith(u"size=\"")) {
1830 567 : format.setFontSize(parseNumProperty(remainder.mid(6)));
1831 567 : return true;
1832 244 : } else if (remainder.startsWith(u"face=\"")) {
1833 244 : String face = parseStringProperty(remainder.mid(6));
1834 244 : face = unEscape(face);
1835 244 : format.setFontFamily(face);
1836 244 : return true;
1837 244 : } else {
1838 0 : LOGD("cannot parse html property <%s> in text <%s>", muPrintable(token), muPrintable(m_text));
1839 : }
1840 811 : }
1841 7149 : return false;
1842 : }
1843 :
1844 : //---------------------------------------------------------
1845 : // prepareFormat - used when reading from XML
1846 : //---------------------------------------------------------
1847 8386 : void TextBase::prepareFormat(const String& token, TextCursor& cursor)
1848 : {
1849 8386 : if (prepareFormat(token, *cursor.format()) && cursor.format()->fontFamily() != propertyDefault(Pid::FONT_FACE).value<String>()) {
1850 229 : setPropertyFlags(Pid::FONT_FACE, PropertyFlags::UNSTYLED);
1851 : }
1852 8386 : }
1853 :
1854 : //---------------------------------------------------------
1855 : // layoutFrame
1856 : //---------------------------------------------------------
1857 :
1858 0 : void TextBase::layoutFrame()
1859 : {
1860 0 : layoutFrame(mutldata());
1861 0 : }
1862 :
1863 144 : void TextBase::layoutFrame(LayoutData* ldata) const
1864 : {
1865 : // if (empty()) { // or bbox.width() <= 1.0
1866 144 : if (ldata->bbox().width() <= 1.0 || ldata->bbox().height() < 1.0) { // or bbox.width() <= 1.0
1867 : // this does not work for Harmony:
1868 0 : FontMetrics fm(font());
1869 0 : double ch = fm.ascent();
1870 0 : double cw = fm.width('n');
1871 0 : ldata->frame = RectF(0.0, -ch, cw, ch);
1872 0 : } else {
1873 144 : ldata->frame = ldata->bbox();
1874 : }
1875 :
1876 144 : if (rectangle()) {
1877 : // make sure width >= height, if only one row (basically: make square for single characters)
1878 134 : if (ldata->frame.height() > ldata->frame.width() && ldata->rows() == 1) {
1879 74 : double w = ldata->frame.height() - ldata->frame.width();
1880 74 : ldata->frame.adjust(-w * .5, 0.0, w * .5, 0.0);
1881 : }
1882 10 : } else if (circle()) {
1883 10 : if (ldata->frame.width() > ldata->frame.height()) {
1884 2 : ldata->frame.setTop(ldata->frame.y() + (ldata->frame.width() - ldata->frame.height()) * -.5);
1885 2 : ldata->frame.setHeight(ldata->frame.width());
1886 : } else {
1887 8 : ldata->frame.setLeft(ldata->frame.x() + (ldata->frame.height() - ldata->frame.width()) * -.5);
1888 8 : ldata->frame.setWidth(ldata->frame.height());
1889 : }
1890 : }
1891 144 : double _spatium = spatium();
1892 144 : double w = (paddingWidth() + frameWidth() * .5f).val() * _spatium;
1893 144 : ldata->frame.adjust(-w, -w, w, w);
1894 144 : w = 0.5 * frameWidth().val() * _spatium;
1895 144 : ldata->setBbox(ldata->frame.adjusted(-w, -w, w, w));
1896 144 : }
1897 :
1898 : //---------------------------------------------------------
1899 : // lineSpacing
1900 : //---------------------------------------------------------
1901 :
1902 2525 : double TextBase::lineSpacing() const
1903 : {
1904 2525 : return fontMetrics().lineSpacing();
1905 : }
1906 :
1907 : //---------------------------------------------------------
1908 : // lineHeight
1909 : //---------------------------------------------------------
1910 :
1911 0 : double TextBase::lineHeight() const
1912 : {
1913 0 : return fontMetrics().height();
1914 : }
1915 :
1916 : //---------------------------------------------------------
1917 : // baseLine
1918 : //---------------------------------------------------------
1919 :
1920 0 : double TextBase::baseLine() const
1921 : {
1922 0 : return fontMetrics().ascent();
1923 : }
1924 :
1925 27623 : FontStyle TextBase::fontStyle() const
1926 : {
1927 27623 : return m_cursor->format()->style();
1928 : }
1929 :
1930 7256 : String TextBase::family() const
1931 : {
1932 7256 : return m_cursor->format()->fontFamily();
1933 : }
1934 :
1935 24866 : double TextBase::size() const
1936 : {
1937 24866 : return m_cursor->format()->fontSize();
1938 : }
1939 :
1940 44226 : void TextBase::setFontStyle(const FontStyle& val)
1941 : {
1942 44226 : m_cursor->setFormat(FormatId::Bold, val & FontStyle::Bold);
1943 44226 : m_cursor->setFormat(FormatId::Italic, val & FontStyle::Italic);
1944 44226 : m_cursor->setFormat(FormatId::Underline, val & FontStyle::Underline);
1945 44226 : m_cursor->setFormat(FormatId::Strike, val & FontStyle::Strike);
1946 44226 : }
1947 :
1948 44221 : void TextBase::setFamily(const String& val)
1949 : {
1950 44221 : m_cursor->setFormat(FormatId::FontFamily, val);
1951 44221 : }
1952 :
1953 44330 : void TextBase::setSize(const double& val)
1954 : {
1955 44330 : m_cursor->setFormat(FormatId::FontSize, val);
1956 44330 : }
1957 :
1958 : //---------------------------------------------------------
1959 : // XmlNesting
1960 : //---------------------------------------------------------
1961 :
1962 : class XmlNesting : public std::stack<String>
1963 : {
1964 : OBJECT_ALLOCATOR(engraving, XmlNesting)
1965 :
1966 : String* _s;
1967 :
1968 : public:
1969 39385 : XmlNesting(String* s) { _s = s; }
1970 186 : void pushToken(const String& t)
1971 : {
1972 186 : *_s += u"<";
1973 186 : *_s += t;
1974 186 : *_s += u">";
1975 186 : push(t);
1976 186 : }
1977 :
1978 161 : void pushB() { pushToken(u"b"); }
1979 13 : void pushI() { pushToken(u"i"); }
1980 11 : void pushU() { pushToken(u"u"); }
1981 1 : void pushS() { pushToken(u"s"); }
1982 :
1983 186 : String popToken()
1984 : {
1985 186 : String s = top();
1986 186 : pop();
1987 186 : *_s += u"</";
1988 186 : *_s += s;
1989 186 : *_s += u">";
1990 186 : return s;
1991 0 : }
1992 :
1993 157 : void popToken(const char* t)
1994 : {
1995 157 : StringList ps;
1996 : for (;;) {
1997 157 : String s = popToken();
1998 157 : if (s == t) {
1999 157 : break;
2000 : }
2001 0 : ps << s;
2002 157 : }
2003 157 : for (const String& s : ps) {
2004 0 : pushToken(s);
2005 : }
2006 157 : }
2007 :
2008 150 : void popB() { popToken("b"); }
2009 1 : void popI() { popToken("i"); }
2010 6 : void popU() { popToken("u"); }
2011 0 : void popS() { popToken("s"); }
2012 : };
2013 :
2014 : //---------------------------------------------------------
2015 : // genText
2016 : //---------------------------------------------------------
2017 39385 : String TextBase::genText(const LayoutData* ldata) const
2018 : {
2019 39385 : if (!ldata) {
2020 0 : return String();
2021 : }
2022 :
2023 39385 : String text;
2024 39385 : XmlNesting xmlNesting(&text);
2025 :
2026 39385 : CharFormat fmt;
2027 39385 : fmt.setFontFamily(propertyDefault(Pid::FONT_FACE).value<String>());
2028 39385 : fmt.setFontSize(propertyDefault(Pid::FONT_SIZE).toReal());
2029 39385 : fmt.setStyle(static_cast<FontStyle>(propertyDefault(Pid::FONT_STYLE).toInt()));
2030 :
2031 : // Prepare the initial style tags (if any).
2032 : // We only need those if there is any fragment in the blocks with a different style than the default one.
2033 39385 : bool bold_ = false;
2034 39385 : bool italic_ = false;
2035 39385 : bool underline_ = false;
2036 39385 : bool strike_ = false;
2037 76247 : for (const TextBlock& block : ldata->blocks) {
2038 73999 : for (const TextFragment& f : block.fragments()) {
2039 37137 : if (!f.format.bold() && fmt.bold()) {
2040 0 : bold_ = true;
2041 : }
2042 37137 : if (!f.format.italic() && fmt.italic()) {
2043 1 : italic_ = true;
2044 : }
2045 37137 : if (!f.format.underline() && fmt.underline()) {
2046 0 : underline_ = true;
2047 : }
2048 37137 : if (!f.format.strike() && fmt.strike()) {
2049 0 : strike_ = true;
2050 : }
2051 : }
2052 : }
2053 39385 : if (bold_) {
2054 0 : xmlNesting.pushB();
2055 : }
2056 39385 : if (italic_) {
2057 1 : xmlNesting.pushI();
2058 : }
2059 39385 : if (underline_) {
2060 0 : xmlNesting.pushU();
2061 : }
2062 39385 : if (strike_) {
2063 0 : xmlNesting.pushS();
2064 : }
2065 :
2066 : // And here for the actual formatting of the text.
2067 76247 : for (const TextBlock& block : ldata->blocks) {
2068 73999 : for (const TextFragment& f : block.fragments()) {
2069 : // don't skip, empty text fragments hold information for empty lines
2070 : // if (f.text.isEmpty()) // skip empty fragments, not to
2071 : // continue; // insert extra HTML formatting
2072 :
2073 : // Push or Pop XML tags according to the current format changes
2074 37137 : const CharFormat& format = f.format;
2075 37137 : if (fmt.bold() != format.bold()) {
2076 311 : if (format.bold()) {
2077 161 : xmlNesting.pushB();
2078 : } else {
2079 150 : xmlNesting.popB();
2080 : }
2081 : }
2082 37137 : if (fmt.italic() != format.italic()) {
2083 13 : if (format.italic()) {
2084 12 : xmlNesting.pushI();
2085 : } else {
2086 1 : xmlNesting.popI();
2087 : }
2088 : }
2089 37137 : if (fmt.underline() != format.underline()) {
2090 17 : if (format.underline()) {
2091 11 : xmlNesting.pushU();
2092 : } else {
2093 6 : xmlNesting.popU();
2094 : }
2095 : }
2096 37137 : if (fmt.strike() != format.strike()) {
2097 1 : if (format.strike()) {
2098 1 : xmlNesting.pushS();
2099 : } else {
2100 0 : xmlNesting.popS();
2101 : }
2102 : }
2103 :
2104 37137 : if (format.fontSize() != fmt.fontSize()) {
2105 295 : text += String(u"<font size=\"%1\"/>").arg(format.fontSize());
2106 : }
2107 37137 : if (format.fontFamily() != "ScoreText" && format.fontFamily() != fmt.fontFamily()) {
2108 3 : text += String(u"<font face=\"%1\"/>").arg(TextBase::escape(format.fontFamily()));
2109 : }
2110 :
2111 37137 : VerticalAlignment va = format.valign();
2112 37137 : VerticalAlignment cva = fmt.valign();
2113 37137 : if (cva != va) {
2114 0 : switch (va) {
2115 0 : case VerticalAlignment::AlignNormal:
2116 0 : xmlNesting.popToken(cva == VerticalAlignment::AlignSuperScript ? "sup" : "sub");
2117 0 : break;
2118 0 : case VerticalAlignment::AlignSuperScript:
2119 0 : xmlNesting.pushToken(u"sup");
2120 0 : break;
2121 0 : case VerticalAlignment::AlignSubScript:
2122 0 : xmlNesting.pushToken(u"sub");
2123 0 : break;
2124 0 : case VerticalAlignment::AlignUndefined:
2125 0 : break;
2126 : }
2127 : }
2128 37137 : if (format.fontFamily() == u"ScoreText") {
2129 102 : for (size_t i = 0; i < f.text.size(); ++i) {
2130 66 : text += toSymbolXml(f.text.at(i), this);
2131 : }
2132 : } else {
2133 37101 : text += XmlWriter::xmlString(f.text);
2134 : }
2135 37137 : fmt = format;
2136 : }
2137 36862 : if (block.eol()) {
2138 88 : text += Char::LineFeed;
2139 : }
2140 : }
2141 39414 : while (!xmlNesting.empty()) {
2142 29 : xmlNesting.popToken();
2143 : }
2144 :
2145 39385 : return text;
2146 39385 : }
2147 :
2148 21200 : void TextBase::genText()
2149 : {
2150 21200 : m_text = genText(ldata());
2151 21200 : m_textInvalid = false;
2152 21200 : }
2153 :
2154 : //---------------------------------------------------------
2155 : // selectAll
2156 : //---------------------------------------------------------
2157 :
2158 265454 : void TextBase::selectAll()
2159 : {
2160 265454 : const LayoutData* ldata = this->ldata();
2161 265454 : if (!ldata || ldata->blocks.empty()) {
2162 233690 : return;
2163 : }
2164 :
2165 31764 : cursor()->setSelectColumn(0);
2166 31764 : cursor()->setSelectLine(0);
2167 31764 : cursor()->setRow(ldata->rows() - 1);
2168 31764 : cursor()->setColumn(cursor()->curLine().columns());
2169 : }
2170 :
2171 0 : void TextBase::select(SelectTextType type)
2172 : {
2173 0 : switch (type) {
2174 0 : case SelectTextType::Word:
2175 0 : cursor()->selectWord();
2176 0 : break;
2177 0 : case SelectTextType::All:
2178 0 : selectAll();
2179 0 : break;
2180 : }
2181 0 : }
2182 :
2183 : //---------------------------------------------------------
2184 : // pageRectangle
2185 : //---------------------------------------------------------
2186 :
2187 0 : RectF TextBase::pageRectangle() const
2188 : {
2189 0 : if (ownershipParent() && (ownershipParent()->isHBox() || ownershipParent()->isVBox() || ownershipParent()->isTBox())) {
2190 0 : Box* box = toBox(ownershipParent());
2191 0 : RectF r = box->pageBoundingRect();
2192 0 : double x = r.x() + box->leftMargin() * DPMM;
2193 0 : double y = r.y() + box->topMargin() * DPMM;
2194 0 : double h = r.height() - (box->topMargin() + box->bottomMargin()) * DPMM;
2195 0 : double w = r.width() - (box->leftMargin() + box->rightMargin()) * DPMM;
2196 :
2197 : // SizeF ps = _doc->pageSize();
2198 : // return RectF(x, y, ps.width(), ps.height());
2199 :
2200 0 : return RectF(x, y, w, h);
2201 : }
2202 0 : if (ownershipParent() && ownershipParent()->isPage()) {
2203 0 : Page* box = toPage(ownershipParent());
2204 0 : RectF r = box->pageBoundingRect();
2205 0 : double x = r.x() + box->lm();
2206 0 : double y = r.y() + box->tm();
2207 0 : double h = r.height() - box->tm() - box->bm();
2208 0 : double w = r.width() - box->lm() - box->rm();
2209 0 : return RectF(x, y, w, h);
2210 : }
2211 0 : return pageBoundingRect();
2212 : }
2213 :
2214 : //---------------------------------------------------------
2215 : // dragTo
2216 : //---------------------------------------------------------
2217 :
2218 0 : void TextBase::dragTo(EditData& ed)
2219 : {
2220 0 : cursor()->set(ed.pos, TextCursor::MoveMode::KeepAnchor);
2221 0 : score()->setUpdateAll();
2222 0 : score()->update();
2223 0 : }
2224 :
2225 : //---------------------------------------------------------
2226 : // dragAnchorLines
2227 : //---------------------------------------------------------
2228 :
2229 0 : std::vector<LineF> TextBase::dragAnchorLines() const
2230 : {
2231 0 : std::vector<LineF> result(genericDragAnchorLines());
2232 :
2233 0 : if (layoutToParentWidth() && !result.empty()) {
2234 0 : LineF& line = result[0];
2235 0 : line.setP2(line.p2() + ldata()->bbox().topLeft());
2236 : }
2237 :
2238 0 : return result;
2239 0 : }
2240 :
2241 : //---------------------------------------------------------
2242 : // mousePress
2243 : // set text cursor
2244 : //---------------------------------------------------------
2245 :
2246 0 : bool TextBase::mousePress(EditData& ed)
2247 : {
2248 0 : bool shift = ed.modifiers & ShiftModifier;
2249 0 : if (!cursor()->set(ed.startMove, shift ? TextCursor::MoveMode::KeepAnchor : TextCursor::MoveMode::MoveAnchor)) {
2250 0 : return false;
2251 : }
2252 :
2253 0 : score()->setUpdateAll();
2254 0 : return true;
2255 : }
2256 :
2257 : //---------------------------------------------------------
2258 : // acceptDrop
2259 : //---------------------------------------------------------
2260 :
2261 0 : bool TextBase::acceptDrop(EditData& data) const
2262 : {
2263 : // do not accept the drop if this text element is not being edited
2264 0 : ElementEditDataPtr eed = data.getData(this);
2265 0 : if (!eed || eed->type() != EditDataType::TextEditData) {
2266 0 : return false;
2267 : }
2268 0 : ElementType type = data.dropElement->type();
2269 0 : return type == ElementType::SYMBOL || type == ElementType::FSYMBOL;
2270 0 : }
2271 :
2272 : //--------------------------------------------------------
2273 : // setXmlText
2274 : //---------------------------------------------------------
2275 :
2276 30881 : void TextBase::setXmlText(const String& s)
2277 : {
2278 30881 : m_text = s;
2279 30881 : m_textInvalid = false;
2280 30881 : mutldata()->blocks.clear();
2281 30881 : mutldata()->layoutInvalid = true;
2282 30881 : }
2283 :
2284 4374 : void TextBase::checkCustomFormatting(const String& s)
2285 : {
2286 4374 : if (s.contains(u"<font size")) {
2287 56 : setPropertyFlags(Pid::FONT_SIZE, PropertyFlags::UNSTYLED);
2288 : }
2289 4374 : if (s.contains(u"<b>") || s.contains(u"<i>") || s.contains(u"<u>") || s.contains(u"<s>")) {
2290 84 : setPropertyFlags(Pid::FONT_STYLE, PropertyFlags::UNSTYLED);
2291 : }
2292 4374 : }
2293 :
2294 5 : void TextBase::resetFormatting()
2295 : {
2296 : // reset any formatting properties that can be changed per-character (doesn't change existing text)
2297 5 : cursor()->format()->setFontFamily(propertyDefault(Pid::FONT_FACE).value<String>());
2298 5 : cursor()->format()->setFontSize(propertyDefault(Pid::FONT_SIZE).toReal());
2299 5 : cursor()->format()->setStyle(static_cast<FontStyle>(propertyDefault(Pid::FONT_STYLE).toInt()));
2300 5 : cursor()->format()->setValign(VerticalAlignment::AlignNormal);
2301 5 : }
2302 :
2303 : //---------------------------------------------------------
2304 : // fragmentList
2305 : //---------------------------------------------------------
2306 :
2307 : /*
2308 : Return the text as a single list of TextFragment
2309 : Used by the MusicXML formatted export to avoid parsing the xml text format
2310 : */
2311 :
2312 751 : std::list<TextFragment> TextBase::fragmentList() const
2313 : {
2314 751 : std::list<TextFragment> res;
2315 :
2316 751 : const TextBase* text = this;
2317 751 : std::unique_ptr<TextBase> tmpText;
2318 751 : const LayoutData* ldata = this->ldata();
2319 751 : if (!ldata || ldata->layoutInvalid) {
2320 : // Create temporary text object to avoid side effects
2321 : // of createLayout() call.
2322 0 : tmpText.reset(toTextBase(this->clone()));
2323 0 : tmpText->createBlocks();
2324 0 : text = tmpText.get();
2325 : }
2326 :
2327 751 : const LayoutData* tmlldata = text->ldata();
2328 1510 : for (const TextBlock& block : tmlldata->blocks) {
2329 1630 : for (const TextFragment& f : block.fragments()) {
2330 : /* TODO TBD
2331 : if (f.text.empty()) // skip empty fragments, not to
2332 : continue; // insert extra HTML formatting
2333 : */
2334 871 : res.push_back(f);
2335 871 : if (block.eol()) {
2336 : // simply append a newline
2337 8 : res.back().text += u"\n";
2338 : }
2339 : }
2340 : }
2341 751 : return res;
2342 751 : }
2343 :
2344 : //---------------------------------------------------------
2345 : // plainText
2346 : // return plain text with symbols
2347 : //---------------------------------------------------------
2348 :
2349 6154 : String TextBase::plainText() const
2350 : {
2351 6154 : String s;
2352 :
2353 6154 : const TextBase* text = this;
2354 6154 : std::unique_ptr<TextBase> tmpText;
2355 6154 : const LayoutData* ldata = this->ldata();
2356 6154 : if (!ldata || ldata->layoutInvalid) {
2357 : // Create temporary text object to avoid side effects
2358 : // of createLayout() call.
2359 1859 : tmpText.reset(toTextBase(this->clone()));
2360 1859 : tmpText->createBlocks();
2361 1859 : text = tmpText.get();
2362 : }
2363 :
2364 6154 : const LayoutData* tmlldata = text->ldata();
2365 12339 : for (const TextBlock& block : tmlldata->blocks) {
2366 12714 : for (const TextFragment& f : block.fragments()) {
2367 6529 : s += f.text;
2368 : }
2369 6185 : if (block.eol()) {
2370 31 : s += Char::LineFeed;
2371 : }
2372 : }
2373 6154 : return s;
2374 6154 : }
2375 :
2376 : //---------------------------------------------------------
2377 : // xmlText
2378 : //---------------------------------------------------------
2379 :
2380 97741 : String TextBase::xmlText() const
2381 : {
2382 97741 : if (!m_textInvalid) {
2383 79556 : return m_text;
2384 : }
2385 :
2386 18185 : String text = genText(ldata());
2387 18185 : return text;
2388 18185 : }
2389 :
2390 : //---------------------------------------------------------
2391 : // unEscape
2392 : //---------------------------------------------------------
2393 :
2394 508 : String TextBase::unEscape(String s)
2395 : {
2396 508 : s.replace(u"<", u"<");
2397 508 : s.replace(u">", u">");
2398 508 : s.replace(u"&", u"&");
2399 508 : s.replace(u"'", u"'");
2400 508 : s.replace(u""", u"\"");
2401 508 : return s;
2402 : }
2403 :
2404 : //---------------------------------------------------------
2405 : // escape
2406 : //---------------------------------------------------------
2407 :
2408 3 : String TextBase::escape(String s)
2409 : {
2410 3 : s.replace(u"<", u"<");
2411 3 : s.replace(u">", u">");
2412 3 : s.replace(u"&", u"&");
2413 3 : s.replace(u"'", u"'");
2414 3 : s.replace(u"\"", u""");
2415 3 : return s;
2416 : }
2417 :
2418 : //---------------------------------------------------------
2419 : // accessibleInfo
2420 : //---------------------------------------------------------
2421 :
2422 0 : String TextBase::accessibleInfo() const
2423 : {
2424 0 : String rez;
2425 0 : switch (textStyleType()) {
2426 0 : case TextStyleType::TITLE:
2427 : case TextStyleType::SUBTITLE:
2428 : case TextStyleType::COMPOSER:
2429 : case TextStyleType::LYRICIST:
2430 : case TextStyleType::TRANSLATOR:
2431 : case TextStyleType::MEASURE_NUMBER:
2432 : case TextStyleType::MMREST_RANGE:
2433 0 : rez = translatedSubtypeUserName();
2434 0 : break;
2435 0 : default:
2436 0 : rez = EngravingItem::accessibleInfo();
2437 0 : break;
2438 : }
2439 0 : String s = plainText().simplified();
2440 0 : if (s.size() > 20) {
2441 0 : s.truncate(20);
2442 0 : s += u"…";
2443 : }
2444 0 : return String(u"%1: %2").arg(rez, s);
2445 0 : }
2446 :
2447 : //---------------------------------------------------------
2448 : // screenReaderInfo
2449 : //---------------------------------------------------------
2450 :
2451 0 : String TextBase::screenReaderInfo() const
2452 : {
2453 0 : String rez;
2454 :
2455 0 : switch (textStyleType()) {
2456 0 : case TextStyleType::TITLE:
2457 : case TextStyleType::SUBTITLE:
2458 : case TextStyleType::COMPOSER:
2459 : case TextStyleType::LYRICIST:
2460 : case TextStyleType::TRANSLATOR:
2461 : case TextStyleType::MEASURE_NUMBER:
2462 : case TextStyleType::MMREST_RANGE:
2463 0 : rez = translatedSubtypeUserName();
2464 0 : break;
2465 0 : default:
2466 0 : rez = EngravingItem::accessibleInfo();
2467 0 : break;
2468 : }
2469 0 : String s = plainText().simplified();
2470 0 : return String(u"%1: %2").arg(rez, s);
2471 0 : }
2472 :
2473 : //---------------------------------------------------------
2474 : // subtype
2475 : //---------------------------------------------------------
2476 :
2477 0 : int TextBase::subtype() const
2478 : {
2479 0 : return int(textStyleType());
2480 : }
2481 :
2482 : //---------------------------------------------------------
2483 : // subtypeUserName
2484 : //---------------------------------------------------------
2485 :
2486 0 : TranslatableString TextBase::subtypeUserName() const
2487 : {
2488 0 : return score() ? score()->getTextStyleUserName(textStyleType()) : TConv::userName(textStyleType());
2489 : }
2490 :
2491 : //---------------------------------------------------------
2492 : // validateText
2493 : // check if s is a valid musescore xml text string
2494 : // - simple bugs are automatically adjusted
2495 : // return true if text is valid or could be fixed
2496 : // (this is incomplete/experimental)
2497 : //---------------------------------------------------------
2498 :
2499 0 : bool TextBase::validateText(String& s)
2500 : {
2501 0 : String d;
2502 0 : for (size_t i = 0; i < s.size(); ++i) {
2503 0 : Char c = s.at(i);
2504 0 : if (c == u'&') {
2505 0 : const char16_t* ok[] { u"amp;", u"lt;", u"gt;", u"quot;" };
2506 0 : String t = s.mid(i + 1);
2507 0 : bool found = false;
2508 0 : for (auto k : ok) {
2509 0 : if (t.startsWith(k)) {
2510 0 : d.append(c);
2511 0 : d.append(k);
2512 0 : i += int(std::u16string_view(k).size());
2513 0 : found = true;
2514 0 : break;
2515 : }
2516 : }
2517 0 : if (!found) {
2518 0 : d.append(u"&");
2519 : }
2520 0 : } else if (c == u'<') {
2521 0 : const char16_t* ok[] { u"b>", u"/b>", u"i>", u"/i>", u"u>", u"/u", u"s>", u"/s>", u"font ", u"/font>", u"sym>", u"/sym>",
2522 : u"sub>", u"/sub>", u"sup>", u"/sup>" };
2523 0 : String t = s.mid(i + 1);
2524 0 : bool found = false;
2525 0 : for (auto k : ok) {
2526 0 : if (t.startsWith(k)) {
2527 0 : d.append(c);
2528 0 : d.append(k);
2529 0 : i += int(std::u16string_view(k).size());
2530 0 : found = true;
2531 0 : break;
2532 : }
2533 : }
2534 0 : if (!found) {
2535 0 : d.append(u"<");
2536 : }
2537 0 : } else {
2538 0 : d.append(c);
2539 : }
2540 : }
2541 :
2542 0 : String ss = u"<data>" + d + u"</data>\n";
2543 0 : muse::ByteArray ba = ss.toUtf8();
2544 0 : XmlReader xml(ba);
2545 0 : while (xml.readNextStartElement()) {
2546 : // LOGD(" token %d <%s>", int(xml.tokenType()), muPrintable(xml.name().toString()));
2547 : }
2548 0 : if (xml.error() == XmlReader::NoError) {
2549 0 : s = d;
2550 0 : return true;
2551 : }
2552 0 : LOGD() << "xml error at byte offset " << xml.byteOffset()
2553 0 : << ": " << xml.errorString();
2554 0 : LOGD() << "text: |" << ss << "|";
2555 0 : return false;
2556 0 : }
2557 :
2558 : //---------------------------------------------------------
2559 : // font
2560 : //---------------------------------------------------------
2561 :
2562 27173 : Font TextBase::font() const
2563 : {
2564 27173 : double m = size();
2565 27173 : if (sizeIsSpatiumDependent()) {
2566 27103 : m *= spatium() / defaultSpatium();
2567 : }
2568 27173 : Font f(family(), Font::Type::Unknown);
2569 27173 : f.setPointSizeF(m);
2570 27173 : f.setBold(bold());
2571 27173 : f.setItalic(italic());
2572 27173 : if (underline()) {
2573 0 : f.setUnderline(underline());
2574 : }
2575 27173 : if (strike()) {
2576 0 : f.setStrike(strike());
2577 : }
2578 :
2579 27173 : return f;
2580 0 : }
2581 :
2582 : //---------------------------------------------------------
2583 : // fontMetrics
2584 : //---------------------------------------------------------
2585 :
2586 6612 : FontMetrics TextBase::fontMetrics() const
2587 : {
2588 6612 : return FontMetrics(font());
2589 : }
2590 :
2591 2 : bool TextBase::isPropertyLinkedToMaster(Pid id) const
2592 : {
2593 2 : if (propertyGroup(id) == PropertyGroup::TEXT) {
2594 2 : return isTextLinkedToMaster();
2595 : }
2596 :
2597 0 : return EngravingItem::isPropertyLinkedToMaster(id);
2598 : }
2599 :
2600 0 : bool TextBase::isUnlinkedFromMaster() const
2601 : {
2602 0 : EngravingItem* parent = ownershipParentItem();
2603 0 : if (parent && parent->isUnlinkedFromMaster()) {
2604 0 : return true;
2605 : }
2606 :
2607 0 : return !getProperty(Pid::TEXT_LINKED_TO_MASTER).toBool() || EngravingItem::isUnlinkedFromMaster();
2608 : }
2609 :
2610 : //---------------------------------------------------------
2611 : // getProperty
2612 : //---------------------------------------------------------
2613 :
2614 72094 : PropertyValue TextBase::getProperty(Pid propertyId) const
2615 : {
2616 72094 : switch (propertyId) {
2617 4919 : case Pid::TEXT_STYLE:
2618 4919 : return textStyleType();
2619 576 : case Pid::FONT_FACE:
2620 576 : return m_cursor->selectedFragmentsFormat().fontFamily();
2621 560 : case Pid::FONT_SIZE:
2622 560 : return m_cursor->selectedFragmentsFormat().fontSize();
2623 567 : case Pid::FONT_STYLE:
2624 567 : return static_cast<int>(m_cursor->selectedFragmentsFormat().style());
2625 576 : case Pid::TEXT_LINE_SPACING:
2626 576 : return textLineSpacing();
2627 493 : case Pid::FRAME_TYPE:
2628 493 : return static_cast<int>(frameType());
2629 475 : case Pid::FRAME_WIDTH:
2630 475 : return frameWidth();
2631 475 : case Pid::FRAME_PADDING:
2632 475 : return paddingWidth();
2633 475 : case Pid::FRAME_ROUND:
2634 475 : return frameRound();
2635 475 : case Pid::FRAME_FG_COLOR:
2636 475 : return PropertyValue::fromValue(frameColor());
2637 475 : case Pid::FRAME_BG_COLOR:
2638 475 : return PropertyValue::fromValue(bgColor());
2639 710 : case Pid::ALIGN:
2640 710 : return PropertyValue::fromValue(align());
2641 2130 : case Pid::POSITION:
2642 2130 : return PropertyValue::fromValue(position());
2643 2 : case Pid::TEXT_SCRIPT_ALIGN:
2644 2 : return static_cast<int>(m_cursor->selectedFragmentsFormat().valign());
2645 794 : case Pid::TEXT:
2646 794 : return xmlText();
2647 4326 : case Pid::TEXT_LINKED_TO_MASTER:
2648 4326 : return isTextLinkedToMaster();
2649 1883 : case Pid::DIRECTION:
2650 1883 : return direction();
2651 2763 : case Pid::CENTER_BETWEEN_STAVES:
2652 2763 : return centerBetweenStaves();
2653 2383 : case Pid::VOICE_ASSIGNMENT:
2654 2383 : return voiceAssignment();
2655 4264 : case Pid::MUSIC_SYMBOL_SIZE:
2656 4264 : return symbolSize();
2657 3947 : case Pid::MUSICAL_SYMBOLS_SCALE:
2658 3947 : return symbolScale();
2659 38826 : default:
2660 38826 : return EngravingItem::getProperty(propertyId);
2661 : }
2662 : }
2663 :
2664 : //---------------------------------------------------------
2665 : // setProperty
2666 : //---------------------------------------------------------
2667 :
2668 714116 : bool TextBase::setProperty(Pid pid, const PropertyValue& v)
2669 : {
2670 714116 : if (m_textInvalid && ldata() && ldata()->isValid()) {
2671 15875 : genText();
2672 : }
2673 :
2674 714116 : bool rv = true;
2675 714116 : switch (pid) {
2676 42058 : case Pid::COLOR:
2677 42058 : if (color() == frameColor()) {
2678 42058 : setFrameColor(v.value<Color>());
2679 : }
2680 42058 : EngravingItem::setProperty(pid, v);
2681 42058 : break;
2682 2882 : case Pid::TEXT_STYLE:
2683 2882 : initTextStyleType(v.value<TextStyleType>());
2684 2882 : break;
2685 42928 : case Pid::FONT_FACE:
2686 42928 : setFamily(v.value<String>());
2687 42928 : break;
2688 42975 : case Pid::FONT_SIZE:
2689 42975 : setSize(v.toReal());
2690 42975 : break;
2691 42921 : case Pid::FONT_STYLE:
2692 42921 : setFontStyle(FontStyle(v.toInt()));
2693 42921 : break;
2694 42039 : case Pid::TEXT_LINE_SPACING:
2695 42039 : setTextLineSpacing(v.toReal());
2696 42039 : break;
2697 42044 : case Pid::FRAME_TYPE:
2698 42044 : setFrameType(FrameType(v.toInt()));
2699 42044 : break;
2700 42039 : case Pid::FRAME_WIDTH:
2701 42039 : setFrameWidth(v.value<Spatium>());
2702 42039 : break;
2703 42039 : case Pid::FRAME_PADDING:
2704 42039 : setPaddingWidth(v.value<Spatium>());
2705 42039 : break;
2706 42039 : case Pid::FRAME_ROUND:
2707 42039 : setFrameRound(v.value<Spatium>());
2708 42039 : break;
2709 42036 : case Pid::FRAME_FG_COLOR:
2710 42036 : setFrameColor(v.value<Color>());
2711 42036 : break;
2712 42039 : case Pid::FRAME_BG_COLOR:
2713 42039 : setBgColor(v.value<Color>());
2714 42039 : break;
2715 77 : case Pid::TEXT:
2716 77 : setXmlText(v.value<String>());
2717 77 : break;
2718 42538 : case Pid::ALIGN:
2719 42538 : setAlign(v.value<Align>());
2720 42538 : break;
2721 42650 : case Pid::POSITION:
2722 42650 : setPosition(v.value<AlignH>());
2723 42650 : break;
2724 0 : case Pid::TEXT_SCRIPT_ALIGN:
2725 0 : m_cursor->setFormat(FormatId::Valign, v.toInt());
2726 0 : break;
2727 0 : case Pid::TEXT_LINKED_TO_MASTER:
2728 0 : if (isTextLinkedToMaster() == v.toBool()) {
2729 0 : break;
2730 : }
2731 0 : if (!isTextLinkedToMaster()) {
2732 0 : relinkPropertiesToMaster(PropertyGroup::TEXT);
2733 : }
2734 0 : setTextLinkedToMaster(v.toBool());
2735 0 : break;
2736 147 : case Pid::DIRECTION:
2737 147 : setDirection(v.value<DirectionV>());
2738 147 : break;
2739 52 : case Pid::CENTER_BETWEEN_STAVES:
2740 52 : setCenterBetweenStaves(v.value<AutoOnOff>());
2741 52 : break;
2742 13 : case Pid::VOICE_ASSIGNMENT:
2743 13 : setVoiceAssignment(v.value<VoiceAssignment>());
2744 13 : break;
2745 44179 : case Pid::MUSIC_SYMBOL_SIZE:
2746 44179 : setSymbolSize(v.toDouble());
2747 44179 : break;
2748 42044 : case Pid::MUSICAL_SYMBOLS_SCALE:
2749 42044 : setSymbolScale(v.toDouble());
2750 42044 : break;
2751 74377 : default:
2752 74377 : rv = EngravingItem::setProperty(pid, v);
2753 74377 : break;
2754 : }
2755 :
2756 714116 : triggerLayout();
2757 :
2758 714116 : return rv;
2759 : }
2760 :
2761 : //---------------------------------------------------------
2762 : // propertyDefault
2763 : //---------------------------------------------------------
2764 :
2765 292783 : PropertyValue TextBase::propertyDefault(Pid id) const
2766 : {
2767 292783 : if (id == Pid::Z) {
2768 4642 : return EngravingItem::propertyDefault(id);
2769 : }
2770 :
2771 288141 : if (composition()) {
2772 5537 : PropertyValue v = ownershipParent()->propertyDefault(id);
2773 5537 : if (v.isValid()) {
2774 5537 : return v;
2775 : }
2776 5537 : }
2777 :
2778 282604 : Sid sid = getPropertyStyle(id);
2779 282604 : if (sid != Sid::NOSTYLE) {
2780 217036 : return styleValue(id, sid);
2781 : }
2782 :
2783 65568 : switch (id) {
2784 233 : case Pid::TEXT_STYLE:
2785 233 : return TextStyleType::DEFAULT;
2786 51 : case Pid::TEXT:
2787 102 : return String();
2788 28330 : case Pid::TEXT_SCRIPT_ALIGN:
2789 28330 : return static_cast<int>(VerticalAlignment::AlignNormal);
2790 4326 : case Pid::TEXT_LINKED_TO_MASTER:
2791 4326 : return true;
2792 601 : case Pid::DIRECTION:
2793 601 : return DirectionV::AUTO;
2794 1044 : case Pid::CENTER_BETWEEN_STAVES:
2795 2088 : return AutoOnOff::AUTO;
2796 601 : case Pid::VOICE_ASSIGNMENT:
2797 1202 : return VoiceAssignment::ALL_VOICE_IN_INSTRUMENT;
2798 30382 : default:
2799 513820 : for (const auto& p : *textStyle(TextStyleType::DEFAULT)) {
2800 483642 : if (p.pid == id) {
2801 204 : return styleValue(id, p.sid);
2802 : }
2803 : }
2804 : }
2805 :
2806 30178 : return EngravingItem::propertyDefault(id);
2807 : }
2808 :
2809 : //---------------------------------------------------------
2810 : // getPropertyFlagsIdx
2811 : //---------------------------------------------------------
2812 :
2813 190201 : int TextBase::getPropertyFlagsIdx(Pid id) const
2814 : {
2815 190201 : int i = 0;
2816 626498 : for (const StyledProperty& p : *m_elementStyle) {
2817 453986 : if (p.pid == id) {
2818 17689 : return i;
2819 : }
2820 436297 : ++i;
2821 : }
2822 1953739 : for (const auto& p : *textStyle(textStyleType())) {
2823 1889926 : if (p.pid == id) {
2824 108699 : return i;
2825 : }
2826 1781227 : ++i;
2827 : }
2828 63813 : return -1;
2829 : }
2830 :
2831 : //---------------------------------------------------------
2832 : // offsetSid
2833 : //---------------------------------------------------------
2834 :
2835 73763 : Sid TextBase::defaultPosSid() const
2836 : {
2837 73763 : const OffsetSids offsets = textStyle(textStyleType())->offsetSids;
2838 147526 : return placeAbove() ? offsets.above : offsets.below;
2839 : }
2840 :
2841 76276 : PointF TextBase::defaultPos() const
2842 : {
2843 76276 : if (parent()->isTextLineBaseSegment()) {
2844 2513 : return PointF();
2845 : }
2846 :
2847 73763 : return EngravingItem::defaultPos();
2848 : }
2849 :
2850 : //---------------------------------------------------------
2851 : // getHtmlStartTag - helper function for extractText with withFormat = true
2852 : //---------------------------------------------------------
2853 0 : String TextBase::getHtmlStartTag(double newSize, double& curSize, const String& newFamily, String& curFamily, FontStyle style,
2854 : VerticalAlignment vAlign)
2855 : {
2856 0 : String s;
2857 0 : if (std::fabs(newSize - curSize) > 0.1) {
2858 0 : curSize = newSize;
2859 0 : s += String(u"<font size=\"%1\"/>").arg(newSize);
2860 : }
2861 0 : if (newFamily != curFamily && newFamily != "ScoreText") {
2862 0 : curFamily = newFamily;
2863 0 : s += String(u"<font face=\"%1\"/>").arg(newFamily);
2864 : }
2865 0 : if (style & FontStyle::Bold) {
2866 0 : s += u"<b>";
2867 : }
2868 0 : if (style & FontStyle::Italic) {
2869 0 : s += u"<i>";
2870 : }
2871 0 : if (style & FontStyle::Underline) {
2872 0 : s += u"<u>";
2873 : }
2874 0 : if (style & mu::engraving::FontStyle::Strike) {
2875 0 : s += u"<s>";
2876 : }
2877 0 : if (vAlign == VerticalAlignment::AlignSubScript) {
2878 0 : s += u"<sub>";
2879 0 : } else if (vAlign == VerticalAlignment::AlignSuperScript) {
2880 0 : s += u"<sup>";
2881 : }
2882 0 : return s;
2883 0 : }
2884 :
2885 : //---------------------------------------------------------
2886 : // getHtmlEndTag - helper function for extractText with withFormat = true
2887 : //---------------------------------------------------------
2888 0 : String TextBase::getHtmlEndTag(FontStyle style, VerticalAlignment vAlign)
2889 : {
2890 0 : String s;
2891 0 : if (vAlign == VerticalAlignment::AlignSubScript) {
2892 0 : s += u"</sub>";
2893 0 : } else if (vAlign == VerticalAlignment::AlignSuperScript) {
2894 0 : s += u"</sup>";
2895 : }
2896 0 : if (style & FontStyle::Strike) {
2897 0 : s += u"</s>";
2898 : }
2899 0 : if (style & FontStyle::Underline) {
2900 0 : s += u"</u>";
2901 : }
2902 0 : if (style & FontStyle::Italic) {
2903 0 : s += u"</i>";
2904 : }
2905 0 : if (style & FontStyle::Bold) {
2906 0 : s += u"</b>";
2907 : }
2908 0 : return s;
2909 0 : }
2910 :
2911 : #ifndef ENGRAVING_NO_ACCESSIBILITY
2912 0 : AccessibleItemPtr TextBase::createAccessible()
2913 : {
2914 0 : return std::make_shared<AccessibleItem>(this, AccessibleItem::EditableText);
2915 : }
2916 :
2917 : #endif
2918 :
2919 0 : void TextBase::notifyAboutTextCursorChanged()
2920 : {
2921 : #ifndef ENGRAVING_NO_ACCESSIBILITY
2922 : using namespace muse::accessibility;
2923 0 : if (accessible()) {
2924 0 : accessible()->accessiblePropertyChanged().send(IAccessible::Property::TextCursor, muse::Val());
2925 : }
2926 : #endif
2927 0 : }
2928 :
2929 1 : void TextBase::notifyAboutTextInserted(int startPosition, int endPosition, const String& text)
2930 : {
2931 : #ifndef ENGRAVING_NO_ACCESSIBILITY
2932 : using namespace muse::accessibility;
2933 1 : if (accessible()) {
2934 0 : auto range = IAccessible::TextRange(startPosition, endPosition, text);
2935 0 : accessible()->accessiblePropertyChanged().send(IAccessible::Property::TextInsert, muse::Val::fromQVariant(range.toMap()));
2936 0 : }
2937 : #else
2938 : UNUSED(startPosition);
2939 : UNUSED(endPosition);
2940 : UNUSED(text);
2941 : #endif
2942 1 : }
2943 :
2944 0 : void TextBase::notifyAboutTextRemoved(int startPosition, int endPosition, const String& text)
2945 : {
2946 : #ifndef ENGRAVING_NO_ACCESSIBILITY
2947 : using namespace muse::accessibility;
2948 0 : if (accessible()) {
2949 0 : auto range = IAccessible::TextRange(startPosition, endPosition, text);
2950 0 : accessible()->accessiblePropertyChanged().send(IAccessible::Property::TextRemove, muse::Val::fromQVariant(range.toMap()));
2951 0 : }
2952 : #else
2953 : UNUSED(startPosition);
2954 : UNUSED(endPosition);
2955 : UNUSED(text);
2956 : #endif
2957 0 : }
2958 :
2959 : //---------------------------------------------------------
2960 : // getPropertyStyle
2961 : //---------------------------------------------------------
2962 :
2963 295713 : Sid TextBase::getPropertyStyle(Pid id) const
2964 : {
2965 500188 : for (const StyledProperty& p : *m_elementStyle) {
2966 206035 : if (p.pid == id) {
2967 1560 : return p.sid;
2968 : }
2969 : }
2970 1902139 : for (const auto& p : *textStyle(textStyleType())) {
2971 1835081 : if (p.pid == id) {
2972 227095 : return p.sid;
2973 : }
2974 : }
2975 67058 : return Sid::NOSTYLE;
2976 : }
2977 :
2978 : //---------------------------------------------------------
2979 : // styleChanged
2980 : //---------------------------------------------------------
2981 :
2982 659 : void TextBase::styleChanged()
2983 : {
2984 659 : if (!styledProperties()) {
2985 0 : LOGD("no styled properties");
2986 0 : return;
2987 : }
2988 659 : int i = 0;
2989 1977 : for (const StyledProperty& spp : *m_elementStyle) {
2990 1318 : PropertyFlags f = m_propertyFlagsList[i];
2991 1318 : if (f == PropertyFlags::STYLED) {
2992 1317 : setProperty(spp.pid, styleValue(spp.pid, getPropertyStyle(spp.pid)));
2993 : }
2994 1318 : ++i;
2995 : }
2996 11203 : for (const auto& spp : *textStyle(textStyleType())) {
2997 10544 : PropertyFlags f = m_propertyFlagsList[i];
2998 10544 : if (f == PropertyFlags::STYLED) {
2999 10512 : setProperty(spp.pid, styleValue(spp.pid, getPropertyStyle(spp.pid)));
3000 : }
3001 10544 : ++i;
3002 : }
3003 : }
3004 :
3005 : //---------------------------------------------------------
3006 : // initElementStyle
3007 : //---------------------------------------------------------
3008 :
3009 36351 : void TextBase::initElementStyle(const ElementStyle* ss)
3010 : {
3011 36351 : m_elementStyle = ss;
3012 36351 : size_t n = ss->size() + TEXT_STYLE_SIZE;
3013 :
3014 36351 : delete[] m_propertyFlagsList;
3015 36351 : m_propertyFlagsList = new PropertyFlags[n];
3016 659030 : for (size_t i = 0; i < n; ++i) {
3017 622679 : m_propertyFlagsList[i] = PropertyFlags::STYLED;
3018 : }
3019 77414 : for (const StyledProperty& p : *m_elementStyle) {
3020 41063 : setProperty(p.pid, styleValue(p.pid, p.sid));
3021 : }
3022 617967 : for (const auto& p : *textStyle(textStyleType())) {
3023 581616 : setProperty(p.pid, styleValue(p.pid, p.sid));
3024 : }
3025 36351 : }
3026 :
3027 : //---------------------------------------------------------
3028 : // initTid
3029 : //---------------------------------------------------------
3030 :
3031 473 : void TextBase::initTextStyleType(TextStyleType tid, bool preserveDifferent)
3032 : {
3033 473 : if (!preserveDifferent) {
3034 0 : initTextStyleType(tid);
3035 0 : return;
3036 : }
3037 :
3038 473 : const LayoutData* ldata = this->ldata();
3039 473 : if (!ldata || ldata->layoutInvalid) {
3040 473 : createBlocks();
3041 : }
3042 :
3043 : // Before setting the new style - check if any fragments contain custom formatting. If they do, preserve
3044 : // the old values for face, size, and style...
3045 473 : const bool hadCustomFragments = hasCustomFormatting();
3046 :
3047 473 : setTextStyleType(tid);
3048 :
3049 8041 : for (const auto& p : *textStyle(tid)) {
3050 7568 : const bool isFragmentStyle = p.pid == Pid::FONT_FACE || p.pid == Pid::FONT_SIZE || p.pid == Pid::FONT_STYLE;
3051 7568 : if (isFragmentStyle && hadCustomFragments) {
3052 66 : continue;
3053 : }
3054 7502 : if (getProperty(p.pid) == propertyDefault(p.pid)) {
3055 7494 : setProperty(p.pid, styleValue(p.pid, p.sid));
3056 : }
3057 : }
3058 : }
3059 :
3060 4558 : void TextBase::initTextStyleType(TextStyleType tid)
3061 : {
3062 72928 : auto getTextPID = [&](Pid p) -> Pid {
3063 : static const std::vector<std::pair<Pid, Pid> > TEXT_LINE_PID_MAP = { { Pid::FONT_FACE, Pid::BEGIN_FONT_FACE },
3064 : { Pid::FONT_SIZE, Pid::BEGIN_FONT_SIZE },
3065 : { Pid::FONT_STYLE, Pid::BEGIN_FONT_STYLE },
3066 : { Pid::ALIGN, Pid::BEGIN_TEXT_ALIGN },
3067 : { Pid::POSITION, Pid::BEGIN_TEXT_POSITION },
3068 72954 : };
3069 :
3070 72928 : const bool isTextLine = parent()->isTextLineBaseSegment();
3071 369214 : for (const auto& pidPair : TEXT_LINE_PID_MAP) {
3072 319060 : const Pid textPid = pidPair.first;
3073 319060 : const Pid textLinePid = pidPair.second;
3074 :
3075 319060 : if (p == textLinePid || p == textPid) {
3076 22774 : return isTextLine ? textLinePid : textPid;
3077 : }
3078 : }
3079 :
3080 50154 : return p;
3081 4558 : };
3082 :
3083 4558 : setTextStyleType(tid);
3084 77486 : for (const auto& p : *textStyle(tid)) {
3085 72928 : setProperty(getTextPID(p.pid), styleValue(p.pid, p.sid));
3086 : }
3087 4558 : }
3088 :
3089 0 : RectF TextBase::drag(EditData& ed)
3090 : {
3091 0 : RectF result = EngravingItem::drag(ed);
3092 :
3093 0 : MoveElementAnchors::moveElementAnchorsOnDrag(this, ed);
3094 :
3095 0 : return result;
3096 : }
3097 :
3098 0 : void TextBase::endDrag(EditData& ed)
3099 : {
3100 0 : if (m_cursor->editing()) {
3101 0 : return;
3102 : }
3103 0 : EngravingItem::endDrag(ed);
3104 : }
3105 :
3106 : //---------------------------------------------------------
3107 : // editCut
3108 : //---------------------------------------------------------
3109 :
3110 0 : void TextBase::editCut(EditData& ed)
3111 : {
3112 0 : TextEditData* ted = static_cast<TextEditData*>(ed.getData(this).get());
3113 0 : String s = cursor()->selectedText(true);
3114 :
3115 0 : if (!s.isEmpty()) {
3116 0 : ted->selectedText = cursor()->selectedText(true);
3117 0 : ed.curGrip = Grip::START;
3118 0 : ed.key = Key_Delete;
3119 0 : ed.s = String();
3120 0 : edit(ed);
3121 : }
3122 0 : }
3123 :
3124 : //---------------------------------------------------------
3125 : // editCopy
3126 : //---------------------------------------------------------
3127 :
3128 0 : void TextBase::editCopy(EditData& ed)
3129 : {
3130 : //
3131 : // store selection as rich and plain text
3132 : //
3133 0 : TextEditData* ted = static_cast<TextEditData*>(ed.getData(this).get());
3134 0 : ted->selectedText = cursor()->selectedText(true);
3135 0 : ted->selectedPlainText = cursor()->selectedText(false);
3136 0 : }
3137 :
3138 0 : bool TextBase::nudge(const EditData& ed)
3139 : {
3140 0 : bool ctrlMod = ed.modifiers & ControlModifier;
3141 0 : double step = spatium() * (ctrlMod ? MScore::nudgeStep10 : MScore::nudgeStep);
3142 0 : PointF addOffset = PointF();
3143 0 : switch (ed.key) {
3144 0 : case Key_Up:
3145 0 : addOffset = PointF(0.0, -step);
3146 0 : break;
3147 0 : case Key_Down:
3148 0 : addOffset = PointF(0.0, step);
3149 0 : break;
3150 0 : case Key_Left:
3151 0 : addOffset = PointF(-step, 0.0);
3152 0 : break;
3153 0 : case Key_Right:
3154 0 : addOffset = PointF(step, 0.0);
3155 0 : break;
3156 0 : default:
3157 0 : return false;
3158 : }
3159 0 : undoChangeProperty(Pid::OFFSET, offset() + addOffset, PropertyFlags::UNSTYLED);
3160 0 : return true;
3161 : }
3162 :
3163 : //---------------------------------------------------------
3164 : // hasCustomFormatting
3165 : //---------------------------------------------------------
3166 :
3167 473 : bool TextBase::hasCustomFormatting() const
3168 : {
3169 473 : const LayoutData* ldata = this->ldata();
3170 473 : IF_ASSERT_FAILED(ldata) {
3171 0 : return false;
3172 : }
3173 :
3174 473 : CharFormat fmt;
3175 473 : fmt.setFontFamily(family());
3176 473 : fmt.setFontSize(size());
3177 473 : fmt.setStyle(fontStyle());
3178 473 : fmt.setValign(VerticalAlignment::AlignNormal);
3179 :
3180 924 : for (const TextBlock& block : ldata->blocks) {
3181 939 : for (const TextFragment& f : block.fragments()) {
3182 488 : if (f.text.isEmpty()) { // skip empty fragments, not to
3183 0 : continue; // insert extra HTML formatting
3184 : }
3185 488 : const CharFormat& format = f.format;
3186 488 : if (fmt.style() != format.style()) {
3187 22 : return true;
3188 : }
3189 487 : if (format.fontSize() != fmt.fontSize()) {
3190 0 : return true;
3191 : }
3192 487 : if (format.fontFamily() != fmt.fontFamily()) {
3193 21 : return true;
3194 : }
3195 :
3196 466 : VerticalAlignment va = format.valign();
3197 466 : VerticalAlignment cva = fmt.valign();
3198 466 : if (cva != va) {
3199 0 : return true;
3200 : }
3201 : }
3202 : }
3203 451 : return false;
3204 473 : }
3205 :
3206 : //---------------------------------------------------------
3207 : // stripText
3208 : // remove some custom text formatting and return
3209 : // result as xml string
3210 : //---------------------------------------------------------
3211 :
3212 0 : String TextBase::stripText(bool removeStyle, bool removeSize, bool removeFace) const
3213 : {
3214 0 : const LayoutData* ldata = this->ldata();
3215 0 : IF_ASSERT_FAILED(ldata) {
3216 0 : return String();
3217 : }
3218 :
3219 0 : String _txt;
3220 0 : bool bold_ = false;
3221 0 : bool italic_ = false;
3222 0 : bool underline_ = false;
3223 0 : bool strike_ = false;
3224 :
3225 0 : for (const TextBlock& block : ldata->blocks) {
3226 0 : for (const TextFragment& f : block.fragments()) {
3227 0 : if (!f.format.bold() && bold()) {
3228 0 : bold_ = true;
3229 : }
3230 0 : if (!f.format.italic() && italic()) {
3231 0 : italic_ = true;
3232 : }
3233 0 : if (!f.format.underline() && underline()) {
3234 0 : underline_ = true;
3235 : }
3236 0 : if (!f.format.strike() && strike()) {
3237 0 : strike_ = true;
3238 : }
3239 : }
3240 : }
3241 0 : CharFormat fmt;
3242 0 : fmt.setFontFamily(family());
3243 0 : fmt.setFontSize(size());
3244 0 : fmt.setStyle(fontStyle());
3245 0 : fmt.setValign(VerticalAlignment::AlignNormal);
3246 :
3247 0 : XmlNesting xmlNesting(&_txt);
3248 0 : if (!removeStyle) {
3249 0 : if (bold_) {
3250 0 : xmlNesting.pushB();
3251 : }
3252 0 : if (italic_) {
3253 0 : xmlNesting.pushI();
3254 : }
3255 0 : if (underline_) {
3256 0 : xmlNesting.pushU();
3257 : }
3258 0 : if (strike_) {
3259 0 : xmlNesting.pushS();
3260 : }
3261 : }
3262 :
3263 0 : for (const TextBlock& block : ldata->blocks) {
3264 0 : for (const TextFragment& f : block.fragments()) {
3265 0 : if (f.text.isEmpty()) { // skip empty fragments, not to
3266 0 : continue; // insert extra HTML formatting
3267 : }
3268 0 : const CharFormat& format = f.format;
3269 0 : if (!removeStyle) {
3270 0 : if (fmt.bold() != format.bold()) {
3271 0 : if (format.bold()) {
3272 0 : xmlNesting.pushB();
3273 : } else {
3274 0 : xmlNesting.popB();
3275 : }
3276 : }
3277 0 : if (fmt.italic() != format.italic()) {
3278 0 : if (format.italic()) {
3279 0 : xmlNesting.pushI();
3280 : } else {
3281 0 : xmlNesting.popI();
3282 : }
3283 : }
3284 0 : if (fmt.underline() != format.underline()) {
3285 0 : if (format.underline()) {
3286 0 : xmlNesting.pushU();
3287 : } else {
3288 0 : xmlNesting.popU();
3289 : }
3290 : }
3291 0 : if (fmt.strike() != format.strike()) {
3292 0 : if (format.strike()) {
3293 0 : xmlNesting.pushS();
3294 : } else {
3295 0 : xmlNesting.popS();
3296 : }
3297 : }
3298 : }
3299 :
3300 0 : if (!removeSize && (format.fontSize() != fmt.fontSize())) {
3301 0 : _txt += String(u"<font size=\"%1\"/>").arg(format.fontSize());
3302 : }
3303 0 : if (!removeFace && (format.fontFamily() != fmt.fontFamily())) {
3304 0 : _txt += String(u"<font face=\"%1\"/>").arg(TextBase::escape(format.fontFamily()));
3305 : }
3306 :
3307 0 : VerticalAlignment va = format.valign();
3308 0 : VerticalAlignment cva = fmt.valign();
3309 0 : if (cva != va) {
3310 0 : switch (va) {
3311 0 : case VerticalAlignment::AlignNormal:
3312 0 : xmlNesting.popToken(cva == VerticalAlignment::AlignSuperScript ? "sup" : "sub");
3313 0 : break;
3314 0 : case VerticalAlignment::AlignSuperScript:
3315 0 : xmlNesting.pushToken(u"sup");
3316 0 : break;
3317 0 : case VerticalAlignment::AlignSubScript:
3318 0 : xmlNesting.pushToken(u"sub");
3319 0 : break;
3320 0 : case VerticalAlignment::AlignUndefined:
3321 0 : break;
3322 : }
3323 : }
3324 0 : _txt += XmlWriter::xmlString(f.text);
3325 0 : fmt = format;
3326 : }
3327 0 : if (block.eol()) {
3328 0 : _txt += Char::LineFeed;
3329 : }
3330 : }
3331 0 : while (!xmlNesting.empty()) {
3332 0 : xmlNesting.popToken();
3333 : }
3334 0 : return _txt;
3335 0 : }
3336 :
3337 : //---------------------------------------------------------
3338 : // undoChangeProperty
3339 : //---------------------------------------------------------
3340 :
3341 262 : void TextBase::undoChangeProperty(Pid id, const PropertyValue& v, PropertyFlags ps)
3342 : {
3343 262 : if (ps == PropertyFlags::STYLED && v == propertyDefault(id)) {
3344 : // this is a reset
3345 : // remove some custom formatting
3346 99 : if (id == Pid::FONT_STYLE) {
3347 0 : undoChangeProperty(Pid::TEXT, stripText(true, false, false), propertyFlags(id));
3348 99 : } else if (id == Pid::FONT_SIZE) {
3349 0 : undoChangeProperty(Pid::TEXT, stripText(false, true, false), propertyFlags(id));
3350 99 : } else if (id == Pid::FONT_FACE) {
3351 0 : undoChangeProperty(Pid::TEXT, stripText(false, false, true), propertyFlags(id));
3352 : }
3353 : }
3354 :
3355 : static const PropertyIdSet CHARACTER_SPECIFIC_PROPERTIES {
3356 : Pid::FONT_STYLE,
3357 : Pid::FONT_FACE,
3358 : Pid::FONT_SIZE,
3359 : Pid::TEXT_SCRIPT_ALIGN
3360 270 : };
3361 :
3362 262 : if (!muse::contains(CHARACTER_SPECIFIC_PROPERTIES, id)) {
3363 256 : EngravingItem::undoChangeProperty(id, v, ps);
3364 256 : return;
3365 : }
3366 :
3367 6 : score()->undo(new ChangeTextProperties(m_cursor, id, v, ps));
3368 :
3369 6 : if (m_cursor->editing()) {
3370 : // If we're in edit mode, changes will be propagated later when
3371 : // propagating the Pid::TEXT property, so don't propagate now.
3372 0 : return;
3373 : }
3374 :
3375 6 : const std::list<EngravingObject*> linkedObjects = linkListForPropertyPropagation();
3376 12 : for (EngravingObject* linkedObject : linkedObjects) {
3377 6 : TextBase* linkedText = toTextBase(linkedObject);
3378 6 : if (linkedText == this) {
3379 6 : continue;
3380 : }
3381 :
3382 0 : Score* linkedScore = linkedText->score();
3383 0 : TextCursor* linkedCursor = linkedText->cursor();
3384 0 : PropertyPropagation propertyPropagate = propertyPropagation(linkedText, id);
3385 :
3386 0 : switch (propertyPropagate) {
3387 0 : case PropertyPropagation::PROPAGATE:
3388 0 : linkedScore->undo(new ChangeTextProperties(linkedCursor, id, v, ps));
3389 0 : break;
3390 0 : case PropertyPropagation::UNLINK:
3391 0 : unlinkPropertyFromMaster(id);
3392 0 : break;
3393 0 : default:
3394 0 : break;
3395 : }
3396 : }
3397 :
3398 6 : for (EngravingItem* originItem : originItems()) {
3399 : // This is a shared item: propagate to all origin items
3400 0 : score()->undo(new ChangeTextProperties(toTextBase(originItem)->cursor(), id, v, ps));
3401 : }
3402 6 : if (EngravingItem* sharedEl = sharedItem(); sharedEl && sharedEl->originItems().front() == this) {
3403 : // This is the first origin item of the shared item: propagate to shared item
3404 0 : score()->undo(new ChangeTextProperties(toTextBase(sharedEl)->cursor(), id, v, ps));
3405 : }
3406 6 : }
3407 :
3408 11044 : bool mu::engraving::TextBase::hasSymbolScale() const
3409 : {
3410 11044 : bool hasSymbolScale = isDynamic()
3411 8475 : || isStringTunings()
3412 8475 : || isPlayTechAnnotation()
3413 8406 : || (isHarpPedalDiagram() && toHarpPedalDiagram(this)->isDiagram())
3414 8379 : || (parent() && parent()->isOttavaSegment())
3415 7890 : || (parent() && parent()->isTuplet())
3416 19519 : || (parent() && parent()->isPedalSegment());
3417 :
3418 11044 : return hasSymbolScale;
3419 : }
3420 :
3421 161780 : double TextBase::spatiumScaling() const
3422 : {
3423 : double spatiumScaling;
3424 :
3425 161780 : if (isInstrumentName()) {
3426 102501 : spatiumScaling = toInstrumentName(this)->largestStaffSpatium() / defaultSpatium();
3427 : } else {
3428 59279 : spatiumScaling = spatium() / defaultSpatium();
3429 : }
3430 :
3431 161780 : return spatiumScaling;
3432 : }
3433 : }
|