//============================================================================= // MuseScore // Music Composition & Notation // // Copyright (C) 2011-2014 Werner Schweer // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 // as published by the Free Software Foundation and appearing in // the file LICENCE.GPL //============================================================================= #include "text.h" #include "textedit.h" #include "jump.h" #include "marker.h" #include "score.h" #include "segment.h" #include "measure.h" #include "system.h" #include "box.h" #include "page.h" #include "textframe.h" #include "sym.h" #include "xml.h" #include "undo.h" #include "mscore.h" namespace Ms { #ifdef Q_OS_MAC #define CONTROL_MODIFIER Qt::AltModifier #else #define CONTROL_MODIFIER Qt::ControlModifier #endif static const qreal subScriptSize = 0.6; static const qreal subScriptOffset = 0.5; // of x-height static const qreal superScriptOffset = -.9; // of x-height //static const qreal tempotextOffset = 0.4; // of x-height // 80% of 50% = 2 spatiums //--------------------------------------------------------- // operator== //--------------------------------------------------------- bool CharFormat::operator==(const CharFormat& cf) const { return cf.style() == style() && cf.preedit() == preedit() && cf.valign() == valign() && cf.fontSize() == fontSize() && cf.fontFamily() == fontFamily(); } //--------------------------------------------------------- // clearSelection //--------------------------------------------------------- void TextCursor::clearSelection() { _selectLine = _row; _selectColumn = _column; } //--------------------------------------------------------- // init //--------------------------------------------------------- void TextCursor::init() { _format.setFontFamily(_text->family()); _format.setFontSize(_text->size()); _format.setStyle(_text->fontStyle()); _format.setPreedit(false); _format.setValign(VerticalAlignment::AlignNormal); } //--------------------------------------------------------- // columns //--------------------------------------------------------- int TextCursor::columns() const { return _text->textBlock(_row).columns(); } //--------------------------------------------------------- // currentCharacter //--------------------------------------------------------- QChar TextCursor::currentCharacter() const { const TextBlock& t = _text->_layout[row()]; QString s = t.text(column(), 1); if (s.isEmpty()) return QChar(); return s[0]; } //--------------------------------------------------------- // updateCursorFormat //--------------------------------------------------------- void TextCursor::updateCursorFormat() { TextBlock* block = &_text->_layout[_row]; int col = hasSelection() ? selectColumn() : column(); const CharFormat* format = block->formatAt(col); if (!format || format->fontFamily() == "ScoreText") init(); else setFormat(*format); } //--------------------------------------------------------- // cursorRect //--------------------------------------------------------- QRectF TextCursor::cursorRect() const { const TextBlock& tline = curLine(); const TextFragment* fragment = tline.fragment(column()); QFont _font = fragment ? fragment->font(_text) : _text->font(); qreal ascent = QFontMetricsF(_font, MScore::paintDevice()).ascent(); qreal h = ascent; qreal x = tline.xpos(column(), _text); qreal y = tline.y() - ascent * .9; return QRectF(x, y, 4.0, h); } //--------------------------------------------------------- // curLine // return the current text line in edit mode //--------------------------------------------------------- TextBlock& TextCursor::curLine() const { Q_ASSERT(!_text->_layout.empty()); return _text->_layout[_row]; } //--------------------------------------------------------- // changeSelectionFormat //--------------------------------------------------------- void TextCursor::changeSelectionFormat(FormatId id, QVariant val) { if (!hasSelection()) return; int r1 = selectLine(); int r2 = row(); int c1 = selectColumn(); int c2 = column(); if (r1 > r2) { qSwap(r1, r2); qSwap(c1, c2); } else if (r1 == r2) { if (c1 > c2) qSwap(c1, c2); } int rows = _text->rows(); for (int row = 0; row < rows; ++row) { TextBlock& t = _text->_layout[row]; if (row < r1) continue; if (row > r2) break; if (row == r1 && r1 == r2) t.changeFormat(id, val, c1, c2 - c1); else if (row == r1) t.changeFormat(id, val, c1, t.columns() - c1); else if (row == r2) t.changeFormat(id, val, 0, c2); else t.changeFormat(id, val, 0, t.columns()); } _text->layout1(); } //--------------------------------------------------------- // setFormat //--------------------------------------------------------- void TextCursor::setFormat(FormatId id, QVariant val) { changeSelectionFormat(id, val); format()->setFormat(id, val); text()->setTextInvalid(); } //--------------------------------------------------------- // movePosition //--------------------------------------------------------- bool TextCursor::movePosition(QTextCursor::MoveOperation op, QTextCursor::MoveMode mode, int count) { for (int i = 0; i < count; i++) { switch (op) { case QTextCursor::Left: if (hasSelection() && mode == QTextCursor::MoveAnchor) { int r1 = _selectLine; int r2 = _row; int c1 = _selectColumn; int c2 = _column; if (r1 > r2) { qSwap(r1, r2); qSwap(c1, c2); } else if (r1 == r2) { if (c1 > c2) qSwap(c1, c2); } clearSelection(); _row = r1; _column = c1; } else if (_column == 0) { if (_row == 0) return false; --_row; _column = curLine().columns(); } else --_column; break; case QTextCursor::Right: if (hasSelection() && mode == QTextCursor::MoveAnchor) { int r1 = _selectLine; int r2 = _row; int c1 = _selectColumn; int c2 = _column; if (r1 > r2) { qSwap(r1, r2); qSwap(c1, c2); } else if (r1 == r2) { if (c1 > c2) qSwap(c1, c2); } clearSelection(); _row = r2; _column = c2; } else if (column() >= curLine().columns()) { if (_row >= _text->rows() - 1) return false; ++_row; _column = 0; } else ++_column; break; case QTextCursor::Up: if (_row == 0) return false; --_row; if (_column > curLine().columns()) _column = curLine().columns(); break; case QTextCursor::Down: if (_row >= _text->rows() - 1) return false; ++_row; if (_column > curLine().columns()) _column = curLine().columns(); break; case QTextCursor::Start: _row = 0; _column = 0; break; case QTextCursor::End: _row = _text->rows() - 1; _column = curLine().columns(); break; case QTextCursor::StartOfLine: _column = 0; break; case QTextCursor::EndOfLine: _column = curLine().columns(); break; case QTextCursor::WordLeft: if (_column > 0) { --_column; while (_column > 0 && currentCharacter().isSpace()) --_column; while (_column > 0 && !currentCharacter().isSpace()) --_column; if (currentCharacter().isSpace()) ++_column; } break; case QTextCursor::NextWord: { int cols = columns(); if (_column < cols) { ++_column; while (_column < cols && !currentCharacter().isSpace()) ++_column; while (_column < cols && currentCharacter().isSpace()) ++_column; } } break; default: qDebug("Text::movePosition: not implemented"); return false; } if (mode == QTextCursor::MoveAnchor) clearSelection(); } updateCursorFormat(); _text->score()->addRefresh(_text->canvasBoundingRect()); return true; } //--------------------------------------------------------- // doubleClickSelect //--------------------------------------------------------- void TextCursor::doubleClickSelect() { clearSelection(); // if clicked on a space, select surrounding spaces // otherwise select surround non-spaces const bool selectSpaces = currentCharacter().isSpace(); //handle double-clicking inside a word int startPosition = _column; while (_column > 0 && currentCharacter().isSpace() == selectSpaces) --_column; if (currentCharacter().isSpace() != selectSpaces) ++_column; _selectColumn = _column; _column = startPosition; while (_column < curLine().columns() && currentCharacter().isSpace() == selectSpaces) ++_column; updateCursorFormat(); _text->score()->addRefresh(_text->canvasBoundingRect()); } //--------------------------------------------------------- // set //--------------------------------------------------------- bool TextCursor::set(const QPointF& p, QTextCursor::MoveMode mode) { QPointF pt = p - _text->canvasPos(); if (!_text->bbox().contains(pt)) return false; int oldRow = _row; int oldColumn = _column; // if (_text->_layout.empty()) // _text->_layout.append(TextBlock()); _row = 0; for (int row = 0; row < _text->rows(); ++row) { const TextBlock& l = _text->_layout.at(row); if (l.y() > pt.y()) { _row = row; break; } } _column = curLine().column(pt.x(), _text); if (oldRow != _row || oldColumn != _column) { _text->score()->setUpdateAll(); if (mode == QTextCursor::MoveAnchor) clearSelection(); if (hasSelection()) QApplication::clipboard()->setText(selectedText(), QClipboard::Selection); } updateCursorFormat(); return true; } //--------------------------------------------------------- // selectedText // return current selection //--------------------------------------------------------- QString TextCursor::selectedText() const { QString s; int r1 = selectLine(); int r2 = _row; int c1 = selectColumn(); int c2 = column(); if (r1 > r2) { qSwap(r1, r2); qSwap(c1, c2); } else if (r1 == r2) { if (c1 > c2) qSwap(c1, c2); } int rows = _text->rows(); for (int row = 0; row < rows; ++row) { const TextBlock& t = _text->_layout.at(row); if (row >= r1 && row <= r2) { if (row == r1 && r1 == r2) s += t.text(c1, c2 - c1); else if (row == r1) { s += t.text(c1, -1); s += "\n"; } else if (row == r2) s += t.text(0, c2); else { s += t.text(0, -1); s += "\n"; } } } return s; } //--------------------------------------------------------- // TextFragment //--------------------------------------------------------- TextFragment::TextFragment() { } TextFragment::TextFragment(const QString& s) { text = s; } TextFragment::TextFragment(TextCursor* cursor, const QString& s) { format = *cursor->format(); text = s; } //--------------------------------------------------------- // split //--------------------------------------------------------- TextFragment TextFragment::split(int column) { int idx = 0; int col = 0; TextFragment f; f.format = format; for (const QChar& c : text) { if (col == column) { if (idx) { if (idx < text.size()) { f.text = text.mid(idx); text = text.left(idx); } } return f; } ++idx; if (c.isHighSurrogate()) continue; ++col; } return f; } //--------------------------------------------------------- // columns //--------------------------------------------------------- int TextFragment::columns() const { int col = 0; for (const QChar& c : text) { if (c.isHighSurrogate()) continue; ++col; } return col; } //--------------------------------------------------------- // operator == //--------------------------------------------------------- bool TextFragment::operator ==(const TextFragment& f) const { return format == f.format && text == f.text; } //--------------------------------------------------------- // draw //--------------------------------------------------------- void TextFragment::draw(QPainter* p, const TextBase* t) const { QFont f(font(t)); f.setPointSizeF(f.pointSizeF() * MScore::pixelRatio); p->setFont(f); p->drawText(pos, text); } //--------------------------------------------------------- // font //--------------------------------------------------------- QFont TextFragment::font(const TextBase* t) const { QFont font; qreal m = format.fontSize(); if (t->sizeIsSpatiumDependent()) m *= t->spatium() / SPATIUM20; if (format.valign() != VerticalAlignment::AlignNormal) m *= subScriptSize; font.setUnderline(format.underline() || format.preedit()); QString family; if (format.fontFamily() == "ScoreText") { family = t->score()->styleSt(Sid::MusicalTextFont); // check if all symbols are available font.setFamily(family); QFontMetricsF fm(font); bool fail = false; for (int i = 0; i < text.size(); ++i) { QChar c = text[i]; if (c.isHighSurrogate()) { if (i+1 == text.size()) qFatal("bad string"); QChar c2 = text[i+1]; ++i; uint v = QChar::surrogateToUcs4(c, c2); if (!fm.inFontUcs4(v)) { fail = true; break; } } else { if (!fm.inFont(c)) { fail = true; break; } } } if (fail) family = ScoreFont::fallbackTextFont(); } else family = format.fontFamily(); font.setFamily(family); font.setBold(format.bold()); font.setItalic(format.italic()); Q_ASSERT(m > 0.0); font.setPointSizeF(m); return font; } //--------------------------------------------------------- // draw //--------------------------------------------------------- void TextBlock::draw(QPainter* p, const TextBase* t) const { p->translate(0.0, _y); for (const TextFragment& f : _fragments) f.draw(p, t); p->translate(0.0, -_y); } //--------------------------------------------------------- // layout //--------------------------------------------------------- void TextBlock::layout(TextBase* t) { _bbox = QRectF(); qreal x = 0.0; _lineSpacing = 0.0; qreal lm = 0.0; qreal layoutWidth = 0; Element* e = t->parent(); if (e && t->layoutToParentWidth()) { layoutWidth = e->width(); switch(e->type()) { case ElementType::HBOX: case ElementType::VBOX: case ElementType::TBOX: { Box* b = toBox(e); layoutWidth -= ((b->leftMargin() + b->rightMargin()) * DPMM); lm = b->leftMargin() * DPMM; } break; case ElementType::PAGE: { Page* p = toPage(e); layoutWidth -= (p->lm() + p->rm()); lm = p->lm(); } break; case ElementType::MEASURE: { Measure* m = toMeasure(e); layoutWidth = m->bbox().width(); } break; default: break; } } if (_fragments.empty()) { QFontMetricsF fm = t->fontMetrics(); _bbox.setRect(0.0, -fm.ascent(), 1.0, fm.descent()); _lineSpacing = fm.lineSpacing(); } else { for (TextFragment& f : _fragments) { f.pos.setX(x); QFontMetricsF fm(f.font(t), MScore::paintDevice()); if (f.format.valign() != VerticalAlignment::AlignNormal) { qreal voffset = fm.xHeight() / subScriptSize; // use original height if (f.format.valign() == VerticalAlignment::AlignSubScript) voffset *= subScriptOffset; else voffset *= superScriptOffset; f.pos.setY(voffset); } else f.pos.setY(0.0); qreal w = fm.width(f.text); _bbox |= fm.tightBoundingRect(f.text).translated(f.pos); x += w; _lineSpacing = qMax(_lineSpacing, fm.lineSpacing()); } } qreal rx; if (t->align() & Align::RIGHT) rx = layoutWidth-_bbox.right(); else if (t->align() & Align::HCENTER) rx = (layoutWidth - (_bbox.left() + _bbox.right())) * .5; else // Align::LEFT rx = -_bbox.left(); rx += lm; for (TextFragment& f : _fragments) f.pos.rx() += rx; _bbox.translate(rx, 0.0); } //--------------------------------------------------------- // xpos //--------------------------------------------------------- qreal TextBlock::xpos(int column, const TextBase* t) const { int col = 0; for (const TextFragment& f : _fragments) { if (column == col) return f.pos.x(); QFontMetricsF fm(f.font(t), MScore::paintDevice()); int idx = 0; for (const QChar& c : f.text) { ++idx; if (c.isHighSurrogate()) continue; ++col; if (column == col) return f.pos.x() + fm.width(f.text.left(idx)); } } return _bbox.x(); } //--------------------------------------------------------- // fragment //--------------------------------------------------------- const TextFragment* TextBlock::fragment(int column) const { if (_fragments.empty()) return 0; int col = 0; auto f = _fragments.begin(); for (; f != _fragments.end(); ++f) { for (const QChar& c : f->text) { if (c.isHighSurrogate()) continue; if (column == col) return &*f; ++col; } } if (column == col) return &*(f-1); return 0; } //--------------------------------------------------------- // formatAt //--------------------------------------------------------- const CharFormat* TextBlock::formatAt(int column) const { const TextFragment* f = fragment(column); if (f) return &(f->format); return 0; } //--------------------------------------------------------- // boundingRect //--------------------------------------------------------- QRectF TextBlock::boundingRect(int col1, int col2, const TextBase* t) const { qreal x1 = xpos(col1, t); qreal x2 = xpos(col2, t); return QRectF(x1, _bbox.y(), x2-x1, _bbox.height()); } //--------------------------------------------------------- // columns //--------------------------------------------------------- int TextBlock::columns() const { int col = 0; for (const TextFragment& f : _fragments) { for (const QChar& c : f.text) { if (!c.isHighSurrogate()) ++col; } } return col; } //--------------------------------------------------------- // column // Return nearest column for position x. X is in // Text coordinate system //--------------------------------------------------------- int TextBlock::column(qreal x, TextBase* t) const { int col = 0; for (const TextFragment& f : _fragments) { int idx = 0; if (x <= f.pos.x()) return col; qreal px = 0.0; for (const QChar& c : f.text) { ++idx; if (c.isHighSurrogate()) continue; QFontMetricsF fm(f.font(t), MScore::paintDevice()); qreal xo = fm.width(f.text.left(idx)); if (x <= f.pos.x() + px + (xo-px)*.5) return col; ++col; px = xo; } } return col; } //--------------------------------------------------------- // insert //--------------------------------------------------------- void TextBlock::insert(TextCursor* cursor, const QString& s) { int rcol, ridx; auto i = fragment(cursor->column(), &rcol, &ridx); if (i != _fragments.end()) { if (!(i->format == *cursor->format())) { if (rcol == 0) _fragments.insert(i, TextFragment(cursor, s)); else { TextFragment f2 = i->split(rcol); i = _fragments.insert(i+1, TextFragment(cursor, s)); _fragments.insert(i+1, f2); } } else i->text.insert(ridx, s); } else { if (!_fragments.empty() && _fragments.back().format == *cursor->format()) _fragments.back().text.append(s); else _fragments.append(TextFragment(cursor, s)); } } //--------------------------------------------------------- // fragment // inputs: // column is the column relative to the start of the TextBlock. // outputs: // rcol will be the column relative to the start of the TextFragment that the input column is in. // ridx will be the QChar index into TextFragment's text QString relative to the start of that TextFragment. // //--------------------------------------------------------- QList::iterator TextBlock::fragment(int column, int* rcol, int* ridx) { int col = 0; for (auto i = _fragments.begin(); i != _fragments.end(); ++i) { *rcol = 0; *ridx = 0; for (const QChar& c : i->text) { if (col == column) return i; ++*ridx; if (c.isHighSurrogate()) continue; ++col; ++*rcol; } } return _fragments.end(); } //--------------------------------------------------------- // remove //--------------------------------------------------------- QString TextBlock::remove(int column) { int col = 0; QString s; for (auto i = _fragments.begin(); i != _fragments.end(); ++i) { int idx = 0; int rcol = 0; for (const QChar& c : i->text) { if (col == column) { if (c.isSurrogate()) { s = i->text.mid(idx, 2); i->text.remove(idx, 2); } else { s = i->text.mid(idx, 1); i->text.remove(idx, 1); } if (i->text.isEmpty()) _fragments.erase(i); simplify(); return s; } ++idx; if (c.isHighSurrogate()) continue; ++col; ++rcol; } } return s; // qDebug("TextBlock::remove: column %d not found", column); } //--------------------------------------------------------- // simplify //--------------------------------------------------------- void TextBlock::simplify() { if (_fragments.size() < 2) return; auto i = _fragments.begin(); TextFragment* f = &*i; ++i; for (; i != _fragments.end(); ++i) { while (i != _fragments.end() && (i->format == f->format)) { f->text.append(i->text); i = _fragments.erase(i); } if (i == _fragments.end()) break; f = &*i; } } //--------------------------------------------------------- // remove //--------------------------------------------------------- QString TextBlock::remove(int start, int n) { if (n == 0) return QString(); int col = 0; QString s; for (auto i = _fragments.begin(); i != _fragments.end();) { int rcol = 0; bool inc = true; for( int idx = 0; idx < i->text.length(); ) { QChar c = i->text[idx]; if (col == start) { if (c.isHighSurrogate()) { s += c; i->text.remove(idx, 1); c = i->text[idx]; } s += c; i->text.remove(idx, 1); if (i->text.isEmpty() && (_fragments.size() > 1)) { i = _fragments.erase(i); inc = false; } --n; if (n == 0) return s; continue; } ++idx; if (c.isHighSurrogate()) continue; ++col; ++rcol; } if (inc) ++i; } return s; } //--------------------------------------------------------- // changeFormat //--------------------------------------------------------- void TextBlock::changeFormat(FormatId id, QVariant data, int start, int n) { int col = 0; for (auto i = _fragments.begin(); i != _fragments.end(); ++i) { int columns = i->columns(); if (start + n <= col) break; if (start >= col + columns) { col += i->columns(); continue; } int endCol = col + columns; if ((start <= col) && (start < endCol) && ((start+n) < endCol)) { // left TextFragment f = i->split(start + n - col); i->changeFormat(id, data); i = _fragments.insert(i+1, f); } else if (start > col && ((start+n) < endCol)) { // middle TextFragment lf = i->split(start+n - col); TextFragment mf = i->split(start - col); mf.changeFormat(id, data); i = _fragments.insert(i+1, mf); i = _fragments.insert(i+1, lf); } else if (start > col) { // right TextFragment f = i->split(start - col); f.changeFormat(id, data); i = _fragments.insert(i+1, f); } else { // complete fragment i->changeFormat(id, data); } col = endCol; } } //--------------------------------------------------------- // setFormat //--------------------------------------------------------- void CharFormat::setFormat(FormatId id, QVariant data) { switch (id) { case FormatId::Bold: setBold(data.toBool()); break; case FormatId::Italic: setItalic(data.toBool()); break; case FormatId::Underline: setUnderline(data.toBool()); break; case FormatId::Valign: _valign = static_cast(data.toInt()); break; case FormatId::FontSize: _fontSize = data.toDouble(); break; case FormatId::FontFamily: _fontFamily = data.toString(); break; } } //--------------------------------------------------------- // changeFormat //--------------------------------------------------------- void TextFragment::changeFormat(FormatId id, QVariant data) { format.setFormat(id, data); } //--------------------------------------------------------- // split //--------------------------------------------------------- TextBlock TextBlock::split(int column) { TextBlock tl; int col = 0; for (auto i = _fragments.begin(); i != _fragments.end(); ++i) { int idx = 0; for (const QChar& c : i->text) { if (col == column) { if (idx) { if (idx < i->text.size()) { TextFragment tf(i->text.mid(idx)); tf.format = i->format; tl._fragments.append(tf); i->text = i->text.left(idx); ++i; } } for (; i != _fragments.end(); i = _fragments.erase(i)) tl._fragments.append(*i); return tl; } ++idx; if (c.isHighSurrogate()) continue; ++col; } } TextFragment tf(""); if (_fragments.size() > 0) tf.format = _fragments.last().format; tl._fragments.append(tf); return tl; } //--------------------------------------------------------- // text // extract text, symbols are marked with xxx //--------------------------------------------------------- QString TextBlock::text(int col1, int len) const { QString s; int col = 0; for (auto f : _fragments) { if (f.text.isEmpty()) continue; for (const QChar& c : f.text) { if (col >= col1 && (len < 0 || ((col-col1) < len))) s += XmlWriter::xmlString(c.unicode()); if (!c.isHighSurrogate()) ++col; } } return s; } //--------------------------------------------------------- // Text //--------------------------------------------------------- TextBase::TextBase(Score* s, Tid tid, ElementFlags f) : Element(s, f | ElementFlag::MOVABLE) { _tid = tid; _family = "FreeSerif"; _size = 10.0; _fontStyle = FontStyle::Normal; _bgColor = QColor(255, 255, 255, 0); _frameColor = QColor(0, 0, 0, 255); _align = Align::LEFT; _frameType = FrameType::NO_FRAME; _frameWidth = Spatium(0.1); _paddingWidth = Spatium(0.2); _frameRound = 0; } TextBase::TextBase(Score* s, ElementFlags f) : TextBase(s, Tid::DEFAULT, f) { } TextBase::TextBase(const TextBase& st) : Element(st) { _text = st._text; _layout = st._layout; textInvalid = st.textInvalid; layoutInvalid = st.layoutInvalid; frame = st.frame; _layoutToParentWidth = st._layoutToParentWidth; hexState = -1; _tid = st._tid; _family = st._family; _size = st._size; _fontStyle = st._fontStyle; _bgColor = st._bgColor; _frameColor = st._frameColor; _align = st._align; _frameType = st._frameType; _frameWidth = st._frameWidth; _paddingWidth = st._paddingWidth; _frameRound = st._frameRound; size_t n = _elementStyle->size() + TEXT_STYLE_SIZE; delete[] _propertyFlagsList; _propertyFlagsList = new PropertyFlags[n]; for (size_t i = 0; i < n; ++i) _propertyFlagsList[i] = st._propertyFlagsList[i]; _links = 0; } //--------------------------------------------------------- // drawSelection //--------------------------------------------------------- void TextBase::drawSelection(QPainter* p, const QRectF& r) const { QBrush bg(QColor("steelblue")); p->setCompositionMode(QPainter::CompositionMode_HardLight); p->setBrush(bg); p->setPen(Qt::NoPen); p->drawRect(r); p->setCompositionMode(QPainter::CompositionMode_SourceOver); p->setPen(textColor()); } //--------------------------------------------------------- // textColor //--------------------------------------------------------- QColor TextBase::textColor() const { return curColor(); } //--------------------------------------------------------- // insert // insert character //--------------------------------------------------------- void TextBase::insert(TextCursor* cursor, uint code) { if (cursor->row() >= rows()) _layout.append(TextBlock()); if (code == '\t') code = ' '; QString s; if (QChar::requiresSurrogates(code)) s = QString(QChar(QChar::highSurrogate(code))).append(QChar(QChar::lowSurrogate(code))); else s = QString(code); _layout[cursor->row()].insert(cursor, s); cursor->setColumn(cursor->column() + 1); cursor->clearSelection(); } //--------------------------------------------------------- // parseStringProperty //--------------------------------------------------------- static QString parseStringProperty(const QString& s) { QString rs; for (const QChar& c : s) { if (c == '"') break; rs += c; } return rs; } //--------------------------------------------------------- // parseNumProperty //--------------------------------------------------------- static qreal parseNumProperty(const QString& s) { return parseStringProperty(s).toDouble(); } //--------------------------------------------------------- // createLayout // create layout from text //--------------------------------------------------------- void TextBase::createLayout() { _layout.clear(); TextCursor cursor((Text*)this); cursor.init(); int state = 0; QString token; QString sym; bool symState = false; for (int i = 0; i < _text.length(); i++) { const QChar& c = _text[i]; if (state == 0) { if (c == '<') { state = 1; token.clear(); } else if (c == '&') { state = 2; token.clear(); } else if (c == '\n') { if (rows() <= cursor.row()) _layout.append(TextBlock()); _layout[cursor.row()].setEol(true); cursor.setRow(cursor.row() + 1); cursor.setColumn(0); if (rows() <= cursor.row()) _layout.append(TextBlock()); } else { if (symState) sym += c; else { if (c.isHighSurrogate()) { i++; Q_ASSERT(i < _text.length()); insert(&cursor, QChar::surrogateToUcs4(c, _text[i])); } else insert(&cursor, c.unicode()); } } } else if (state == 1) { if (c == '>') { bool unstyleFontStyle = false; state = 0; if (token == "b") { cursor.format()->setBold(true); unstyleFontStyle = true; } else if (token == "/b") cursor.format()->setBold(false); else if (token == "i") { cursor.format()->setItalic(true); unstyleFontStyle = true; } else if (token == "/i") cursor.format()->setItalic(false); else if (token == "u") { cursor.format()->setUnderline(true); unstyleFontStyle = true; } else if (token == "/u") cursor.format()->setUnderline(false); else if (token == "sub") cursor.format()->setValign(VerticalAlignment::AlignSubScript); else if (token == "/sub") cursor.format()->setValign(VerticalAlignment::AlignNormal); else if (token == "sup") cursor.format()->setValign(VerticalAlignment::AlignSuperScript); else if (token == "/sup") cursor.format()->setValign(VerticalAlignment::AlignNormal); else if (token == "sym") { symState = true; sym.clear(); } else if (token == "/sym") { symState = false; SymId id = Sym::name2id(sym); if (id != SymId::noSym) { CharFormat fmt = *cursor.format(); // save format // uint code = score()->scoreFont()->sym(id).code(); uint code = ScoreFont::fallbackFont()->sym(id).code(); cursor.format()->setFontFamily("ScoreText"); cursor.format()->setBold(false); cursor.format()->setItalic(false); insert(&cursor, code); cursor.setFormat(fmt); // restore format } else { qDebug("unknown symbol <%s>", qPrintable(sym)); } } else if (token.startsWith("font ")) { token = token.mid(5); if (token.startsWith("size=\"")) { cursor.format()->setFontSize(parseNumProperty(token.mid(6))); setPropertyFlags(Pid::FONT_SIZE, PropertyFlags::UNSTYLED); } else if (token.startsWith("face=\"")) { QString face = parseStringProperty(token.mid(6)); face = unEscape(face); cursor.format()->setFontFamily(face); setPropertyFlags(Pid::FONT_FACE, PropertyFlags::UNSTYLED); } else qDebug("cannot parse html property <%s> in text <%s>", qPrintable(token), qPrintable(_text)); } if (unstyleFontStyle) setPropertyFlags(Pid::FONT_STYLE, PropertyFlags::UNSTYLED); } else token += c; } else if (state == 2) { if (c == ';') { state = 0; if (token == "lt") insert(&cursor, '<'); else if (token == "gt") insert(&cursor, '>'); else if (token == "amp") insert(&cursor, '&'); else if (token == "quot") insert(&cursor, '"'); else { // TODO insert(&cursor, Sym::name2id(token)); } } else token += c; } } if (_layout.empty()) _layout.append(TextBlock()); layoutInvalid = false; } //--------------------------------------------------------- // layout //--------------------------------------------------------- void TextBase::layout() { setPos(QPointF()); if (!parent()) setOffset(0.0, 0.0); // else if (isStyled(Pid::OFFSET)) // TODO: should be set already // setOffset(propertyDefault(Pid::OFFSET).toPointF()); if (placeBelow()) rypos() = staff() ? staff()->height() : 0.0; layout1(); } //--------------------------------------------------------- // layout1 //--------------------------------------------------------- void TextBase::layout1() { if (layoutInvalid) createLayout(); if (_layout.empty()) _layout.append(TextBlock()); QRectF bb; qreal y = 0; for (int i = 0; i < rows(); ++i) { TextBlock* t = &_layout[i]; t->layout(this); const QRectF* r = &t->boundingRect(); if (r->height() == 0) r = &_layout[i-i].boundingRect(); y += t->lineSpacing(); t->setY(y); bb |= r->translated(0.0, y); } qreal yoff = 0; qreal h = 0; if (parent()) { if (layoutToParentWidth()) { if (parent()->isTBox()) { // hack: vertical alignment is always TOP _align = Align(((char)_align) & ((char)Align::HMASK)) | Align::TOP; } else if (parent()->isBox()) { // consider inner margins of frame Box* b = toBox(parent()); yoff = b->topMargin() * DPMM; h = b->height() - yoff - b->bottomMargin() * DPMM; } else if (parent()->isPage()) { Page* p = toPage(parent()); h = p->height() - p->tm() - p->bm(); yoff = p->tm(); } else if (parent()->isMeasure()) ; else h = parent()->height(); } } else setPos(QPointF()); if (align() & Align::BOTTOM) yoff += h - bb.bottom(); else if (align() & Align::VCENTER) { yoff += (h - (bb.top() + bb.bottom())) * .5; } else if (align() & Align::BASELINE) yoff += h * .5 - _layout.front().lineSpacing(); else yoff += -bb.top(); for (TextBlock& t : _layout) t.setY(t.y() + yoff); bb.translate(0.0, yoff); setbbox(bb); if (hasFrame()) layoutFrame(); score()->addRefresh(canvasBoundingRect()); } //--------------------------------------------------------- // layoutFrame //--------------------------------------------------------- void TextBase::layoutFrame() { // if (empty()) { // or bbox.width() <= 1.0 if (bbox().width() <= 1.0 || bbox().height() < 1.0) { // or bbox.width() <= 1.0 // this does not work for Harmony: QFontMetricsF fm = QFontMetricsF(font(), MScore::paintDevice()); qreal ch = fm.ascent(); qreal cw = fm.width('n'); frame = QRectF(0.0, -ch, cw, ch); } else frame = bbox(); if (square()) { #if 0 // "real" square if (frame.width() > frame.height()) { qreal w = frame.width() - frame.height(); frame.adjust(0.0, -w * .5, 0.0, w * .5); } else { qreal w = frame.height() - frame.width(); frame.adjust(-w * .5, 0.0, w * .5, 0.0); } #else // make sure width >= height if (frame.height() > frame.width()) { qreal w = frame.height() - frame.width(); frame.adjust(-w * .5, 0.0, w * .5, 0.0); } #endif } else if (circle()) { if (frame.width() > frame.height()) { frame.setY(frame.y() + (frame.width() - frame.height()) * -.5); frame.setHeight(frame.width()); } else { frame.setX(frame.x() + (frame.height() - frame.width()) * -.5); frame.setWidth(frame.height()); } } qreal _spatium = spatium(); qreal w = (paddingWidth() + frameWidth() * .5f).val() * _spatium; frame.adjust(-w, -w, w, w); w = frameWidth().val() * _spatium; setbbox(frame.adjusted(-w, -w, w, w)); } //--------------------------------------------------------- // lineSpacing //--------------------------------------------------------- qreal TextBase::lineSpacing() const { return fontMetrics().lineSpacing() * MScore::pixelRatio; } //--------------------------------------------------------- // lineHeight //--------------------------------------------------------- qreal TextBase::lineHeight() const { return fontMetrics().height(); } //--------------------------------------------------------- // baseLine //--------------------------------------------------------- qreal TextBase::baseLine() const { return fontMetrics().ascent(); } //--------------------------------------------------------- // XmlNesting //--------------------------------------------------------- class XmlNesting : public QStack { QString* _s; public: XmlNesting(QString* s) { _s = s; } void pushToken(const QString& t) { *_s += "<"; *_s += t; *_s += ">"; push(t); } void pushB() { pushToken("b"); } void pushI() { pushToken("i"); } void pushU() { pushToken("u"); } QString popToken() { QString s = pop(); *_s += ""; return s; } void popToken(const char* t) { QStringList ps; for (;;) { QString s = popToken(); if (s == t) break; ps += s; } for (const QString& s : ps) pushToken(s); } void popB() { popToken("b"); } void popI() { popToken("i"); } void popU() { popToken("u"); } }; //--------------------------------------------------------- // genText //--------------------------------------------------------- void TextBase::genText() const { _text.clear(); bool bold_ = false; bool italic_ = false; bool underline_ = false; for (const TextBlock& block : _layout) { for (const TextFragment& f : block.fragments()) { if (!f.format.bold() && bold()) bold_ = true; if (!f.format.italic() && italic()) italic_ = true; if (!f.format.underline() && underline()) underline_ = true; } } CharFormat fmt; fmt.setFontFamily(family()); fmt.setFontSize(size()); fmt.setStyle(fontStyle()); fmt.setPreedit(false); fmt.setValign(VerticalAlignment::AlignNormal); XmlNesting xmlNesting(&_text); if (bold_) xmlNesting.pushB(); if (italic_) xmlNesting.pushI(); if (underline_) xmlNesting.pushU(); for (const TextBlock& block : _layout) { for (const TextFragment& f : block.fragments()) { if (f.text.isEmpty()) // skip empty fragments, not to continue; // insert extra HTML formatting const CharFormat& format = f.format; if (fmt.bold() != format.bold()) { if (format.bold()) xmlNesting.pushB(); else xmlNesting.popB(); } if (fmt.italic() != format.italic()) { if (format.italic()) xmlNesting.pushI(); else xmlNesting.popI(); } if (fmt.underline() != format.underline()) { if (format.underline()) xmlNesting.pushU(); else xmlNesting.popU(); } if (format.fontSize() != fmt.fontSize()) _text += QString("").arg(format.fontSize()); if (format.fontFamily() != fmt.fontFamily()) _text += QString("").arg(TextBase::escape(format.fontFamily())); VerticalAlignment va = format.valign(); VerticalAlignment cva = fmt.valign(); if (cva != va) { switch (va) { case VerticalAlignment::AlignNormal: xmlNesting.popToken(cva == VerticalAlignment::AlignSuperScript ? "sup" : "sub"); break; case VerticalAlignment::AlignSuperScript: xmlNesting.pushToken("sup"); break; case VerticalAlignment::AlignSubScript: xmlNesting.pushToken("sub"); break; } } _text += XmlWriter::xmlString(f.text); fmt = format; } if (block.eol()) _text += QChar::LineFeed; } while (!xmlNesting.empty()) xmlNesting.popToken(); textInvalid = false; } //--------------------------------------------------------- // selectAll //--------------------------------------------------------- void TextBase::selectAll(TextCursor* _cursor) { _cursor->setSelectLine(0); _cursor->setSelectColumn(0); _cursor->setRow(rows() - 1); _cursor->setColumn(_cursor->curLine().columns()); } //--------------------------------------------------------- // multiClickSelect // for double and triple clicks //--------------------------------------------------------- void TextBase::multiClickSelect(EditData& editData, MultiClick clicks) { switch (clicks) { case MultiClick::Double: cursor(editData)->doubleClickSelect(); break; case MultiClick::Triple: selectAll(cursor(editData)); break; } } //--------------------------------------------------------- // write //--------------------------------------------------------- void TextBase::write(XmlWriter& xml) const { if (!xml.canWrite(this)) return; xml.stag(this); writeProperties(xml, true, true); xml.etag(); } //--------------------------------------------------------- // read //--------------------------------------------------------- void TextBase::read(XmlReader& e) { while (e.readNextStartElement()) { if (!readProperties(e)) e.unknown(); } } //--------------------------------------------------------- // writeProperties //--------------------------------------------------------- void TextBase::writeProperties(XmlWriter& xml, bool writeText, bool /*writeStyle*/) const { Element::writeProperties(xml); writeProperty(xml, Pid::SUB_STYLE); for (const StyledProperty& spp : *_elementStyle) { if (!isStyled(spp.pid)) writeProperty(xml, spp.pid); } for (const StyledProperty& spp : *textStyle(tid())) { if (!isStyled(spp.pid)) writeProperty(xml, spp.pid); } if (writeText) xml.writeXml("text", xmlText()); } static constexpr std::array pids { { Pid::SUB_STYLE, Pid::FONT_FACE, Pid::FONT_SIZE, Pid::FONT_STYLE, Pid::COLOR, Pid::FRAME_TYPE, Pid::FRAME_WIDTH, Pid::FRAME_PADDING, Pid::FRAME_ROUND, Pid::FRAME_FG_COLOR, Pid::FRAME_BG_COLOR, Pid::ALIGN, } }; //--------------------------------------------------------- // readProperties //--------------------------------------------------------- bool TextBase::readProperties(XmlReader& e) { const QStringRef& tag(e.name()); for (Pid i :pids) { if (readProperty(tag, e, i)) return true; } if (tag == "text") setXmlText(e.readXml()); else if (tag == "bold") { bool val = e.readInt(); if (val) _fontStyle = _fontStyle + FontStyle::Bold; else _fontStyle = _fontStyle - FontStyle::Bold; if (isStyled(Pid::FONT_STYLE)) setPropertyFlags(Pid::FONT_STYLE, PropertyFlags::UNSTYLED); } else if (tag == "italic") { bool val = e.readInt(); if (val) _fontStyle = _fontStyle + FontStyle::Italic; else _fontStyle = _fontStyle - FontStyle::Italic; if (isStyled(Pid::FONT_STYLE)) setPropertyFlags(Pid::FONT_STYLE, PropertyFlags::UNSTYLED); } else if (tag == "underline") { bool val = e.readInt(); if (val) _fontStyle = _fontStyle + FontStyle::Underline; else _fontStyle = _fontStyle - FontStyle::Underline; if (isStyled(Pid::FONT_STYLE)) setPropertyFlags(Pid::FONT_STYLE, PropertyFlags::UNSTYLED); } else if (!Element::readProperties(e)) return false; return true; } //--------------------------------------------------------- // propertyId //--------------------------------------------------------- Pid TextBase::propertyId(const QStringRef& name) const { if (name == "text") return Pid::TEXT; for (Pid pid : pids) { if (propertyName(pid) == name) return pid; } return Element::propertyId(name); } //--------------------------------------------------------- // pageRectangle //--------------------------------------------------------- QRectF TextBase::pageRectangle() const { if (parent() && (parent()->isHBox() || parent()->isVBox() || parent()->isTBox())) { Box* box = toBox(parent()); QRectF r = box->abbox(); qreal x = r.x() + box->leftMargin() * DPMM; qreal y = r.y() + box->topMargin() * DPMM; qreal h = r.height() - (box->topMargin() + box->bottomMargin()) * DPMM; qreal w = r.width() - (box->leftMargin() + box->rightMargin()) * DPMM; // QSizeF ps = _doc->pageSize(); // return QRectF(x, y, ps.width(), ps.height()); return QRectF(x, y, w, h); } if (parent() && parent()->isPage()) { Page* box = toPage(parent()); QRectF r = box->abbox(); qreal x = r.x() + box->lm(); qreal y = r.y() + box->tm(); qreal h = r.height() - box->tm() - box->bm(); qreal w = r.width() - box->lm() - box->rm(); return QRectF(x, y, w, h); } return abbox(); } //--------------------------------------------------------- // dragTo //--------------------------------------------------------- void TextBase::dragTo(EditData& ed) { TextEditData* ted = static_cast(ed.getData(this)); TextCursor* _cursor = &ted->cursor; _cursor->set(ed.pos, QTextCursor::KeepAnchor); score()->setUpdateAll(); score()->update(); } //--------------------------------------------------------- // dragAnchor //--------------------------------------------------------- QLineF TextBase::dragAnchor() const { qreal xp = 0.0; for (Element* e = parent(); e; e = e->parent()) xp += e->x(); qreal yp; if (parent()->isSegment()) { System* system = toSegment(parent())->measure()->system(); yp = system->staffCanvasYpage(staffIdx()); } else yp = parent()->canvasPos().y(); QPointF p1(xp, yp); QPointF p2 = canvasPos(); if (layoutToParentWidth()) p2 += bbox().topLeft(); return QLineF(p1, p2); } //--------------------------------------------------------- // mousePress // set text cursor //--------------------------------------------------------- bool TextBase::mousePress(EditData& ed) { bool shift = ed.modifiers & Qt::ShiftModifier; TextEditData* ted = static_cast(ed.getData(this)); if (!ted->cursor.set(ed.startMove, shift ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor)) return false; if (ed.buttons == Qt::MidButton) paste(ed); score()->setUpdateAll(); return true; } //--------------------------------------------------------- // layoutEdit //--------------------------------------------------------- void TextBase::layoutEdit() { layout(); if (parent() && parent()->type() == ElementType::TBOX) { TBox* tbox = toTBox(parent()); tbox->layout(); System* system = tbox->system(); system->setHeight(tbox->height()); triggerLayout(); } else { static const qreal w = 2.0; // 8.0 / view->matrix().m11(); score()->addRefresh(canvasBoundingRect().adjusted(-w, -w, w, w)); } } //--------------------------------------------------------- // acceptDrop //--------------------------------------------------------- bool TextBase::acceptDrop(EditData& data) const { // do not accept the drop if this text element is not being edited ElementEditData* eed = data.getData(this); if (!eed || eed->type() != EditDataType::TextEditData) return false; ElementType type = data.dropElement->type(); return type == ElementType::SYMBOL || type == ElementType::FSYMBOL; } //--------------------------------------------------------- // setPlainText //--------------------------------------------------------- void TextBase::setPlainText(const QString& s) { setXmlText(s.toHtmlEscaped()); } //--------------------------------------------------------- // setXmlText //--------------------------------------------------------- void TextBase::setXmlText(const QString& s) { _text = s; layoutInvalid = true; textInvalid = false; } //--------------------------------------------------------- // plainText // return plain text with symbols //--------------------------------------------------------- QString TextBase::plainText() const { QString s; const TextBase* text = this; std::unique_ptr tmpText; if (layoutInvalid) { // Create temporary text object to avoid side effects // of createLayout() call. tmpText.reset(toTextBase(this->clone())); tmpText->createLayout(); text = tmpText.get(); } for (const TextBlock& block : text->_layout) { for (const TextFragment& f : block.fragments()) s += f.text; if (block.eol()) s += QChar::LineFeed; } return s; } //--------------------------------------------------------- // xmlText //--------------------------------------------------------- QString TextBase::xmlText() const { #if 1 // this is way too expensive // what side effects has genText() ? // this method is const by design const TextBase* text = this; std::unique_ptr tmpText; if (textInvalid) { // Create temporary text object to avoid side effects // of genText() call. tmpText.reset(toTextBase(this->clone())); tmpText->genText(); text = tmpText.get(); } return text->_text; #else if (textInvalid) genText(); return _text; #endif } //--------------------------------------------------------- // convertFromHtml //--------------------------------------------------------- QString TextBase::convertFromHtml(const QString& ss) const { QTextDocument doc; doc.setHtml(ss); QString s; qreal size_ = size(); QString family_ = family(); for (auto b = doc.firstBlock(); b.isValid() ; b = b.next()) { if (!s.isEmpty()) s += "\n"; for (auto it = b.begin(); !it.atEnd(); ++it) { QTextFragment f = it.fragment(); if (f.isValid()) { QTextCharFormat tf = f.charFormat(); QFont font = tf.font(); qreal htmlSize = font.pointSizeF(); // html font sizes may have spatium adjustments; need to undo this if (sizeIsSpatiumDependent()) htmlSize *= SPATIUM20 / spatium(); if (fabs(size_ - htmlSize) > 0.1) { size_ = htmlSize; s += QString("").arg(size_); } if (family_ != font.family()) { family_ = font.family(); s += QString("").arg(family_); } if (font.bold()) s += ""; if (font.italic()) s += ""; if (font.underline()) s += ""; s += f.text().toHtmlEscaped(); if (font.underline()) s += ""; if (font.italic()) s += ""; if (font.bold()) s += ""; } } } if (score() && score()->mscVersion() <= 114) { s.replace(QChar(0xe10e), QString("accidentalNatural")); //natural s.replace(QChar(0xe10c), QString("accidentalSharp")); // sharp s.replace(QChar(0xe10d), QString("accidentalFlat")); // flat s.replace(QChar(0xe104), QString("metNoteHalfUp")), // note2_Sym s.replace(QChar(0xe105), QString("metNoteQuarterUp")); // note4_Sym s.replace(QChar(0xe106), QString("metNote8thUp")); // note8_Sym s.replace(QChar(0xe107), QString("metNote16thUp")); // note16_Sym s.replace(QChar(0xe108), QString("metNote32ndUp")); // note32_Sym s.replace(QChar(0xe109), QString("metNote64thUp")); // note64_Sym s.replace(QChar(0xe10a), QString("metAugmentationDot")); // dot s.replace(QChar(0xe10b), QString("metAugmentationDotspacemetAugmentationDot")); // dotdot s.replace(QChar(0xe167), QString("segno")); // segno s.replace(QChar(0xe168), QString("coda")); // coda s.replace(QChar(0xe169), QString("codaSquare")); // varcoda } return s; } //--------------------------------------------------------- // convertToHtml // convert from internal html format to Qt //--------------------------------------------------------- QString TextBase::convertToHtml(const QString& s, const TextStyle& /*st*/) { //TODO qreal size = st.size(); // QString family = st.family(); qreal size = 10; QString family = "arial"; return QString("%3").arg(family).arg(size).arg(s); } //--------------------------------------------------------- // tagEscape //--------------------------------------------------------- QString TextBase::tagEscape(QString s) { QStringList tags = { "sym", "b", "i", "u", "sub", "sup" }; for (QString tag : tags) { QString openTag = "<" + tag + ">"; QString openProxy = "!!" + tag + "!!"; QString closeTag = ""; QString closeProxy = "!!/" + tag + "!!"; s.replace(openTag, openProxy); s.replace(closeTag, closeProxy); } s = XmlWriter::xmlString(s); for (QString tag : tags) { QString openTag = "<" + tag + ">"; QString openProxy = "!!" + tag + "!!"; QString closeTag = ""; QString closeProxy = "!!/" + tag + "!!"; s.replace(openProxy, openTag); s.replace(closeProxy, closeTag); } return s; } //--------------------------------------------------------- // unEscape //--------------------------------------------------------- QString TextBase::unEscape(QString s) { s.replace("<", "<"); s.replace(">", ">"); s.replace("&", "&"); s.replace(""", "\""); return s; } //--------------------------------------------------------- // escape //--------------------------------------------------------- QString TextBase::escape(QString s) { s.replace("<", "<"); s.replace(">", ">"); s.replace("&", "&"); s.replace("\"", """); return s; } //--------------------------------------------------------- // accessibleInfo //--------------------------------------------------------- QString TextBase::accessibleInfo() const { QString rez; switch (tid()) { case Tid::TITLE: case Tid::SUBTITLE: case Tid::COMPOSER: case Tid::POET: case Tid::TRANSLATOR: case Tid::MEASURE_NUMBER: rez = score() ? score()->getTextStyleUserName(tid()) : textStyleUserName(tid()); break; default: rez = Element::accessibleInfo(); break; } QString s = plainText().simplified(); if (s.length() > 20) { s.truncate(20); s += "…"; } return QString("%1: %2").arg(rez).arg(s); } //--------------------------------------------------------- // screenReaderInfo //--------------------------------------------------------- QString TextBase::screenReaderInfo() const { QString rez; switch (tid()) { case Tid::TITLE: case Tid::SUBTITLE: case Tid::COMPOSER: case Tid::POET: case Tid::TRANSLATOR: case Tid::MEASURE_NUMBER: rez = score() ? score()->getTextStyleUserName(tid()) : textStyleUserName(tid()); break; default: rez = Element::accessibleInfo(); break; } QString s = plainText().simplified(); return QString("%1: %2").arg(rez).arg(s); } //--------------------------------------------------------- // subtype //--------------------------------------------------------- int TextBase::subtype() const { return int(tid()); } //--------------------------------------------------------- // subtypeName //--------------------------------------------------------- QString TextBase::subtypeName() const { return score() ? score()->getTextStyleUserName(tid()) : textStyleUserName(tid()); } //--------------------------------------------------------- // fragmentList //--------------------------------------------------------- /* Return the text as a single list of TextFragment Used by the MusicXML formatted export to avoid parsing the xml text format */ QList TextBase::fragmentList() const { QList res; for (const TextBlock& block : _layout) { for (const TextFragment& f : block.fragments()) { /* TODO TBD if (f.text.empty()) // skip empty fragments, not to continue; // insert extra HTML formatting */ res.append(f); if (block.eol()) { // simply append a newline res.last().text += "\n"; } } } return res; } //--------------------------------------------------------- // validateText // check if s is a valid musescore xml text string // - simple bugs are automatically adjusted // return true if text is valid or could be fixed // (this is incomplete/experimental) //--------------------------------------------------------- bool TextBase::validateText(QString& s) { QString d; for (int i = 0; i < s.size(); ++i) { QChar c = s[i]; if (c == '&') { const char* ok[] { "amp;", "lt;", "gt;", "quot;" }; QString t = s.mid(i+1); bool found = false; for (auto k : ok) { if (t.startsWith(k)) { d.append(c); d.append(k); i += int(strlen(k)); found = true; break; } } if (!found) d.append("&"); } else if (c == '<') { const char* ok[] { "b>", "/b>", "i>", "/i>", "u>", "/u", "font ", "/font>", "sym>", "/sym>" }; QString t = s.mid(i+1); bool found = false; for (auto k : ok) { if (t.startsWith(k)) { d.append(c); d.append(k); i += int(strlen(k)); found = true; break; } } if (!found) d.append("<"); } else d.append(c); } QString ss = "" + d + "\n"; XmlReader xml(ss); while (xml.readNextStartElement()) ; // qDebug(" token %d <%s>", int(xml.tokenType()), qPrintable(xml.name().toString())); if (xml.error() == QXmlStreamReader::NoError) { s = d; return true; } qDebug("xml error at line %lld column %lld: %s", xml.lineNumber(), xml.columnNumber(), qPrintable(xml.errorString())); qDebug ("text: |%s|", qPrintable(ss)); return false; } //--------------------------------------------------------- // font //--------------------------------------------------------- QFont TextBase::font() const { qreal m = _size; if (sizeIsSpatiumDependent()) m *= spatium() / SPATIUM20; QFont f(_family, m, bold() ? QFont::Bold : QFont::Normal, italic()); if (underline()) f.setUnderline(underline()); return f; } //--------------------------------------------------------- // fontMetrics //--------------------------------------------------------- QFontMetricsF TextBase::fontMetrics() const { return QFontMetricsF(font()); } //--------------------------------------------------------- // getProperty //--------------------------------------------------------- QVariant TextBase::getProperty(Pid propertyId) const { switch (propertyId) { case Pid::SUB_STYLE: return int(tid()); case Pid::FONT_FACE: return family(); case Pid::FONT_SIZE: return size(); case Pid::FONT_STYLE: return int(fontStyle()); case Pid::FRAME_TYPE: return int(frameType()); case Pid::FRAME_WIDTH: return frameWidth(); case Pid::FRAME_PADDING: return paddingWidth(); case Pid::FRAME_ROUND: return frameRound(); case Pid::FRAME_FG_COLOR: return frameColor(); case Pid::FRAME_BG_COLOR: return bgColor(); case Pid::ALIGN: return QVariant::fromValue(align()); case Pid::TEXT: return xmlText(); default: return Element::getProperty(propertyId); } } //--------------------------------------------------------- // setProperty //--------------------------------------------------------- bool TextBase::setProperty(Pid pid, const QVariant& v) { if (textInvalid) genText(); bool rv = true; switch (pid) { case Pid::SUB_STYLE: initTid(Tid(v.toInt())); break; case Pid::FONT_FACE: setFamily(v.toString()); break; case Pid::FONT_SIZE: setSize(v.toReal()); break; case Pid::FONT_STYLE: setFontStyle(FontStyle(v.toInt())); break; case Pid::FRAME_TYPE: setFrameType(FrameType(v.toInt())); break; case Pid::FRAME_WIDTH: setFrameWidth(v.value()); break; case Pid::FRAME_PADDING: setPaddingWidth(v.value()); break; case Pid::FRAME_ROUND: setFrameRound(v.toInt()); break; case Pid::FRAME_FG_COLOR: setFrameColor(v.value()); break; case Pid::FRAME_BG_COLOR: setBgColor(v.value()); break; case Pid::TEXT: setXmlText(v.toString()); break; case Pid::ALIGN: setAlign(v.value()); break; default: rv = Element::setProperty(pid, v); break; } layoutInvalid = true; triggerLayout(); return rv; } //--------------------------------------------------------- // propertyDefault //--------------------------------------------------------- QVariant TextBase::propertyDefault(Pid id) const { if (id == Pid::Z) return Element::propertyDefault(id); if (composition()) { QVariant v = parent()->propertyDefault(id); if (v.isValid()) return v; } Sid sid = getPropertyStyle(id); if (sid != Sid::NOSTYLE) return styleValue(id, sid); QVariant v; switch (id) { case Pid::SUB_STYLE: v = int(Tid::DEFAULT); break; case Pid::TEXT: v = QString(); break; default: for (const StyledProperty& p : *textStyle(Tid::DEFAULT)) { if (p.pid == id) return styleValue(id, p.sid); } return Element::propertyDefault(id); } return v; } //--------------------------------------------------------- // getPropertyFlagsIdx //--------------------------------------------------------- int TextBase::getPropertyFlagsIdx(Pid id) const { int i = 0; for (const StyledProperty& p : *_elementStyle) { if (p.pid == id) return i; ++i; } for (const StyledProperty& p : *textStyle(tid())) { if (p.pid == id) return i; ++i; } return -1; } //--------------------------------------------------------- // getPropertyStyle //--------------------------------------------------------- Sid TextBase::getPropertyStyle(Pid id) const { for (const StyledProperty& p : *_elementStyle) { if (p.pid == id) return p.sid; } for (const StyledProperty& p : *textStyle(tid())) { if (p.pid == id) return p.sid; } return Sid::NOSTYLE; } //--------------------------------------------------------- // styleChanged //--------------------------------------------------------- void TextBase::styleChanged() { if (!styledProperties()) { qDebug("no styled properties"); return; } int i = 0; for (const StyledProperty& spp : *_elementStyle) { PropertyFlags f = _propertyFlagsList[i]; if (f == PropertyFlags::STYLED) setProperty(spp.pid, styleValue(spp.pid, getPropertyStyle(spp.pid))); ++i; } for (const StyledProperty& spp : *textStyle(tid())) { PropertyFlags f = _propertyFlagsList[i]; if (f == PropertyFlags::STYLED) setProperty(spp.pid, styleValue(spp.pid, getPropertyStyle(spp.pid))); ++i; } } //--------------------------------------------------------- // initElementStyle //--------------------------------------------------------- void TextBase::initElementStyle(const ElementStyle* ss) { _elementStyle = ss; size_t n = ss->size() + TEXT_STYLE_SIZE; delete[] _propertyFlagsList; _propertyFlagsList = new PropertyFlags[n]; for (size_t i = 0; i < n; ++i) _propertyFlagsList[i] = PropertyFlags::STYLED; for (const StyledProperty& p : *_elementStyle) setProperty(p.pid, styleValue(p.pid, p.sid)); for (const StyledProperty& p : *textStyle(tid())) setProperty(p.pid, styleValue(p.pid, p.sid)); } //--------------------------------------------------------- // initTid //--------------------------------------------------------- void TextBase::initTid(Tid tid, bool preserveDifferent) { if (! preserveDifferent) initTid(tid); else { setTid(tid); for (const StyledProperty& p : *textStyle(tid)) { if (getProperty(p.pid) == propertyDefault(p.pid)) setProperty(p.pid, styleValue(p.pid, p.sid)); } } } void TextBase::initTid(Tid tid) { setTid(tid); for (const StyledProperty& p : *textStyle(tid)) { setProperty(p.pid, styleValue(p.pid, p.sid)); } } //--------------------------------------------------------- // editCut //--------------------------------------------------------- void TextBase::editCut(EditData& ed) { TextEditData* ted = static_cast(ed.getData(this)); TextCursor* _cursor = &ted->cursor; QString s = _cursor->selectedText(); if (!s.isEmpty()) { QApplication::clipboard()->setText(s, QClipboard::Clipboard); ed.curGrip = Grip::START; ed.key = Qt::Key_Delete; ed.s = QString(); edit(ed); } } //--------------------------------------------------------- // editCopy //--------------------------------------------------------- void TextBase::editCopy(EditData& ed) { // // store selection as plain text // TextEditData* ted = static_cast(ed.getData(this)); TextCursor* _cursor = &ted->cursor; QString s = _cursor->selectedText(); if (!s.isEmpty()) QApplication::clipboard()->setText(s, QClipboard::Clipboard); } //--------------------------------------------------------- // cursor //--------------------------------------------------------- TextCursor* TextBase::cursor(const EditData& ed) { TextEditData* ted = static_cast(ed.getData(this)); Q_ASSERT(ted); return &ted->cursor; } //--------------------------------------------------------- // draw //--------------------------------------------------------- void TextBase::draw(QPainter* p) const { if (hasFrame()) { qreal baseSpatium = MScore::baseStyle().value(Sid::spatium).toDouble(); if (frameWidth().val() != 0.0) { QColor fColor = curColor(visible(), frameColor()); qreal frameWidthVal = frameWidth().val() * (sizeIsSpatiumDependent() ? spatium() : baseSpatium); QPen pen(fColor, frameWidthVal, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin); p->setPen(pen); } else p->setPen(Qt::NoPen); QColor bg(bgColor()); p->setBrush(bg.alpha() ? QBrush(bg) : Qt::NoBrush); if (circle()) p->drawEllipse(frame); else { qreal frameRoundFactor = (sizeIsSpatiumDependent() ? (spatium()/baseSpatium) / 2 : 0.5f); int r2 = frameRound() * frameRoundFactor; if (r2 > 99) r2 = 99; p->drawRoundedRect(frame, frameRound() * frameRoundFactor, r2); } } p->setBrush(Qt::NoBrush); p->setPen(textColor()); for (const TextBlock& t : _layout) t.draw(p, this); } //--------------------------------------------------------- // drawEditMode // draw edit mode decorations //--------------------------------------------------------- void TextBase::drawEditMode(QPainter* p, EditData& ed) { QPointF pos(canvasPos()); p->translate(pos); TextEditData* ted = static_cast(ed.getData(this)); if (!ted) { qDebug("ted not found"); return; } TextCursor* _cursor = &ted->cursor; if (_cursor->hasSelection()) { p->setBrush(Qt::NoBrush); p->setPen(textColor()); int r1 = _cursor->selectLine(); int r2 = _cursor->row(); int c1 = _cursor->selectColumn(); int c2 = _cursor->column(); if (r1 > r2) { qSwap(r1, r2); qSwap(c1, c2); } else if (r1 == r2) { if (c1 > c2) qSwap(c1, c2); } int row = 0; for (const TextBlock& t : _layout) { t.draw(p, this); if (row >= r1 && row <= r2) { QRectF br; if (row == r1 && r1 == r2) br = t.boundingRect(c1, c2, this); else if (row == r1) br = t.boundingRect(c1, t.columns(), this); else if (row == r2) br = t.boundingRect(0, c2, this); else br = t.boundingRect(); br.translate(0.0, t.y()); drawSelection(p, br); } ++row; } } p->setBrush(curColor()); QPen pen(curColor()); pen.setJoinStyle(Qt::MiterJoin); p->setPen(pen); // Don't draw cursor if there is a selection if (!_cursor->hasSelection()) p->drawRect(_cursor->cursorRect()); QMatrix matrix = p->matrix(); p->translate(-pos); p->setPen(QPen(QBrush(Qt::lightGray), 4.0 / matrix.m11())); // 4 pixel pen size p->setBrush(Qt::NoBrush); qreal m = spatium(); QRectF r = canvasBoundingRect().adjusted(-m, -m, m, m); // qDebug("%f %f %f %f\n", r.x(), r.y(), r.width(), r.height()); p->drawRect(r); pen = QPen(MScore::defaultColor, 0.0); } //--------------------------------------------------------- // hasCustomFormatting //--------------------------------------------------------- bool TextBase::hasCustomFormatting() const { CharFormat fmt; fmt.setFontFamily(family()); fmt.setFontSize(size()); fmt.setStyle(fontStyle()); fmt.setPreedit(false); fmt.setValign(VerticalAlignment::AlignNormal); for (const TextBlock& block : _layout) { for (const TextFragment& f : block.fragments()) { if (f.text.isEmpty()) // skip empty fragments, not to continue; // insert extra HTML formatting const CharFormat& format = f.format; if (fmt.style() != format.style()) return true; if (format.fontSize() != fmt.fontSize()) return true; if (format.fontFamily() != fmt.fontFamily()) return true; VerticalAlignment va = format.valign(); VerticalAlignment cva = fmt.valign(); if (cva != va) return true; } } return false; } //--------------------------------------------------------- // stripText // remove some custom text formatting and return // result as xml string //--------------------------------------------------------- QString TextBase::stripText(bool removeStyle, bool removeSize, bool removeFace) const { QString _txt; bool bold_ = false; bool italic_ = false; bool underline_ = false; for (const TextBlock& block : _layout) { for (const TextFragment& f : block.fragments()) { if (!f.format.bold() && bold()) bold_ = true; if (!f.format.italic() && italic()) italic_ = true; if (!f.format.underline() && underline()) underline_ = true; } } CharFormat fmt; fmt.setFontFamily(family()); fmt.setFontSize(size()); fmt.setStyle(fontStyle()); fmt.setPreedit(false); fmt.setValign(VerticalAlignment::AlignNormal); XmlNesting xmlNesting(&_txt); if (!removeStyle) { if (bold_) xmlNesting.pushB(); if (italic_) xmlNesting.pushI(); if (underline_) xmlNesting.pushU(); } for (const TextBlock& block : _layout) { for (const TextFragment& f : block.fragments()) { if (f.text.isEmpty()) // skip empty fragments, not to continue; // insert extra HTML formatting const CharFormat& format = f.format; if (!removeStyle) { if (fmt.bold() != format.bold()) { if (format.bold()) xmlNesting.pushB(); else xmlNesting.popB(); } if (fmt.italic() != format.italic()) { if (format.italic()) xmlNesting.pushI(); else xmlNesting.popI(); } if (fmt.underline() != format.underline()) { if (format.underline()) xmlNesting.pushU(); else xmlNesting.popU(); } } if (!removeSize && (format.fontSize() != fmt.fontSize())) _txt += QString("").arg(format.fontSize()); if (!removeFace && (format.fontFamily() != fmt.fontFamily())) _txt += QString("").arg(TextBase::escape(format.fontFamily())); VerticalAlignment va = format.valign(); VerticalAlignment cva = fmt.valign(); if (cva != va) { switch (va) { case VerticalAlignment::AlignNormal: xmlNesting.popToken(cva == VerticalAlignment::AlignSuperScript ? "sup" : "sub"); break; case VerticalAlignment::AlignSuperScript: xmlNesting.pushToken("sup"); break; case VerticalAlignment::AlignSubScript: xmlNesting.pushToken("sub"); break; } } _txt += XmlWriter::xmlString(f.text); fmt = format; } if (block.eol()) _txt += QChar::LineFeed; } while (!xmlNesting.empty()) xmlNesting.popToken(); return _txt; } //--------------------------------------------------------- // undoChangeProperty //--------------------------------------------------------- void TextBase::undoChangeProperty(Pid id, const QVariant& v, PropertyFlags ps) { if (ps == PropertyFlags::STYLED && v == propertyDefault(id)) { // this is a reset // remove some custom formatting if (id == Pid::FONT_STYLE) undoChangeProperty(Pid::TEXT, stripText(true, false, false), propertyFlags(id)); else if (id == Pid::FONT_SIZE) undoChangeProperty(Pid::TEXT, stripText(false, true, false), propertyFlags(id)); else if (id == Pid::FONT_FACE) undoChangeProperty(Pid::TEXT, stripText(false, false, true), propertyFlags(id)); } Element::undoChangeProperty(id, v, ps); } }