LCOV - code coverage report
Current view: top level - src/engraving/editing - textedit.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 27.3 % 605 165
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-Studio-CLA-applies
       4              :  *
       5              :  * MuseScore Studio
       6              :  * Music Composition & Notation
       7              :  *
       8              :  * Copyright (C) 2021 MuseScore Limited and others
       9              :  *
      10              :  * This program is free software: you can redistribute it and/or modify
      11              :  * it under the terms of the GNU General Public License version 3 as
      12              :  * published by the Free Software Foundation.
      13              :  *
      14              :  * This program is distributed in the hope that it will be useful,
      15              :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      16              :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17              :  * GNU General Public License for more details.
      18              :  *
      19              :  * You should have received a copy of the GNU General Public License
      20              :  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
      21              :  */
      22              : 
      23              : #include "textedit.h"
      24              : 
      25              : #include "mscoreview.h"
      26              : #include "navigation.h"
      27              : #include "transaction/transaction.h"
      28              : #include "transaction/undostack.h"
      29              : 
      30              : #include "iengravingfont.h"
      31              : #include "types/symnames.h"
      32              : 
      33              : #include "../dom/fret.h"
      34              : #include "../dom/lyrics.h"
      35              : #include "../dom/masterscore.h"
      36              : #include "../dom/score.h"
      37              : #include "../dom/symbol.h"
      38              : #include "../dom/utils.h"
      39              : 
      40              : #include "../rendering/iscorerenderer.h"
      41              : 
      42              : #include "log.h"
      43              : 
      44              : using namespace mu::engraving;
      45              : 
      46              : //---------------------------------------------------------
      47              : //   ~TextEditData
      48              : //---------------------------------------------------------
      49              : 
      50            4 : TextEditData::~TextEditData()
      51              : {
      52            4 :     if (deleteText) {
      53            0 :         for (EngravingObject* se : m_textBase->linkList()) {
      54            0 :             toTextBase(se)->deleteLater();
      55            0 :         }
      56              :     }
      57            4 : }
      58              : 
      59              : //---------------------------------------------------------
      60              : //   editInsertText
      61              : //---------------------------------------------------------
      62              : 
      63            5 : void TextBase::editInsertText(TextCursor* cursor, const String& s)
      64              : {
      65            5 :     const LayoutData* ldata = this->ldata();
      66            5 :     IF_ASSERT_FAILED(ldata && !ldata->layoutInvalid) {
      67            0 :         return;
      68              :     }
      69              : 
      70            5 :     m_textInvalid = true;
      71              : 
      72            5 :     int col = 0;
      73           46 :     for (size_t i = 0; i < s.size(); ++i) {
      74           41 :         if (!s.at(i).isHighSurrogate()) {
      75           41 :             ++col;
      76              :         }
      77              :     }
      78              : 
      79            5 :     const TextBlock& block = ldata->blocks.at(cursor->row());
      80            5 :     const CharFormat* previousFormat = block.formatAt(std::max(int(cursor->column()) - 1, 0));
      81            5 :     if (previousFormat && previousFormat->fontFamily() == "ScoreText" && s == " ") {
      82              :         // This space would be ignored by the xml parser (see #15629)
      83              :         // We must use the nonBreaking space character instead
      84            0 :         String nonBreakingSpace = String(Char(0xa0));
      85            0 :         cursor->curLine().insert(cursor, nonBreakingSpace);
      86            0 :     } else {
      87            5 :         cursor->curLine().insert(cursor, s);
      88              :     }
      89              : 
      90            5 :     cursor->setColumn(cursor->column() + col);
      91            5 :     cursor->clearSelection();
      92              : 
      93            5 :     triggerLayout();
      94              : }
      95              : 
      96              : //---------------------------------------------------------
      97              : //   startEdit
      98              : //---------------------------------------------------------
      99              : 
     100            4 : void TextBase::startEdit(EditData& ed)
     101              : {
     102            4 :     std::shared_ptr<TextEditData> ted = std::make_shared<TextEditData>(this);
     103            4 :     ted->e = this;
     104            4 :     cursor()->startEdit();
     105              : 
     106            4 :     assert(!score()->undoStack()->hasActiveTransaction());
     107              : 
     108            4 :     ted->oldXmlText = xmlText();
     109            4 :     ted->startUndoIdx = score()->undoStack()->currentIndex();
     110              : 
     111            4 :     const LayoutData* ldata = this->ldata();
     112            4 :     if (!ldata || ldata->layoutInvalid) {
     113            4 :         renderer()->layoutItem(this);
     114              :     }
     115              : 
     116              :     //! NOTE: startMove will be null if we didn't use the mouse (e.g. we added a lyric with the spacebar)
     117            4 :     if (!ed.startMove.isNull() && !cursor()->set(ed.startMove)) {
     118            0 :         resetFormatting();
     119              :     }
     120            4 :     double _spatium = spatium();
     121              :     // refresh edit bounding box
     122            4 :     score()->addRefresh(canvasBoundingRect().adjusted(-_spatium, -_spatium, _spatium, _spatium));
     123            4 :     ed.addData(ted);
     124            4 : }
     125              : 
     126              : //---------------------------------------------------------
     127              : //   endEdit
     128              : //---------------------------------------------------------
     129              : 
     130            4 : void TextBase::endEdit(EditData& ed)
     131              : {
     132            4 :     TextEditData* ted = static_cast<TextEditData*>(ed.getData(this).get());
     133            4 :     IF_ASSERT_FAILED(ted) {
     134            1 :         return;
     135              :     }
     136              : 
     137            4 :     cursor()->endEdit();
     138              : 
     139            4 :     UndoStack* undo = score()->undoStack();
     140            4 :     IF_ASSERT_FAILED(undo) {
     141            0 :         return;
     142              :     }
     143              : 
     144            4 :     const String actualXmlText = xmlText();
     145            4 :     const String actualPlainText = plainText();
     146              : 
     147              :     // replace all undo/redo records collected during text editing with
     148              :     // one property change
     149              : 
     150              :     //! NOTE: Current index can be less than the start index if the text element is newly added and immediately removed through
     151              :     //! undo (the "add element" command will have been popped from the stack before the calling of this method)...
     152            4 :     const bool textWasEdited = this->textWasEdited(ed);
     153            4 :     if (textWasEdited) {
     154            3 :         undo->mergeTransactions(ted->startUndoIdx);
     155            3 :         undo->last()->removeCommandsMatchingFilter(UndoableCommandFilter::TextEdit, this);
     156              :     } else {
     157              :         // No text changes in "undo" part of undo stack,
     158              :         // hence nothing to merge and filter.
     159            1 :         undo->cleanRedoStack(); // prevent text editing commands from remaining in undo stack
     160              : 
     161              :         // already removed, no further processing required...
     162            1 :         if (!ed.element) {
     163            1 :             return;
     164              :         }
     165              :     }
     166              : 
     167            3 :     const bool textStartedEmpty = ted->oldXmlText.isEmpty();
     168            3 :     const UndoableTransaction* addTransaction = !textStartedEmpty ? nullptr
     169            0 :                                                 : (textWasEdited ? undo->prev() : undo->last());
     170              :     const bool newlyAdded = addTransaction
     171            3 :                             && addTransaction->hasCommandsMatchingFilter(UndoableCommandFilter::AddElement, this);
     172            3 :     if (newlyAdded) {
     173              :         // We have just added this element to a score.
     174              :         // Combine undo records of text creation with text editing.
     175            0 :         undo->mergeTransactions(ted->startUndoIdx - 1);
     176              :     }
     177              : 
     178              :     // TBox'es manage their Text themselves and are not removed if text is empty
     179            3 :     const bool removeTextIfEmpty = !(ownershipParent() && ownershipParent()->isTBox());
     180              : 
     181            3 :     if (actualPlainText.isEmpty() && removeTextIfEmpty) {
     182            0 :         LOGD("actual text is empty");
     183              : 
     184            0 :         if (newlyAdded) {
     185            0 :             score()->masterScore()->transactionManager()->reopen();
     186              :             // Rollback the "AddElement" command, but don't delete the rolled-back element(s):
     187            0 :             score()->endCmd(true, false, /*keepRolledBackElements*/ true);
     188              :             // Defer deletion (to ~TextEditData):
     189            0 :             ted->deleteText = true;
     190              :         } else {
     191            0 :             if (textWasEdited) {
     192              :                 // The text was just edited down to empty: the top transaction is this element's own edit:
     193            0 :                 score()->masterScore()->transactionManager()->reopen();
     194              :             } else {
     195              :                 /* The text was already empty before editing began (e.g. an empty text from a
     196              :                  * legacy/corrupt file). No undo command relevant to this text resides on undo
     197              :                  * stack and reopen() would corrupt the previous command: */
     198            0 :                 score()->startCmd(TranslatableString("undoableAction", "Remove empty text"));
     199              :             }
     200              : 
     201            0 :             score()->undoRemoveElement(this);
     202              : 
     203            0 :             if (isLyrics()) {
     204            0 :                 PartialLyricsLine* partialDash = findPrevPartialLyricsLineDash(toLyrics(this));
     205            0 :                 if (partialDash) {
     206              :                     // TODO: Making this operation undoable only makes sense if the text is restored too which is currently isn't
     207              :                     // Otherwise, we end up with a trailing partial dash with no ending syllable
     208            0 :                     score()->removeElement(partialDash);
     209              :                 }
     210              :             }
     211              : 
     212            0 :             commitText();
     213              :         }
     214            0 :         ed.element = 0;
     215              : 
     216            0 :         if (isLyrics()) {
     217            0 :             Lyrics* prev = Navigation::prevLyrics(toLyrics(this));
     218            0 :             if (prev) {
     219            0 :                 prev->setNeedRemoveInvalidSegments();
     220            0 :                 renderer()->layoutItem(prev);
     221              :             }
     222              :         }
     223              : 
     224            0 :         if (isHarmony() && ownershipParent()->isFretDiagram()) {
     225            0 :             score()->select(toFretDiagram(ownershipParent()), SelectType::SINGLE);
     226              :         }
     227              : 
     228            0 :         return;
     229              :     }
     230              : 
     231            3 :     if (textWasEdited) {
     232            3 :         setXmlText(ted->oldXmlText); // reset text to value before editing
     233            3 :         score()->masterScore()->transactionManager()->reopen();
     234            3 :         resetFormatting();
     235              : 
     236              :         // change property to set text to actual value again - this also changes text of linked elements
     237            3 :         undoChangeProperty(Pid::TEXT, actualXmlText);
     238              : 
     239            3 :         if (isDynamic()) {
     240            3 :             undoChangeProperty(Pid::DYNAMIC_TYPE, actualXmlText);
     241              :         }
     242              : 
     243            3 :         renderer()->layoutText1(this);
     244              :     }
     245              : 
     246            3 :     triggerLayout(); // force relayout even if text did not change
     247              : 
     248            3 :     if (isLyrics()) {
     249              :         // we must adjust previous lyrics before the call to commitText(), in order to make the adjustments
     250              :         // part of the same undo command. there is logic above that will skip this call if the text is empty
     251            0 :         toLyrics(this)->adjustPrevious();
     252              :     }
     253              : 
     254              :     static const double w = 2.0;
     255            3 :     score()->addRefresh(canvasBoundingRect().adjusted(-w, -w, w, w));
     256              : 
     257            3 :     commitText();
     258            5 : }
     259              : 
     260            3 : void TextBase::commitText()
     261              : {
     262            3 :     score()->endCmd();
     263            3 : }
     264              : 
     265            4 : bool TextBase::textWasEdited(EditData& ed) const
     266              : {
     267              :     // Only for use in overriden endEdit functions BEFORE TextBase::endEdit is called
     268            4 :     TextEditData* ted = static_cast<TextEditData*>(ed.getData(this).get());
     269            4 :     IF_ASSERT_FAILED(ted) {
     270            0 :         return false;
     271              :     }
     272              : 
     273            4 :     UndoStack* undo = score()->undoStack();
     274            4 :     IF_ASSERT_FAILED(undo) {
     275            0 :         return false;
     276              :     }
     277              : 
     278            4 :     return undo->currentIndex() > ted->startUndoIdx;
     279              : }
     280              : 
     281              : //---------------------------------------------------------
     282              : //   insertSym
     283              : //---------------------------------------------------------
     284              : 
     285            0 : void TextBase::insertSym(SymId id)
     286              : {
     287            0 :     deleteSelectedText();
     288            0 :     String s = score()->engravingFont()->toString(id);
     289            0 :     cursor()->format()->setFontFamily(u"ScoreText");
     290            0 :     score()->undo(new InsertText(cursor(), s));
     291            0 : }
     292              : 
     293              : //---------------------------------------------------------
     294              : //   insertText
     295              : //---------------------------------------------------------
     296              : 
     297            0 : void TextBase::insertText(const String& s)
     298              : {
     299            0 :     score()->undo(new InsertText(cursor(), s));
     300            0 : }
     301              : 
     302            1 : bool TextBase::isEditAllowed(EditData& ed) const
     303              : {
     304              :     // Keep this method closely in sync with TextBase::edit()!
     305              : 
     306            1 :     if (ed.key == Key_Shift || ed.key == Key_Escape || ed.key == Key_Tab) {
     307            0 :         return false;
     308              :     }
     309              : 
     310            1 :     bool ctrlPressed  = ed.modifiers & ControlModifier;
     311            1 :     bool shiftPressed = ed.modifiers & ShiftModifier;
     312            1 :     bool altPressed = ed.modifiers & AltModifier;
     313              : 
     314              :     // Hex
     315            1 :     if (ed.modifiers == (ControlModifier | ShiftModifier | KeypadModifier)) {
     316            0 :         switch (ed.key) {
     317            0 :         case Key_0:
     318              :         case Key_1:
     319              :         case Key_2:
     320              :         case Key_3:
     321              :         case Key_4:
     322              :         case Key_5:
     323              :         case Key_6:
     324              :         case Key_7:
     325              :         case Key_8:
     326              :         case Key_9:
     327            0 :             return true;
     328            0 :         default: break;
     329              :         }
     330            1 :     } else if (ed.modifiers == (ControlModifier | ShiftModifier)) {
     331            0 :         switch (ed.key) {
     332            0 :         case Key_A:
     333              :         case Key_B:
     334              :         case Key_C:
     335              :         case Key_D:
     336              :         case Key_E:
     337              :         case Key_F:
     338            0 :             return true;
     339            0 :         default: break;
     340              :         }
     341              :     }
     342              : 
     343            1 :     switch (ed.key) {
     344              :     // Basic editing / navigation
     345            0 :     case Key_Enter:
     346              :     case Key_Return:
     347              :     case Key_Delete:
     348              :     case Key_Backspace:
     349              :     case Key_Left:
     350              :     case Key_Right:
     351              :     case Key_Up:
     352              :     case Key_Down:
     353              :     case Key_Home:
     354              :     case Key_End:
     355              :     case Key_Space:
     356              :     case Key_Minus:
     357              :     case Key_Underscore:
     358            0 :         return true;
     359              :     // Select all is handled by us
     360            0 :     case Key_A:
     361            0 :         if (ctrlPressed && !shiftPressed) {
     362            0 :             return true;
     363              :         }
     364            0 :         break;
     365            0 :     case Key_Z:
     366            0 :         if (ctrlPressed /* undo */ || (ctrlPressed && shiftPressed) /* redo */) {
     367            0 :             return false;
     368              :         }
     369            0 :         break;
     370              :     // Several commands
     371              :     // TODO: looks like we're hard-coding keyboard shortcuts here, but what if user has edited those?
     372            0 :     case Key_B:
     373              :     case Key_C:
     374              :     case Key_I:
     375              :     case Key_U:
     376              :     case Key_V:
     377              :     case Key_X:
     378              :     case Key_Y:
     379            0 :         if (ctrlPressed && !shiftPressed) {
     380            0 :             return false; // handled at application level
     381              :         }
     382            0 :         break;
     383            1 :     default:
     384            1 :         break;
     385              :     }
     386              : 
     387              :     // Insert special characters
     388            1 :     if (ctrlPressed && shiftPressed) {
     389            0 :         switch (ed.key) {
     390            0 :         case Key_U:
     391              :         case Key_B:
     392              :         case Key_NumberSign: // e.g. QWERTY (US)
     393              :         case Key_AsciiTilde: // e.g. QWERTY (GB)
     394              :         case Key_Apostrophe: // e.g. QWERTZ (DE)
     395              :         case Key_periodcentered: // e.g. QWERTY (ES)
     396              :         case Key_3: // e.g. AZERTY (FR, BE)
     397              :         case Key_H:
     398              :         case Key_Space:
     399              :         case Key_F:
     400              :         case Key_M:
     401              :         case Key_N:
     402              :         case Key_P:
     403              :         case Key_S:
     404              :         case Key_R:
     405            0 :             return true;
     406              :         }
     407              :     }
     408            1 :     if (ctrlPressed && altPressed) {
     409            0 :         if (ed.key == Key_Minus) {
     410            0 :             return true;
     411              :         }
     412              : 
     413              : #if defined(Q_OS_WIN)
     414              :         // Allow special characters to be typed with AltGr / Ctrl+Alt (they are the same in Windows)
     415              :         if (!ed.s.empty() && (ed.key < Key_A || ed.key > Key_Z) && (ed.key < Key_0 || ed.key > Key_9)) {
     416              :             return true;
     417              :         }
     418              : #endif
     419              :     }
     420              : 
     421              :     // At least on non-macOS, sometimes ed.s is not empty even if Ctrl is pressed
     422            1 :     return !ctrlPressed && !ed.s.empty();
     423              : }
     424              : 
     425              : //---------------------------------------------------------
     426              : //   edit
     427              : //---------------------------------------------------------
     428              : 
     429            1 : bool TextBase::edit(EditData& ed)
     430              : {
     431              :     // Keep this method closely in sync with TextBase::isEditAllowed()!
     432              : 
     433            1 :     if (!isEditAllowed(ed)) {
     434            0 :         return false;
     435              :     }
     436              : 
     437            1 :     TextEditData* ted = static_cast<TextEditData*>(ed.getData(this).get());
     438            1 :     if (!ted) {
     439            0 :         return false;
     440              :     }
     441            1 :     TextCursor* cursor = this->cursor();
     442            1 :     CharFormat* currentFormat = cursor->format();
     443              : 
     444            1 :     String s         = ed.s;
     445            1 :     bool ctrlPressed  = ed.modifiers & ControlModifier;
     446            1 :     bool shiftPressed = ed.modifiers & ShiftModifier;
     447            1 :     bool altPressed = ed.modifiers & AltModifier;
     448              : 
     449            1 :     TextCursor::MoveMode mm = shiftPressed ? TextCursor::MoveMode::KeepAnchor : TextCursor::MoveMode::MoveAnchor;
     450              : 
     451            1 :     bool wasHex = false;
     452            1 :     if (m_hexState >= 0) {
     453            0 :         if (ed.modifiers == (ControlModifier | ShiftModifier | KeypadModifier)) {
     454            0 :             switch (ed.key) {
     455            0 :             case Key_0:
     456              :             case Key_1:
     457              :             case Key_2:
     458              :             case Key_3:
     459              :             case Key_4:
     460              :             case Key_5:
     461              :             case Key_6:
     462              :             case Key_7:
     463              :             case Key_8:
     464              :             case Key_9:
     465            0 :                 s = Char::fromAscii(ed.key);
     466            0 :                 ++m_hexState;
     467            0 :                 wasHex = true;
     468            0 :                 break;
     469            0 :             default:
     470            0 :                 break;
     471              :             }
     472            0 :         } else if (ed.modifiers == (ControlModifier | ShiftModifier)) {
     473            0 :             switch (ed.key) {
     474            0 :             case Key_A:
     475              :             case Key_B:
     476              :             case Key_C:
     477              :             case Key_D:
     478              :             case Key_E:
     479              :             case Key_F:
     480            0 :                 s = Char::fromAscii(ed.key);
     481            0 :                 ++m_hexState;
     482            0 :                 wasHex = true;
     483            0 :                 break;
     484            0 :             default:
     485            0 :                 break;
     486              :             }
     487              :         }
     488              :     }
     489              : 
     490            1 :     if (!wasHex) {
     491              : //printf("======%x\n", s.isEmpty() ? -1 : s[0].unicode());
     492              : 
     493            1 :         switch (ed.key) {
     494            0 :         case Key_Enter:
     495              :         case Key_Return:
     496            0 :             deleteSelectedText();
     497            0 :             score()->undo(new SplitText(cursor));
     498              : 
     499            0 :             notifyAboutTextCursorChanged();
     500              : 
     501            0 :             return true;
     502              : 
     503            0 :         case Key_Delete: {
     504            0 :             int startPosition = cursor->currentPosition();
     505              : 
     506            0 :             if (ctrlPressed) {
     507              :                 // delete next word
     508            0 :                 cursor->movePosition(TextCursor::MoveOperation::NextWord, TextCursor::MoveMode::KeepAnchor);
     509            0 :                 int endPosition = cursor->currentPosition();
     510            0 :                 String text = cursor->selectedText();
     511              : 
     512            0 :                 s.clear();
     513              : 
     514            0 :                 if (deleteSelectedText()) {
     515            0 :                     notifyAboutTextRemoved(startPosition, endPosition, text);
     516              :                 }
     517            0 :             } else {
     518            0 :                 String text = cursor->selectedText();
     519              : 
     520            0 :                 if (!deleteSelectedText()) {
     521              :                     // if you are on the end of the line, delete the newline char
     522            0 :                     if (cursor->column() == cursor->columns()) {
     523            0 :                         size_t cursorRow = cursor->row();
     524            0 :                         cursor->movePosition(TextCursor::MoveOperation::Down);
     525            0 :                         if (cursor->row() != cursorRow) {
     526            0 :                             cursor->movePosition(TextCursor::MoveOperation::StartOfLine);
     527            0 :                             score()->undo(new JoinText(cursor));
     528              :                         }
     529              :                     } else {
     530            0 :                         score()->undo(new RemoveText(cursor, String(cursor->currentCharacter())));
     531              :                     }
     532              :                 } else {
     533            0 :                     notifyAboutTextRemoved(startPosition + 1, startPosition, text);
     534              :                 }
     535            0 :             }
     536            0 :             return true;
     537              :         }
     538              : 
     539            0 :         case Key_Backspace: {
     540            0 :             int startPosition = cursor->currentPosition();
     541              : 
     542            0 :             if (ctrlPressed) {
     543              :                 // delete last word
     544            0 :                 cursor->movePosition(TextCursor::MoveOperation::WordLeft, TextCursor::MoveMode::KeepAnchor);
     545            0 :                 int endPosition = cursor->currentPosition();
     546            0 :                 String text = cursor->selectedText();
     547              : 
     548            0 :                 s.clear();
     549              : 
     550            0 :                 if (deleteSelectedText()) {
     551            0 :                     notifyAboutTextRemoved(startPosition, endPosition, text);
     552              :                 }
     553            0 :             } else {
     554            0 :                 String text = cursor->selectedText();
     555              : 
     556            0 :                 if (!deleteSelectedText()) {
     557            0 :                     if (cursor->column() == 0 && m_cursor->row() != 0) {
     558            0 :                         score()->undo(new JoinText(cursor));
     559              :                     } else {
     560            0 :                         if (!cursor->movePosition(TextCursor::MoveOperation::Left)) {
     561            0 :                             return false;
     562              :                         }
     563            0 :                         score()->undo(new RemoveText(cursor, String(cursor->currentCharacter())));
     564              :                     }
     565              :                 } else {
     566            0 :                     notifyAboutTextRemoved(startPosition, startPosition - 1, text);
     567              :                 }
     568            0 :             }
     569            0 :             return true;
     570              :         }
     571              : 
     572            0 :         case Key_Left:
     573            0 :             if (!m_cursor->movePosition(ctrlPressed ? TextCursor::MoveOperation::WordLeft : TextCursor::MoveOperation::Left, mm)
     574            0 :                 && isLyrics()) {
     575            0 :                 return false;
     576              :             }
     577            0 :             s.clear();
     578              : 
     579            0 :             notifyAboutTextCursorChanged();
     580              : 
     581            0 :             break;
     582              : 
     583            0 :         case Key_Right:
     584            0 :             if (!m_cursor->movePosition(ctrlPressed ? TextCursor::MoveOperation::NextWord : TextCursor::MoveOperation::Right, mm)
     585            0 :                 && isLyrics()) {
     586            0 :                 return false;
     587              :             }
     588            0 :             s.clear();
     589              : 
     590            0 :             notifyAboutTextCursorChanged();
     591              : 
     592            0 :             break;
     593              : 
     594            0 :         case Key_Up:
     595              : #if defined(Q_OS_MAC)
     596              :             if (!cursor->movePosition(TextCursor::MoveOperation::Up, mm)) {
     597              :                 cursor->movePosition(TextCursor::MoveOperation::StartOfLine, mm);
     598              :             }
     599              : #else
     600            0 :             cursor->movePosition(TextCursor::MoveOperation::Up, mm);
     601              : #endif
     602            0 :             s.clear();
     603              : 
     604            0 :             notifyAboutTextCursorChanged();
     605              : 
     606            0 :             break;
     607              : 
     608            0 :         case Key_Down:
     609              : #if defined(Q_OS_MAC)
     610              :             if (!cursor->movePosition(TextCursor::MoveOperation::Down, mm)) {
     611              :                 cursor->movePosition(TextCursor::MoveOperation::EndOfLine, mm);
     612              :             }
     613              : #else
     614            0 :             cursor->movePosition(TextCursor::MoveOperation::Down, mm);
     615              : #endif
     616            0 :             s.clear();
     617              : 
     618            0 :             notifyAboutTextCursorChanged();
     619              : 
     620            0 :             break;
     621              : 
     622            0 :         case Key_Home:
     623            0 :             if (ctrlPressed) {
     624            0 :                 cursor->movePosition(TextCursor::MoveOperation::Start, mm);
     625              :             } else {
     626            0 :                 cursor->movePosition(TextCursor::MoveOperation::StartOfLine, mm);
     627              :             }
     628              : 
     629            0 :             s.clear();
     630              : 
     631            0 :             notifyAboutTextCursorChanged();
     632              : 
     633            0 :             break;
     634              : 
     635            0 :         case Key_End:
     636            0 :             if (ctrlPressed) {
     637            0 :                 cursor->movePosition(TextCursor::MoveOperation::End, mm);
     638              :             } else {
     639            0 :                 cursor->movePosition(TextCursor::MoveOperation::EndOfLine, mm);
     640              :             }
     641              : 
     642            0 :             s.clear();
     643              : 
     644            0 :             notifyAboutTextCursorChanged();
     645              : 
     646            0 :             break;
     647              : 
     648            0 :         case Key_Space:
     649            0 :             if ((ed.modifiers & TextEditingControlModifier) || currentFormat->fontFamily() == u"ScoreText") {
     650            0 :                 s = String(Char(0xa0)); // non-breaking space
     651              :             } else {
     652            0 :                 if (isFingering() && ed.view()) {
     653            0 :                     score()->endCmd();
     654            0 :                     ed.view()->textTab(ed.modifiers & ShiftModifier);
     655            0 :                     return true;
     656              :                 }
     657            0 :                 s = u" ";
     658              :             }
     659            0 :             ed.modifiers = {};
     660            0 :             break;
     661              : 
     662            0 :         case Key_Minus:
     663            0 :             if (ed.modifiers == 0) {
     664            0 :                 s = u"-";
     665              :             }
     666            0 :             break;
     667              : 
     668            0 :         case Key_Underscore:
     669            0 :             if (ed.modifiers == 0) {
     670            0 :                 s = u"_";
     671              :             }
     672            0 :             break;
     673              : 
     674            0 :         case Key_A:
     675            0 :             if (ctrlPressed && !shiftPressed) {
     676            0 :                 cursor->movePosition(TextCursor::MoveOperation::Start, TextCursor::MoveMode::MoveAnchor);
     677            0 :                 cursor->movePosition(TextCursor::MoveOperation::End, TextCursor::MoveMode::KeepAnchor);
     678            0 :                 s.clear();
     679              : 
     680            0 :                 notifyAboutTextCursorChanged();
     681              :             }
     682            0 :             break;
     683            0 :         case Key_Z:
     684            0 :             if (ctrlPressed /* undo */ || (ctrlPressed && shiftPressed) /* redo */) {
     685            0 :                 return false;
     686              :             }
     687            0 :             break;
     688            0 :         case Key_B:
     689              :         case Key_C:
     690              :         case Key_I:
     691              :         case Key_U:
     692              :         case Key_V:
     693              :         case Key_X:
     694              :         case Key_Y:
     695            0 :             if (ctrlPressed && !shiftPressed) {
     696            0 :                 return false; // handled at application level
     697              :             }
     698            0 :             break;
     699            1 :         default:
     700            1 :             break;
     701              :         }
     702            1 :         if (ctrlPressed && shiftPressed) {
     703            0 :             switch (ed.key) {
     704            0 :             case Key_U:
     705            0 :                 if (m_hexState == -1) {
     706            0 :                     m_hexState = 0;
     707            0 :                     s = u"u";
     708              :                 }
     709            0 :                 break;
     710            0 :             case Key_B:
     711            0 :                 s = u"\u266d";               // Unicode flat
     712            0 :                 break;
     713            0 :             case Key_NumberSign: // e.g. QWERTY (US)
     714              :             case Key_AsciiTilde: // e.g. QWERTY (GB)
     715              :             case Key_Apostrophe: // e.g. QWERTZ (DE)
     716              :             case Key_periodcentered: // e.g. QWERTY (ES)
     717              :             case Key_3: // e.g. AZERTY (FR, BE)
     718            0 :                 s = u"\u266f";               // Unicode sharp
     719            0 :                 break;
     720            0 :             case Key_H:
     721            0 :                 s = u"\u266e";               // Unicode natural
     722            0 :                 break;
     723            0 :             case Key_Space:
     724            0 :                 insertSym(SymId::space);
     725            0 :                 return true;
     726            0 :             case Key_F:
     727            0 :                 insertSym(SymId::dynamicForte);
     728            0 :                 return true;
     729            0 :             case Key_M:
     730            0 :                 insertSym(SymId::dynamicMezzo);
     731            0 :                 return true;
     732            0 :             case Key_N:
     733            0 :                 insertSym(SymId::dynamicNiente);
     734            0 :                 return true;
     735            0 :             case Key_P:
     736            0 :                 insertSym(SymId::dynamicPiano);
     737            0 :                 return true;
     738            0 :             case Key_S:
     739            0 :                 insertSym(SymId::dynamicSforzando);
     740            0 :                 return true;
     741            0 :             case Key_R:
     742            0 :                 insertSym(SymId::dynamicRinforzando);
     743            0 :                 return true;
     744            0 :             case Key_Z:
     745              :                 // Ctrl+Z is normally "undo"
     746              :                 // but this code gets hit even if you are also holding Shift
     747              :                 // so Shift+Ctrl+Z works
     748            0 :                 insertSym(SymId::dynamicZ);
     749            0 :                 return true;
     750              :             }
     751              :         }
     752            1 :         if (ctrlPressed && altPressed) {
     753            0 :             if (ed.key == Key_Minus || ed.key == Key_Underscore) {
     754            0 :                 insertSym(SymId::lyricsElision);
     755            0 :                 return true;
     756              :             }
     757              :         }
     758              :     }
     759            1 :     if (!s.isEmpty()) {
     760            1 :         deleteSelectedText();
     761            1 :         if (currentFormat->fontFamily() == u"ScoreText") {
     762            1 :             currentFormat->setFontFamily(propertyDefault(Pid::FONT_FACE).value<String>());
     763              :         }
     764            1 :         score()->undo(new InsertText(m_cursor, s));
     765              : 
     766            1 :         int startPosition = cursor->currentPosition();
     767            1 :         notifyAboutTextInserted(startPosition, startPosition + static_cast<int>(s.size()), s);
     768              :     }
     769            1 :     return true;
     770            1 : }
     771              : 
     772              : //---------------------------------------------------------
     773              : //   movePosition
     774              : //---------------------------------------------------------
     775              : 
     776            0 : void TextBase::movePosition(TextCursor::MoveOperation op)
     777              : {
     778            0 :     cursor()->movePosition(op);
     779            0 :     score()->addRefresh(canvasBoundingRect());
     780            0 :     score()->update();
     781            0 : }
     782              : 
     783              : //---------------------------------------------------------
     784              : //  ChangeText::insertText
     785              : //---------------------------------------------------------
     786              : 
     787            5 : void ChangeText::insertText()
     788              : {
     789            5 :     TextCursor tc = m_cursor;
     790            5 :     tc.setFormat(m_format);                              // To undo TextCursor::updateCursorFormat()
     791            5 :     tc.text()->editInsertText(&tc, m_s);
     792            5 :     *tc.text()->cursor() = tc;
     793            5 : }
     794              : 
     795              : //---------------------------------------------------------
     796              : //  ChangeText::removeText
     797              : //---------------------------------------------------------
     798              : 
     799            0 : void ChangeText::removeText()
     800              : {
     801            0 :     TextCursor tc = m_cursor;
     802            0 :     TextBlock& l  = m_cursor.curLine();
     803            0 :     size_t column = m_cursor.column();
     804            0 :     m_format = *l.formatAt(static_cast<int>(column + m_s.size()) - 1);
     805              : 
     806            0 :     for (size_t n = 0; n < m_s.size(); ++n) {
     807            0 :         l.remove(static_cast<int>(column), &m_cursor);
     808              :     }
     809            0 :     m_cursor.text()->triggerLayout();
     810            0 :     *m_cursor.text()->cursor() = tc;
     811            0 :     m_cursor.text()->setTextInvalid();
     812            0 : }
     813              : 
     814              : //---------------------------------------------------------
     815              : //   SplitJoinText
     816              : //---------------------------------------------------------
     817              : 
     818            0 : void SplitJoinText::join()
     819              : {
     820            0 :     TextBase* t   = m_cursor.text();
     821            0 :     size_t line   = m_cursor.row();
     822            0 :     t->setTextInvalid();
     823            0 :     t->triggerLayout();
     824              : 
     825            0 :     TextBase::LayoutData* ldata = t->mutldata();
     826            0 :     CharFormat* charFmt = m_cursor.format();         // take current format
     827            0 :     size_t col          = ldata->textBlock(static_cast<int>(line) - 1).columns();
     828            0 :     int eol             = ldata->textBlock(static_cast<int>(line)).eol();
     829            0 :     auto fragmentsList = ldata->textBlock(static_cast<int>(line)).fragmentsWithoutEmpty();
     830              : 
     831            0 :     if (fragmentsList.size() > 0) {
     832            0 :         ldata->textBlock(static_cast<int>(line) - 1).removeEmptyFragment();
     833              :     }
     834            0 :     muse::join(ldata->textBlock(static_cast<int>(line) - 1).fragments(), fragmentsList);
     835              : 
     836            0 :     ldata->blocks.erase(ldata->blocks.begin() + line);
     837              : 
     838            0 :     m_cursor.setRow(line - 1);
     839            0 :     m_cursor.curLine().setEol(eol);
     840            0 :     m_cursor.setColumn(col);
     841            0 :     m_cursor.setFormat(*charFmt);             // restore orig. format at new line
     842            0 :     m_cursor.clearSelection();
     843              : 
     844            0 :     *t->cursor() = m_cursor;
     845            0 :     t->setTextInvalid();
     846            0 : }
     847              : 
     848            0 : void SplitJoinText::split()
     849              : {
     850            0 :     TextBase* t   = m_cursor.text();
     851            0 :     size_t line   = m_cursor.row();
     852            0 :     bool eol      = m_cursor.curLine().eol();
     853            0 :     t->setTextInvalid();
     854            0 :     t->triggerLayout();
     855              : 
     856            0 :     TextBase::LayoutData* ldata = t->mutldata();
     857            0 :     CharFormat* charFmt = m_cursor.format();           // take current format
     858            0 :     ldata->blocks.insert(ldata->blocks.begin() + line + 1,
     859            0 :                          m_cursor.curLine().split(static_cast<int>(m_cursor.column()), t->cursor()));
     860            0 :     m_cursor.curLine().setEol(true);
     861              : 
     862            0 :     m_cursor.setRow(line + 1);
     863            0 :     m_cursor.curLine().setEol(eol);
     864            0 :     m_cursor.setColumn(0);
     865            0 :     m_cursor.setFormat(*charFmt);               // restore orig. format at new line
     866            0 :     m_cursor.clearSelection();
     867              : 
     868            0 :     *t->cursor() = m_cursor;
     869            0 :     t->setTextInvalid();
     870            0 : }
     871              : 
     872              : //---------------------------------------------------------
     873              : //   drop
     874              : //---------------------------------------------------------
     875              : 
     876            0 : EngravingItem* TextBase::drop(Transaction&, EditData& ed)
     877              : {
     878            0 :     TextCursor* cursor = this->cursor();
     879              : 
     880            0 :     EngravingItem* e = ed.dropElement;
     881              : 
     882            0 :     switch (e->type()) {
     883            0 :     case ElementType::SYMBOL:
     884              :     {
     885            0 :         SymId id = toSymbol(e)->sym();
     886            0 :         delete e;
     887              : 
     888            0 :         insertSym(id);
     889              :     }
     890            0 :     break;
     891              : 
     892            0 :     case ElementType::FSYMBOL:
     893              :     {
     894            0 :         String s = toFSymbol(e)->toString();
     895            0 :         delete e;
     896              : 
     897            0 :         CharFormat* currentFormat = cursor->format();
     898            0 :         if (currentFormat->fontFamily() == u"ScoreText") {
     899            0 :             currentFormat->setFontFamily(propertyDefault(Pid::FONT_FACE).value<String>());
     900              :         }
     901              : 
     902            0 :         deleteSelectedText();
     903            0 :         score()->undo(new InsertText(cursor, s));
     904            0 :     }
     905            0 :     break;
     906              : 
     907            0 :     default:
     908            0 :         LOGD("drop <%s> not handled", e->typeName());
     909            0 :         break;
     910              :     }
     911            0 :     return 0;
     912              : }
     913              : 
     914              : //---------------------------------------------------------
     915              : //   paste
     916              : //---------------------------------------------------------
     917              : 
     918            0 : void TextBase::paste(const String& txt)
     919              : {
     920            0 :     if (MScore::debugMode) {
     921            0 :         LOGD("<%s>", muPrintable(txt));
     922              :     }
     923              : 
     924            0 :     int state = 0;
     925            0 :     String token;
     926            0 :     String sym;
     927            0 :     bool symState = false;
     928            0 :     CharFormat format = *cursor()->format();
     929              : 
     930            0 :     score()->startCmd(TranslatableString("undoableAction", "Paste text"));
     931            0 :     for (size_t i = 0; i < txt.size(); i++) {
     932            0 :         Char c = txt.at(i);
     933            0 :         if (state == 0) {
     934            0 :             if (c == '<') {
     935            0 :                 state = 1;
     936            0 :                 token.clear();
     937            0 :             } else if (c == '&') {
     938            0 :                 state = 2;
     939            0 :                 token.clear();
     940              :             } else {
     941            0 :                 if (symState) {
     942            0 :                     sym += c;
     943              :                 } else {
     944            0 :                     deleteSelectedText();
     945            0 :                     cursor()->setFormat(format);
     946            0 :                     if (c.isHighSurrogate()) {
     947            0 :                         Char highSurrogate = c;
     948            0 :                         assert(i + 1 < txt.size());
     949            0 :                         i++;
     950            0 :                         Char lowSurrogate = txt.at(i);
     951            0 :                         insertText(String::fromUcs4(Char::surrogateToUcs4(highSurrogate, lowSurrogate)));
     952            0 :                     } else if (c == '\r') {
     953            0 :                         continue;
     954            0 :                     } else if (c == '\n') {
     955            0 :                         score()->undo(new SplitText(cursor()));
     956              :                     } else {
     957            0 :                         insertText(String(c));
     958              :                     }
     959              :                 }
     960              :             }
     961            0 :         } else if (state == 1) {
     962            0 :             if (c == '>') {
     963            0 :                 state = 0;
     964            0 :                 if (token == "sym") {
     965            0 :                     symState = true;
     966            0 :                     sym.clear();
     967            0 :                 } else if (token == "/sym") {
     968            0 :                     symState = false;
     969            0 :                     cursor()->setFormat(format);
     970            0 :                     insertSym(SymNames::symIdByName(sym));
     971              :                 } else {
     972            0 :                     prepareFormat(token, format);
     973              :                 }
     974              :             } else {
     975            0 :                 token += c;
     976              :             }
     977            0 :         } else if (state == 2) {
     978            0 :             if (c == ';') {
     979            0 :                 state = 0;
     980            0 :                 if (token == "lt") {
     981            0 :                     insertText(u"<");
     982            0 :                 } else if (token == "gt") {
     983            0 :                     insertText(u">");
     984            0 :                 } else if (token == "amp") {
     985            0 :                     insertText(u"&");
     986            0 :                 } else if (token == "quot") {
     987            0 :                     insertText(u"\"");
     988              :                 } else {
     989            0 :                     insertSym(SymNames::symIdByName(token));
     990              :                 }
     991            0 :             } else if (!c.isLetter()) {
     992            0 :                 state = 0;
     993            0 :                 insertText(u"&");
     994            0 :                 insertText(token);
     995            0 :                 insertText(c);
     996              :             } else {
     997            0 :                 token += c;
     998              :             }
     999              :         }
    1000              :     }
    1001            0 :     if (state == 2) {
    1002            0 :         insertText(u"&");
    1003            0 :         insertText(token);
    1004              :     }
    1005            0 :     score()->endCmd();
    1006            0 : }
    1007              : 
    1008              : //---------------------------------------------------------
    1009              : //   endHexState
    1010              : //---------------------------------------------------------
    1011              : 
    1012            0 : void TextBase::endHexState()
    1013              : {
    1014            0 :     TextCursor* cursor = this->cursor();
    1015              : 
    1016            0 :     if (m_hexState >= 0) {
    1017            0 :         if (m_hexState > 0) {
    1018            0 :             size_t c2 = cursor->column();
    1019            0 :             size_t c1 = c2 - (m_hexState + 1);
    1020              : 
    1021            0 :             TextBlock& t = mutldata()->blocks[cursor->row()];
    1022            0 :             String ss   = t.remove(static_cast<int>(c1), m_hexState + 1, cursor);
    1023              :             bool ok;
    1024            0 :             char32_t code = ss.mid(1).toInt(&ok, 16);
    1025            0 :             cursor->setColumn(c1);
    1026            0 :             cursor->clearSelection();
    1027            0 :             if (ok) {
    1028            0 :                 editInsertText(cursor, String::fromUcs4(code));
    1029              :             } else {
    1030            0 :                 LOGD("cannot convert hex string <%s>, state %d (%zu-%zu)",
    1031            0 :                      muPrintable(ss.mid(1)), m_hexState, c1, c2);
    1032              :             }
    1033            0 :         }
    1034            0 :         m_hexState = -1;
    1035              :     }
    1036            0 : }
    1037              : 
    1038              : //---------------------------------------------------------
    1039              : //   deleteSelectedText
    1040              : //---------------------------------------------------------
    1041              : 
    1042            1 : bool TextBase::deleteSelectedText()
    1043              : {
    1044            1 :     TextCursor* cursor = this->cursor();
    1045              : 
    1046            1 :     if (!cursor->hasSelection()) {
    1047            1 :         return false;
    1048              :     }
    1049              : 
    1050            0 :     size_t r1 = cursor->selectLine();
    1051            0 :     size_t c1 = cursor->selectColumn();
    1052              : 
    1053            0 :     if (r1 > cursor->row() || (r1 == cursor->row() && c1 > cursor->column())) {
    1054              :         // swap start end of selection
    1055            0 :         r1 = cursor->row();
    1056            0 :         c1 = cursor->column();
    1057            0 :         cursor->setRow(m_cursor->selectLine());
    1058            0 :         cursor->setColumn(cursor->selectColumn());
    1059              :     }
    1060              : 
    1061            0 :     cursor->clearSelection();
    1062              :     for (;;) {
    1063            0 :         if (r1 == cursor->row() && c1 == cursor->column()) {
    1064            0 :             break;
    1065              :         }
    1066            0 :         if (cursor->column() == 0 && cursor->row() != 0) {
    1067            0 :             score()->undo(new JoinText(cursor));
    1068              :         } else {
    1069              :             // move cursor left:
    1070            0 :             if (!m_cursor->movePosition(TextCursor::MoveOperation::Left)) {
    1071            0 :                 break;
    1072              :             }
    1073            0 :             TextCursor undoCursor(*m_cursor);
    1074            0 :             score()->undo(new RemoveText(&undoCursor, String(m_cursor->currentCharacter())));
    1075            0 :         }
    1076            0 :     }
    1077            0 :     return true;
    1078              : }
    1079              : 
    1080              : //---------------------------------------------------------
    1081              : //   ChangeTextProperties
    1082              : //---------------------------------------------------------
    1083              : 
    1084           10 : void ChangeTextProperties::restoreSelection()
    1085              : {
    1086           10 :     TextCursor& tc = cursor();
    1087           10 :     tc.text()->cursor()->setSelectLine(tc.selectLine());
    1088           10 :     tc.text()->cursor()->setSelectColumn(tc.selectColumn());
    1089           10 :     tc.text()->cursor()->setRow(tc.row());
    1090           10 :     tc.text()->cursor()->setColumn(tc.column());
    1091           10 : }
    1092              : 
    1093            6 : ChangeTextProperties::ChangeTextProperties(const TextCursor* tc, Pid propId, const PropertyValue& propVal, PropertyFlags flags_)
    1094            6 :     : TextEditUndoableCommand(*tc)
    1095              : {
    1096            6 :     m_propertyId = propId;
    1097            6 :     m_propertyVal = propVal;
    1098            6 :     m_flags = flags_;
    1099            6 :     if (m_propertyId == Pid::FONT_STYLE) {
    1100            5 :         m_existingStyle = static_cast<FontStyle>(cursor().text()->getProperty(propId).toInt());
    1101              :     }
    1102            6 : }
    1103              : 
    1104            2 : void ChangeTextProperties::undo()
    1105              : {
    1106            2 :     cursor().text()->resetFormatting();
    1107            2 :     cursor().text()->setXmlText(m_xmlText);
    1108            2 :     restoreSelection();
    1109            2 :     cursor().text()->renderer()->layoutText1(cursor().text());
    1110            2 : }
    1111              : 
    1112            8 : void ChangeTextProperties::redo()
    1113              : {
    1114            8 :     m_xmlText = cursor().text()->xmlText();
    1115            8 :     restoreSelection();
    1116            8 :     cursor().text()->setPropertyFlags(m_propertyId, m_flags);
    1117              : 
    1118            8 :     if (m_propertyId == Pid::FONT_STYLE) {
    1119            6 :         FontStyle setStyle = static_cast<FontStyle>(m_propertyVal.toInt());
    1120            6 :         TextCursor* tc = cursor().text()->cursor();
    1121              :         // user turned on bold/italic/underline/strike for text where it's not set, or turned it off for text where it is set,
    1122              :         // note this logic only works because the user can only click one at a time
    1123            6 :         if ((setStyle& FontStyle::Bold) != (m_existingStyle & FontStyle::Bold)) {
    1124            3 :             tc->setFormat(FormatId::Bold, setStyle & FontStyle::Bold);
    1125              :         }
    1126            6 :         if ((setStyle& FontStyle::Italic) != (m_existingStyle & FontStyle::Italic)) {
    1127            2 :             tc->setFormat(FormatId::Italic, setStyle & FontStyle::Italic);
    1128              :         }
    1129            6 :         if ((setStyle& FontStyle::Underline) != (m_existingStyle & FontStyle::Underline)) {
    1130            2 :             tc->setFormat(FormatId::Underline, setStyle & FontStyle::Underline);
    1131              :         }
    1132            6 :         if ((setStyle& FontStyle::Strike) != (m_existingStyle & FontStyle::Strike)) {
    1133            0 :             tc->setFormat(FormatId::Strike, setStyle & FontStyle::Strike);
    1134              :         }
    1135              :     } else {
    1136            2 :         cursor().text()->setProperty(m_propertyId, m_propertyVal);
    1137              :     }
    1138            8 : }
        

Generated by: LCOV version 2.0-1