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 : }
|