//============================================================================= // MuseScore // Music Composition & Notation // // Copyright (C) 2018 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 "textedit.h" #include "score.h" namespace Ms { //--------------------------------------------------------- // editInsertText //--------------------------------------------------------- void TextBase::editInsertText(TextCursor* cursor, const QString& s) { Q_ASSERT(!layoutInvalid); textInvalid = true; int col = 0; for (const QChar& c : s) { if (!c.isHighSurrogate()) ++col; } cursor->curLine().insert(cursor, s); cursor->setColumn(cursor->column() + col); cursor->clearSelection(); triggerLayout(); } //--------------------------------------------------------- // startEdit //--------------------------------------------------------- void TextBase::startEdit(EditData& ed) { ed.grips = 0; TextEditData* ted = new TextEditData(this); ted->e = this; ted->cursor.setRow(0); ted->cursor.setColumn(0); ted->cursor.clearSelection(); Q_ASSERT(!score()->undoStack()->active()); // make sure we are not in a Cmd ted->oldXmlText = xmlText(); ted->startUndoIdx = score()->undoStack()->getCurIdx(); if (layoutInvalid) layout(); if (!ted->cursor.set(ed.startMove)) ted->cursor.init(); qreal _spatium = spatium(); // refresh edit bounding box score()->addRefresh(canvasBoundingRect().adjusted(-_spatium, -_spatium, _spatium, _spatium)); ed.addData(ted); } //--------------------------------------------------------- // endEdit //--------------------------------------------------------- void TextBase::endEdit(EditData& ed) { TextEditData* ted = static_cast(ed.getData(this)); const QString actualText = xmlText(); UndoStack* undo = score()->undoStack(); while (undo->getCurIdx() > ted->startUndoIdx) undo->undo(&ed); // replace all undo/redo records collected during text editing with // one property change if (ted->oldXmlText.isEmpty()) { UndoStack* us = score()->undoStack(); UndoCommand* ucmd = us->last(); if (ucmd) { const QList& cl = ucmd->commands(); const UndoCommand* cmd = cl.back(); if (strncmp(cmd->name(), "Add:", 4) == 0) { const AddElement* ae = static_cast(cmd); if (ae->getElement() == this) { if (actualText.isEmpty()) { // we just created this empty text, rollback that operation us->rollback(); score()->update(); ed.element = 0; } else { us->reopen(); // combine undo records of text creation with text editing undoChangeProperty(Pid::TEXT, actualText); layout1(); score()->endCmd(); } return; } } } } if (actualText.isEmpty()) { qDebug("actual text is empty"); score()->startCmd(); score()->undoRemoveElement(this); ed.element = 0; score()->endCmd(); return; } score()->startCmd(); undoChangeProperty(Pid::TEXT, actualText); // change property to set text to actual value again // this also changes text of linked elements layout1(); triggerLayout(); // force relayout even if text did not change score()->endCmd(); static const qreal w = 2.0; score()->addRefresh(canvasBoundingRect().adjusted(-w, -w, w, w)); } //--------------------------------------------------------- // insertSym //--------------------------------------------------------- void TextBase::insertSym(EditData& ed, SymId id) { TextEditData* ted = static_cast(ed.getData(this)); TextCursor* _cursor = &ted->cursor; deleteSelectedText(ed); QString s = score()->scoreFont()->toString(id); CharFormat fmt = *_cursor->format(); // save format // uint code = ScoreFont::fallbackFont()->sym(id).code(); _cursor->format()->setFontFamily("ScoreText"); _cursor->format()->setBold(false); _cursor->format()->setItalic(false); score()->undo(new InsertText(_cursor, s), &ed); _cursor->setFormat(fmt); // restore format } //--------------------------------------------------------- // insertText //--------------------------------------------------------- void TextBase::insertText(EditData& ed, const QString& s) { TextEditData* ted = static_cast(ed.getData(this)); TextCursor* _cursor = &ted->cursor; score()->undo(new InsertText(_cursor, s), &ed); } //--------------------------------------------------------- // edit //--------------------------------------------------------- bool TextBase::edit(EditData& ed) { TextEditData* ted = static_cast(ed.getData(this)); TextCursor* _cursor = &ted->cursor; // do nothing on Shift, it messes up IME on Windows. See #64046 if (ed.key == Qt::Key_Shift) return false; QString s = ed.s; bool ctrlPressed = ed.modifiers & Qt::ControlModifier; bool shiftPressed = ed.modifiers & Qt::ShiftModifier; QTextCursor::MoveMode mm = shiftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor; bool wasHex = false; if (hexState >= 0) { if (ed.modifiers == (Qt::ControlModifier | Qt::ShiftModifier | Qt::KeypadModifier)) { switch (ed.key) { case Qt::Key_0: case Qt::Key_1: case Qt::Key_2: case Qt::Key_3: case Qt::Key_4: case Qt::Key_5: case Qt::Key_6: case Qt::Key_7: case Qt::Key_8: case Qt::Key_9: s = QChar::fromLatin1(ed.key); ++hexState; wasHex = true; break; default: break; } } else if (ed.modifiers == (Qt::ControlModifier | Qt::ShiftModifier)) { switch (ed.key) { case Qt::Key_A: case Qt::Key_B: case Qt::Key_C: case Qt::Key_D: case Qt::Key_E: case Qt::Key_F: s = QChar::fromLatin1(ed.key); ++hexState; wasHex = true; break; default: break; } } } if (!wasHex) { //printf("======%x\n", s.isEmpty() ? -1 : s[0].unicode()); switch (ed.key) { case Qt::Key_Z: // happens when the undo stack is empty if (ed.modifiers == Qt::ControlModifier) return true; break; case Qt::Key_Enter: case Qt::Key_Return: deleteSelectedText(ed); score()->undo(new SplitText(_cursor), &ed); return true; case Qt::Key_Delete: if (!deleteSelectedText(ed)) score()->undo(new RemoveText(_cursor, QString(_cursor->currentCharacter())), &ed); return true; case Qt::Key_Backspace: if (!deleteSelectedText(ed)) { if (_cursor->column() == 0 && _cursor->row() != 0) score()->undo(new JoinText(_cursor), &ed); else { if (!_cursor->movePosition(QTextCursor::Left)) return false; score()->undo(new RemoveText(_cursor, QString(_cursor->currentCharacter())), &ed); } } return true; case Qt::Key_Left: if (!_cursor->movePosition(ctrlPressed ? QTextCursor::WordLeft : QTextCursor::Left, mm) && type() == ElementType::LYRICS) return false; s.clear(); break; case Qt::Key_Right: if (!_cursor->movePosition(ctrlPressed ? QTextCursor::NextWord : QTextCursor::Right, mm) && type() == ElementType::LYRICS) return false; s.clear(); break; case Qt::Key_Up: #if defined(Q_OS_MAC) if (!_cursor->movePosition(QTextCursor::Up, mm)) _cursor->movePosition(QTextCursor::StartOfLine, mm); #else _cursor->movePosition(QTextCursor::Up, mm); #endif s.clear(); break; case Qt::Key_Down: #if defined(Q_OS_MAC) if (!_cursor->movePosition(QTextCursor::Down, mm)) _cursor->movePosition(QTextCursor::EndOfLine, mm); #else _cursor->movePosition(QTextCursor::Down, mm); #endif s.clear(); break; case Qt::Key_Home: if (ctrlPressed) _cursor->movePosition(QTextCursor::Start, mm); else _cursor->movePosition(QTextCursor::StartOfLine, mm); s.clear(); break; case Qt::Key_End: if (ctrlPressed) _cursor->movePosition(QTextCursor::End, mm); else _cursor->movePosition(QTextCursor::EndOfLine, mm); s.clear(); break; case Qt::Key_Tab: s = " "; ed.modifiers = 0; break; case Qt::Key_Space: if (ed.modifiers & CONTROL_MODIFIER) { s = QString(QChar(0xa0)); // non-breaking space } else { if (isFingering() && ed.view) { score()->endCmd(); ed.view->textTab(ed.modifiers & Qt::ShiftModifier); return true; } s = " "; } ed.modifiers = 0; break; case Qt::Key_Minus: if (ed.modifiers == 0) s = "-"; break; case Qt::Key_Underscore: if (ed.modifiers == 0) s = "_"; break; case Qt::Key_A: if (ctrlPressed) { selectAll(_cursor); s.clear(); } break; default: break; } if (ctrlPressed && shiftPressed) { switch (ed.key) { case Qt::Key_U: if (hexState == -1) { hexState = 0; s = "u"; } break; case Qt::Key_B: s = "\u266d"; // Unicode flat break; case Qt::Key_NumberSign: s = "\u266f"; // Unicode sharp break; case Qt::Key_H: s = "\u266e"; // Unicode natural break; case Qt::Key_Space: insertSym(ed, SymId::space); return true; case Qt::Key_F: insertSym(ed, SymId::dynamicForte); return true; case Qt::Key_M: insertSym(ed, SymId::dynamicMezzo); return true; case Qt::Key_N: insertSym(ed, SymId::dynamicNiente); return true; case Qt::Key_P: insertSym(ed, SymId::dynamicPiano); return true; case Qt::Key_S: insertSym(ed, SymId::dynamicSforzando); return true; case Qt::Key_R: insertSym(ed, SymId::dynamicRinforzando); return true; case Qt::Key_Z: // Ctrl+Z is normally "undo" // but this code gets hit even if you are also holding Shift // so Shift+Ctrl+Z works insertSym(ed, SymId::dynamicZ); return true; } } } if (!s.isEmpty()) { deleteSelectedText(ed); score()->undo(new InsertText(_cursor, s), &ed); } return true; } //--------------------------------------------------------- // movePosition //--------------------------------------------------------- void TextBase::movePosition(EditData& ed, QTextCursor::MoveOperation op) { TextEditData* ted = static_cast(ed.getData(this)); TextCursor* _cursor = &ted->cursor; _cursor->movePosition(op); score()->addRefresh(canvasBoundingRect()); score()->update(); } //--------------------------------------------------------- // ChangeText::insertText //--------------------------------------------------------- void ChangeText::insertText(EditData* ed) { TextCursor tc = c; c.text()->editInsertText(&tc, s); if (ed) { TextCursor* ttc = c.text()->cursor(*ed); *ttc = tc; } } //--------------------------------------------------------- // ChangeText::removeText //--------------------------------------------------------- void ChangeText::removeText(EditData* ed) { TextCursor tc = c; TextBlock& l = c.curLine(); int column = c.column(); for (int n = 0; n < s.size(); ++n) l.remove(column); c.text()->triggerLayout(); if (ed) *c.text()->cursor(*ed) = tc; c.text()->setTextInvalid(); } //--------------------------------------------------------- // SplitJoinText //--------------------------------------------------------- void SplitJoinText::join(EditData* ed) { TextBase* t = c.text(); int line = c.row(); t->setTextInvalid(); t->triggerLayout(); CharFormat* charFmt = c.format(); // take current format int col = t->textBlock(line-1).columns(); int eol = t->textBlock(line).eol(); t->textBlock(line-1).fragments().append(t->textBlock(line).fragments()); int lines = t->rows(); if (line < lines) t->textBlock(line).setEol(eol); t->textBlockList().removeAt(line); c.setRow(line-1); c.setColumn(col); c.setFormat(*charFmt); // restore orig. format at new line c.clearSelection(); if (ed) *t->cursor(*ed) = c; c.text()->setTextInvalid(); } void SplitJoinText::split(EditData* ed) { TextBase* t = c.text(); int line = c.row(); t->setTextInvalid(); t->triggerLayout(); CharFormat* charFmt = c.format(); // take current format t->textBlockList().insert(line + 1, c.curLine().split(c.column())); c.curLine().setEol(true); c.setRow(line+1); c.curLine().setEol(true); c.setColumn(0); c.setFormat(*charFmt); // restore orig. format at new line c.clearSelection(); if (ed) *t->cursor(*ed) = c; c.text()->setTextInvalid(); } //--------------------------------------------------------- // drop //--------------------------------------------------------- Element* TextBase::drop(EditData& ed) { TextCursor* _cursor = cursor(ed); Element* e = ed.dropElement; switch (e->type()) { case ElementType::SYMBOL: { SymId id = toSymbol(e)->sym(); delete e; insertSym(ed, id); } break; case ElementType::FSYMBOL: { uint code = toFSymbol(e)->code(); QString s = QString::fromUcs4(&code, 1); delete e; deleteSelectedText(ed); score()->undo(new InsertText(_cursor, s), &ed); } break; default: qDebug("drop <%s> not handled", e->name()); break; } return 0; } //--------------------------------------------------------- // paste //--------------------------------------------------------- void TextBase::paste(EditData& ed) { QString txt = QApplication::clipboard()->text(QClipboard::Clipboard); if (MScore::debugMode) qDebug("<%s>", qPrintable(txt)); int state = 0; QString token; QString sym; bool symState = false; score()->startCmd(); for (int i = 0; i < txt.length(); i++ ) { QChar c = txt[i]; if (state == 0) { if (c == '<') { state = 1; token.clear(); } else if (c == '&') { state = 2; token.clear(); } else { if (symState) sym += c; else { deleteSelectedText(ed); if (c.isHighSurrogate()) { QChar highSurrogate = c; Q_ASSERT(i + 1 < txt.length()); i++; QChar lowSurrogate = txt[i]; insertText(ed, QString(QChar::surrogateToUcs4(highSurrogate, lowSurrogate))); } else { insertText(ed, QString(QChar(c.unicode()))); } } } } else if (state == 1) { if (c == '>') { state = 0; if (token == "sym") { symState = true; sym.clear(); } else if (token == "/sym") { symState = false; insertSym(ed, Sym::name2id(sym)); } } else token += c; } else if (state == 2) { if (c == ';') { state = 0; if (token == "lt") insertText(ed, "<"); else if (token == "gt") insertText(ed, ">"); else if (token == "amp") insertText(ed, "&"); else if (token == "quot") insertText(ed, "\""); else insertSym(ed, Sym::name2id(token)); } else if (!c.isLetter()) { state = 0; insertText(ed, "&"); insertText(ed, token); insertText(ed, c); } else token += c; } } if (state == 2) { insertText(ed, "&"); insertText(ed, token); } score()->endCmd(); } //--------------------------------------------------------- // inputTransition // - preedit string should not influence then undo/redo stack // - commit string goes onto the undo/redo stack //--------------------------------------------------------- void TextBase::inputTransition(EditData& ed, QInputMethodEvent* ie) { TextEditData* ted = static_cast(ed.getData(this)); TextCursor* _cursor = &ted->cursor; // remove preedit string int n = preEdit.size(); while (n--) { if (_cursor->movePosition(QTextCursor::Left)) { TextBlock& l = _cursor->curLine(); l.remove(_cursor->column()); _cursor->text()->triggerLayout(); _cursor->text()->setTextInvalid(); } } qDebug("<%s><%s> len %d start %d, preEdit size %d", qPrintable(ie->commitString()), qPrintable(ie->preeditString()), ie->replacementLength(), ie->replacementStart(), preEdit.size()); if (!ie->commitString().isEmpty()) { _cursor->format()->setPreedit(false); score()->startCmd(); insertText(ed, ie->commitString()); score()->endCmd(); preEdit.clear(); } else { preEdit = ie->preeditString(); if (!preEdit.isEmpty()) { #if 0 for (auto a : ie->attributes()) { switch(a.type) { case QInputMethodEvent::TextFormat: { qDebug(" attribute TextFormat: %d-%d", a.start, a.length); QTextFormat tf = a.value.value(); } break; case QInputMethodEvent::Cursor: qDebug(" attribute Cursor at %d", a.start); break; default: qDebug(" attribute %d", a.type); } } #endif _cursor->format()->setPreedit(true); _cursor->updateCursorFormat(); editInsertText(_cursor, preEdit); setTextInvalid(); layout1(); score()->update(); } } ie->accept(); } //--------------------------------------------------------- // endHexState //--------------------------------------------------------- void TextBase::endHexState(EditData& ed) { TextEditData* ted = static_cast(ed.getData(this)); TextCursor* _cursor = &ted->cursor; if (hexState >= 0) { if (hexState > 0) { int c2 = _cursor->column(); int c1 = c2 - (hexState + 1); TextBlock& t = _layout[_cursor->row()]; QString ss = t.remove(c1, hexState + 1); bool ok; int code = ss.mid(1).toInt(&ok, 16); _cursor->setColumn(c1); _cursor->clearSelection(); if (ok) editInsertText(_cursor, QString(code)); else qDebug("cannot convert hex string <%s>, state %d (%d-%d)", qPrintable(ss.mid(1)), hexState, c1, c2); } hexState = -1; } } //--------------------------------------------------------- // deleteSelectedText //--------------------------------------------------------- bool TextBase::deleteSelectedText(EditData& ed) { TextCursor* _cursor = cursor(ed); if (!_cursor->hasSelection()) return false; int r1 = _cursor->selectLine(); int c1 = _cursor->selectColumn(); if (r1 > _cursor->row() || (r1 == _cursor->row() && c1 > _cursor->column())) { // swap start end of selection r1 = _cursor->row(); c1 = _cursor->column(); _cursor->setRow(_cursor->selectLine()); _cursor->setColumn(_cursor->selectColumn()); } _cursor->clearSelection(); for (;;) { if (r1 == _cursor->row() && c1 == _cursor->column()) break; if (_cursor->column() == 0 && _cursor->row() != 0) score()->undo(new JoinText(_cursor), &ed); else { // move cursor left: if (!_cursor->movePosition(QTextCursor::Left)) break; score()->undo(new RemoveText(_cursor, QString(_cursor->currentCharacter())), &ed); } } return true; } } // namespace Ms