MuseScore/libmscore/textbase.cpp
James Thistlewood 48693311d4 fix #300635: crash when dropping symbol on text that had just been moved
This happened because the acceptDrop check wasn't checking that the
editData it had for the text element was TextEditData. So, if the text
had been moved before, then it would have ElementEditData, and
acceptDrop would incorrectly return true.

This fixes the problem by adding a type() method to ElementEditData and
its derived classes so a proper check can be carried out in acceptDrop.
2020-02-09 09:25:11 +00:00

2874 lines
96 KiB
C++

//=============================================================================
// 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<TextFragment>::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<VerticalAlignment>(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 <sym>xxx</sym>
//---------------------------------------------------------
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> {
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 += "</";
*_s += s;
*_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("<font size=\"%1\"/>").arg(format.fontSize());
if (format.fontFamily() != fmt.fontFamily())
_text += QString("<font face=\"%1\"/>").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<Pid, 18> 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<TextEditData*>(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<TextEditData*>(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<TextBase> 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<TextBase> 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("<font size=\"%1\"/>").arg(size_);
}
if (family_ != font.family()) {
family_ = font.family();
s += QString("<font face=\"%1\"/>").arg(family_);
}
if (font.bold())
s += "<b>";
if (font.italic())
s += "<i>";
if (font.underline())
s += "<u>";
s += f.text().toHtmlEscaped();
if (font.underline())
s += "</u>";
if (font.italic())
s += "</i>";
if (font.bold())
s += "</b>";
}
}
}
if (score() && score()->mscVersion() <= 114) {
s.replace(QChar(0xe10e), QString("<sym>accidentalNatural</sym>")); //natural
s.replace(QChar(0xe10c), QString("<sym>accidentalSharp</sym>")); // sharp
s.replace(QChar(0xe10d), QString("<sym>accidentalFlat</sym>")); // flat
s.replace(QChar(0xe104), QString("<sym>metNoteHalfUp</sym>")), // note2_Sym
s.replace(QChar(0xe105), QString("<sym>metNoteQuarterUp</sym>")); // note4_Sym
s.replace(QChar(0xe106), QString("<sym>metNote8thUp</sym>")); // note8_Sym
s.replace(QChar(0xe107), QString("<sym>metNote16thUp</sym>")); // note16_Sym
s.replace(QChar(0xe108), QString("<sym>metNote32ndUp</sym>")); // note32_Sym
s.replace(QChar(0xe109), QString("<sym>metNote64thUp</sym>")); // note64_Sym
s.replace(QChar(0xe10a), QString("<sym>metAugmentationDot</sym>")); // dot
s.replace(QChar(0xe10b), QString("<sym>metAugmentationDot</sym><sym>space</sym><sym>metAugmentationDot</sym>")); // dotdot
s.replace(QChar(0xe167), QString("<sym>segno</sym>")); // segno
s.replace(QChar(0xe168), QString("<sym>coda</sym>")); // coda
s.replace(QChar(0xe169), QString("<sym>codaSquare</sym>")); // 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("<html><body style=\"font-family:'%1'; font-size:%2pt;\">%3</body></html>").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 = "</" + tag + ">";
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 = "</" + tag + ">";
QString closeProxy = "!!/" + tag + "!!";
s.replace(openProxy, openTag);
s.replace(closeProxy, closeTag);
}
return s;
}
//---------------------------------------------------------
// unEscape
//---------------------------------------------------------
QString TextBase::unEscape(QString s)
{
s.replace("&lt;", "<");
s.replace("&gt;", ">");
s.replace("&amp;", "&");
s.replace("&quot;", "\"");
return s;
}
//---------------------------------------------------------
// escape
//---------------------------------------------------------
QString TextBase::escape(QString s)
{
s.replace("<", "&lt;");
s.replace(">", "&gt;");
s.replace("&", "&amp;");
s.replace("\"", "&quot;");
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<TextFragment> TextBase::fragmentList() const
{
QList<TextFragment> 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("&amp;");
}
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("&lt;");
}
else
d.append(c);
}
QString ss = "<data>" + d + "</data>\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<Spatium>());
break;
case Pid::FRAME_PADDING:
setPaddingWidth(v.value<Spatium>());
break;
case Pid::FRAME_ROUND:
setFrameRound(v.toInt());
break;
case Pid::FRAME_FG_COLOR:
setFrameColor(v.value<QColor>());
break;
case Pid::FRAME_BG_COLOR:
setBgColor(v.value<QColor>());
break;
case Pid::TEXT:
setXmlText(v.toString());
break;
case Pid::ALIGN:
setAlign(v.value<Align>());
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<TextEditData*>(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<TextEditData*>(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<TextEditData*>(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<TextEditData*>(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("<font size=\"%1\"/>").arg(format.fontSize());
if (!removeFace && (format.fontFamily() != fmt.fontFamily()))
_txt += QString("<font face=\"%1\"/>").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);
}
}