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 "notationinteraction.h"
24 :
25 : #include "log.h"
26 :
27 : #include <memory>
28 : #include <limits>
29 :
30 : #include <QApplication> // for QApplication::cursorFlashTime()
31 : #include <QClipboard>
32 : #include <QDrag>
33 : #include <QEventLoop>
34 : #include <QGuiApplication>
35 : #include <QKeyEvent>
36 : #include <QKeySequence>
37 : #include <QMimeData>
38 : #include <QPainter>
39 : #include <QRectF>
40 : #include <QRegularExpression>
41 : #include <QRegularExpressionMatch>
42 :
43 : #include "defer.h"
44 : #include "ptrutils.h"
45 : #include "containers.h"
46 :
47 : #include "draw/painter.h"
48 : #include "draw/types/pen.h"
49 :
50 : #include "engraving/iengravingconfiguration.h" // IWYU pragma: keep
51 :
52 : // TODO: Don't include from engraving/internal
53 : #include "engraving/internal/qmimedataadapter.h"
54 :
55 : #include "engraving/dom/accidental.h"
56 : #include "engraving/dom/actionicon.h"
57 : #include "engraving/dom/anchors.h"
58 : #include "engraving/dom/articulation.h"
59 : #include "engraving/dom/barline.h"
60 : #include "engraving/dom/box.h"
61 : #include "engraving/dom/bracket.h"
62 : #include "engraving/dom/chord.h"
63 : #include "engraving/dom/drumset.h"
64 : #include "engraving/dom/dummyparent.h"
65 : #include "engraving/dom/dynamic.h"
66 : #include "engraving/dom/elementgroup.h"
67 : #include "engraving/dom/factory.h"
68 : #include "engraving/dom/figuredbass.h"
69 : #include "engraving/dom/fret.h"
70 : #include "engraving/dom/gradualtempochange.h"
71 : #include "engraving/dom/guitarbend.h"
72 : #include "engraving/dom/hammeronpulloff.h"
73 : #include "engraving/dom/image.h"
74 : #include "engraving/dom/instrchange.h"
75 : #include "engraving/dom/keysig.h"
76 : #include "engraving/dom/lasso.h"
77 : #include "engraving/dom/layoutbreak.h"
78 : #include "engraving/dom/linkedobjects.h" // IWYU pragma: keep
79 : #include "engraving/dom/lyrics.h"
80 : #include "engraving/dom/masterscore.h"
81 : #include "engraving/dom/measure.h"
82 : #include "engraving/dom/mscore.h"
83 : #include "engraving/dom/page.h"
84 : #include "engraving/dom/part.h"
85 : #include "engraving/dom/pitchspelling.h"
86 : #include "engraving/dom/realizedharmony.h"
87 : #include "engraving/dom/rest.h"
88 : #include "engraving/dom/shadownote.h"
89 : #include "engraving/dom/slur.h"
90 : #include "engraving/dom/soundflag.h"
91 : #include "engraving/dom/staff.h"
92 : #include "engraving/dom/stafflines.h"
93 : #include "engraving/dom/stafftype.h"
94 : #include "engraving/dom/stafftypechange.h"
95 : #include "engraving/dom/system.h"
96 : #include "engraving/dom/tuplet.h"
97 : #include "engraving/dom/utils.h"
98 : #include "engraving/editing/editbeam.h"
99 : #include "engraving/editing/editchord.h"
100 : #include "engraving/editing/editclef.h"
101 : #include "engraving/editing/editcrossstaff.h"
102 : #include "engraving/editing/editduration.h"
103 : #include "engraving/editing/editenharmonicspelling.h"
104 : #include "engraving/editing/edithairpin.h"
105 : #include "engraving/editing/editnote.h"
106 : #include "engraving/editing/editspanner.h"
107 : #include "engraving/editing/editaccidentalbrackets.h"
108 : #include "engraving/editing/editparentheses.h"
109 : #include "engraving/editing/editrehearsalmark.h"
110 : #include "engraving/editing/editvisibility.h"
111 : #include "engraving/editing/editvoice.h"
112 : #include "engraving/editing/paste.h"
113 : #include "engraving/editing/realizechordsymbols.h"
114 : #include "engraving/editing/regroup.h"
115 : #include "engraving/editing/reset.h"
116 : #include "engraving/editing/navigation.h"
117 : #include "engraving/editing/noteinput.h"
118 : #include "engraving/editing/editpart.h"
119 : #include "engraving/editing/editslashnotation.h"
120 : #include "engraving/editing/editstretch.h"
121 : #include "engraving/editing/edittie.h"
122 : #include "engraving/editing/edittimesig.h"
123 : #include "engraving/editing/edittuplet.h"
124 : #include "engraving/editing/editpagelocks.h"
125 : #include "engraving/editing/editposition.h"
126 : #include "engraving/editing/editsystemlocks.h"
127 : #include "engraving/editing/flip.h"
128 : #include "engraving/editing/exchangevoices.h"
129 : #include "engraving/editing/implodeexplode.h"
130 : #include "engraving/editing/splitjoinmeasure.h"
131 : #include "engraving/editing/textedit.h"
132 : #include "engraving/editing/transaction/transaction.h"
133 : #include "engraving/editing/transpose.h"
134 : #include "engraving/rendering/iscorerenderer.h"
135 : #include "engraving/rw/rwregister.h"
136 : #include "engraving/rw/xmlreader.h"
137 :
138 : #include "inotationviewstate.h"
139 : #include "notationerrors.h"
140 : #include "notation.h"
141 : #include "notationnoteinput.h"
142 : #include "notationselection.h"
143 : #include "notationselectionfilter.h"
144 : #include "scorecallbacks.h"
145 : #include "inotationelements.h"
146 :
147 : #include "utilities/scorerangeutilities.h"
148 :
149 : #include "app_config.h"
150 :
151 : using namespace mu::notation;
152 : using namespace mu::engraving;
153 : using namespace muse;
154 : using namespace muse::draw;
155 : using namespace muse::io;
156 :
157 0 : static mu::engraving::KeyboardModifier keyboardModifier(Qt::KeyboardModifiers km)
158 : {
159 0 : return mu::engraving::KeyboardModifier(int(km));
160 : }
161 :
162 0 : static double nudgeDistance(const mu::engraving::EditData& editData)
163 : {
164 0 : double spatium = editData.element->spatium();
165 :
166 0 : if (editData.element->isBeam()) {
167 0 : if (editData.modifiers & Qt::ControlModifier) {
168 0 : return spatium;
169 0 : } else if (editData.modifiers & Qt::AltModifier) {
170 0 : return spatium * 4;
171 : }
172 :
173 0 : return spatium * 0.25;
174 : }
175 :
176 0 : if (editData.modifiers & Qt::ControlModifier) {
177 0 : return spatium * mu::engraving::MScore::nudgeStep10;
178 0 : } else if (editData.modifiers & Qt::AltModifier) {
179 0 : return spatium * mu::engraving::MScore::nudgeStep50;
180 : }
181 :
182 0 : return spatium * mu::engraving::MScore::nudgeStep;
183 : }
184 :
185 0 : static double nudgeDistance(const mu::engraving::EditData& editData, double raster)
186 : {
187 0 : double distance = nudgeDistance(editData);
188 0 : if (raster > 0) {
189 0 : raster = editData.element->spatium() / raster;
190 0 : if (distance < raster) {
191 0 : distance = raster;
192 : }
193 : }
194 :
195 0 : return distance;
196 : }
197 :
198 0 : static PointF bindCursorPosToText(const PointF& cursorPos, const EngravingItem* text)
199 : {
200 0 : if (cursorPos.isNull() || !text || !text->isTextBase()) {
201 0 : return PointF();
202 : }
203 :
204 0 : muse::RectF bbox = text->canvasBoundingRect();
205 : muse::PointF boundPos = muse::PointF(
206 0 : cursorPos.x() < bbox.left() ? bbox.left()
207 0 : : cursorPos.x() >= bbox.right() ? bbox.right() - 1 : cursorPos.x(),
208 0 : cursorPos.y() < bbox.top() ? bbox.top()
209 0 : : cursorPos.y() >= bbox.bottom() ? bbox.bottom() - 1 : cursorPos.y());
210 :
211 0 : return boundPos;
212 : }
213 :
214 0 : static AccidentalType accidentalType(const InputState& is, const NoteVal& nval, int line)
215 : {
216 0 : if (is.rest()) {
217 0 : return AccidentalType::NONE;
218 : }
219 :
220 0 : AccidentalVal accVal = AccidentalVal::NATURAL;
221 0 : if (is.accidentalType() != AccidentalType::NONE) {
222 0 : accVal = Accidental::subtype2value(is.accidentalType());
223 : } else {
224 0 : accVal = mu::engraving::noteValToAccidentalVal(nval, is.staff(), is.tick());
225 : }
226 :
227 0 : bool error = false;
228 0 : const AccidentalVal existingAccVal = is.segment()->measure()->findAccidental(is.segment(), is.staffIdx(), line, error);
229 :
230 0 : if (!error && accVal != existingAccVal) {
231 0 : AccidentalType type = Accidental::value2subtype(accVal);
232 0 : if (type == AccidentalType::NONE) {
233 0 : type = AccidentalType::NATURAL;
234 : }
235 :
236 0 : return type;
237 : }
238 :
239 0 : return AccidentalType::NONE;
240 : }
241 :
242 0 : inline QString extractSyllable(const QString& text)
243 : {
244 0 : QString _text = text;
245 :
246 0 : static const QRegularExpression lineBreaks("(\r|\n)+");
247 0 : _text.replace(lineBreaks, "\n");
248 0 : if (_text.startsWith(u"\n")) {
249 0 : _text.remove("\n");
250 : }
251 :
252 0 : static const QRegularExpression nonWhitespace("\\S");
253 0 : int textPos = _text.indexOf(nonWhitespace);
254 0 : if (textPos == -1) {
255 0 : return QString();
256 : }
257 :
258 0 : static const QRegularExpression separators("(_| |-|\n)");
259 0 : QRegularExpressionMatch match;
260 0 : int splitPos = _text.indexOf(separators, textPos, &match);
261 0 : if (splitPos == -1) {
262 0 : splitPos = _text.size();
263 : } else {
264 0 : splitPos += match.capturedLength();
265 : }
266 :
267 0 : return _text.mid(textPos, splitPos - textPos);
268 0 : }
269 :
270 0 : NotationInteraction::NotationInteraction(Notation* notation, INotationUndoStackPtr undoStack)
271 0 : : muse::Contextable(notation->iocContext()), m_notation(notation), m_undoStack(undoStack), m_editData(&m_scoreCallbacks)
272 : {
273 0 : m_selection = std::make_shared<NotationSelection>(notation, this);
274 0 : m_noteInput = std::make_shared<NotationNoteInput>(notation, this, m_undoStack, iocContext());
275 :
276 0 : m_selectionFilter = std::make_shared<NotationSelectionFilter>(notation, [this]() {
277 0 : m_selection->notifyAboutSelectionChangedIfNeed();
278 0 : });
279 :
280 0 : m_errorsController = std::make_shared<MScoreErrorsController>(iocContext());
281 :
282 0 : m_noteInput->stateChanged().onNotify(this, [this]() {
283 0 : if (!m_noteInput->isNoteInputMode()) {
284 0 : hideShadowNote();
285 : }
286 0 : });
287 :
288 0 : m_undoStack->undoRedoNotification().onNotify(this, [this]() {
289 0 : if (!isTextEditingStarted()) {
290 0 : endEditElement();
291 : }
292 0 : });
293 :
294 0 : m_undoStack->stackChanged().onNotify(this, [this]() {
295 0 : m_selection->notifyAboutSelectionChangedIfNeed();
296 0 : notifyAboutNoteInputStateChanged();
297 0 : });
298 :
299 0 : m_selection->selectionChanged().onNotify(this, [this]() {
300 0 : if (noteInput()->isNoteInputMode()) {
301 0 : notifyAboutNoteInputStateChanged();
302 : }
303 0 : });
304 :
305 0 : m_dragData.ed = mu::engraving::EditData(&m_scoreCallbacks);
306 :
307 0 : m_scoreCallbacks.setNotationInteraction(this);
308 :
309 0 : m_notation->scoreInited().onNotify(this, [this]() {
310 0 : onScoreInited();
311 0 : });
312 :
313 0 : m_notation->viewModeChanged().onNotify(this, [this]() {
314 0 : onViewModeChanged();
315 0 : });
316 :
317 0 : QObject::connect(&m_textCursorBlinkTimer, &QTimer::timeout, [this]() { blinkTextCursor(); });
318 0 : textEditingChanged().onNotify(this, [this]() {
319 0 : onTextEditingChanged();
320 0 : });
321 0 : }
322 :
323 0 : mu::engraving::Score* NotationInteraction::score() const
324 : {
325 0 : return m_notation->score();
326 : }
327 :
328 0 : void NotationInteraction::onScoreInited()
329 : {
330 0 : if (!score()) {
331 0 : return;
332 : }
333 :
334 0 : m_previewMeasure.setScore(score());
335 0 : m_scoreCallbacks.setScore(score());
336 :
337 0 : score()->elementDestroyed().onReceive(this, [this](mu::engraving::EngravingItem* element) {
338 0 : onElementDestroyed(element);
339 0 : });
340 : }
341 :
342 0 : void NotationInteraction::onViewModeChanged()
343 : {
344 0 : if (!score()) {
345 0 : return;
346 : }
347 :
348 0 : if (!score()->isLayoutMode(LayoutMode::LINE) && !score()->isLayoutMode(LayoutMode::HORIZONTAL_FIXED)) {
349 0 : return;
350 : }
351 :
352 : // VBoxes are not included in horizontal layouts - deselect them (and their contents) when switching to horizontal mode...
353 0 : const std::vector<EngravingItem*> sel = selection()->elements();
354 0 : for (EngravingItem* item : sel) {
355 0 : if (!item->findAncestor(ElementType::VBOX) || !item->findAncestor(ElementType::FBOX)) {
356 0 : continue;
357 : }
358 0 : score()->deselect(item);
359 0 : if (item == m_editData.element) {
360 0 : endEditElement();
361 : }
362 : }
363 0 : }
364 :
365 0 : void NotationInteraction::transaction(const muse::TranslatableString& actionName, std::function<void(mu::engraving::Transaction&)> func)
366 : {
367 0 : m_undoStack->transaction(actionName, func);
368 0 : }
369 :
370 0 : void NotationInteraction::startEdit(const muse::TranslatableString& actionName)
371 : {
372 0 : m_notifyAboutDropChanged = false;
373 :
374 0 : m_undoStack->prepareChanges(actionName);
375 0 : }
376 :
377 0 : void NotationInteraction::apply()
378 : {
379 0 : m_undoStack->commitChanges();
380 :
381 0 : if (m_notifyAboutDropChanged) {
382 0 : notifyAboutDropChanged();
383 : } else {
384 0 : notifyAboutNotationChanged();
385 : }
386 0 : }
387 :
388 0 : void NotationInteraction::rollback()
389 : {
390 0 : m_undoStack->rollbackChanges();
391 0 : }
392 :
393 0 : void NotationInteraction::notifyAboutDragChanged()
394 : {
395 0 : m_dragChanged.notify();
396 0 : }
397 :
398 0 : void NotationInteraction::notifyAboutDropChanged()
399 : {
400 0 : m_dropChanged.notify();
401 0 : }
402 :
403 0 : void NotationInteraction::notifyAboutNotationChanged(const muse::RectF& updateRect)
404 : {
405 0 : TRACEFUNC;
406 :
407 0 : m_notation->notifyAboutNotationChanged(updateRect);
408 0 : }
409 :
410 0 : void NotationInteraction::notifyAboutTextEditingStarted()
411 : {
412 0 : m_textEditingStarted.notify();
413 0 : }
414 :
415 0 : void NotationInteraction::notifyAboutTextEditingChanged()
416 : {
417 0 : m_textEditingChanged.notify();
418 0 : }
419 :
420 0 : void NotationInteraction::notifyAboutTextEditingEnded(TextBase* text)
421 : {
422 0 : m_textEditingEnded.send(text);
423 0 : }
424 :
425 0 : void NotationInteraction::notifyAboutNoteInputStateChanged()
426 : {
427 0 : m_noteInput->stateChanged().notify();
428 0 : }
429 :
430 0 : void NotationInteraction::paint(Painter* painter, const engraving::rendering::PaintOptions& opt)
431 : {
432 0 : m_previewMeasure.paint(painter);
433 :
434 0 : if (shouldDrawInputPreview()) {
435 0 : drawInputPreview(painter, opt);
436 : }
437 :
438 0 : score()->renderer()->drawItem(score()->shadowNote(), painter, opt);
439 :
440 0 : drawAnchorLines(painter);
441 0 : drawTextEditMode(painter, opt);
442 0 : drawSelectionRange(painter);
443 0 : drawGripPoints(painter, opt);
444 0 : drawLasso(painter, opt);
445 0 : drawDrop(painter);
446 0 : }
447 :
448 0 : INotationNoteInputPtr NotationInteraction::noteInput() const
449 : {
450 0 : return m_noteInput;
451 : }
452 :
453 0 : mu::engraving::ShadowNote* NotationInteraction::shadowNote() const
454 : {
455 0 : return score()->shadowNote();
456 : }
457 :
458 0 : void NotationInteraction::showShadowNoteForPosition(const PointF& pos)
459 : {
460 0 : const mu::engraving::InputState& inputState = score()->inputState();
461 0 : mu::engraving::ShadowNote& shadowNote = *score()->shadowNote();
462 :
463 0 : ShadowNoteParams params;
464 0 : if (!score()->getPosition(¶ms.position, pos, inputState.voice())) {
465 0 : shadowNote.setVisible(false);
466 0 : m_shadowNoteChanged.send(/*visible*/ false);
467 0 : return;
468 : }
469 :
470 0 : params.duration = inputState.duration();
471 0 : params.accidentalType = inputState.accidentalType();
472 0 : params.articulationIds = inputState.articulationIds();
473 :
474 0 : const bool show = doShowShadowNote(shadowNote, params);
475 0 : m_shadowNoteChanged.send(show);
476 0 : }
477 :
478 0 : void NotationInteraction::showShadowNoteForMidiPitch(const uint8_t pitch)
479 : {
480 0 : const mu::engraving::InputState& inputState = score()->inputState();
481 0 : mu::engraving::ShadowNote& shadowNote = *score()->shadowNote();
482 :
483 0 : Segment* inputSegment = inputState.segment();
484 0 : const Staff* inputStaff = inputState.staff();
485 0 : const Fraction tick = inputSegment->tick();
486 0 : if (!inputSegment || !inputStaff || !tick.isValid() || inputStaff->isDrumStaff(tick) || inputStaff->isTabStaff(tick)) {
487 : //! NOTE: Not yet compatible with drum or tab staves...
488 0 : hideShadowNote();
489 0 : return;
490 : }
491 :
492 0 : const double mag = inputStaff->staffMag(tick);
493 0 : const double lineDist = inputStaff->staffType(tick)->lineDistance().val()
494 0 : * 0.5 * mag * score()->style().spatium();
495 :
496 0 : const int octave = pitch / 12;
497 0 : const int line = octave * 7 + mu::engraving::pitch2step(pitch);
498 0 : const ClefType clef = inputStaff->clef(tick);
499 0 : const int rLine = mu::engraving::relStep(line, clef);
500 :
501 0 : const double xPos = inputSegment->canvasPos().x();
502 0 : const double yPos = inputSegment->system()->staffCanvasYpage(inputStaff->idx()) + rLine * lineDist;
503 :
504 0 : const PointF point = { xPos, yPos };
505 :
506 0 : const Position position { inputSegment, inputStaff->idx(), rLine, INVALID_FRET_INDEX, point };
507 0 : ShadowNoteParams params { inputState.duration(), inputState.accidentalType(), inputState.articulationIds(), position };
508 :
509 0 : const bool show = doShowShadowNote(shadowNote, params);
510 0 : m_shadowNoteChanged.send(show);
511 0 : }
512 :
513 0 : bool NotationInteraction::doShowShadowNote(ShadowNote& shadowNote, ShadowNoteParams& params)
514 : {
515 0 : Position& position = params.position;
516 :
517 0 : const mu::engraving::InputState& inputState = score()->inputState();
518 0 : const Staff* staff = score()->staff(position.staffIdx);
519 :
520 0 : mu::engraving::Segment* segment = position.segment;
521 :
522 0 : mu::engraving::Segment* shadowNoteActualSegment = position.segment->prev1enabled();
523 0 : if (shadowNoteActualSegment) {
524 0 : segment = shadowNoteActualSegment;
525 : }
526 :
527 0 : Fraction tick = segment->tick();
528 0 : qreal mag = staff->staffMag(tick);
529 :
530 0 : const mu::engraving::Instrument* instr = staff->part()->instrument(tick);
531 :
532 0 : mu::engraving::NoteHeadGroup noteheadGroup = mu::engraving::NoteHeadGroup::HEAD_NORMAL;
533 0 : mu::engraving::NoteHeadType noteHead = params.duration.headType();
534 :
535 0 : int line = position.line;
536 0 : voice_idx_t voice = 0;
537 0 : int drumNotePitch = -1;
538 :
539 0 : if (instr->useDrumset()) {
540 0 : const Drumset* ds = instr->drumset();
541 :
542 0 : const int noteInputPitch = inputState.drumNote();
543 :
544 0 : if (ds->isValid(noteInputPitch) && ds->line(noteInputPitch) == line) {
545 0 : noteheadGroup = ds->noteHead(noteInputPitch);
546 0 : voice = ds->voice(noteInputPitch);
547 0 : drumNotePitch = noteInputPitch;
548 : } else {
549 0 : const int pitch = ds->defaultPitchForLine(line);
550 0 : if (ds->isValid(pitch)) {
551 0 : noteheadGroup = ds->noteHead(pitch);
552 0 : voice = ds->voice(pitch);
553 0 : drumNotePitch = pitch;
554 : }
555 0 : if (drumNotePitch < 0) {
556 0 : shadowNote.setVisible(false);
557 0 : return false;
558 : }
559 : }
560 : } else {
561 0 : voice = inputState.voice();
562 : }
563 :
564 0 : shadowNote.setVisible(true);
565 0 : shadowNote.mutldata()->setMag(mag);
566 0 : shadowNote.setTick(tick);
567 0 : shadowNote.setStaffIdx(position.staffIdx);
568 0 : shadowNote.setVoice(voice);
569 0 : shadowNote.setLineIndex(line);
570 :
571 0 : const Color color = configuration()->noteInputPreviewColor();
572 0 : if (color.isValid() && color != configuration()->selectionColor()) {
573 0 : shadowNote.setColor(color);
574 : } else {
575 0 : shadowNote.setColor(configuration()->highlightSelectionColor(voice));
576 : }
577 :
578 : mu::engraving::SymId symNotehead;
579 :
580 0 : if (inputState.rest()) {
581 0 : Score* s = score()->paletteScore() ? score()->paletteScore() : score();
582 0 : mu::engraving::Rest* rest = mu::engraving::Factory::createRest(s->dummy(), params.duration.type());
583 0 : rest->setTicks(params.duration.fraction());
584 0 : symNotehead = rest->getSymbol(params.duration.type(), 0, staff->lines(position.segment->tick()));
585 0 : shadowNote.setState(symNotehead, params.duration, true, params.position.beyondScore);
586 0 : delete rest;
587 : } else {
588 0 : if (mu::engraving::NoteHeadGroup::HEAD_CUSTOM == noteheadGroup) {
589 0 : symNotehead = instr->drumset()->noteHeads(drumNotePitch, noteHead);
590 : } else {
591 0 : symNotehead = Note::noteHead(0, noteheadGroup, noteHead);
592 : }
593 :
594 0 : shadowNote.setState(symNotehead, params.duration, false, params.position.beyondScore, params.accidentalType,
595 0 : params.articulationIds);
596 : }
597 :
598 : // if upscaled, note appears by default a distance away from the segment start,
599 : // as described in ChordLayout::centreChords
600 0 : if (mag > 1.0) {
601 0 : qreal xOffset = (mag - 1.0) * 0.5 * shadowNote.symWidth(symNotehead);
602 0 : if (shadowNote.computeUp()) {
603 0 : position.pos.rx() += xOffset;
604 : } else {
605 0 : position.pos.rx() -= xOffset;
606 : }
607 : }
608 :
609 : // in any empty measure, pos will be right next to barline
610 : // so pad this by barNoteDistance
611 0 : qreal relX = position.pos.x() - position.segment->measure()->canvasPos().x();
612 0 : position.pos.rx() -= qMin(relX - score()->style().styleAbsolute(mu::engraving::Sid::barNoteDistance), 0.0);
613 :
614 0 : score()->renderer()->layoutItem(&shadowNote);
615 :
616 0 : shadowNote.setPos(position.pos);
617 :
618 0 : return true;
619 : }
620 :
621 0 : void NotationInteraction::hideShadowNote()
622 : {
623 0 : score()->shadowNote()->setVisible(false);
624 0 : m_shadowNoteChanged.send(/*visible*/ false);
625 0 : }
626 :
627 0 : RectF NotationInteraction::shadowNoteRect() const
628 : {
629 0 : const ShadowNote* note = score()->shadowNote();
630 0 : if (!note) {
631 0 : return RectF();
632 : }
633 :
634 0 : RectF rect = note->canvasBoundingRect();
635 :
636 0 : double penWidth = note->style().styleAbsolute(Sid::stemWidth);
637 0 : if (note->ledgerLinesVisible()) {
638 0 : double w = note->style().styleAbsolute(Sid::ledgerLineWidth);
639 0 : penWidth = std::max(penWidth, w);
640 : }
641 :
642 0 : penWidth *= note->mag();
643 0 : rect.adjust(-penWidth, -penWidth, penWidth, penWidth);
644 :
645 0 : return rect;
646 : }
647 :
648 0 : RectF NotationInteraction::previewMeasureRect() const
649 : {
650 0 : return m_previewMeasure.rect();
651 : }
652 :
653 0 : muse::async::Channel<bool> NotationInteraction::shadowNoteChanged() const
654 : {
655 0 : return m_shadowNoteChanged;
656 : }
657 :
658 0 : void NotationInteraction::toggleVisible()
659 : {
660 0 : transaction(TranslatableString("undoableAction", "Toggle visible"), [this](Transaction& tx) {
661 0 : EditVisibility::toggleVisible(tx, score());
662 0 : });
663 0 : }
664 :
665 0 : void NotationInteraction::setSelectionVisible(bool visible)
666 : {
667 0 : if (visible) {
668 0 : transaction(TranslatableString("undoableAction", "Make element(s) visible"), [this](Transaction& tx) {
669 0 : EditVisibility::setSelectedElementsVisible(tx, score());
670 0 : });
671 : } else {
672 0 : transaction(TranslatableString("undoableAction", "Make element(s) invisible"), [this](Transaction& tx) {
673 0 : EditVisibility::setSelectedElementsInvisible(tx, score());
674 0 : });
675 : }
676 0 : }
677 :
678 0 : EngravingItem* NotationInteraction::hitElement(const PointF& pos, float width) const
679 : {
680 0 : std::vector<mu::engraving::EngravingItem*> elements = hitElements(pos, width);
681 0 : if (elements.empty()) {
682 0 : return nullptr;
683 : }
684 0 : m_selection->onElementHit(elements.back());
685 0 : return elements.back();
686 0 : }
687 :
688 0 : Staff* NotationInteraction::hitStaff(const PointF& pos) const
689 : {
690 0 : return hitMeasure(pos).staff;
691 : }
692 :
693 0 : mu::engraving::Page* NotationInteraction::point2page(const PointF& p, bool useNearestPage) const
694 : {
695 0 : if (score()->linearMode()) {
696 0 : return score()->pages().empty() ? nullptr : score()->pages().front();
697 : }
698 :
699 0 : double shortestDistance = std::numeric_limits<double>::max();
700 0 : Page* nearestPage = nullptr;
701 0 : for (mu::engraving::Page* page : score()->pages()) {
702 0 : auto pageRect = page->ldata()->bbox().translated(page->pos());
703 0 : if (pageRect.contains(p)) {
704 0 : return page;
705 0 : } else if (useNearestPage && pageRect.distanceTo(p) < shortestDistance) {
706 0 : shortestDistance = pageRect.distanceTo(p);
707 0 : nearestPage = page;
708 : }
709 : }
710 0 : return nearestPage;
711 : }
712 :
713 0 : std::vector<EngravingItem*> NotationInteraction::elementsAt(const PointF& p) const
714 : {
715 0 : mu::engraving::Page* page = point2page(p);
716 0 : if (!page) {
717 0 : return {};
718 : }
719 :
720 0 : std::vector<EngravingItem*> el = page->items(p - page->pos());
721 0 : if (el.empty()) {
722 0 : return {};
723 : }
724 :
725 0 : std::sort(el.begin(), el.end(), NotationInteraction::elementIsLess);
726 :
727 0 : return el;
728 0 : }
729 :
730 0 : EngravingItem* NotationInteraction::elementAt(const PointF& p) const
731 : {
732 0 : std::vector<EngravingItem*> el = elementsAt(p);
733 0 : return el.empty() || el.back()->isPage() ? nullptr : el.back();
734 0 : }
735 :
736 0 : std::vector<EngravingItem*> NotationInteraction::hitElements(const PointF& pos, float width) const
737 : {
738 0 : mu::engraving::Page* page = point2page(pos);
739 0 : if (!page) {
740 0 : return {};
741 : }
742 :
743 0 : std::vector<EngravingItem*> hitElements;
744 :
745 0 : PointF posOnPage = pos - page->pos();
746 :
747 0 : if (isTextEditingStarted() && page == m_editData.element->findAncestor(ElementType::PAGE)) {
748 0 : auto editW = width * 2;
749 0 : RectF editHitRect(posOnPage.x() - editW, posOnPage.y() - editW, 2.0 * editW, 2.0 * editW);
750 0 : if (m_editData.element->intersects(editHitRect)) {
751 0 : hitElements.push_back(m_editData.element);
752 0 : return hitElements;
753 : }
754 : }
755 :
756 0 : RectF hitRect(posOnPage.x() - width, posOnPage.y() - width, 3.0 * width, 3.0 * width);
757 :
758 0 : std::vector<EngravingItem*> potentiallyHitElements = page->items(hitRect);
759 :
760 0 : auto canHitElement = [](const EngravingItem* element) {
761 0 : if (!element->selectable() || element->isPage()) {
762 0 : return false;
763 : }
764 :
765 0 : if (element->isSoundFlag()) {
766 0 : return !toSoundFlag(element)->shouldHide();
767 0 : } else if (!element->isInteractionAvailable()) {
768 0 : return false;
769 : }
770 :
771 0 : return true;
772 : };
773 :
774 0 : for (EngravingItem* element : potentiallyHitElements) {
775 0 : element->itemDiscovered = 0;
776 :
777 0 : if (!canHitElement(element)) {
778 0 : continue;
779 : }
780 :
781 0 : if (element->hitShapeContains(posOnPage)) {
782 0 : hitElements.push_back(element);
783 : }
784 : }
785 :
786 0 : if (hitElements.empty() || (hitElements.size() == 1 && hitElements.front()->isMeasure())) {
787 : //
788 : // if no relevant element hit, look nearby
789 : //
790 0 : for (EngravingItem* element : potentiallyHitElements) {
791 0 : if (!canHitElement(element)) {
792 0 : continue;
793 : }
794 :
795 0 : if (element->hitShapeIntersects(hitRect)) {
796 0 : hitElements.push_back(element);
797 : }
798 : }
799 : }
800 :
801 0 : if (!hitElements.empty()) {
802 0 : std::sort(hitElements.begin(), hitElements.end(), NotationInteraction::elementIsLess);
803 : } else {
804 0 : Measure* measure = hitMeasure(pos).measure;
805 0 : if (measure) {
806 0 : hitElements.push_back(measure);
807 : }
808 : }
809 :
810 0 : return hitElements;
811 0 : }
812 :
813 0 : NotationInteraction::HitMeasureData NotationInteraction::hitMeasure(const PointF& pos) const
814 : {
815 0 : mu::engraving::staff_idx_t staffIndex = muse::nidx;
816 0 : mu::engraving::Segment* segment = nullptr;
817 0 : PointF offset;
818 :
819 0 : Measure* measure = score()->pos2measure(pos, &staffIndex, /*pitch*/ 0, &segment, &offset);
820 :
821 0 : HitMeasureData result;
822 0 : if (measure && measure->staffLines(staffIndex)->canvasHitShape().contains(pos)) {
823 0 : result.measure = measure;
824 0 : result.staff = score()->staff(staffIndex);
825 : }
826 :
827 0 : return result;
828 : }
829 :
830 0 : bool NotationInteraction::elementIsLess(const EngravingItem* e1, const EngravingItem* e2)
831 : {
832 0 : if (e1->selectable() && !e2->selectable()) {
833 0 : return false;
834 : }
835 0 : if (!e1->selectable() && e2->selectable()) {
836 0 : return true;
837 : }
838 0 : if (e1->isNote() && (e2->isStem() || e2->isHook())) {
839 0 : return false;
840 : }
841 0 : if (e2->isNote() && (e1->isStem() || e1->isHook())) {
842 0 : return true;
843 : }
844 0 : if (e1->isStem() && e2->isHook()) {
845 0 : return false;
846 : }
847 0 : if (e2->isStem() && e1->isHook()) {
848 0 : return true;
849 : }
850 0 : if (e1->isText() && e2->isBox()) {
851 0 : return false;
852 : }
853 0 : if (e1->isBox() && e2->isText()) {
854 0 : return true;
855 : }
856 0 : if (e1->isImage() && e2->isBox()) {
857 0 : return false;
858 : }
859 0 : if (e1->isBox() && e2->isImage()) {
860 0 : return true;
861 : }
862 0 : if ((e1->isHarmony() || e1->isFretDiagram()) && e2->isFBox()) {
863 0 : return false;
864 : }
865 0 : if (e1->isFBox() && (e2->isHarmony() || e2->isFretDiagram())) {
866 0 : return true;
867 : }
868 0 : if (e1->z() == e2->z()) {
869 : // same stacking order, prefer non-hidden elements
870 0 : if (e1->type() == e2->type()) {
871 0 : if (e1->isNoteDot()) {
872 0 : const NoteDot* n1 = toNoteDot(e1);
873 0 : const NoteDot* n2 = toNoteDot(e2);
874 0 : if (n1->note() && n1->note()->hidden()) {
875 0 : return false;
876 0 : } else if (n2->note() && n2->note()->hidden()) {
877 0 : return true;
878 : }
879 0 : } else if (e1->isNote()) {
880 0 : const Note* n1 = toNote(e1);
881 0 : const Note* n2 = toNote(e2);
882 0 : if (n1->hidden()) {
883 0 : return false;
884 0 : } else if (n2->hidden()) {
885 0 : return true;
886 : }
887 : }
888 : }
889 : // different types, or same type but nothing hidden - use track
890 0 : return e1->track() < e2->track();
891 : }
892 :
893 : // default case, use stacking order
894 0 : return e1->z() < e2->z();
895 : }
896 :
897 0 : const NotationInteraction::HitElementContext& NotationInteraction::hitElementContext() const
898 : {
899 0 : return m_hitElementContext;
900 : }
901 :
902 0 : void NotationInteraction::setHitElementContext(const HitElementContext& context)
903 : {
904 0 : m_hitElementContext = context;
905 0 : }
906 :
907 0 : EngravingItem* NotationInteraction::contextItem() const
908 : {
909 0 : EngravingItem* item = selection()->element();
910 0 : return item ? item : hitElementContext().element;
911 : }
912 :
913 0 : void NotationInteraction::findAndSelectChordRest(const Fraction& tick)
914 : {
915 0 : IF_ASSERT_FAILED(selection() && score()) {
916 0 : return;
917 : }
918 :
919 0 : Segment* startSeg = score()->tick2leftSegment(tick, /*useMMrest*/ false, SegmentType::ChordRest);
920 0 : IF_ASSERT_FAILED(startSeg && startSeg->isChordRestType()) {
921 0 : return;
922 : }
923 :
924 0 : staff_idx_t lastSelectedStaffIdx = 0;
925 0 : voice_idx_t lastSelectedVoiceIdx = 0;
926 0 : if (selection()->isRange()) {
927 0 : lastSelectedStaffIdx = selection()->range()->startStaffIndex();
928 0 : } else if (!selection()->elements().empty()) {
929 0 : const EngravingItem* lastSelectedItem = selection()->elements().back();
930 0 : if (lastSelectedItem) {
931 0 : lastSelectedStaffIdx = lastSelectedItem->staffIdx();
932 0 : lastSelectedVoiceIdx = lastSelectedItem->voice();
933 : }
934 : }
935 :
936 : // startSeg could be a CR segment in any staff. The idea of this method is to select the CR in the last selected
937 : // staff (or the top staff if none was selected). The following outer loop iterates backwards through previous CR
938 : // segments until a valid CR is found in the desired staff...
939 0 : EngravingItem* toSelect = nullptr;
940 0 : for (Segment* currSeg = startSeg; currSeg && !toSelect; currSeg = currSeg->prev(SegmentType::ChordRest)) {
941 0 : toSelect = currSeg->element(staff2track(lastSelectedStaffIdx, lastSelectedVoiceIdx));
942 0 : if (toSelect && toSelect->isChordRest()) {
943 0 : break;
944 : }
945 0 : toSelect = nullptr;
946 : // Inner loop: Couldn't find a valid CR in the last selected voice - try other voices in the same staff...
947 0 : for (voice_idx_t currVoice = 0; currVoice < VOICES; ++currVoice) {
948 0 : if (currVoice == lastSelectedVoiceIdx) {
949 : // Already tried this...
950 0 : continue;
951 : }
952 0 : toSelect = currSeg->element(staff2track(lastSelectedStaffIdx, currVoice));
953 0 : if (toSelect && toSelect->isChordRest()) {
954 0 : break;
955 : }
956 0 : toSelect = nullptr;
957 : }
958 : }
959 :
960 0 : IF_ASSERT_FAILED(toSelect) {
961 0 : return;
962 : }
963 :
964 0 : std::vector<EngravingItem*> itemsToSelect;
965 0 : if (toSelect->isChord()) {
966 0 : const std::vector<Note*>& notes = toChord(toSelect)->notes();
967 0 : itemsToSelect.insert(itemsToSelect.end(), notes.begin(), notes.end());
968 : } else {
969 0 : itemsToSelect = { toSelect };
970 : }
971 :
972 0 : select(itemsToSelect, SelectType::REPLACE);
973 0 : }
974 :
975 0 : void NotationInteraction::select(const std::vector<EngravingItem*>& elements, SelectType type, staff_idx_t staffIndex)
976 : {
977 0 : if (needEndTextEditing(elements)) {
978 0 : endEditText();
979 0 : } else if (needEndElementEditing(elements)) {
980 0 : endEditElement();
981 : }
982 :
983 0 : m_selection->select(elements, type, staffIndex);
984 0 : }
985 :
986 0 : void NotationInteraction::select(SelectionTarget target)
987 : {
988 0 : m_selection->select(target);
989 0 : }
990 :
991 0 : void NotationInteraction::addToSelection(SelectionTarget target)
992 : {
993 0 : m_selection->addToSelection(target);
994 0 : }
995 :
996 0 : void NotationInteraction::selectAndStartEditIfNeeded(EngravingItem* element)
997 : {
998 0 : if (element->isSpanner() && !toSpanner(element)->segmentsEmpty()) {
999 0 : element = toSpanner(element)->frontSegment();
1000 : }
1001 :
1002 0 : select({ element }, SelectType::SINGLE);
1003 :
1004 0 : if (element->needStartEditingAfterSelecting()) {
1005 0 : startEditElement(element);
1006 : }
1007 0 : }
1008 :
1009 0 : INotationSelectionPtr NotationInteraction::selection() const
1010 : {
1011 0 : return m_selection;
1012 : }
1013 :
1014 0 : void NotationInteraction::clearSelection()
1015 : {
1016 0 : TRACEFUNC;
1017 :
1018 0 : if (isEditingElement()) {
1019 0 : endEditElement();
1020 : }
1021 :
1022 0 : if (isDragStarted()) {
1023 0 : doEndDrag();
1024 0 : rollback();
1025 0 : notifyAboutNotationChanged();
1026 : }
1027 :
1028 0 : if (selection()->isNone()) {
1029 0 : return;
1030 : }
1031 :
1032 0 : m_selection->clearSelection();
1033 :
1034 0 : setHitElementContext(HitElementContext());
1035 0 : }
1036 :
1037 0 : muse::async::Notification NotationInteraction::selectionChanged() const
1038 : {
1039 0 : return m_selection->selectionChanged();
1040 : }
1041 :
1042 0 : INotationSelectionFilterPtr NotationInteraction::selectionFilter() const
1043 : {
1044 0 : return m_selectionFilter;
1045 : }
1046 :
1047 0 : INotationElementsPtr NotationInteraction::elements() const
1048 : {
1049 0 : return m_notation->elements();
1050 : }
1051 :
1052 0 : bool NotationInteraction::isDragStarted() const
1053 : {
1054 0 : if (m_dragData.dragGroups.size() > 0) {
1055 0 : return true;
1056 : }
1057 :
1058 0 : if (m_lasso && !m_lasso->isEmpty()) {
1059 0 : return true;
1060 : }
1061 :
1062 0 : return false;
1063 : }
1064 :
1065 0 : void NotationInteraction::DragData::reset()
1066 : {
1067 0 : beginMove = QPointF();
1068 0 : elementOffset = QPointF();
1069 0 : ed = mu::engraving::EditData(ed.view());
1070 0 : dragGroups.clear();
1071 0 : }
1072 :
1073 0 : void NotationInteraction::startDrag(const std::vector<EngravingItem*>& elems,
1074 : const PointF& eoffset,
1075 : const IsDraggable& isDraggable)
1076 : {
1077 0 : m_dragData.reset();
1078 0 : m_dragData.elements = elems;
1079 0 : m_dragData.elementOffset = eoffset;
1080 0 : m_editData.modifiers = keyboardModifier(QGuiApplication::keyboardModifiers());
1081 :
1082 0 : for (EngravingItem* e : m_dragData.elements) {
1083 0 : bool draggable = isDraggable(e);
1084 :
1085 0 : if (!draggable && e->isSpanner()) {
1086 0 : Spanner* s = toSpanner(e);
1087 0 : draggable = !s->segmentsEmpty() && isDraggable(s->frontSegment());
1088 : }
1089 :
1090 0 : if (!draggable) {
1091 0 : continue;
1092 : }
1093 :
1094 0 : std::unique_ptr<mu::engraving::ElementGroup> g = e->getDragGroup(isDraggable);
1095 0 : if (g && g->enabled()) {
1096 0 : m_dragData.dragGroups.push_back(std::move(g));
1097 : }
1098 0 : }
1099 :
1100 0 : startEdit(TranslatableString("undoableAction", "Drag element(s)", nullptr, int(m_dragData.elements.size())));
1101 :
1102 0 : double scaling = m_notation->viewState()->matrix().m11();
1103 0 : double proximity = configuration()->selectionProximity() * 0.5 / scaling;
1104 0 : m_scoreCallbacks.setSelectionProximity(proximity);
1105 :
1106 0 : if (isGripEditStarted()) {
1107 0 : m_editData.element->startDragGrip(m_editData);
1108 0 : return;
1109 : }
1110 :
1111 0 : m_dragData.ed.modifiers = keyboardModifier(QGuiApplication::keyboardModifiers());
1112 0 : for (auto& group : m_dragData.dragGroups) {
1113 0 : group->startDrag(m_dragData.ed);
1114 : }
1115 : }
1116 :
1117 0 : void NotationInteraction::doDragLasso(const PointF& pt)
1118 : {
1119 0 : if (!m_lasso) {
1120 0 : m_lasso = new mu::engraving::Lasso(score());
1121 : }
1122 :
1123 0 : score()->addRefresh(m_lasso->canvasBoundingRect());
1124 0 : RectF r;
1125 0 : r.setCoords(m_dragData.beginMove.x(), m_dragData.beginMove.y(), pt.x(), pt.y());
1126 0 : m_lasso->setbbox(r.normalized());
1127 0 : score()->addRefresh(m_lasso->canvasBoundingRect());
1128 0 : score()->lassoSelect(m_lasso->ldata()->bbox());
1129 0 : score()->update();
1130 0 : }
1131 :
1132 0 : void NotationInteraction::endLasso()
1133 : {
1134 0 : if (!m_lasso) {
1135 0 : return;
1136 : }
1137 :
1138 0 : score()->addRefresh(m_lasso->canvasBoundingRect());
1139 0 : m_lasso->setbbox(RectF());
1140 0 : score()->lassoSelectEnd();
1141 0 : score()->update();
1142 : }
1143 :
1144 0 : void NotationInteraction::drag(const PointF& fromPos, const PointF& toPos, DragMode mode)
1145 : {
1146 0 : if (m_dragData.beginMove.isNull()) {
1147 0 : m_dragData.beginMove = fromPos;
1148 0 : m_dragData.ed.pos = fromPos;
1149 : }
1150 :
1151 0 : PointF normalizedBegin = m_dragData.beginMove - m_dragData.elementOffset;
1152 0 : PointF delta = toPos - normalizedBegin;
1153 0 : PointF evtDelta = toPos - m_dragData.ed.pos;
1154 0 : PointF moveDelta = delta - m_dragData.elementOffset;
1155 :
1156 0 : bool constrainDirection = !(isGripEditStarted() && m_editData.element && m_editData.element->isBarLine());
1157 0 : if (constrainDirection) {
1158 0 : switch (mode) {
1159 0 : case DragMode::BothXY:
1160 0 : break;
1161 0 : case DragMode::OnlyX:
1162 0 : delta.setY(m_dragData.ed.delta.y());
1163 0 : moveDelta.setY(m_dragData.ed.moveDelta.y());
1164 0 : evtDelta.setY(0.0);
1165 0 : break;
1166 0 : case DragMode::OnlyY:
1167 0 : delta.setX(m_dragData.ed.delta.x());
1168 0 : moveDelta.setX(m_dragData.ed.moveDelta.x());
1169 0 : evtDelta.setX(0.0);
1170 0 : break;
1171 : }
1172 : }
1173 :
1174 0 : m_dragData.ed.lastPos = m_dragData.ed.pos;
1175 :
1176 0 : m_dragData.ed.hRaster = configuration()->isSnappedToGrid(muse::Orientation::Horizontal);
1177 0 : m_dragData.ed.vRaster = configuration()->isSnappedToGrid(muse::Orientation::Vertical);
1178 0 : m_dragData.ed.delta = delta;
1179 0 : m_dragData.ed.moveDelta = moveDelta;
1180 0 : m_dragData.ed.evtDelta = evtDelta;
1181 0 : m_dragData.ed.pos = toPos;
1182 0 : m_dragData.ed.modifiers = keyboardModifier(QGuiApplication::keyboardModifiers());
1183 :
1184 0 : m_dragData.ed.isEditMode = isEditingElement();
1185 :
1186 0 : if (isTextEditingStarted()) {
1187 0 : m_editData.pos = toPos;
1188 0 : toTextBase(m_editData.element)->dragTo(m_editData);
1189 :
1190 0 : notifyAboutTextEditingChanged();
1191 0 : return;
1192 : }
1193 :
1194 0 : if (isGripEditStarted()) {
1195 0 : m_dragData.ed.curGrip = m_editData.curGrip;
1196 0 : m_dragData.ed.delta = evtDelta;
1197 0 : m_dragData.ed.moveDelta = m_dragData.ed.delta - m_dragData.elementOffset;
1198 0 : m_dragData.ed.addData(m_editData.getData(m_editData.element));
1199 0 : m_editData.element->dragGrip(m_dragData.ed);
1200 :
1201 0 : if (m_editData.element->isDynamic()) {
1202 : // When the dynamic has no left grip, the right grip will have index zero, a.k.a. Grip::LEFT.
1203 : // TODO: refactor all code that works with Grips, so that this is not necessary
1204 0 : Dynamic* dynamic = toDynamic(m_editData.element);
1205 0 : bool isLeftGrip = dynamic->hasLeftGrip() ? m_editData.curGrip == Grip::LEFT : false;
1206 0 : addHairpinOnGripDrag(m_editData, isLeftGrip);
1207 : }
1208 : } else {
1209 0 : for (auto& group : m_dragData.dragGroups) {
1210 0 : score()->addRefresh(group->drag(m_dragData.ed));
1211 : }
1212 : }
1213 :
1214 0 : score()->update();
1215 :
1216 0 : if (isGripEditStarted()) {
1217 0 : if (m_editData.element->isDynamic() && !m_editData.isStartEndGrip()) {
1218 0 : updateDragAnchorLines();
1219 : } else {
1220 0 : updateGripAnchorLines();
1221 : }
1222 : } else {
1223 0 : updateDragAnchorLines();
1224 : }
1225 :
1226 0 : if (m_dragData.elements.size() == 0) {
1227 0 : doDragLasso(toPos);
1228 : }
1229 :
1230 0 : notifyAboutDragChanged();
1231 : }
1232 :
1233 0 : void NotationInteraction::doEndDrag()
1234 : {
1235 0 : if (isGripEditStarted()) {
1236 0 : m_editData.element->endDragGrip(m_editData);
1237 0 : m_editData.element->endEdit(m_editData);
1238 : } else {
1239 0 : for (auto& group : m_dragData.dragGroups) {
1240 0 : group->endDrag(m_dragData.ed);
1241 : }
1242 0 : if (m_lasso && !m_lasso->isEmpty()) {
1243 0 : endLasso();
1244 : }
1245 : }
1246 :
1247 0 : m_dragData.reset();
1248 :
1249 0 : if (m_dropData.elementDropData.has_value()) {
1250 0 : setDropTarget(nullptr, false);
1251 : } else {
1252 0 : resetAnchorLines();
1253 : }
1254 0 : }
1255 :
1256 0 : void NotationInteraction::endDrag()
1257 : {
1258 0 : doEndDrag();
1259 0 : apply();
1260 :
1261 0 : if (m_editData.isHairpinDragCreatedFromDynamic) {
1262 : // Merge the two actions of hairpin creation + hairpin drag
1263 0 : assert(m_undoStack->currentStateIndex() >= 2);
1264 0 : if (m_undoStack->currentStateIndex() >= 2) {
1265 0 : m_undoStack->mergeTransactions(m_undoStack->currentStateIndex() - 2);
1266 : }
1267 0 : m_editData.isHairpinDragCreatedFromDynamic = false;
1268 : }
1269 :
1270 0 : notifyAboutDragChanged();
1271 0 : checkAndShowError();
1272 0 : }
1273 :
1274 0 : muse::async::Notification NotationInteraction::dragChanged() const
1275 : {
1276 0 : return m_dragChanged;
1277 : }
1278 :
1279 0 : bool NotationInteraction::isOutgoingDragElementAllowed(const EngravingItem* element) const
1280 : {
1281 0 : if (!element) {
1282 0 : return false;
1283 : }
1284 :
1285 0 : switch (element->type()) {
1286 0 : case ElementType::MEASURE:
1287 : case ElementType::NOTE:
1288 : case ElementType::VBOX:
1289 : case ElementType::HBOX:
1290 : case ElementType::TBOX:
1291 : case ElementType::FBOX:
1292 : // TODO: Bends & NoteLines can't be copy-dragged until corresponding SingleLayout::layout and SingleDraw::draw methods have been implemented
1293 : case ElementType::GUITAR_BEND:
1294 : case ElementType::GUITAR_BEND_SEGMENT:
1295 : case ElementType::GUITAR_BEND_HOLD:
1296 : case ElementType::GUITAR_BEND_HOLD_SEGMENT:
1297 : case ElementType::GUITAR_BEND_TEXT:
1298 : case ElementType::NOTELINE:
1299 : case ElementType::NOTELINE_SEGMENT:
1300 0 : return false;
1301 0 : default: return true;
1302 : }
1303 : }
1304 :
1305 : //! NOTE: Copied from ScoreView::cloneElement
1306 0 : void NotationInteraction::startOutgoingDragElement(const EngravingItem* element, QObject* dragSource, const PointF& hotSpot)
1307 : {
1308 0 : if (isDragStarted()) {
1309 0 : endOutgoingDrag();
1310 : }
1311 :
1312 0 : QMimeData* mimeData = new QMimeData();
1313 0 : if (element->isSpannerSegment()) {
1314 0 : Spanner* s = toSpannerSegment(element)->spanner();
1315 0 : mimeData->setData(mu::engraving::mimeSymbolFormat, s->mimeData().toQByteArray());
1316 : } else {
1317 0 : mimeData->setData(mu::engraving::mimeSymbolFormat, element->mimeData().toQByteArray());
1318 : }
1319 :
1320 0 : m_outgoingDrag = new QDrag(dragSource);
1321 0 : m_outgoingDrag->setMimeData(mimeData);
1322 :
1323 0 : QObject::connect(m_outgoingDrag, &QDrag::destroyed, [this]() {
1324 0 : m_outgoingDrag = nullptr;
1325 0 : });
1326 :
1327 0 : const QScreen* screen = QGuiApplication::primaryScreen();
1328 0 : const qreal physDpiRatio = screen->physicalDotsPerInch() / mu::engraving::DPI;
1329 0 : const qreal devicePixelRatio = screen->devicePixelRatio();
1330 :
1331 0 : const RectF bbox = element->ldata()->bbox().adjusted(-4.0, -4.0, 4.0, 4.0);
1332 0 : const double scaledWidth = bbox.width() * physDpiRatio;
1333 0 : const double scaledHeight = bbox.height() * physDpiRatio;
1334 :
1335 : QPixmap pixmap(qCeil(scaledWidth * devicePixelRatio),
1336 0 : qCeil(scaledHeight * devicePixelRatio));
1337 0 : pixmap.setDevicePixelRatio(devicePixelRatio);
1338 0 : pixmap.fill(Qt::transparent);
1339 :
1340 0 : QPainter qp(&pixmap);
1341 :
1342 0 : Painter p(&qp, "startOutgoingDragElement");
1343 0 : p.setAntialiasing(true);
1344 :
1345 0 : p.scale(physDpiRatio, physDpiRatio);
1346 0 : p.translate(qAbs(bbox.x()), qAbs(bbox.y()));
1347 :
1348 0 : mu::engraving::rendering::PaintOptions opt;
1349 0 : opt.invertColors = configuration()->shouldInvertScore();
1350 0 : engravingRenderer()->drawItem(element, &p, opt);
1351 :
1352 0 : m_outgoingDrag->setPixmap(pixmap);
1353 0 : m_outgoingDrag->setHotSpot((hotSpot * physDpiRatio).toQPoint());
1354 :
1355 0 : m_outgoingDrag->exec(Qt::CopyAction);
1356 0 : }
1357 :
1358 0 : void NotationInteraction::startOutgoingDragRange(QObject* dragSource)
1359 : {
1360 0 : if (isDragStarted()) {
1361 0 : endOutgoingDrag();
1362 : }
1363 :
1364 0 : if (!selection()->isRange() || !selection()->canCopy()) {
1365 0 : return;
1366 : }
1367 :
1368 0 : QMimeData* mimeData = selection()->qMimeData();
1369 0 : if (!mimeData) {
1370 0 : return;
1371 : }
1372 :
1373 0 : m_outgoingDrag = new QDrag(dragSource);
1374 0 : m_outgoingDrag->setMimeData(mimeData);
1375 :
1376 0 : QObject::connect(m_outgoingDrag, &QDrag::destroyed, [this]() {
1377 0 : m_outgoingDrag = nullptr;
1378 0 : });
1379 :
1380 0 : QPixmap pixmap(1, 1);
1381 0 : pixmap.fill(Qt::transparent);
1382 0 : m_outgoingDrag->setPixmap(pixmap);
1383 :
1384 0 : m_outgoingDrag->exec(Qt::MoveAction | Qt::CopyAction);
1385 0 : }
1386 :
1387 0 : bool NotationInteraction::isOutgoingDragStarted() const
1388 : {
1389 0 : return m_outgoingDrag != nullptr;
1390 : }
1391 :
1392 0 : void NotationInteraction::endOutgoingDrag()
1393 : {
1394 0 : if (m_outgoingDrag) {
1395 0 : delete m_outgoingDrag;
1396 0 : m_outgoingDrag = nullptr;
1397 : }
1398 0 : }
1399 :
1400 : //! NOTE Copied from ScoreView::dragEnterEvent
1401 0 : bool NotationInteraction::startDropSingle(const QByteArray& edata)
1402 : {
1403 0 : resetDropData();
1404 :
1405 0 : m_dropData.elementDropData = ElementDropData();
1406 0 : ElementDropData& edd = m_dropData.elementDropData.value();
1407 0 : edd.ed = mu::engraving::EditData(&m_scoreCallbacks);
1408 :
1409 0 : mu::engraving::XmlReader e(edata);
1410 0 : edd.ed.dragOffset = QPointF();
1411 0 : Fraction duration; // dummy
1412 0 : ElementType type = EngravingItem::readType(e, &edd.ed.dragOffset, &duration);
1413 :
1414 0 : EngravingItem* el = engraving::Factory::createItem(type, score()->dummy());
1415 0 : if (el) {
1416 0 : if (type == ElementType::BAR_LINE || type == ElementType::BRACKET) {
1417 0 : double spatium = score()->style().spatium();
1418 0 : el->setHeight(spatium * 5);
1419 : }
1420 0 : edd.ed.dropElement = el;
1421 :
1422 0 : rw::RWRegister::reader()->readItem(edd.ed.dropElement, e);
1423 :
1424 0 : engravingRenderer()->layoutItem(edd.ed.dropElement);
1425 0 : return true;
1426 : }
1427 :
1428 0 : resetDropData();
1429 0 : return false;
1430 0 : }
1431 :
1432 0 : bool NotationInteraction::startDropRange(const QByteArray& data)
1433 : {
1434 0 : resetDropData();
1435 :
1436 0 : m_dropData.rangeDropData = RangeDropData();
1437 0 : RangeDropData& rdd = m_dropData.rangeDropData.value();
1438 :
1439 0 : mu::engraving::XmlReader reader(data);
1440 0 : while (reader.readNextStartElement()) {
1441 0 : if (reader.name() == "StaffList") {
1442 0 : rdd.sourceTick = Fraction::fromString(reader.attribute("tick"));
1443 0 : rdd.tickLength = Fraction::fromString(reader.attribute("len"));
1444 0 : rdd.timeStretch = reader.hasAttribute("timeStretch") ? Fraction::fromString(reader.attribute("timeStretch")) : Fraction(1, 1);
1445 0 : rdd.sourceStaffIdx = static_cast<staff_idx_t>(reader.intAttribute("staff", -1));
1446 0 : rdd.numStaves = reader.intAttribute("staves", 0);
1447 0 : break;
1448 : }
1449 : }
1450 :
1451 0 : if (rdd.tickLength.isZero() || rdd.numStaves == 0) {
1452 0 : resetDropData();
1453 0 : return false;
1454 : }
1455 :
1456 0 : return true;
1457 0 : }
1458 :
1459 0 : bool NotationInteraction::startDropRange(const Fraction& sourceTick, const Fraction& tickLength,
1460 : engraving::staff_idx_t sourceStaffIdx, size_t numStaves, bool preserveMeasureAlignment)
1461 : {
1462 0 : if (tickLength.isZero() || numStaves == 0) {
1463 0 : return false;
1464 : }
1465 :
1466 0 : resetDropData();
1467 :
1468 0 : m_dropData.rangeDropData = RangeDropData();
1469 0 : RangeDropData& rdd = m_dropData.rangeDropData.value();
1470 :
1471 0 : rdd.sourceTick = sourceTick;
1472 0 : rdd.tickLength = tickLength;
1473 0 : rdd.timeStretch = score()->staff(sourceStaffIdx)->timeStretch(sourceTick);
1474 0 : rdd.sourceStaffIdx = sourceStaffIdx;
1475 0 : rdd.numStaves = numStaves;
1476 0 : rdd.preserveMeasureAlignment = preserveMeasureAlignment;
1477 :
1478 0 : return true;
1479 : }
1480 :
1481 0 : bool NotationInteraction::startDropImage(const QUrl& url)
1482 : {
1483 0 : if (url.scheme() != "file") {
1484 0 : return false;
1485 : }
1486 :
1487 0 : auto image = toImage(Factory::createItem(mu::engraving::ElementType::IMAGE, score()->dummy()));
1488 0 : if (!image->loadFromFile(url.toLocalFile())) {
1489 0 : return false;
1490 : }
1491 :
1492 0 : resetDropData();
1493 :
1494 0 : m_dropData.elementDropData = ElementDropData();
1495 0 : ElementDropData& edd = m_dropData.elementDropData.value();
1496 0 : edd.ed = mu::engraving::EditData(&m_scoreCallbacks);
1497 :
1498 0 : edd.ed.dropElement = image;
1499 0 : edd.ed.dragOffset = QPointF();
1500 0 : edd.ed.dropElement->setOwnershipParent(nullptr);
1501 :
1502 0 : engravingRenderer()->layoutItem(edd.ed.dropElement);
1503 :
1504 0 : return true;
1505 : }
1506 :
1507 : //! NOTE Copied from ScoreView::dragMoveEvent
1508 0 : bool NotationInteraction::updateDropSingle(const PointF& pos, Qt::KeyboardModifiers modifiers)
1509 : {
1510 0 : if (!m_dropData.elementDropData.has_value()) {
1511 0 : return false;
1512 : }
1513 :
1514 0 : ElementDropData& edd = m_dropData.elementDropData.value();
1515 :
1516 0 : EngravingItem* dropElem = edd.ed.dropElement;
1517 0 : if (!dropElem) {
1518 0 : return false;
1519 : }
1520 :
1521 0 : switch (dropElem->type()) {
1522 0 : case ElementType::BRACKET:
1523 : case ElementType::GRADUAL_TEMPO_CHANGE:
1524 : case ElementType::JUMP:
1525 : case ElementType::KEYSIG:
1526 : case ElementType::LAYOUT_BREAK:
1527 : case ElementType::MARKER:
1528 : case ElementType::MEASURE_REPEAT:
1529 : case ElementType::MEASURE_NUMBER:
1530 : case ElementType::TIMESIG:
1531 : case ElementType::SPACER:
1532 : case ElementType::STAFFTYPE_CHANGE:
1533 : case ElementType::STRING_TUNINGS:
1534 : case ElementType::VOLTA: {
1535 0 : edd.ed.modifiers = keyboardModifier(modifiers);
1536 0 : return prepareDropMeasureBaseAnchorElement(pos);
1537 : }
1538 0 : case ElementType::PEDAL:
1539 : case ElementType::LET_RING:
1540 : case ElementType::VIBRATO:
1541 : case ElementType::PALM_MUTE:
1542 : case ElementType::OTTAVA:
1543 : case ElementType::TRILL:
1544 : case ElementType::HAIRPIN:
1545 : case ElementType::TEXTLINE:
1546 : case ElementType::WHAMMY_BAR: {
1547 0 : edd.ed.modifiers = keyboardModifier(modifiers);
1548 0 : return prepareDropTimeAnchorElement(pos);
1549 : }
1550 0 : case ElementType::IMAGE:
1551 : case ElementType::SYMBOL:
1552 : case ElementType::FSYMBOL:
1553 : case ElementType::DYNAMIC:
1554 : case ElementType::CLEF:
1555 : case ElementType::BAR_LINE:
1556 : case ElementType::ARPEGGIO:
1557 : case ElementType::CHORD_BRACKET:
1558 : case ElementType::BREATH:
1559 : case ElementType::GLISSANDO:
1560 : case ElementType::ARTICULATION:
1561 : case ElementType::TAPPING:
1562 : case ElementType::FERMATA:
1563 : case ElementType::CHORDLINE:
1564 : case ElementType::BEND:
1565 : case ElementType::ACCIDENTAL:
1566 : case ElementType::TEXT:
1567 : case ElementType::FINGERING:
1568 : case ElementType::TEMPO_TEXT:
1569 : case ElementType::ORNAMENT:
1570 : case ElementType::EXPRESSION:
1571 : case ElementType::STAFF_TEXT:
1572 : case ElementType::STAVE_SHARING_LABEL:
1573 : case ElementType::SYSTEM_TEXT:
1574 : case ElementType::TRIPLET_FEEL:
1575 : case ElementType::PLAYTECH_ANNOTATION:
1576 : case ElementType::CAPO:
1577 : case ElementType::NOTEHEAD:
1578 : case ElementType::TREMOLO_SINGLECHORD:
1579 : case ElementType::TREMOLO_TWOCHORD:
1580 : case ElementType::STAFF_STATE:
1581 : case ElementType::INSTRUMENT_CHANGE:
1582 : case ElementType::REHEARSAL_MARK:
1583 : case ElementType::CHORD:
1584 : case ElementType::SLUR:
1585 : case ElementType::HAMMER_ON_PULL_OFF:
1586 : case ElementType::HARMONY:
1587 : case ElementType::BAGPIPE_EMBELLISHMENT:
1588 : case ElementType::AMBITUS:
1589 : case ElementType::TREMOLOBAR:
1590 : case ElementType::FIGURED_BASS:
1591 : case ElementType::LYRICS:
1592 : case ElementType::FRET_DIAGRAM:
1593 : case ElementType::HARP_DIAGRAM: {
1594 0 : return prepareDropStandardElement(pos, modifiers);
1595 : }
1596 : //! NOTE: See Measure::acceptDrop
1597 0 : case ElementType::ACTION_ICON: {
1598 0 : switch (toActionIcon(dropElem)->actionType()) {
1599 0 : case ActionIconType::VFRAME:
1600 : case ActionIconType::HFRAME:
1601 : case ActionIconType::TFRAME:
1602 : case ActionIconType::FFRAME:
1603 : case ActionIconType::MEASURE:
1604 : case ActionIconType::SYSTEM_LOCK:
1605 : case ActionIconType::PAGE_LOCK:
1606 : case ActionIconType::STAFF_TYPE_CHANGE: {
1607 0 : edd.ed.modifiers = keyboardModifier(modifiers);
1608 0 : return prepareDropMeasureBaseAnchorElement(pos);
1609 : }
1610 : // Other action icons (e.g parenthesis) can be dragged normally
1611 0 : default: return prepareDropStandardElement(pos, modifiers);
1612 : }
1613 : }
1614 0 : default:
1615 0 : break;
1616 : }
1617 :
1618 0 : return false;
1619 : }
1620 :
1621 0 : static Measure* rangeEndMeasure(Score* score, const Fraction& endTick)
1622 : {
1623 0 : Measure* endMeasure = score->tick2measure(endTick);
1624 0 : if (!endMeasure) {
1625 0 : endMeasure = score->lastMeasure();
1626 0 : if (!endMeasure) {
1627 0 : return nullptr;
1628 : }
1629 : }
1630 0 : if (endMeasure->tick() == endTick) {
1631 : // If the end tick is at the start of a measure, return the previous measure
1632 0 : return endMeasure->prevMeasure();
1633 : }
1634 0 : return endMeasure;
1635 : }
1636 :
1637 0 : static Segment* rangeEndSegment(Score* score, const Fraction& endTick)
1638 : {
1639 0 : Segment* endSegment = score->tick2rightSegment(endTick,
1640 : true,
1641 : SegmentType::Duration);
1642 :
1643 0 : if (endSegment && !endSegment->enabled()) {
1644 0 : endSegment = endSegment->next1MMenabled();
1645 : }
1646 :
1647 0 : return endSegment ? endSegment : score->lastSegmentMM();
1648 : }
1649 :
1650 0 : static bool dropRangePosition(Score* score, const PointF& pos,
1651 : Fraction sourceStartTick, Fraction tickLength, Fraction timeStretch,
1652 : staff_idx_t numStaves, staff_idx_t* targetStartStaffIdx,
1653 : Segment** targetStartSegment, const Segment** targetEndSegment = nullptr,
1654 : bool preserveMeasureAlignment = false,
1655 : ShowAnchors* showAnchors = nullptr)
1656 : {
1657 0 : IF_ASSERT_FAILED(score && targetStartStaffIdx && targetStartSegment) {
1658 0 : return false;
1659 : }
1660 :
1661 0 : Measure* targetStartMeasure = nullptr;
1662 0 : if (!dragPositionToMeasure(pos, score, &targetStartMeasure, targetStartStaffIdx)) {
1663 0 : return false;
1664 : }
1665 :
1666 0 : Fraction targetStartTick;
1667 0 : Fraction targetEndTick;
1668 :
1669 0 : if (preserveMeasureAlignment) {
1670 0 : Measure* sourceStartMeasure = score->tick2measure(sourceStartTick);
1671 0 : IF_ASSERT_FAILED(sourceStartMeasure) {
1672 0 : return false;
1673 : }
1674 :
1675 0 : Fraction startTickOffset = sourceStartTick - sourceStartMeasure->tick();
1676 0 : targetStartTick = targetStartMeasure->tick() + startTickOffset;
1677 0 : targetEndTick = targetStartTick + tickLength;
1678 :
1679 0 : if (targetStartTick >= targetStartMeasure->endTick()) {
1680 : // Target start tick is beyond the end of the measure, so cannot preserve alignment
1681 : // This can happen when the target measure is shorter than the source measure
1682 0 : preserveMeasureAlignment = false;
1683 : }
1684 : }
1685 0 : if (!preserveMeasureAlignment) {
1686 : // Assign temporary values until target segment is determined
1687 0 : targetStartTick = targetStartMeasure->tick();
1688 0 : targetEndTick = targetStartMeasure->endTick() - Fraction::fromTicks(1) + tickLength;
1689 : }
1690 :
1691 0 : Measure* targetEndMeasure = rangeEndMeasure(score, targetEndTick);
1692 0 : if (!targetEndMeasure) {
1693 0 : return false;
1694 : }
1695 :
1696 0 : IF_ASSERT_FAILED(targetStartMeasure == targetEndMeasure || targetStartMeasure->isBefore(targetEndMeasure)) {
1697 0 : return false;
1698 : }
1699 :
1700 0 : const staff_idx_t targetEndStaffIdx = std::min(*targetStartStaffIdx + numStaves, score->nstaves());
1701 :
1702 : // Add time tick anchors throughout these measures
1703 0 : for (Measure* m = targetStartMeasure; m && m->tick() <= targetEndMeasure->tick(); m = m->nextMeasure()) {
1704 0 : std::set<Fraction> additionalAnchorRelTicks;
1705 0 : if (preserveMeasureAlignment) {
1706 0 : if (m == targetStartMeasure) {
1707 0 : additionalAnchorRelTicks.insert(targetStartTick - m->tick());
1708 : }
1709 0 : if (m == targetEndMeasure) {
1710 0 : additionalAnchorRelTicks.insert(targetEndTick - m->tick());
1711 : }
1712 : }
1713 0 : for (staff_idx_t i = *targetStartStaffIdx; i < targetEndStaffIdx; ++i) {
1714 0 : EditTimeTickAnchors::updateAnchors(m, i, additionalAnchorRelTicks);
1715 : }
1716 0 : }
1717 :
1718 0 : if (preserveMeasureAlignment) {
1719 0 : *targetStartSegment = segmentOrChordRestSegmentAtSameTick(
1720 : targetStartMeasure->findSegment(SegmentType::Duration, targetStartTick));
1721 : } else {
1722 : static constexpr double spacingFactor = 0.5;
1723 : static constexpr bool useTimeAnchors = true;
1724 :
1725 : // Depends on time tick anchors that we just created
1726 0 : if (!dragPositionToSegment(pos, targetStartMeasure, *targetStartStaffIdx, targetStartSegment, spacingFactor, useTimeAnchors)) {
1727 0 : return false;
1728 : }
1729 :
1730 0 : targetStartTick = (*targetStartSegment)->tick();
1731 0 : targetEndTick = targetStartTick + tickLength;
1732 :
1733 0 : targetEndMeasure = rangeEndMeasure(score, targetEndTick);
1734 0 : if (!targetEndMeasure) {
1735 0 : return false;
1736 : }
1737 : }
1738 :
1739 0 : if (targetEndSegment) {
1740 0 : *targetEndSegment = rangeEndSegment(score, targetEndTick);
1741 0 : if (!*targetEndSegment) {
1742 0 : return false;
1743 : }
1744 : }
1745 :
1746 : // Check the time stretch for all measures overlapping the destination range.
1747 0 : for (Measure* m = targetStartMeasure; m && m->tick() <= targetEndMeasure->tick(); m = m->nextMeasure()) {
1748 0 : for (staff_idx_t staffIdx = *targetStartStaffIdx; staffIdx < targetEndStaffIdx; ++staffIdx) {
1749 0 : Fraction mTimeStretch = score->staff(staffIdx)->timeStretch(m->tick());
1750 0 : if (mTimeStretch != timeStretch) {
1751 0 : return false;
1752 : }
1753 : }
1754 : }
1755 :
1756 0 : if (showAnchors) {
1757 0 : *showAnchors = ShowAnchors(0, *targetStartStaffIdx, *targetStartStaffIdx + numStaves,
1758 : targetStartTick, targetEndTick,
1759 0 : targetStartMeasure->tick(), targetEndMeasure->endTick());
1760 : }
1761 :
1762 0 : return true;
1763 : }
1764 :
1765 0 : bool NotationInteraction::updateDropRange(const PointF& pos, std::optional<bool> preserveMeasureAlignment)
1766 : {
1767 0 : IF_ASSERT_FAILED(m_dropData.rangeDropData.has_value()) {
1768 0 : return false;
1769 : }
1770 :
1771 0 : RangeDropData& rdd = m_dropData.rangeDropData.value();
1772 :
1773 0 : if (preserveMeasureAlignment.has_value()) {
1774 0 : rdd.preserveMeasureAlignment = preserveMeasureAlignment.value();
1775 : }
1776 :
1777 0 : staff_idx_t staffIdx = muse::nidx;
1778 0 : Segment* segment = nullptr;
1779 0 : const Segment* endSegment = nullptr;
1780 0 : ShowAnchors showAnchors;
1781 :
1782 0 : const bool ok = dropRangePosition(score(), pos,
1783 : rdd.sourceTick, rdd.tickLength, rdd.timeStretch, rdd.numStaves,
1784 : &staffIdx, &segment, &endSegment,
1785 0 : rdd.preserveMeasureAlignment, &showAnchors);
1786 :
1787 0 : if (showAnchors != score()->showAnchors()) {
1788 0 : score()->setShowAnchors(showAnchors);
1789 :
1790 : // Invalidate BSP tree of affected pages
1791 0 : System* lastSeenSystem = nullptr;
1792 0 : Page* lastSeenPage = nullptr;
1793 0 : for (MeasureBase* mb = score()->tick2measure(showAnchors.startTickExtendedRegion);
1794 0 : mb && mb->tick() <= showAnchors.endTickExtendedRegion;
1795 0 : mb = mb->next()) {
1796 0 : System* s = mb->system();
1797 0 : if (!s || s == lastSeenSystem) {
1798 0 : continue;
1799 : }
1800 :
1801 0 : lastSeenSystem = s;
1802 :
1803 0 : Page* p = s->page();
1804 0 : if (!p || p == lastSeenPage) {
1805 0 : continue;
1806 : }
1807 :
1808 0 : lastSeenPage = p;
1809 :
1810 0 : p->invalidateBspTree();
1811 : }
1812 : }
1813 :
1814 0 : if (ok) {
1815 0 : rdd.targetSegment = segment;
1816 0 : rdd.targetStaffIdx = staffIdx;
1817 :
1818 0 : const staff_idx_t endStaffIdx = std::min(staffIdx + rdd.numStaves, score()->nstaves());
1819 :
1820 0 : rdd.dropRects = ScoreRangeUtilities::boundingArea(score(),
1821 : segment, endSegment,
1822 : staffIdx, endStaffIdx,
1823 0 : /*startBox*/ nullptr, /*endBox*/ nullptr);
1824 : } else {
1825 0 : rdd.targetSegment = nullptr;
1826 0 : rdd.targetStaffIdx = muse::nidx;
1827 0 : rdd.dropRects.clear();
1828 : }
1829 :
1830 0 : notifyAboutDragChanged();
1831 :
1832 0 : return ok;
1833 : }
1834 :
1835 : //! NOTE Copied from ScoreView::dropEvent
1836 0 : bool NotationInteraction::dropSingle(const PointF& pos, Qt::KeyboardModifiers modifiers)
1837 : {
1838 0 : if (!m_dropData.elementDropData.has_value()) {
1839 0 : return false;
1840 : }
1841 :
1842 0 : ElementDropData& edd = m_dropData.elementDropData.value();
1843 :
1844 0 : if (!edd.ed.dropElement) {
1845 0 : return false;
1846 : }
1847 :
1848 0 : IF_ASSERT_FAILED(edd.ed.dropElement->score() == score()) {
1849 0 : return false;
1850 : }
1851 :
1852 0 : bool accepted = false;
1853 :
1854 : // If the drop position hasn't been set already through isDropAccepted's helper methods, use the mouse position
1855 0 : if (edd.ed.pos.isNull()) {
1856 0 : edd.ed.pos = pos;
1857 : }
1858 :
1859 0 : edd.ed.modifiers = keyboardModifier(modifiers);
1860 0 : edd.ed.dropElement->styleChanged();
1861 :
1862 0 : bool systemStavesOnly = false;
1863 0 : bool applyUserOffset = false;
1864 :
1865 0 : startEdit(TranslatableString("undoableAction", "Drop element: %1").arg(edd.ed.dropElement->typeUserName()));
1866 0 : score()->addRefresh(edd.ed.dropElement->canvasBoundingRect());
1867 0 : engraving::Transaction& tx = score()->transactionManager()->currentOrDummyTransaction();
1868 0 : ElementType et = edd.ed.dropElement->type();
1869 0 : switch (et) {
1870 0 : case ElementType::VOLTA:
1871 : case ElementType::GRADUAL_TEMPO_CHANGE:
1872 : // voltas drop to system staves by default, or closest staff if Control is held
1873 0 : systemStavesOnly = !(edd.ed.modifiers & Qt::ControlModifier);
1874 : [[fallthrough]];
1875 0 : case ElementType::TEXTLINE:
1876 : case ElementType::OTTAVA:
1877 : case ElementType::TRILL:
1878 : case ElementType::PEDAL:
1879 : case ElementType::LET_RING:
1880 : case ElementType::VIBRATO:
1881 : case ElementType::PALM_MUTE:
1882 : case ElementType::HAIRPIN:
1883 : case ElementType::WHAMMY_BAR:
1884 : {
1885 0 : systemStavesOnly |= edd.ed.dropElement->systemFlag();
1886 0 : mu::engraving::Spanner* spanner = ptr::checked_cast<mu::engraving::Spanner>(edd.ed.dropElement);
1887 0 : EditSpanner::addSpanner(tx, score(), spanner, pos, systemStavesOnly);
1888 0 : score()->setUpdateAll();
1889 0 : accepted = true;
1890 : }
1891 0 : break;
1892 0 : case ElementType::SYMBOL:
1893 : case ElementType::FSYMBOL:
1894 : case ElementType::IMAGE:
1895 0 : applyUserOffset = true;
1896 : [[fallthrough]];
1897 0 : case ElementType::DYNAMIC:
1898 : case ElementType::HARMONY:
1899 0 : accepted = doDropTextBaseAndSymbols(tx, pos, applyUserOffset);
1900 0 : break;
1901 0 : case ElementType::FRET_DIAGRAM:
1902 0 : accepted = doDropTextBaseAndSymbols(tx, pos, applyUserOffset);
1903 0 : if (accepted) {
1904 0 : doFinishAddFretboardDiagram();
1905 : }
1906 0 : break;
1907 0 : case ElementType::HBOX:
1908 : case ElementType::VBOX:
1909 : case ElementType::KEYSIG:
1910 : case ElementType::CLEF:
1911 : case ElementType::TIMESIG:
1912 : case ElementType::BAR_LINE:
1913 : case ElementType::ARPEGGIO:
1914 : case ElementType::CHORD_BRACKET:
1915 : case ElementType::BREATH:
1916 : case ElementType::GLISSANDO:
1917 : case ElementType::MEASURE_NUMBER:
1918 : case ElementType::BRACKET:
1919 : case ElementType::ARTICULATION:
1920 : case ElementType::TAPPING:
1921 : case ElementType::FERMATA:
1922 : case ElementType::CHORDLINE:
1923 : case ElementType::BEND:
1924 : case ElementType::ACCIDENTAL:
1925 : case ElementType::TEXT:
1926 : case ElementType::FINGERING:
1927 : case ElementType::TEMPO_TEXT:
1928 : case ElementType::ORNAMENT:
1929 : case ElementType::EXPRESSION:
1930 : case ElementType::STAFF_TEXT:
1931 : case ElementType::STAVE_SHARING_LABEL:
1932 : case ElementType::SYSTEM_TEXT:
1933 : case ElementType::TRIPLET_FEEL:
1934 : case ElementType::PLAYTECH_ANNOTATION:
1935 : case ElementType::CAPO:
1936 : case ElementType::STRING_TUNINGS:
1937 : case ElementType::NOTEHEAD:
1938 : case ElementType::TREMOLO_SINGLECHORD:
1939 : case ElementType::TREMOLO_TWOCHORD:
1940 : case ElementType::LAYOUT_BREAK:
1941 : case ElementType::MARKER:
1942 : case ElementType::STAFF_STATE:
1943 : case ElementType::INSTRUMENT_CHANGE:
1944 : case ElementType::REHEARSAL_MARK:
1945 : case ElementType::JUMP:
1946 : case ElementType::MEASURE_REPEAT:
1947 : case ElementType::ACTION_ICON:
1948 : case ElementType::NOTE:
1949 : case ElementType::CHORD:
1950 : case ElementType::SPACER:
1951 : case ElementType::BAGPIPE_EMBELLISHMENT:
1952 : case ElementType::AMBITUS:
1953 : case ElementType::TREMOLOBAR:
1954 : case ElementType::FIGURED_BASS:
1955 : case ElementType::LYRICS:
1956 : case ElementType::HARP_DIAGRAM:
1957 : case ElementType::STAFFTYPE_CHANGE:
1958 0 : accepted = doDropStandard(tx);
1959 0 : break;
1960 0 : case ElementType::SLUR:
1961 : case ElementType::HAMMER_ON_PULL_OFF:
1962 : {
1963 0 : EngravingItem* el = dropTarget(edd.ed);
1964 0 : mu::engraving::Slur* dropElement = toSlur(edd.ed.dropElement);
1965 0 : if (toNote(el)->chord()) {
1966 0 : doAddSlur(toNote(el)->chord(), nullptr, dropElement);
1967 0 : accepted = true;
1968 : }
1969 : }
1970 0 : break;
1971 0 : default:
1972 0 : resetDropData();
1973 0 : break;
1974 : }
1975 :
1976 0 : edd.ed.dropElement = nullptr;
1977 0 : edd.ed.pos = PointF();
1978 0 : edd.ed.modifiers = {};
1979 :
1980 0 : setDropTarget(nullptr); // this also resets dropRectangle and dropAnchor
1981 0 : apply();
1982 :
1983 0 : if (accepted) {
1984 0 : notifyAboutDropChanged();
1985 : }
1986 :
1987 0 : checkAndShowError();
1988 :
1989 0 : return accepted;
1990 : }
1991 :
1992 : //! NOTE: Helper method for NotationInteraction::drop. Handles drop logic for majority of elements (returns "accepted")
1993 0 : bool NotationInteraction::doDropStandard(engraving::Transaction& tx)
1994 : {
1995 0 : if (!m_dropData.elementDropData.has_value()) {
1996 0 : return false;
1997 : }
1998 :
1999 0 : ElementDropData& edd = m_dropData.elementDropData.value();
2000 :
2001 0 : EngravingItem* el = edd.dropTarget ? edd.dropTarget : dropTarget(edd.ed);
2002 0 : if (!el) {
2003 0 : if (!dropCanvas(edd.ed.dropElement)) {
2004 0 : LOGD("cannot drop %s(%p) to canvas", edd.ed.dropElement->typeName(), edd.ed.dropElement);
2005 0 : resetDropData();
2006 : }
2007 0 : return false;
2008 : }
2009 0 : score()->addRefresh(el->canvasBoundingRect());
2010 :
2011 0 : EngravingItem* dropElement = el->drop(tx, edd.ed);
2012 :
2013 0 : if (dropElement && dropElement->isInstrumentChange()) {
2014 0 : if (!selectInstrument(toInstrumentChange(dropElement))) {
2015 0 : rollback();
2016 0 : return true; // Really?
2017 : }
2018 : }
2019 :
2020 0 : score()->addRefresh(el->canvasBoundingRect());
2021 0 : if (dropElement) {
2022 0 : if (!score()->noteEntryMode()) {
2023 0 : selectAndStartEditIfNeeded(dropElement);
2024 : }
2025 0 : score()->addRefresh(dropElement->canvasBoundingRect());
2026 : }
2027 0 : return true;
2028 : }
2029 :
2030 : //! NOTE: Helper method for NotationInteraction::drop. Handles drop logic for text base items & symbols (returns "accepted")
2031 0 : bool NotationInteraction::doDropTextBaseAndSymbols(engraving::Transaction& tx, const PointF& pos, bool applyUserOffset)
2032 : {
2033 0 : IF_ASSERT_FAILED(m_dropData.elementDropData.has_value()) {
2034 0 : return false;
2035 : }
2036 :
2037 0 : ElementDropData& edd = m_dropData.elementDropData.value();
2038 :
2039 0 : EngravingItem* el = edd.dropTarget ? edd.dropTarget : elementAt(pos);
2040 0 : if (!el || el->isStaffLines()) {
2041 : mu::engraving::staff_idx_t staffIdx;
2042 : mu::engraving::Segment* seg;
2043 0 : PointF offset;
2044 0 : el = score()->pos2measure(pos, &staffIdx, /*pitch*/ 0, &seg, &offset);
2045 0 : if (el && el->isMeasure()) {
2046 0 : edd.ed.dropElement->setTrack(staff2track(staffIdx));
2047 0 : edd.ed.dropElement->setOwnershipParent(seg);
2048 :
2049 0 : if (applyUserOffset) {
2050 0 : edd.ed.dropElement->setOffset(offset);
2051 : }
2052 :
2053 0 : score()->undoAddElement(edd.ed.dropElement);
2054 : } else {
2055 0 : LOGD("cannot drop here");
2056 0 : resetDropData();
2057 : }
2058 : } else {
2059 0 : score()->addRefresh(el->canvasBoundingRect());
2060 0 : score()->addRefresh(edd.ed.dropElement->canvasBoundingRect());
2061 :
2062 0 : if (!el->acceptDrop(edd.ed)) {
2063 0 : LOGD("drop %s onto %s not accepted", edd.ed.dropElement->typeName(), el->typeName());
2064 0 : return false;
2065 : }
2066 0 : edd.ed.pos = pos;
2067 0 : EngravingItem* dropElement = el->drop(tx, edd.ed);
2068 0 : score()->addRefresh(el->canvasBoundingRect());
2069 0 : if (dropElement) {
2070 0 : selectAndStartEditIfNeeded(dropElement);
2071 0 : score()->addRefresh(dropElement->canvasBoundingRect());
2072 : }
2073 : }
2074 :
2075 0 : return true;
2076 : }
2077 :
2078 0 : bool NotationInteraction::dropRange(const QByteArray& data, const PointF& pos, bool deleteSourceMaterial)
2079 : {
2080 0 : IF_ASSERT_FAILED(m_dropData.rangeDropData.has_value()) {
2081 0 : return false;
2082 : }
2083 :
2084 0 : RangeDropData& rdd = m_dropData.rangeDropData.value();
2085 :
2086 0 : if (rdd.tickLength.isZero() || rdd.numStaves == 0) {
2087 0 : return false;
2088 : }
2089 :
2090 0 : staff_idx_t staffIdx = muse::nidx;
2091 0 : Segment* segment = nullptr;
2092 :
2093 0 : const bool ok = dropRangePosition(score(), pos,
2094 : rdd.sourceTick, rdd.tickLength, rdd.timeStretch, rdd.numStaves,
2095 0 : &staffIdx, &segment, nullptr, rdd.preserveMeasureAlignment);
2096 0 : if (!ok) {
2097 0 : return false;
2098 : }
2099 :
2100 0 : if (segment->isTupletSubdivisionOnStaff(staffIdx) || segment->isInsideTupletOnStaff(staffIdx)) {
2101 0 : endDrop();
2102 0 : notifyAboutDropChanged();
2103 : //MScore::setError(MsError::DEST_TUPLET);
2104 : // checkAndShowError();
2105 : // NOTE: if we show the error popup here it seems that the mouse-release event is missed
2106 : // so the dragged region stays sticked to the mouse and move around. Don't know how to fix it. [M.S.]
2107 0 : return false;
2108 : }
2109 :
2110 0 : rdd.targetSegment = segment;
2111 0 : rdd.targetStaffIdx = staffIdx;
2112 :
2113 0 : startEdit(deleteSourceMaterial
2114 0 : ? TranslatableString("undoableAction", "Move range")
2115 : : TranslatableString("undoableAction", "Copy range"));
2116 :
2117 0 : if (deleteSourceMaterial && rdd.sourceStaffIdx != muse::nidx) {
2118 0 : Segment* sourceStartSegment = score()->tick2leftSegmentMM(rdd.sourceTick);
2119 0 : if (sourceStartSegment && !sourceStartSegment->enabled()) {
2120 0 : sourceStartSegment = sourceStartSegment->next1MMenabled();
2121 : }
2122 :
2123 0 : Segment* sourceEndSegment = rangeEndSegment(score(), rdd.sourceTick + rdd.tickLength);
2124 :
2125 0 : if (sourceStartSegment && sourceEndSegment) {
2126 0 : score()->deleteRange(sourceStartSegment, sourceEndSegment,
2127 : engraving::staff2track(rdd.sourceStaffIdx),
2128 0 : engraving::staff2track(rdd.sourceStaffIdx + rdd.numStaves),
2129 0 : score()->selectionFilter(), score()->selection().rangeContainsMultiNoteChords());
2130 : }
2131 : }
2132 :
2133 0 : XmlReader e(data);
2134 0 : bool succeeded = Paste::pasteStaff(score()->transactionManager()->currentOrDummyTransaction(), score(), e, segment, staffIdx);
2135 :
2136 0 : endDrop();
2137 :
2138 0 : if (succeeded) {
2139 0 : apply();
2140 : } else {
2141 0 : rollback();
2142 : }
2143 :
2144 0 : checkAndShowError();
2145 :
2146 0 : return succeeded;
2147 0 : }
2148 :
2149 0 : bool NotationInteraction::selectInstrument(mu::engraving::InstrumentChange* instrumentChange)
2150 : {
2151 0 : if (!instrumentChange) {
2152 0 : return false;
2153 : }
2154 :
2155 : #ifdef MUE_CONFIGURATION_IS_APPWEB
2156 : interactive()->info("", muse::trc("notation", "Sorry, instrument change on the web is not supported at this time."));
2157 : return false;
2158 : #else
2159 :
2160 : //! HACK To remake the asynchronous mode, a lot of refactoring is needed,
2161 : //! so for now this is a hack
2162 0 : bool result = false;
2163 0 : QEventLoop loop;
2164 0 : async::Promise<InstrumentTemplate> templ = selectInstrumentScenario()->selectInstrument();
2165 0 : templ.onResolve(this, [this, instrumentChange, &loop, &result](const InstrumentTemplate& val) {
2166 0 : Instrument newInstrument = Instrument::fromTemplate(&val);
2167 :
2168 : // If switching back to the part's original instrument, restore its player number
2169 0 : if (Part* part = instrumentChange->part()) {
2170 0 : const Instrument* baseInstrument = part->instrument();
2171 0 : if (baseInstrument && baseInstrument->id() == newInstrument.id()) {
2172 0 : InstrumentLabel& newLabel = newInstrument.instrumentLabel();
2173 0 : const InstrumentLabel& baseLabel = baseInstrument->instrumentLabel();
2174 0 : newLabel.setNumber(baseLabel.number());
2175 0 : newLabel.setShowNumberLong(baseLabel.showNumberLong());
2176 0 : newLabel.setShowNumberShort(baseLabel.showNumberShort());
2177 : }
2178 : }
2179 :
2180 0 : instrumentChange->setInit(true);
2181 0 : instrumentChange->setupInstrument(&newInstrument);
2182 :
2183 0 : if (newInstrument.useDrumset()) {
2184 0 : cleanupDrumsetChanges(instrumentChange);
2185 : }
2186 :
2187 0 : result = true;
2188 0 : loop.quit();
2189 0 : }).onReject(this, [&loop, &result](int, const std::string&) {
2190 0 : result = false;
2191 0 : loop.quit();
2192 0 : });
2193 :
2194 0 : loop.exec();
2195 :
2196 0 : return result;
2197 : #endif
2198 0 : }
2199 :
2200 0 : void NotationInteraction::cleanupDrumsetChanges(mu::engraving::InstrumentChange* instrumentChange) const
2201 : {
2202 0 : Part* part = instrumentChange ? instrumentChange->part() : nullptr;
2203 0 : Instrument* newInstrument = instrumentChange ? instrumentChange->instrument() : nullptr;
2204 0 : if (!part || !newInstrument) {
2205 0 : return;
2206 : }
2207 :
2208 0 : for (auto pair : part->instruments()) {
2209 0 : const Instrument* otherInst = pair.second;
2210 0 : if (!otherInst || otherInst == newInstrument) {
2211 0 : continue;
2212 : }
2213 :
2214 : // If the following conditional is true, it means that we're trying to change to a drumset that already exists for this part. Due to the fact
2215 : // that we don't create new tracks for identical instruments in a given part, the knock-on effect is that the new drumset won't have a chance
2216 : // to load a MuseSampler patch (see usage of shouldLoadDrumset in PlaybackController). The following logic resolves this by copying the patch
2217 : // from the existing drumset into the new one...
2218 0 : if (otherInst->drumset() && newInstrument->id() == otherInst->id()) {
2219 0 : score()->undo(new engraving::ChangeDrumset(newInstrument, *otherInst->drumset(), part));
2220 0 : return;
2221 : }
2222 : }
2223 : }
2224 :
2225 : //! NOTE Copied from Palette::applyPaletteElement
2226 0 : bool NotationInteraction::applyPaletteElement(mu::engraving::EngravingItem* element, Qt::KeyboardModifiers modifiers)
2227 : {
2228 0 : IF_ASSERT_FAILED(element) {
2229 0 : return false;
2230 : }
2231 :
2232 0 : mu::engraving::Score* score = this->score();
2233 0 : if (!score) {
2234 0 : return false;
2235 : }
2236 :
2237 0 : const mu::engraving::Selection sel = score->selection(); // make a copy of selection state before applying the operation.
2238 0 : if (sel.isNone()) {
2239 0 : return false;
2240 : }
2241 :
2242 0 : startEdit(TranslatableString("undoableAction", "Apply palette element: %1").arg(element->typeUserName()));
2243 :
2244 0 : if (sel.isList()) {
2245 0 : applyPaletteElementToList(element, score, sel, modifiers);
2246 0 : } else if (sel.isRange()) {
2247 0 : applyPaletteElementToRange(element, score, sel, modifiers);
2248 : } else {
2249 0 : LOGD("unknown selection state");
2250 : }
2251 :
2252 0 : m_notifyAboutDropChanged = true;
2253 0 : score->setSelectionChanged(true);
2254 0 : apply();
2255 :
2256 0 : setDropTarget(nullptr);
2257 :
2258 0 : checkAndShowError();
2259 :
2260 0 : return true;
2261 0 : }
2262 :
2263 0 : void NotationInteraction::applyPaletteElementToList(EngravingItem* element, mu::engraving::Score* score,
2264 : const mu::engraving::Selection& sel, Qt::KeyboardModifiers modifiers)
2265 : {
2266 0 : engraving::Transaction& tx = score->transactionManager()->currentOrDummyTransaction();
2267 :
2268 0 : const ElementType elementType = element->type();
2269 :
2270 0 : ChordRest* cr1 = sel.firstChordRest();
2271 0 : ChordRest* cr2 = sel.lastChordRest();
2272 0 : bool addSingle = false; // add a single line only
2273 0 : if (cr1 && cr2 == cr1) {
2274 : // one chordrest selected, ok to add line
2275 0 : addSingle = true;
2276 0 : } else if (sel.elements().size() == 2 && cr1 && cr2 && cr1 != cr2) {
2277 : // two chordrests selected
2278 : // must be on same staff in order to add line, except for slur
2279 0 : if (element->isSlur() || cr1->staffIdx() == cr2->staffIdx()) {
2280 0 : addSingle = true;
2281 0 : } else if (sel.element() && sel.element()->isSlurSegment() && element->isHammerOnPullOff()) {
2282 0 : addSingle = true;
2283 : }
2284 : }
2285 :
2286 0 : auto isEntryDrumStaff = [score]() {
2287 0 : const mu::engraving::InputState& is = score->inputState();
2288 0 : const mu::engraving::Staff* staff = score->staff(track2staff(is.track()));
2289 0 : return staff ? staff->staffType(is.tick())->group() == mu::engraving::StaffGroup::PERCUSSION : false;
2290 0 : };
2291 :
2292 0 : const bool elementIsStandardBend = element->isActionIcon() && (toActionIcon(element)->actionType() == ActionIconType::STANDARD_BEND
2293 0 : || toActionIcon(element)->actionType() == ActionIconType::DIVE);
2294 0 : const bool elementIsNoteLine = element->isActionIcon() && toActionIcon(element)->actionType() == ActionIconType::NOTE_ANCHORED_LINE;
2295 0 : const bool isLineNoteToNote = (element->isGlissando() || elementIsStandardBend || elementIsNoteLine)
2296 0 : && sel.isList() && sel.elements().size() == 2
2297 0 : && sel.elements()[0]->isNote() && sel.elements()[1]->isNote()
2298 0 : && sel.elements()[1]->tick() != sel.elements()[0]->tick();
2299 :
2300 0 : if (isEntryDrumStaff() && element->isChord()) {
2301 0 : mu::engraving::InputState& is = score->inputState();
2302 0 : EngravingItem* e = nullptr;
2303 0 : if (!(modifiers & Qt::ShiftModifier)) {
2304 : // shift+double-click: add note to "chord"
2305 : // use input position rather than selection if possible
2306 : // look for a cr in the voice predefined for the drum in the palette
2307 : // back up if necessary
2308 : // TODO: refactor this with similar code in putNote()
2309 0 : if (is.segment()) {
2310 0 : mu::engraving::Segment* seg = is.segment();
2311 0 : while (seg) {
2312 0 : if (seg->element(is.track())) {
2313 0 : break;
2314 : }
2315 0 : seg = seg->prev(mu::engraving::SegmentType::ChordRest);
2316 : }
2317 0 : if (seg) {
2318 0 : is.setSegment(seg);
2319 : } else {
2320 0 : is.setSegment(is.segment()->measure()->first(mu::engraving::SegmentType::ChordRest));
2321 : }
2322 : }
2323 0 : score->expandVoice();
2324 0 : e = is.cr();
2325 : }
2326 0 : if (!e) {
2327 0 : e = sel.elements().front();
2328 : }
2329 0 : if (e) {
2330 : // get note if selection was full chord
2331 0 : if (e->isChord()) {
2332 0 : e = toChord(e)->upNote();
2333 : }
2334 :
2335 0 : applyDropPaletteElement(score, e, element, modifiers);
2336 : // note has already been played (and what would play otherwise may be *next* input position)
2337 0 : score->setPlayNote(false);
2338 0 : score->setPlayChord(false);
2339 : // continue in same track
2340 0 : is.setTrack(e->track());
2341 : } else {
2342 0 : LOGD("nowhere to place drum note");
2343 : }
2344 0 : return;
2345 : }
2346 :
2347 0 : if (element->isLayoutBreak()) {
2348 0 : mu::engraving::LayoutBreak* breakElement = toLayoutBreak(element);
2349 0 : score->cmdToggleLayoutBreak(breakElement->layoutBreakType());
2350 0 : return;
2351 : }
2352 :
2353 0 : if (element->isActionIcon()) {
2354 0 : const ActionIcon* icon = toActionIcon(element);
2355 0 : switch (icon->actionType()) {
2356 0 : case ActionIconType::SYSTEM_LOCK: {
2357 0 : EditSystemLocks::toggleSystemLock(tx, score, score->selection().selectedSystems());
2358 0 : return;
2359 : }
2360 0 : case ActionIconType::PAGE_LOCK: {
2361 0 : EditPageLocks::togglePageLock(tx, score, score->selection().pagesContainingSelection());
2362 0 : return;
2363 : }
2364 0 : case ActionIconType::PARENTHESES: {
2365 0 : if (!sel.noteList().empty()) {
2366 0 : EditParentheses::addParenthesesToNotes(tx, score);
2367 0 : return;
2368 : }
2369 : }
2370 : default:
2371 0 : break;
2372 : }
2373 : }
2374 :
2375 0 : if (element->isSlur()) {
2376 0 : doAddSlur(toSlur(element));
2377 0 : return;
2378 : }
2379 :
2380 0 : if (element->isSLine() && !element->isGlissando() && !element->isGuitarBend() && addSingle) {
2381 0 : mu::engraving::Segment* startSegment = cr1->segment();
2382 0 : mu::engraving::Segment* endSegment = cr2->segment();
2383 0 : if (elementType == mu::engraving::ElementType::PEDAL && cr2 != cr1) {
2384 0 : endSegment = endSegment->nextCR(cr2->track());
2385 : } else {
2386 : // Ensure that list-selection results in the same endSegment as range selection
2387 0 : endSegment = cr2->nextSegmentAfterCR(SegmentType::ChordRest | SegmentType::EndBarLine | SegmentType::Clef);
2388 : }
2389 0 : mu::engraving::Spanner* spanner = toSpanner(element->clone());
2390 0 : spanner->setScore(score);
2391 0 : spanner->styleChanged();
2392 0 : if (spanner->isHairpin()) {
2393 0 : EditHairpin::addHairpin(tx, score, toHairpin(spanner), cr1, cr2);
2394 0 : if (!spanner->segmentsEmpty() && !score->noteEntryMode()) {
2395 0 : SpannerSegment* frontSegment = spanner->frontSegment();
2396 0 : score->select(frontSegment);
2397 0 : startEditElement(frontSegment);
2398 : }
2399 : } else {
2400 0 : bool firstStaffOnly = isSystemTextLine(element) && !(modifiers & Qt::ControlModifier);
2401 0 : staff_idx_t targetStaff = firstStaffOnly ? 0 : cr1->staffIdx();
2402 0 : EditSpanner::addSpanner(tx, score, spanner, targetStaff, startSegment, endSegment, modifiers & Qt::ControlModifier);
2403 : }
2404 0 : if (spanner->hasVoiceAssignmentProperties()) {
2405 0 : spanner->setInitialTrackAndVoiceAssignment(cr1->track(), modifiers & ControlModifier);
2406 0 : } else if (spanner->isVoiceSpecific()) {
2407 0 : spanner->setTrack(cr1->track());
2408 : }
2409 0 : return;
2410 : }
2411 :
2412 0 : if (element->isArticulationFamily() && sel.elements().size() == 1) {
2413 : // understand adding an articulation to another articulation as adding it to the chord it's attached to
2414 0 : EngravingItem* e = sel.elements().front();
2415 0 : if (e->isArticulationFamily()) {
2416 0 : if (Chord* c = toChord(toArticulation(e)->ownershipParent())) {
2417 0 : applyDropPaletteElement(score, c->notes().front(), element, modifiers);
2418 : }
2419 : } else {
2420 0 : applyDropPaletteElement(score, e, element, modifiers);
2421 : }
2422 0 : return;
2423 : }
2424 :
2425 0 : if (isLineNoteToNote) {
2426 0 : applyLineNoteToNote(score, toNote(sel.elements()[0]), toNote(sel.elements()[1]), element);
2427 0 : return;
2428 : }
2429 :
2430 0 : if ((element->isClef() || element->isTimeSig() || element->isKeySig()) && score->noteEntryMode()) {
2431 : // in note input mode place clef / time sig / key sig before cursor
2432 0 : EngravingItem* e = score->inputState().cr();
2433 0 : if (!e) {
2434 0 : e = sel.elements().front();
2435 0 : } else if (e->isChord()) {
2436 0 : e = toChord(e)->notes().front();
2437 : }
2438 0 : applyDropPaletteElement(score, e, element, modifiers);
2439 0 : return;
2440 : }
2441 :
2442 : bool unique;
2443 0 : std::vector<EngravingItem*> targetElements = mu::engraving::filterTargetElements(sel, element, unique);
2444 :
2445 0 : for (EngravingItem* target : targetElements) {
2446 0 : applyDropPaletteElement(score, target, element, modifiers);
2447 : }
2448 : }
2449 :
2450 0 : void NotationInteraction::applyPaletteElementToRange(EngravingItem* element, mu::engraving::Score* score,
2451 : const mu::engraving::Selection& sel, Qt::KeyboardModifiers modifiers)
2452 : {
2453 0 : engraving::Transaction& tx = score->transactionManager()->currentOrDummyTransaction();
2454 0 : const ElementType elementType = element->type();
2455 :
2456 0 : bool isMeasureAnchoredElement = false;
2457 0 : std::vector<EngravingItem*> targetElements = mu::engraving::filterTargetElements(sel, element, isMeasureAnchoredElement);
2458 0 : if (isMeasureAnchoredElement) {
2459 0 : if (elementType == ElementType::BAR_LINE) {
2460 : // In a range selection, add barlines to all selected measures
2461 0 : for (EngravingItem* target : targetElements) {
2462 0 : applyDropPaletteElement(score, target->findMeasure(), element, modifiers, target->track());
2463 : }
2464 : } else {
2465 0 : for (EngravingItem* target : targetElements) {
2466 0 : applyDropPaletteElement(score, target, element, modifiers);
2467 : }
2468 : }
2469 0 : return;
2470 : }
2471 :
2472 0 : if (elementType == ElementType::LAYOUT_BREAK) {
2473 0 : LayoutBreak* breakElement = toLayoutBreak(element);
2474 0 : score->cmdToggleLayoutBreak(breakElement->layoutBreakType());
2475 0 : return;
2476 : }
2477 :
2478 0 : if (element->isClef() || element->isKeySig() || element->isTimeSig()) {
2479 0 : Measure* m1 = sel.startSegment()->measure();
2480 0 : Measure* m2 = sel.endSegment() ? sel.endSegment()->measure() : nullptr;
2481 0 : if (m2 == m1 && sel.startSegment()->rtick().isZero()) {
2482 0 : m2 = nullptr; // don't restore original if one full measure selected
2483 0 : } else if (m2) {
2484 0 : m2 = m2->nextMeasureMM();
2485 : }
2486 : // Clefs always apply to every selected staff, key and time signatures only when added locally
2487 0 : const bool applyToEachStaff = element->isClef() || (modifiers & Qt::ControlModifier);
2488 0 : staff_idx_t staffIdx1 = sel.staffStart();
2489 0 : staff_idx_t staffIdx2 = applyToEachStaff ? sel.staffEnd() : staffIdx1 + 1;
2490 0 : for (staff_idx_t i = staffIdx1; i < staffIdx2; ++i) {
2491 : // For clefs, use mid-measure changes if appropriate
2492 : // Mid-measure key signature changes are done via list selections
2493 0 : EngravingItem* e1 = nullptr;
2494 0 : EngravingItem* e2 = nullptr;
2495 0 : if (element->isClef()) {
2496 0 : if (sel.startSegment()->isChordRestType() && sel.startSegment()->rtick().isNotZero()) {
2497 0 : ChordRest* cr = toChordRest(sel.startSegment()->nextChordRest(staff2track(i)));
2498 0 : if (cr && cr->isChord()) {
2499 0 : e1 = toChord(cr)->upNote();
2500 : } else {
2501 0 : e1 = cr;
2502 : }
2503 : }
2504 0 : if (sel.endSegment() && sel.endSegment()->segmentType() == mu::engraving::SegmentType::ChordRest) {
2505 0 : ChordRest* cr = toChordRest(sel.endSegment()->nextChordRest(staff2track(i)));
2506 0 : if (cr && cr->isChord()) {
2507 0 : e2 = toChord(cr)->upNote();
2508 : } else {
2509 0 : e2 = cr;
2510 : }
2511 : }
2512 : }
2513 : // restore clef/keysig/timesig that was in effect at end of selection
2514 0 : if (m2 || e2) {
2515 0 : mu::engraving::Staff* staff = score->staff(i);
2516 0 : mu::engraving::Fraction tick2 = sel.endSegment()->tick();
2517 0 : mu::engraving::EngravingItem* oelement = nullptr;
2518 0 : switch (elementType) {
2519 0 : case mu::engraving::ElementType::CLEF:
2520 : {
2521 0 : mu::engraving::Clef* oclef = engraving::Factory::createClef(score->dummy());
2522 0 : oclef->setClefType(staff->clef(tick2));
2523 0 : oelement = oclef;
2524 0 : break;
2525 : }
2526 0 : case mu::engraving::ElementType::KEYSIG:
2527 : {
2528 0 : mu::engraving::KeySig* okeysig = engraving::Factory::createKeySig(score->dummy());
2529 0 : okeysig->setKeySigEvent(staff->keySigEvent(tick2));
2530 0 : Key ck = okeysig->concertKey();
2531 0 : okeysig->setKey(ck);
2532 0 : oelement = okeysig;
2533 0 : break;
2534 : }
2535 0 : case mu::engraving::ElementType::TIMESIG:
2536 : {
2537 0 : mu::engraving::TimeSig* otimesig = engraving::Factory::createTimeSig(score->dummy());
2538 0 : otimesig->setFrom(staff->timeSig(tick2));
2539 0 : oelement = otimesig;
2540 0 : break;
2541 : }
2542 0 : default:
2543 0 : break;
2544 : }
2545 0 : if (oelement) {
2546 0 : if (e2) {
2547 0 : applyDropPaletteElement(score, e2, oelement, modifiers);
2548 : } else {
2549 0 : applyDropPaletteElement(score, m2, oelement, modifiers, staff2track(i));
2550 : }
2551 0 : delete oelement;
2552 : }
2553 : }
2554 : // apply new clef/keysig/timesig
2555 0 : if (e1) {
2556 0 : applyDropPaletteElement(score, e1, element, modifiers);
2557 : } else {
2558 0 : applyDropPaletteElement(score, m1, element, modifiers, staff2track(i));
2559 : }
2560 : }
2561 0 : return;
2562 : }
2563 :
2564 0 : if (element->isSlur()) {
2565 0 : doAddSlur(toSlur(element));
2566 0 : return;
2567 : }
2568 :
2569 0 : if (element->isSLine() && elementType != ElementType::GLISSANDO) {
2570 0 : Segment* startSegment = sel.startSegment();
2571 0 : Segment* endSegment = sel.endSegment();
2572 0 : bool firstStaffOnly = isSystemTextLine(element) && !(modifiers & Qt::ControlModifier);
2573 0 : staff_idx_t startStaff = firstStaffOnly ? 0 : sel.staffStart();
2574 0 : staff_idx_t endStaff = firstStaffOnly ? 1 : sel.staffEnd();
2575 0 : for (staff_idx_t i = startStaff; i < endStaff; ++i) {
2576 0 : Spanner* spanner = toSpanner(element->clone());
2577 0 : spanner->setScore(score);
2578 0 : spanner->styleChanged();
2579 0 : EditSpanner::addSpanner(tx, score, spanner, i, startSegment, endSegment, modifiers & Qt::ControlModifier);
2580 0 : if (spanner->hasVoiceAssignmentProperties()) {
2581 0 : spanner->setInitialTrackAndVoiceAssignment(staff2track(i), modifiers & ControlModifier);
2582 : }
2583 0 : selectAndStartEditIfNeeded(spanner);
2584 : }
2585 0 : return;
2586 : }
2587 :
2588 0 : if (element->isTextBase() && !element->isFingering() && !element->isSticking()) {
2589 0 : Segment* firstSegment = sel.startSegment();
2590 0 : staff_idx_t firstStaffIndex = sel.staffStart();
2591 0 : staff_idx_t lastStaffIndex = sel.staffEnd();
2592 :
2593 : // A text should only be added at the start of the selection
2594 : // There shouldn't be a text at each element
2595 0 : if (element->systemFlag()) {
2596 0 : applyDropPaletteElement(score, firstSegment->firstElementForNavigation(0), element, modifiers);
2597 : } else {
2598 0 : for (staff_idx_t staff = firstStaffIndex; staff < lastStaffIndex; staff++) {
2599 0 : applyDropPaletteElement(score, firstSegment->firstElementForNavigation(staff), element, modifiers);
2600 : }
2601 : }
2602 0 : return;
2603 : }
2604 :
2605 0 : if (element->isActionIcon()) {
2606 0 : const ActionIconType actionType = toActionIcon(element)->actionType();
2607 0 : switch (actionType) {
2608 0 : case ActionIconType::SYSTEM_LOCK: {
2609 0 : EditSystemLocks::toggleSystemLock(tx, score, score->selection().selectedSystems());
2610 0 : return;
2611 : }
2612 0 : case ActionIconType::PAGE_LOCK: {
2613 0 : EditPageLocks::togglePageLock(tx, score, score->selection().pagesContainingSelection());
2614 0 : return;
2615 : }
2616 0 : case ActionIconType::PARENTHESES: {
2617 0 : EditParentheses::addParenthesesToNotes(tx, score);
2618 0 : return;
2619 : }
2620 0 : case ActionIconType::STANDARD_BEND:
2621 : case ActionIconType::PRE_BEND:
2622 : case ActionIconType::GRACE_NOTE_BEND:
2623 : case ActionIconType::SLIGHT_BEND:
2624 : case ActionIconType::DIVE:
2625 : case ActionIconType::PRE_DIVE:
2626 : case ActionIconType::DIP:
2627 : case ActionIconType::SCOOP:
2628 : {
2629 : // Insertion of bend may alter the segment list, so collect the original list here and loop on this
2630 0 : std::vector<Segment*> segList;
2631 0 : for (Segment* seg = sel.startSegment(); seg && seg != sel.endSegment(); seg = seg->next1()) {
2632 0 : if (seg->isChordRestType()) {
2633 0 : segList.push_back(seg);
2634 : }
2635 : }
2636 0 : const track_idx_t track1 = staff2track(sel.staffStart());
2637 0 : const track_idx_t track2 = staff2track(sel.staffEnd());
2638 0 : for (Segment* seg : segList) {
2639 0 : for (track_idx_t track = track1; track < track2; ++track) {
2640 0 : EngravingItem* item = seg->element(track);
2641 0 : if (!item || !item->isChord()) {
2642 0 : continue;
2643 : }
2644 0 : for (Note* note : toChord(item)->notes()) {
2645 0 : applyDropPaletteElement(score, note, element, modifiers);
2646 : }
2647 : }
2648 : }
2649 0 : return;
2650 0 : }
2651 0 : default: break;
2652 : }
2653 : }
2654 :
2655 0 : const track_idx_t track1 = staff2track(sel.staffStart());
2656 0 : const track_idx_t track2 = staff2track(sel.staffEnd());
2657 0 : Segment* startSegment = sel.startSegment();
2658 0 : Segment* endSegment = sel.endSegment(); //keep it, it could change during the loop
2659 :
2660 0 : const bool rangeContainsMultiNoteChords = sel.rangeContainsMultiNoteChords();
2661 :
2662 0 : const auto applyDropPaletteElementToChord = [this, score, element, modifiers, rangeContainsMultiNoteChords](Chord* chord) {
2663 0 : const size_t totalNotesInChord = chord->notes().size();
2664 0 : for (size_t noteIdx = 0; noteIdx < totalNotesInChord; ++noteIdx) {
2665 0 : Note* note = chord->notes().at(noteIdx);
2666 0 : if (!note || !score->selectionFilter().canSelectNoteIdx(noteIdx, totalNotesInChord, rangeContainsMultiNoteChords)) {
2667 0 : continue;
2668 : }
2669 0 : applyDropPaletteElement(score, note, element, modifiers);
2670 0 : if (!element->isAccidental() && !element->isNoteHead() && !element->isGlissando() && !element->isChordLine()) {
2671 : // Only need to apply to one note...
2672 0 : break;
2673 : }
2674 : }
2675 0 : };
2676 :
2677 0 : for (Segment* s = startSegment; s && s != endSegment; s = s->next1()) {
2678 0 : for (track_idx_t track = track1; track < track2; ++track) {
2679 0 : EngravingItem* e = s->element(track);
2680 0 : if (!e || !score->selectionFilter().canSelect(e) || !score->selectionFilter().canSelectVoice(track)) {
2681 0 : continue;
2682 : }
2683 0 : if (!e->isChord()) {
2684 : // do not apply articulation or fermata to barline in a range selection
2685 0 : if (!e->isBarLine() || !element->isArticulationOrFermata()) {
2686 0 : applyDropPaletteElement(score, e, element, modifiers);
2687 : }
2688 0 : continue;
2689 : }
2690 :
2691 0 : Chord* chord = toChord(e);
2692 0 : applyDropPaletteElementToChord(chord);
2693 : }
2694 0 : if (!element->placeMultiple()) {
2695 0 : break;
2696 : }
2697 : }
2698 0 : }
2699 :
2700 : //! NOTE Copied from Palette applyDrop
2701 0 : void NotationInteraction::applyDropPaletteElement(mu::engraving::Score* score, mu::engraving::EngravingItem* target,
2702 : mu::engraving::EngravingItem* e, Qt::KeyboardModifiers modifiers,
2703 : track_idx_t track)
2704 : {
2705 0 : if (!target) {
2706 0 : return;
2707 : }
2708 :
2709 0 : mu::engraving::EditData newData(&m_scoreCallbacks);
2710 0 : mu::engraving::EditData* dropData = &newData;
2711 :
2712 0 : if (isTextEditingStarted()) {
2713 0 : dropData = &m_editData;
2714 : }
2715 :
2716 0 : dropData->modifiers = keyboardModifier(modifiers);
2717 0 : dropData->dropElement = e;
2718 0 : dropData->track = (track == muse::nidx) ? target->track() : track;
2719 0 : dropData->pos = target->pageBoundingRect().topLeft();
2720 :
2721 0 : if (target->acceptDrop(*dropData)) {
2722 : // use same code path as drag&drop
2723 :
2724 0 : ByteArray a = e->mimeData();
2725 :
2726 0 : mu::engraving::XmlReader n(a);
2727 :
2728 0 : Fraction duration; // dummy
2729 0 : PointF dragOffset;
2730 0 : ElementType type = EngravingItem::readType(n, &dragOffset, &duration);
2731 0 : dropData->dropElement = engraving::Factory::createItem(type, score->dummy());
2732 0 : rw::RWRegister::reader()->readItem(dropData->dropElement, n);
2733 0 : dropData->dropElement->styleChanged(); // update to local style
2734 :
2735 0 : engraving::Transaction& tx = score->transactionManager()->currentOrDummyTransaction();
2736 0 : EngravingItem* el = target->drop(tx, *dropData);
2737 :
2738 0 : if (el && el->isInstrumentChange()) {
2739 0 : if (!selectInstrument(toInstrumentChange(el))) {
2740 0 : rollback();
2741 0 : return;
2742 : }
2743 : }
2744 :
2745 0 : if (el && el->hasVoiceAssignmentProperties()) {
2746 : // If target has voice assignment properties, dropped element takes those and discards the default
2747 0 : if (!target->hasVoiceAssignmentProperties()) {
2748 0 : el->setInitialTrackAndVoiceAssignment(target->track(), modifiers & ControlModifier);
2749 : }
2750 : }
2751 :
2752 0 : if (el && !score->inputState().noteEntryMode()) {
2753 0 : selectAndStartEditIfNeeded(el);
2754 : }
2755 :
2756 0 : if (score->selection().elements().size() == 1) {
2757 0 : doFinishAddFretboardDiagram();
2758 : }
2759 :
2760 0 : dropData->dropElement = nullptr;
2761 :
2762 0 : m_notifyAboutDropChanged = true;
2763 0 : }
2764 0 : }
2765 :
2766 0 : void NotationInteraction::applyLineNoteToNote(Score* score, Note* note1, Note* note2, EngravingItem* line)
2767 : {
2768 0 : mu::engraving::EditData newData(&m_scoreCallbacks);
2769 0 : mu::engraving::EditData* dropData = &newData;
2770 :
2771 0 : Chord* chord1 = note1->chord();
2772 0 : Chord* chord2 = note2->chord();
2773 0 : bool swapNotes = chord2->tick() < chord1->tick() || chord2->graceIndex() > chord1->graceIndex()
2774 0 : || (chord2->isGraceBefore() && chord2->parent() == chord1)
2775 0 : || (chord1->isGraceAfter() && chord1->parent() == chord2);
2776 :
2777 0 : if (swapNotes) {
2778 0 : std::swap(note1, note2);
2779 : }
2780 :
2781 0 : if (line->isActionIcon() && (toActionIcon(line)->actionType() == ActionIconType::STANDARD_BEND
2782 0 : || toActionIcon(line)->actionType() == ActionIconType::DIVE)) {
2783 0 : score->addGuitarBend(toActionIcon(line)->actionType() == ActionIconType::STANDARD_BEND
2784 : ? GuitarBendType::BEND : GuitarBendType::DIVE, note1, note2);
2785 0 : } else if (line->isActionIcon() && toActionIcon(line)->actionType() == ActionIconType::NOTE_ANCHORED_LINE) {
2786 0 : score->addNoteLine();
2787 0 : } else if (line->isSLine()) {
2788 0 : SLine* dropLine = ((SLine*)line->clone());
2789 0 : dropData->dropElement = dropLine;
2790 0 : if (note1->acceptDrop(*dropData)) {
2791 0 : dropLine->setEndElement(note2);
2792 0 : dropLine->styleChanged();
2793 :
2794 0 : engraving::Transaction& tx = score->transactionManager()->currentOrDummyTransaction();
2795 0 : mu::engraving::EngravingItem* el = note1->drop(tx, *dropData);
2796 0 : if (el && !score->inputState().noteEntryMode()) {
2797 0 : m_selection->select({ el }, mu::engraving::SelectType::SINGLE, 0);
2798 : }
2799 :
2800 0 : dropData->dropElement = nullptr;
2801 :
2802 0 : m_notifyAboutDropChanged = true;
2803 : }
2804 : }
2805 0 : }
2806 :
2807 : //! NOTE Copied from ScoreView::cmdAddSlur
2808 0 : void NotationInteraction::doAddSlur(const Slur* slurTemplate)
2809 : {
2810 0 : startEdit(TranslatableString("undoableAction", "Add slur"));
2811 0 : m_notifyAboutDropChanged = true;
2812 :
2813 0 : ChordRest* firstChordRest = nullptr;
2814 0 : ChordRest* secondChordRest = nullptr;
2815 0 : const auto& sel = score()->selection();
2816 0 : auto el = sel.uniqueElements();
2817 :
2818 0 : if (sel.isRange()) {
2819 0 : track_idx_t startTrack = staff2track(sel.staffStart());
2820 0 : track_idx_t endTrack = staff2track(sel.staffEnd());
2821 0 : for (track_idx_t track = startTrack; track < endTrack; ++track) {
2822 0 : firstChordRest = nullptr;
2823 0 : secondChordRest = nullptr;
2824 0 : for (EngravingItem* e : el) {
2825 0 : if (e->track() != track) {
2826 0 : continue;
2827 : }
2828 0 : if (e->isNote()) {
2829 0 : e = toNote(e)->chord();
2830 : }
2831 0 : if (!e->isChord()) {
2832 0 : continue;
2833 : }
2834 0 : ChordRest* cr = toChordRest(e);
2835 0 : if (!firstChordRest || firstChordRest->tick() > cr->tick()) {
2836 0 : firstChordRest = cr;
2837 : }
2838 0 : if (!secondChordRest || secondChordRest->tick() < cr->tick()) {
2839 0 : secondChordRest = cr;
2840 : }
2841 : }
2842 :
2843 0 : bool firstCrTrill = firstChordRest && firstChordRest->isChord() && toChord(firstChordRest)->isTrillCueNote();
2844 0 : bool secondCrTrill = secondChordRest && secondChordRest->isChord() && toChord(secondChordRest)->isTrillCueNote();
2845 :
2846 0 : if (firstChordRest && (firstChordRest != secondChordRest)
2847 0 : && !(firstCrTrill || secondCrTrill)) {
2848 0 : doAddSlur(firstChordRest, secondChordRest, slurTemplate);
2849 : }
2850 : }
2851 0 : } else if (sel.isSingle()) {
2852 0 : if (sel.element()->isNote() && !toNote(sel.element())->isTrillCueNote()) {
2853 0 : doAddSlur(toNote(sel.element())->chord(), nullptr, slurTemplate);
2854 0 : } else if (sel.element()->isSlurSegment() && slurTemplate && slurTemplate->isHammerOnPullOff()) {
2855 0 : Slur* slur = toSlurSegment(sel.element())->slur();
2856 0 : doAddSlur(slur->startElement(), slur->endElement(), slurTemplate);
2857 : }
2858 : } else {
2859 0 : EngravingItem* firstItem = nullptr;
2860 0 : EngravingItem* secondItem = nullptr;
2861 0 : for (EngravingItem* e : el) {
2862 0 : if (e->isNote()) {
2863 0 : e = toNote(e)->chord();
2864 : }
2865 :
2866 0 : Measure* meas = e->findMeasure();
2867 0 : bool header = meas && meas->header() && e->tick() == meas->tick();
2868 :
2869 0 : if (!e->isChord() && !e->isBarLine() && !header) {
2870 0 : continue;
2871 : }
2872 :
2873 0 : if (!firstItem || e->isBefore(firstItem)) {
2874 0 : firstItem = e;
2875 : }
2876 0 : if (!secondItem || secondItem->isBefore(e)) {
2877 0 : secondItem = e;
2878 : }
2879 : }
2880 :
2881 0 : bool firstCrTrill = firstItem && firstItem->isChord() && toChord(firstItem)->isTrillCueNote();
2882 0 : bool secondCrTrill = secondItem && secondItem->isChord() && toChord(secondItem)->isTrillCueNote();
2883 :
2884 0 : if (firstItem && firstItem == secondItem) {
2885 : // A single chord is slurred as a single note is
2886 0 : if (firstItem->isChord() && !firstCrTrill) {
2887 0 : doAddSlur(firstItem, nullptr, slurTemplate);
2888 : }
2889 0 : } else if (firstItem && secondItem && (firstItem->isChordRest() || secondItem->isChordRest())
2890 0 : && !(firstCrTrill || secondCrTrill)) {
2891 0 : doAddSlur(firstItem, secondItem, slurTemplate);
2892 : }
2893 : }
2894 :
2895 0 : apply();
2896 0 : }
2897 :
2898 0 : void NotationInteraction::doAddSlur(EngravingItem* firstItem, EngravingItem* secondItem, const Slur* slurTemplate)
2899 : {
2900 0 : ChordRest* firstChordRest = nullptr;
2901 0 : ChordRest* secondChordRest = nullptr;
2902 :
2903 0 : Measure* meas = firstItem ? firstItem->findMeasure() : nullptr;
2904 0 : bool header = meas && meas->header() && firstItem->tick() == meas->tick() && !firstItem->isChordRest();
2905 :
2906 0 : if (firstItem && secondItem
2907 0 : && ((firstItem->isBarLine() != secondItem->isBarLine()) || (header && secondItem->isChordRest()))) {
2908 0 : const bool outgoing = firstItem->isChordRest();
2909 0 : const EngravingItem* otherElement = outgoing ? secondItem : firstItem;
2910 0 : ChordRest* cr = outgoing ? toChordRest(firstItem) : toChordRest(secondItem);
2911 :
2912 : // Check that cr and otherElement are part of the same repeat section
2913 0 : Segment* seg1 = toSegment(firstItem->findAncestor(ElementType::SEGMENT));
2914 0 : Segment* seg2 = toSegment(secondItem->findAncestor(ElementType::SEGMENT));
2915 :
2916 0 : bool differentRepeatSegments = segmentsAreInDifferentRepeatSegments(seg1, seg2);
2917 0 : bool validBarline = isValidBarLineForRepeatSection(seg1, seg2);
2918 :
2919 0 : if (!cr || (!header && (differentRepeatSegments && !validBarline))) {
2920 0 : return;
2921 : }
2922 :
2923 0 : Slur* partialSlur = slurTemplate ? slurTemplate->clone() : Factory::createSlur(score()->dummy());
2924 0 : if (outgoing) {
2925 0 : partialSlur->undoSetOutgoing(true);
2926 0 : firstChordRest = toChordRest(cr);
2927 0 : const Measure* endMeas = otherElement->findMeasure();
2928 0 : ChordRest* endCr = endMeas->lastChordRest(otherElement->track());
2929 0 : secondChordRest = endCr;
2930 : } else {
2931 0 : partialSlur->undoSetIncoming(true);
2932 0 : secondChordRest = toChordRest(cr);
2933 0 : const Measure* startMeas = otherElement->findMeasure();
2934 0 : if (otherElement->isBarLine()) {
2935 0 : if (toBarLine(otherElement)->barLineType() == BarLineType::END_REPEAT) {
2936 0 : startMeas = startMeas->nextMeasure();
2937 : }
2938 : }
2939 0 : ChordRest* startCr = startMeas->firstChordRest(otherElement->track());
2940 0 : firstChordRest = startCr;
2941 : }
2942 0 : slurTemplate = partialSlur;
2943 : } else {
2944 0 : firstChordRest = toChordRest(firstItem);
2945 0 : secondChordRest = toChordRest(secondItem);
2946 : }
2947 :
2948 0 : Slur* slur = firstChordRest->slur(secondChordRest);
2949 0 : if (!slur || slur->slurDirection() != DirectionV::AUTO) {
2950 0 : slur = score()->addSlur(firstChordRest, secondChordRest, slurTemplate);
2951 0 : } else if (slurTemplate && slurTemplate->isHammerOnPullOff()) {
2952 : // Replace existing slur with HOPO
2953 0 : endEditElement();
2954 0 : score()->undoRemoveElement(slur);
2955 0 : slur = score()->addSlur(firstChordRest, secondChordRest, slurTemplate);
2956 0 : SlurSegment* segment = slur->frontSegment();
2957 0 : select({ segment }, SelectType::SINGLE);
2958 0 : startEditGrip(segment, Grip::END);
2959 : }
2960 :
2961 0 : if (m_noteInput->isNoteInputMode()) {
2962 0 : m_noteInput->addSlur(slur);
2963 0 : } else if (!secondItem) {
2964 0 : SlurSegment* segment = slur->frontSegment();
2965 0 : select({ segment }, SelectType::SINGLE);
2966 0 : startEditGrip(segment, Grip::END);
2967 : }
2968 : }
2969 :
2970 0 : bool NotationInteraction::scoreHasMeasure() const
2971 : {
2972 0 : mu::engraving::Page* page = score()->pages().empty() ? nullptr : score()->pages().front();
2973 0 : const std::vector<mu::engraving::System*>* systems = page ? &page->systems() : nullptr;
2974 0 : if (systems == nullptr || systems->empty() || systems->front()->measures().empty()) {
2975 0 : return false;
2976 : }
2977 :
2978 0 : return true;
2979 : }
2980 :
2981 0 : bool NotationInteraction::notesHaveActiculation(const std::vector<Note*>& notes, SymbolId articulationSymbolId) const
2982 : {
2983 0 : for (Note* note: notes) {
2984 0 : Chord* chord = note->chord();
2985 :
2986 0 : std::set<SymbolId> chordArticulations = chord->articulationSymbolIds();
2987 0 : chordArticulations = mu::engraving::flipArticulations(chordArticulations, mu::engraving::PlacementV::ABOVE);
2988 0 : chordArticulations = mu::engraving::splitArticulations(chordArticulations);
2989 :
2990 0 : if (chordArticulations.find(articulationSymbolId) == chordArticulations.end()) {
2991 0 : return false;
2992 : }
2993 0 : }
2994 :
2995 0 : return true;
2996 : }
2997 :
2998 : //! NOTE Copied from ScoreView::dragLeaveEvent
2999 0 : void NotationInteraction::endDrop()
3000 : {
3001 0 : score()->hideAnchors();
3002 0 : score()->setUpdateAll();
3003 0 : setDropTarget(nullptr, /*notify=*/ false);
3004 0 : resetDropData();
3005 0 : score()->update();
3006 0 : notifyAboutDragChanged();
3007 0 : }
3008 :
3009 0 : muse::async::Notification NotationInteraction::dropChanged() const
3010 : {
3011 0 : return m_dropChanged;
3012 : }
3013 :
3014 : //! NOTE Copied from ScoreView::dropCanvas
3015 0 : bool NotationInteraction::dropCanvas(EngravingItem* e)
3016 : {
3017 0 : if (e->isActionIcon()) {
3018 0 : switch (mu::engraving::toActionIcon(e)->actionType()) {
3019 0 : case mu::engraving::ActionIconType::VFRAME
3020 0 : : score()->insertBox(ElementType::VBOX);
3021 0 : break;
3022 0 : case mu::engraving::ActionIconType::HFRAME:
3023 0 : score()->insertBox(ElementType::HBOX);
3024 0 : break;
3025 0 : case mu::engraving::ActionIconType::TFRAME:
3026 0 : score()->insertBox(ElementType::TBOX);
3027 0 : break;
3028 0 : case mu::engraving::ActionIconType::FFRAME:
3029 0 : score()->insertBox(ElementType::FBOX);
3030 0 : break;
3031 0 : case mu::engraving::ActionIconType::MEASURE:
3032 0 : score()->insertMeasure(ElementType::MEASURE);
3033 0 : break;
3034 0 : default:
3035 0 : return false;
3036 : }
3037 0 : delete e;
3038 0 : return true;
3039 : }
3040 0 : return false;
3041 : }
3042 :
3043 : //! NOTE Copied from ScoreView::getDropTarget
3044 0 : EngravingItem* NotationInteraction::dropTarget(mu::engraving::EditData& ed) const
3045 : {
3046 0 : std::vector<EngravingItem*> el = elementsAt(ed.pos);
3047 0 : mu::engraving::Measure* fallbackMeasure = nullptr;
3048 0 : for (EngravingItem* e : el) {
3049 0 : if (e->isStaffLines()) {
3050 0 : fallbackMeasure = mu::engraving::toStaffLines(e)->measure();
3051 0 : continue;
3052 : }
3053 0 : if (e->acceptDrop(ed)) {
3054 0 : return e;
3055 : }
3056 : }
3057 0 : if (fallbackMeasure && fallbackMeasure->acceptDrop(ed)) {
3058 0 : return fallbackMeasure;
3059 : }
3060 0 : return nullptr;
3061 0 : }
3062 :
3063 0 : bool NotationInteraction::prepareDropStandardElement(const PointF& pos, Qt::KeyboardModifiers modifiers)
3064 : {
3065 0 : IF_ASSERT_FAILED(m_dropData.elementDropData.has_value()) {
3066 0 : return false;
3067 : }
3068 :
3069 0 : ElementDropData& edd = m_dropData.elementDropData.value();
3070 :
3071 0 : Page* page = point2page(pos, true);
3072 0 : if (!page) {
3073 0 : return false;
3074 : }
3075 :
3076 : // Update the tree when starting a new drag (ed.pos hasn't been set yet), if the modifier changed, or when crossing a page
3077 0 : if (edd.ed.pos.isNull() || edd.ed.modifiers != modifiers || page != m_currentDropPage) {
3078 0 : m_currentDropPage = page;
3079 0 : edd.ed.modifiers = keyboardModifier(modifiers);
3080 :
3081 : // Where "m" is the number of page elements and "n" is the number of elements accepting a drop...
3082 : // O(log m) operation
3083 0 : m_droppableTree.initialize(page->pageBoundingRect(), static_cast<int>(page->elements().size()));
3084 :
3085 : // O(m log n) operation
3086 0 : for (EngravingItem* elem : page->elements()) {
3087 0 : if (elem->acceptDrop(edd.ed)) {
3088 0 : m_droppableTree.insert(elem);
3089 : }
3090 0 : }
3091 : }
3092 :
3093 0 : PointF posInPage(pos.x() - page->pos().x(), pos.y() - page->pos().y());
3094 : // O(log n) operation
3095 0 : EngravingItem* targetElem = m_droppableTree.nearestNeighbor(posInPage);
3096 0 : EngravingItem* dropElem = edd.ed.dropElement;
3097 :
3098 0 : if (!targetElem || !dropElem) {
3099 0 : return false;
3100 : }
3101 :
3102 : // Clefs are a special case somewhere between a measure anchored and standard element (hence why
3103 : // we handle it here instead of dragMeasureAnchorElement). They can be applied to individual items
3104 : // but we should switch targets to the whole measure if the target element is the first in the measure...
3105 0 : if (dropElem->isClef() && targetElem->rtick() == Fraction(0, 1)) {
3106 0 : Measure* targetMeasure = targetElem->findMeasure();
3107 0 : if (targetMeasure && dropElem->type() != targetElem->type()) {
3108 0 : setDropTarget(targetMeasure, true);
3109 :
3110 0 : RectF measureRect = targetMeasure->staffPageBoundingRect(targetElem->staffIdx());
3111 0 : measureRect.adjust(page->x(), page->y(), page->x(), page->y());
3112 0 : edd.ed.pos = measureRect.center();
3113 0 : edd.ed.track = trackZeroVoice(targetElem->track());
3114 0 : setAnchorLines({ LineF(pos, measureRect.topLeft()) });
3115 :
3116 0 : const bool dropAccepted = targetMeasure->acceptDrop(edd.ed);
3117 0 : if (dropAccepted) {
3118 0 : setDropRects(dropHighlightRects(dropElem, targetMeasure, measureRect, edd.ed.modifiers));
3119 : }
3120 0 : return dropAccepted;
3121 : }
3122 : }
3123 :
3124 0 : setDropTarget(targetElem, true);
3125 0 : edd.ed.pos = targetElem->canvasBoundingRect().center();
3126 0 : setAnchorLines({ LineF(pos, targetElem->canvasBoundingRect().center()) });
3127 :
3128 0 : return true;
3129 : }
3130 :
3131 : //! NOTE Copied from ScoreView::dragMeasureAnchorElement
3132 0 : bool NotationInteraction::prepareDropMeasureBaseAnchorElement(const PointF& pos)
3133 : {
3134 0 : IF_ASSERT_FAILED(m_dropData.elementDropData.has_value()) {
3135 0 : return false;
3136 : }
3137 :
3138 0 : ElementDropData& edd = m_dropData.elementDropData.value();
3139 :
3140 0 : EngravingItem* dropElem = edd.ed.dropElement;
3141 0 : Page* page = point2page(pos);
3142 0 : if (!dropElem || !page) {
3143 0 : return false;
3144 : }
3145 :
3146 : // "Measure anchored only" means we're dropping something can't anchor to other MeasureBase types (i.e. boxes)...
3147 0 : bool isMeasureAnchorOnly = !dropElem->isLayoutBreak();
3148 0 : if (dropElem->isActionIcon()) {
3149 0 : const ActionIconType actionType = toActionIcon(dropElem)->actionType();
3150 0 : isMeasureAnchorOnly = actionType != ActionIconType::PAGE_LOCK && actionType != ActionIconType::SYSTEM_LOCK;
3151 : }
3152 :
3153 0 : mu::engraving::staff_idx_t staffIdx = muse::nidx;
3154 0 : mu::engraving::MeasureBase* mb = score()->pos2measureBase(pos, /*scanMeasuresOnly*/ isMeasureAnchorOnly, &staffIdx, 0, nullptr, 0);
3155 :
3156 : //! NOTE: Should match Measure::acceptDrop / MeasureBase::acceptDrop
3157 0 : switch (dropElem->type()) {
3158 0 : case ElementType::VOLTA:
3159 : case ElementType::GRADUAL_TEMPO_CHANGE:
3160 : case ElementType::KEYSIG:
3161 : case ElementType::TIMESIG: {
3162 : // If ctrl is pressed, break and target a specific staff
3163 0 : if (edd.ed.modifiers & Qt::ControlModifier) {
3164 0 : break;
3165 : }
3166 : }
3167 : // fall through
3168 : case ElementType::JUMP:
3169 : case ElementType::LAYOUT_BREAK:
3170 : case ElementType::MARKER:
3171 : case ElementType::MEASURE_NUMBER:
3172 : case ElementType::VBOX:
3173 : case ElementType::TBOX:
3174 : case ElementType::FBOX:
3175 : case ElementType::HBOX:
3176 : // Target all staves
3177 0 : staffIdx = 0;
3178 : // fall through
3179 0 : default: break;
3180 : }
3181 :
3182 : // Apart from STAFF_TYPE_CHANGE, measure anchored action icons are applied to all staves
3183 0 : if (dropElem->isActionIcon() && toActionIcon(dropElem)->actionType() != ActionIconType::STAFF_TYPE_CHANGE) {
3184 0 : staffIdx = 0;
3185 : }
3186 :
3187 0 : if (mb) {
3188 0 : setDropTarget(mb, true);
3189 0 : edd.ed.track = staff2track(staffIdx);
3190 :
3191 0 : RectF mbRect = mb->isMeasure() ? toMeasure(mb)->staffPageBoundingRect(staffIdx) : mb->pageBoundingRect();
3192 0 : mbRect.adjust(page->x(), page->y(), page->x(), page->y());
3193 0 : edd.ed.pos = mbRect.center();
3194 :
3195 0 : const bool dropAccepted = mb->acceptDrop(edd.ed);
3196 0 : if (dropAccepted) {
3197 0 : setAnchorLines({ LineF(pos, mbRect.topLeft()) });
3198 0 : setDropRects(dropHighlightRects(dropElem, mb, mbRect, edd.ed.modifiers));
3199 : } else {
3200 0 : score()->addRefresh(mb->canvasBoundingRect());
3201 0 : setDropTarget(nullptr);
3202 : }
3203 :
3204 0 : return dropAccepted;
3205 : }
3206 0 : dropElem->score()->addRefresh(dropElem->canvasBoundingRect());
3207 0 : setDropTarget(nullptr);
3208 0 : return false;
3209 : }
3210 :
3211 0 : std::vector<RectF> NotationInteraction::dropHighlightRects(const EngravingItem* dropElem, const MeasureBase* targetMeasureBase,
3212 : const RectF& staffRect, KeyboardModifiers modifiers) const
3213 : {
3214 0 : switch (dropElem->type()) {
3215 0 : case ElementType::VBOX:
3216 : case ElementType::TBOX:
3217 : case ElementType::FBOX:
3218 : case ElementType::HBOX:
3219 : case ElementType::MEASURE_NUMBER:
3220 : case ElementType::JUMP:
3221 : case ElementType::MARKER:
3222 : case ElementType::LAYOUT_BREAK:
3223 0 : return { targetMeasureBase->canvasBoundingRect() };
3224 :
3225 0 : case ElementType::VOLTA:
3226 : case ElementType::GRADUAL_TEMPO_CHANGE:
3227 : case ElementType::KEYSIG:
3228 : case ElementType::TIMESIG:
3229 0 : if (modifiers & ControlModifier) {
3230 0 : return { staffRect };
3231 : }
3232 0 : return { targetMeasureBase->canvasBoundingRect() };
3233 :
3234 0 : case ElementType::BRACKET:
3235 : case ElementType::MEASURE_REPEAT:
3236 : case ElementType::MEASURE:
3237 : case ElementType::SPACER:
3238 : case ElementType::IMAGE:
3239 : case ElementType::BAR_LINE:
3240 : case ElementType::SYMBOL:
3241 : case ElementType::CLEF:
3242 : case ElementType::STAFFTYPE_CHANGE:
3243 : case ElementType::STRING_TUNINGS:
3244 0 : return { staffRect };
3245 :
3246 0 : case ElementType::ACTION_ICON:
3247 0 : switch (toActionIcon(dropElem)->actionType()) {
3248 0 : case ActionIconType::VFRAME:
3249 : case ActionIconType::HFRAME:
3250 : case ActionIconType::TFRAME:
3251 : case ActionIconType::FFRAME:
3252 : case ActionIconType::MEASURE:
3253 0 : return { targetMeasureBase->canvasBoundingRect() };
3254 :
3255 0 : case ActionIconType::STAFF_TYPE_CHANGE:
3256 0 : return { staffRect };
3257 :
3258 0 : case ActionIconType::SYSTEM_LOCK: {
3259 0 : const System* sys = targetMeasureBase->system();
3260 0 : const MeasureBase* first = sys ? sys->first() : nullptr;
3261 0 : const PointF topLeft = first ? first->canvasBoundingRect().topLeft() : PointF(0.0, 0.0);
3262 0 : return { RectF(topLeft, targetMeasureBase->canvasBoundingRect().bottomRight()) };
3263 : }
3264 :
3265 0 : case ActionIconType::PAGE_LOCK: {
3266 0 : std::vector<RectF> dropRects;
3267 0 : for (System* sys : targetMeasureBase->page()->systems()) {
3268 0 : const bool lastSelectedSys = sys == targetMeasureBase->system();
3269 0 : const MeasureBase* first = sys ? sys->first() : nullptr;
3270 0 : const MeasureBase* last = sys ? sys->last() : nullptr;
3271 0 : if (lastSelectedSys) {
3272 0 : last = targetMeasureBase;
3273 : }
3274 0 : if (!first || !last) {
3275 0 : continue;
3276 : }
3277 0 : dropRects.push_back(RectF(first->canvasBoundingRect().topLeft(), last->canvasBoundingRect().bottomRight()));
3278 :
3279 0 : if (lastSelectedSys) {
3280 0 : break;
3281 : }
3282 : }
3283 0 : return dropRects;
3284 0 : }
3285 :
3286 0 : default:
3287 0 : return {};
3288 : }
3289 :
3290 0 : default:
3291 0 : return {};
3292 : }
3293 : }
3294 :
3295 : //! NOTE Copied from ScoreView::dragTimeAnchorElement
3296 0 : bool NotationInteraction::prepareDropTimeAnchorElement(const PointF& pos)
3297 : {
3298 0 : IF_ASSERT_FAILED(m_dropData.elementDropData.has_value()) {
3299 0 : return false;
3300 : }
3301 :
3302 0 : ElementDropData& edd = m_dropData.elementDropData.value();
3303 :
3304 0 : mu::engraving::staff_idx_t staffIdx = 0;
3305 0 : mu::engraving::Segment* seg = nullptr;
3306 0 : mu::engraving::Measure* measure = score()->pos2measure(pos, &staffIdx, /*pitch*/ 0, &seg, /*offset*/ 0);
3307 0 : mu::engraving::track_idx_t track = staff2track(staffIdx);
3308 :
3309 0 : if (measure && seg->element(track)) {
3310 0 : mu::engraving::System* s = measure->system();
3311 0 : qreal y = s->staff(staffIdx)->y() + s->pos().y() + s->page()->pos().y();
3312 0 : PointF anchor(seg->canvasBoundingRect().x(), y);
3313 0 : setAnchorLines({ LineF(pos, anchor) });
3314 0 : edd.ed.dropElement->score()->addRefresh(edd.ed.dropElement->canvasBoundingRect());
3315 0 : edd.ed.dropElement->setTrack(track);
3316 0 : edd.ed.dropElement->score()->addRefresh(edd.ed.dropElement->canvasBoundingRect());
3317 0 : notifyAboutDragChanged();
3318 0 : return true;
3319 : }
3320 :
3321 0 : edd.ed.dropElement->score()->addRefresh(edd.ed.dropElement->canvasBoundingRect());
3322 0 : setDropTarget(nullptr);
3323 :
3324 0 : return false;
3325 : }
3326 :
3327 : //! NOTE Copied from ScoreView::setDropTarget
3328 0 : void NotationInteraction::setDropTarget(EngravingItem* item, bool notify)
3329 : {
3330 0 : if (!m_dropData.elementDropData.has_value()) {
3331 0 : return;
3332 : }
3333 :
3334 0 : ElementDropData& edd = m_dropData.elementDropData.value();
3335 :
3336 0 : if (edd.dropTarget != item) {
3337 0 : if (edd.dropTarget) {
3338 0 : edd.dropTarget->setDropTarget(false);
3339 0 : edd.ed.track = muse::nidx;
3340 0 : edd.dropTarget = nullptr;
3341 : }
3342 :
3343 0 : edd.dropTarget = item;
3344 0 : if (edd.dropTarget) {
3345 0 : edd.dropTarget->setDropTarget(true);
3346 0 : edd.ed.track = edd.dropTarget->track();
3347 : }
3348 : }
3349 :
3350 0 : resetAnchorLines();
3351 :
3352 0 : if (!edd.dropRects.empty()) {
3353 0 : edd.dropRects.clear();
3354 : }
3355 :
3356 0 : if (notify) {
3357 0 : notifyAboutDragChanged();
3358 : }
3359 : }
3360 :
3361 : //! NOTE: Copied from ScoreView::setDropRectangles
3362 0 : void NotationInteraction::setDropRects(const std::vector<RectF>& rects)
3363 : {
3364 0 : if (!m_dropData.elementDropData.has_value()) {
3365 0 : return;
3366 : }
3367 :
3368 0 : ElementDropData& edd = m_dropData.elementDropData.value();
3369 :
3370 0 : if (edd.dropRects == rects) {
3371 0 : return;
3372 : }
3373 :
3374 0 : edd.dropRects = rects;
3375 :
3376 0 : for (const RectF& rect : rects) {
3377 0 : if (rect.isValid()) {
3378 0 : score()->addRefresh(rect);
3379 : }
3380 : }
3381 :
3382 0 : if (edd.dropTarget) {
3383 0 : edd.dropTarget->setDropTarget(false);
3384 0 : score()->addRefresh(edd.dropTarget->canvasBoundingRect());
3385 0 : } else if (!m_anchorLines.empty()) {
3386 0 : RectF rf;
3387 0 : rf.setTopLeft(m_anchorLines.front().p1());
3388 0 : rf.setBottomRight(m_anchorLines.front().p2());
3389 0 : score()->addRefresh(rf.normalized());
3390 0 : resetAnchorLines();
3391 : }
3392 :
3393 0 : notifyAboutDragChanged();
3394 : }
3395 :
3396 0 : void NotationInteraction::resetDropData()
3397 : {
3398 0 : if (m_dropData.elementDropData.has_value()) {
3399 0 : delete m_dropData.elementDropData->ed.dropElement;
3400 : }
3401 :
3402 0 : m_dropData = {};
3403 0 : }
3404 :
3405 0 : void NotationInteraction::doFinishAddFretboardDiagram()
3406 : {
3407 0 : EngravingItem* selectedElement = score()->selection().element();
3408 0 : if (!selectedElement || !selectedElement->isFretDiagram()) {
3409 0 : return;
3410 : }
3411 :
3412 0 : FretDiagram* fretDiagram = toFretDiagram(selectedElement);
3413 0 : if (!fretDiagram->harmonyPlainText().empty()) {
3414 0 : return;
3415 : }
3416 :
3417 : //! first we need to apply changes
3418 0 : apply();
3419 :
3420 : //! then add harmony
3421 : // NOTE: needs to be done on a separate transaction because when text editing ends, if text is empty
3422 : // the last action is rolled back to remove the text, this ensures that only the text insertion is
3423 : // rolled back (and not the diagram insertion).
3424 0 : engraving::TextBase* text = nullptr;
3425 0 : transaction(TranslatableString("undoableAction", "Add chord symbol to fretboard diagram"), [&, this](engraving::Transaction&) {
3426 0 : text = score()->addText(TextStyleType::HARMONY_A, fretDiagram);
3427 0 : });
3428 :
3429 0 : m_selection->select({ text }, SelectType::SINGLE);
3430 0 : startEditElement(text);
3431 : }
3432 :
3433 0 : void NotationInteraction::setAnchorLines(const std::vector<LineF>& anchorList)
3434 : {
3435 0 : m_anchorLines = anchorList;
3436 0 : }
3437 :
3438 0 : void NotationInteraction::resetAnchorLines()
3439 : {
3440 0 : m_anchorLines.clear();
3441 0 : }
3442 :
3443 0 : double NotationInteraction::getHRaster() const
3444 : {
3445 0 : double hRaster = mu::engraving::MScore::hRaster();
3446 :
3447 0 : if (m_editData.element->isBarLine()) {
3448 : // Always adjust by .25sp
3449 0 : hRaster = 4.0;
3450 0 : } else if (m_editData.element->isLyricsLineSegment()) {
3451 : // Ignore grid
3452 0 : hRaster = 0.0;
3453 : }
3454 :
3455 0 : return hRaster;
3456 : }
3457 :
3458 0 : double NotationInteraction::getVRaster() const
3459 : {
3460 0 : double vRaster = mu::engraving::MScore::vRaster();
3461 :
3462 0 : if (m_editData.element->isBarLine()) {
3463 : // Always adjust by .25sp
3464 0 : vRaster = 4.0;
3465 0 : } else if (m_editData.element->isLyricsLineSegment()) {
3466 : // Ignore grid
3467 0 : vRaster = 0.0;
3468 : }
3469 :
3470 0 : return vRaster;
3471 : }
3472 :
3473 0 : double NotationInteraction::currentScaling(Painter* painter) const
3474 : {
3475 0 : qreal guiScaling = contextConfiguration()->guiScaling();
3476 0 : return painter->worldTransform().m11() / guiScaling;
3477 : }
3478 :
3479 0 : std::vector<NotationInteraction::ShadowNoteParams> NotationInteraction::previewNotes() const
3480 : {
3481 0 : std::vector<ShadowNoteParams> result;
3482 :
3483 0 : const InputState& is = score()->inputState();
3484 0 : if (!is.isValid()) {
3485 0 : return result;
3486 : }
3487 :
3488 0 : Segment* segment = is.segment();
3489 0 : const staff_idx_t staffIdx = is.staffIdx();
3490 0 : const Staff* staff = score()->staff(staffIdx);
3491 0 : const System* system = segment->system();
3492 0 : const SysStaff* sysStaff = system ? system->staff(staffIdx) : nullptr;
3493 0 : const Measure* measure = segment->measure();
3494 :
3495 0 : if (!staff || !sysStaff || !measure) {
3496 0 : return result;
3497 : }
3498 :
3499 0 : const Fraction tick = is.tick();
3500 0 : const PointF measurePos = measure->canvasPos();
3501 0 : const double lineDist = staff->staffType(tick)->lineDistance().val()
3502 0 : * (staff->isTabStaff(tick) ? 1 : .5)
3503 0 : * staff->staffMag(tick)
3504 0 : * score()->style().spatium();
3505 :
3506 0 : NoteValList nvals;
3507 0 : if (is.rest()) {
3508 0 : nvals.push_back(NoteVal());
3509 : } else {
3510 0 : nvals = is.notes();
3511 : }
3512 :
3513 0 : ShadowNoteParams params;
3514 0 : params.duration = is.rest() ? is.duration() : TDuration();
3515 0 : params.position.segment = segment;
3516 0 : params.position.staffIdx = staffIdx;
3517 :
3518 0 : result.reserve(nvals.size());
3519 :
3520 0 : for (const NoteVal& nval : nvals) {
3521 0 : const int line = mu::engraving::noteValToLine(nval, staff, tick);
3522 0 : const double y = sysStaff->y() + line * lineDist;
3523 :
3524 0 : params.accidentalType = accidentalType(is, nval, line);
3525 0 : params.position.line = line;
3526 :
3527 0 : if (is.beyondScore()) {
3528 0 : params.position.pos = PointF(measure->ldata()->bbox().width() + score()->style().spatium(), y) + measurePos;
3529 : } else {
3530 0 : params.position.pos = PointF(segment->x(), y) + measurePos;
3531 : }
3532 :
3533 0 : result.push_back(params);
3534 : }
3535 :
3536 0 : std::sort(result.begin(), result.end(), [](const ShadowNoteParams& p1, const ShadowNoteParams& p2) {
3537 0 : return p1.position.line < p2.position.line;
3538 : });
3539 :
3540 0 : return result;
3541 0 : }
3542 :
3543 0 : bool NotationInteraction::shouldDrawInputPreview() const
3544 : {
3545 0 : return m_noteInput->isNoteInputMode() && m_noteInput->usingNoteInputMethod(NoteInputMethod::BY_DURATION);
3546 : }
3547 :
3548 0 : void NotationInteraction::drawInputPreview(Painter* painter, const engraving::rendering::PaintOptions& opt)
3549 : {
3550 0 : std::vector<ShadowNoteParams> paramsList = previewNotes();
3551 0 : if (paramsList.empty()) {
3552 0 : return;
3553 : }
3554 :
3555 0 : std::vector<ShadowNote*> previewList;
3556 0 : previewList.reserve(paramsList.size());
3557 :
3558 0 : const InputState& is = score()->inputState();
3559 :
3560 0 : for (ShadowNoteParams& params : paramsList) {
3561 0 : ShadowNote* preview = new ShadowNote(score());
3562 0 : doShowShadowNote(*preview, params);
3563 0 : previewList.push_back(preview);
3564 : }
3565 :
3566 0 : const bool isUp = !is.rest() && previewList.front()->computeUp();
3567 0 : bool isLeft = isUp;
3568 0 : int prevLine = INT_MAX;
3569 :
3570 0 : auto correctNotePositionIfNeed = [&](ShadowNote* note) {
3571 0 : if (paramsList.size() == 1) {
3572 0 : return;
3573 : }
3574 :
3575 0 : const bool conflict = std::abs(prevLine - note->lineIndex()) < 2;
3576 0 : prevLine = note->lineIndex();
3577 :
3578 0 : if (conflict || (isUp != isLeft)) {
3579 0 : isLeft = !isLeft;
3580 : }
3581 :
3582 0 : if (isUp == isLeft) {
3583 0 : return;
3584 : }
3585 :
3586 0 : const RectF noteheadBbox = note->symBbox(note->noteheadSymbol());
3587 :
3588 0 : if (isLeft) {
3589 0 : note->move(PointF(-noteheadBbox.width(), 0.));
3590 : } else {
3591 0 : note->move(PointF(noteheadBbox.width(), 0.));
3592 : }
3593 0 : };
3594 :
3595 0 : if (isUp) {
3596 0 : for (auto it = previewList.rbegin(); it != previewList.rend(); ++it) {
3597 0 : correctNotePositionIfNeed(*it);
3598 0 : score()->renderer()->drawItem(*it, painter, opt);
3599 : }
3600 : } else {
3601 0 : for (auto it = previewList.begin(); it != previewList.end(); ++it) {
3602 0 : correctNotePositionIfNeed(*it);
3603 0 : score()->renderer()->drawItem(*it, painter, opt);
3604 : }
3605 : }
3606 :
3607 0 : DeleteAll(previewList);
3608 0 : }
3609 :
3610 0 : void NotationInteraction::drawAnchorLines(Painter* painter)
3611 : {
3612 0 : if (m_anchorLines.empty()) {
3613 0 : return;
3614 : }
3615 :
3616 0 : const auto dropAnchorColor = score()->configuration()->formattingColor();
3617 0 : Pen pen(dropAnchorColor, 2.0 / currentScaling(painter), PenStyle::DotLine);
3618 :
3619 0 : for (const LineF& anchor : m_anchorLines) {
3620 0 : painter->setPen(pen);
3621 0 : painter->drawLine(anchor);
3622 :
3623 0 : qreal d = 4.0 / currentScaling(painter);
3624 0 : RectF rect(-d, -d, 2 * d, 2 * d);
3625 :
3626 0 : painter->setBrush(Brush(dropAnchorColor));
3627 0 : painter->setNoPen();
3628 0 : rect.moveCenter(anchor.p1());
3629 0 : painter->drawEllipse(rect);
3630 0 : rect.moveCenter(anchor.p2());
3631 0 : painter->drawEllipse(rect);
3632 : }
3633 0 : }
3634 :
3635 0 : void NotationInteraction::drawTextEditMode(muse::draw::Painter* painter, const engraving::rendering::PaintOptions& opt)
3636 : {
3637 0 : if (!isTextEditingStarted()) {
3638 0 : return;
3639 : }
3640 :
3641 0 : editModeRenderer()->drawItem(m_editData.element, painter, m_editData, currentScaling(painter), opt);
3642 : }
3643 :
3644 0 : void NotationInteraction::drawSelectionRange(muse::draw::Painter* painter)
3645 : {
3646 : using namespace muse::draw;
3647 0 : if (!m_selection->isRange()) {
3648 0 : return;
3649 : }
3650 :
3651 0 : painter->setBrush(BrushStyle::NoBrush);
3652 :
3653 0 : Color selectionColor = configuration()->selectionColor();
3654 0 : double penWidth = 3.0 / currentScaling(painter);
3655 0 : double minPenWidth = 0.20 * m_selection->range()->measureBaseRange().startMeasureBase->spatium();
3656 0 : penWidth = std::max(penWidth, minPenWidth);
3657 :
3658 0 : Pen pen;
3659 0 : pen.setColor(selectionColor);
3660 0 : pen.setWidthF(penWidth);
3661 0 : pen.setStyle(PenStyle::SolidLine);
3662 0 : painter->setPen(pen);
3663 :
3664 0 : std::vector<RectF> rangeArea = m_selection->range()->boundingArea();
3665 0 : for (const RectF& rect: rangeArea) {
3666 0 : PainterPath path;
3667 0 : path.addRoundedRect(rect, 4, 4);
3668 :
3669 0 : Color fillColor = selectionColor;
3670 0 : fillColor.setAlpha(10);
3671 0 : painter->fillPath(path, fillColor);
3672 0 : painter->drawPath(path);
3673 0 : }
3674 0 : }
3675 :
3676 0 : void NotationInteraction::drawGripPoints(muse::draw::Painter* painter, const engraving::rendering::PaintOptions& opt)
3677 : {
3678 0 : if (isDragStarted() && !isGripEditStarted()) {
3679 0 : return;
3680 : }
3681 :
3682 0 : mu::engraving::EngravingItem* editedElement = m_editData.element;
3683 0 : if (!editedElement) {
3684 0 : EngravingItem* selectedElement = m_selection->element();
3685 0 : if (selectedElement && selectedElement->isDynamic()) {
3686 0 : editedElement = selectedElement;
3687 : }
3688 : }
3689 :
3690 0 : if (editedElement && editedElement->isDynamic()) {
3691 0 : toDynamic(editedElement)->findAdjacentHairpins();
3692 : }
3693 :
3694 0 : int gripsCount = editedElement ? editedElement->gripsCount() : 0;
3695 :
3696 0 : if (gripsCount == 0) {
3697 0 : return;
3698 : }
3699 :
3700 0 : m_editData.grips = gripsCount;
3701 0 : m_editData.grip.resize(m_editData.grips);
3702 :
3703 0 : constexpr qreal DEFAULT_GRIP_SIZE = 8;
3704 0 : qreal scaling = currentScaling(painter);
3705 0 : qreal gripSize = DEFAULT_GRIP_SIZE / scaling;
3706 0 : RectF newRect(-gripSize / 2, -gripSize / 2, gripSize, gripSize);
3707 :
3708 0 : const EngravingItem* page = editedElement->findAncestor(ElementType::PAGE);
3709 0 : PointF pageOffset = page ? page->pos() : editedElement->pos();
3710 :
3711 0 : for (RectF& gripRect: m_editData.grip) {
3712 0 : gripRect = newRect.translated(pageOffset);
3713 : }
3714 :
3715 0 : editedElement->updateGrips(m_editData);
3716 0 : editModeRenderer()->drawItem(editedElement, painter, m_editData, scaling, opt);
3717 : }
3718 :
3719 0 : void NotationInteraction::drawLasso(muse::draw::Painter* painter, const engraving::rendering::PaintOptions& opt)
3720 : {
3721 0 : if (!m_lasso || m_lasso->isEmpty()) {
3722 0 : return;
3723 : }
3724 :
3725 0 : score()->renderer()->drawItem(m_lasso, painter, opt);
3726 : }
3727 :
3728 0 : void NotationInteraction::drawDrop(muse::draw::Painter* painter)
3729 : {
3730 0 : if (m_dropData.elementDropData.has_value()) {
3731 0 : const ElementDropData& edd = m_dropData.elementDropData.value();
3732 :
3733 0 : for (const RectF& rect : edd.dropRects) {
3734 0 : if (rect.isValid()) {
3735 0 : painter->fillRect(rect, configuration()->dropRectColor());
3736 : }
3737 : }
3738 : }
3739 :
3740 0 : if (m_dropData.rangeDropData.has_value()) {
3741 0 : const RangeDropData& rdd = m_dropData.rangeDropData.value();
3742 :
3743 0 : Color selectionColor = configuration()->selectionColor();
3744 0 : double penWidth = 3.0 / currentScaling(painter);
3745 0 : double minPenWidth = 0.20 * score()->style().spatium();
3746 0 : penWidth = std::max(penWidth, minPenWidth);
3747 :
3748 0 : Pen pen;
3749 0 : pen.setColor(selectionColor);
3750 0 : pen.setWidthF(penWidth);
3751 0 : pen.setStyle(PenStyle::SolidLine);
3752 0 : painter->setPen(pen);
3753 :
3754 0 : for (const RectF& rect: rdd.dropRects) {
3755 0 : PainterPath path;
3756 0 : path.addRoundedRect(rect, 4, 4);
3757 :
3758 0 : Color fillColor = selectionColor;
3759 0 : fillColor.setAlpha(10);
3760 0 : painter->fillPath(path, fillColor);
3761 0 : painter->drawPath(path);
3762 0 : }
3763 0 : }
3764 0 : }
3765 :
3766 0 : ChordRest* activeCr(mu::engraving::Score* score)
3767 : {
3768 0 : ChordRest* cr = score->selection().activeCR();
3769 0 : if (!cr) {
3770 0 : cr = score->selection().lastChordRest();
3771 0 : if (!cr && score->noteEntryMode()) {
3772 0 : cr = score->inputState().cr();
3773 : }
3774 : }
3775 0 : return cr;
3776 : }
3777 :
3778 0 : void NotationInteraction::expandSelection(ExpandSelectionMode mode)
3779 : {
3780 0 : ChordRest* cr = activeCr(score());
3781 0 : if (!cr) {
3782 0 : return;
3783 : }
3784 0 : ChordRest* el = 0;
3785 0 : switch (mode) {
3786 0 : case ExpandSelectionMode::BeginSystem: {
3787 0 : Measure* measure = cr->segment()->measure()->system()->firstMeasure();
3788 0 : if (measure) {
3789 0 : el = measure->first()->nextChordRest(cr->track());
3790 : }
3791 0 : break;
3792 : }
3793 0 : case ExpandSelectionMode::EndSystem: {
3794 0 : Measure* measure = cr->segment()->measure()->system()->lastMeasure();
3795 0 : if (measure) {
3796 0 : el = measure->last()->nextChordRest(cr->track(), true);
3797 : }
3798 0 : break;
3799 : }
3800 0 : case ExpandSelectionMode::BeginScore: {
3801 0 : Measure* measure = score()->firstMeasureMM();
3802 0 : if (measure) {
3803 0 : el = measure->first()->nextChordRest(cr->track());
3804 : }
3805 0 : break;
3806 : }
3807 0 : case ExpandSelectionMode::EndScore: {
3808 0 : Measure* measure = score()->lastMeasureMM();
3809 0 : if (measure) {
3810 0 : el = measure->last()->nextChordRest(cr->track(), true);
3811 : }
3812 0 : break;
3813 : }
3814 : }
3815 :
3816 0 : if (el) {
3817 0 : select({ el }, SelectType::RANGE, el->staffIdx());
3818 : }
3819 : }
3820 :
3821 0 : bool NotationInteraction::moveSelectionAvailable(MoveSelectionType type) const
3822 : {
3823 0 : if (type != MoveSelectionType::EngravingItem) {
3824 0 : return !isEditingElement();
3825 : }
3826 :
3827 0 : EngravingItem* el = score()->selection().element();
3828 : const std::set<ElementType> allowedTextEditTypes = {
3829 : ElementType::STAFF_TEXT,
3830 : ElementType::SYSTEM_TEXT,
3831 : ElementType::EXPRESSION,
3832 : ElementType::REHEARSAL_MARK
3833 0 : };
3834 0 : if (isTextEditingStarted() && el && allowedTextEditTypes.find(el->type()) != allowedTextEditTypes.end()) {
3835 0 : return true;
3836 : }
3837 :
3838 0 : if (isGripEditStarted()) {
3839 0 : return true;
3840 : }
3841 :
3842 0 : return m_editData.element && m_editData.element->isTextBase() ? !isTextEditingStarted() : !isEditingElement();
3843 0 : }
3844 :
3845 0 : void NotationInteraction::moveSelectionDeprecated(MoveDirection d, MoveSelectionType type)
3846 : {
3847 0 : m_selection->moveSelection(d, type);
3848 0 : }
3849 :
3850 0 : static ChordRest* asChordRest(EngravingItem* e)
3851 : {
3852 0 : if (e && e->isNote()) {
3853 0 : return toNote(e)->chord();
3854 0 : } else if (e && e->isChordRest()) {
3855 0 : return toChordRest(e);
3856 : }
3857 0 : return nullptr;
3858 : }
3859 :
3860 0 : void NotationInteraction::moveChordRestToStaff(MoveDirection dir)
3861 : {
3862 0 : startEdit(TranslatableString("undoableAction", "Move chord/rest to staff"));
3863 :
3864 0 : engraving::Transaction& tx = score()->transactionManager()->currentOrDummyTransaction();
3865 0 : for (EngravingItem* e: score()->selection().uniqueElements()) {
3866 0 : ChordRest* cr = asChordRest(e);
3867 0 : if (cr != nullptr) {
3868 0 : if (dir == MoveDirection::Up) {
3869 0 : EditCrossStaff::moveUp(tx, score(), cr);
3870 0 : } else if (dir == MoveDirection::Down) {
3871 0 : EditCrossStaff::moveDown(tx, score(), cr);
3872 : }
3873 : }
3874 0 : }
3875 :
3876 0 : apply();
3877 0 : }
3878 :
3879 0 : void NotationInteraction::swapChordRest(MoveDirection direction)
3880 : {
3881 0 : ChordRest* cr = asChordRest(score()->getSelectedElement());
3882 0 : if (!cr) {
3883 0 : return;
3884 : }
3885 0 : QList<ChordRest*> crl;
3886 0 : if (cr->links()) {
3887 0 : for (auto* l : *cr->links()) {
3888 0 : crl.append(toChordRest(l));
3889 : }
3890 : } else {
3891 0 : crl.append(cr);
3892 : }
3893 0 : startEdit(TranslatableString("undoableAction", "Move chord/rest"));
3894 0 : for (ChordRest* cr1 : crl) {
3895 0 : if (cr1->isRest()) {
3896 0 : Measure* m = toRest(cr1)->measure();
3897 0 : if (m && m->isMMRest()) {
3898 0 : break;
3899 : }
3900 : }
3901 : ChordRest* cr2;
3902 : // ensures cr1 is the left chord, useful in SwapCR::flip()
3903 0 : if (direction == MoveDirection::Left) {
3904 0 : cr2 = cr1;
3905 0 : cr1 = engraving::Navigation::prevChordRest(cr2);
3906 : } else {
3907 0 : cr2 = engraving::Navigation::nextChordRest(cr1);
3908 : }
3909 0 : if (cr1 && cr2 && cr1->measure() == cr2->measure() && !cr1->tuplet() && !cr2->tuplet()
3910 0 : && cr1->durationType() == cr2->durationType() && cr1->ticks() == cr2->ticks()
3911 : // if two chords belong to different two-note tremolos, abort
3912 0 : && !(cr1->isChord() && toChord(cr1)->tremoloTwoChord()
3913 0 : && cr2->isChord() && toChord(cr2)->tremoloTwoChord()
3914 0 : && toChord(cr1)->tremoloTwoChord() != toChord(cr2)->tremoloTwoChord())) {
3915 0 : score()->undo(new mu::engraving::SwapCR(cr1, cr2));
3916 : }
3917 : }
3918 0 : apply();
3919 0 : }
3920 :
3921 0 : void NotationInteraction::toggleSnapToPrevious()
3922 : {
3923 0 : bool newSnapValue = false;
3924 :
3925 : // Collect items to toggle...
3926 0 : std::unordered_set<Hairpin*> hairpins;
3927 0 : for (EngravingItem* e : score()->selection().elements()) {
3928 0 : if (e->isHairpinSegment()) {
3929 0 : Hairpin* h = toHairpinSegment(e)->hairpin();
3930 : // If any item in the selection has a false snapping value, then we should
3931 : // toggle all to true (handles mixed/indeterminate state)
3932 0 : if (!h->snapToItemBefore() && !newSnapValue) {
3933 0 : newSnapValue = true;
3934 : }
3935 0 : hairpins.emplace(h);
3936 : }
3937 : }
3938 0 : if (hairpins.empty()) {
3939 0 : return;
3940 : }
3941 :
3942 : // Do toggle...
3943 0 : startEdit(TranslatableString("undoableAction", "Toggle snap to previous"));
3944 0 : for (Hairpin* h : hairpins) {
3945 0 : if (h->snapToItemBefore() == newSnapValue) {
3946 0 : continue;
3947 : }
3948 0 : h->undoChangeProperty(Pid::SNAP_BEFORE, PropertyValue(newSnapValue));
3949 : }
3950 0 : apply();
3951 0 : }
3952 :
3953 0 : void NotationInteraction::toggleSnapToNext()
3954 : {
3955 0 : bool newSnapValue = false;
3956 :
3957 : // Collect items to toggle...
3958 0 : std::unordered_set<Hairpin*> hairpins;
3959 0 : std::unordered_set<GradualTempoChange*> gradualTempoChanges;
3960 0 : for (EngravingItem* e : score()->selection().elements()) {
3961 0 : if (e->isHairpinSegment()) {
3962 0 : Hairpin* h = toHairpinSegment(e)->hairpin();
3963 : // If any item in the selection has a false snapping value, then we should
3964 : // toggle all to true (handles mixed/indeterminate state)
3965 0 : if (!h->snapToItemAfter() && !newSnapValue) {
3966 0 : newSnapValue = true;
3967 : }
3968 0 : hairpins.emplace(h);
3969 : }
3970 0 : if (e->isGradualTempoChangeSegment()) {
3971 0 : GradualTempoChange* gtc = toGradualTempoChangeSegment(e)->tempoChange();
3972 : // Same here (see above)
3973 0 : if (!gtc->snapToItemAfter() && !newSnapValue) {
3974 0 : newSnapValue = true;
3975 : }
3976 0 : gradualTempoChanges.emplace(gtc);
3977 : }
3978 : }
3979 0 : if (hairpins.empty() && gradualTempoChanges.empty()) {
3980 0 : return;
3981 : }
3982 :
3983 : // Do toggle...
3984 0 : startEdit(TranslatableString("undoableAction", "Toggle snap to next"));
3985 0 : for (Hairpin* h : hairpins) {
3986 0 : if (h->snapToItemAfter() == newSnapValue) {
3987 0 : continue;
3988 : }
3989 0 : h->undoChangeProperty(Pid::SNAP_AFTER, PropertyValue(newSnapValue));
3990 : }
3991 0 : for (GradualTempoChange* gtc : gradualTempoChanges) {
3992 0 : if (gtc->snapToItemAfter() == newSnapValue) {
3993 0 : continue;
3994 : }
3995 0 : gtc->undoChangeProperty(Pid::SNAP_AFTER, PropertyValue(newSnapValue));
3996 : }
3997 0 : apply();
3998 0 : }
3999 :
4000 0 : inline mu::engraving::DirectionV toDirection(MoveDirection d)
4001 : {
4002 0 : return d == MoveDirection::Up ? mu::engraving::DirectionV::UP : mu::engraving::DirectionV::DOWN;
4003 : }
4004 :
4005 0 : void NotationInteraction::movePitch(MoveDirection d, PitchMode mode)
4006 : {
4007 0 : IF_ASSERT_FAILED(MoveDirection::Up == d || MoveDirection::Down == d) {
4008 0 : return;
4009 : }
4010 0 : EngravingItem* selected = score()->selection().element();
4011 0 : if (selected && selected->isRest()) {
4012 0 : if (noteInput()->state().staffGroup() == mu::engraving::StaffGroup::TAB) {
4013 : // The rest won't be visible - don't try to move it...
4014 0 : return;
4015 : }
4016 0 : startEdit(TranslatableString("undoableAction", "Change vertical position"));
4017 0 : score()->cmdMoveRest(toRest(selected), toDirection(d));
4018 : } else {
4019 0 : startEdit(TranslatableString("undoableAction", "Change pitch"));
4020 0 : EditNote::upDown(score(), MoveDirection::Up == d, mode);
4021 : }
4022 :
4023 0 : apply();
4024 : }
4025 :
4026 0 : void NotationInteraction::moveLyrics(MoveDirection d)
4027 : {
4028 0 : EngravingItem* el = score()->selection().element();
4029 0 : IF_ASSERT_FAILED(el && el->isLyrics()) {
4030 0 : return;
4031 : }
4032 0 : startEdit(TranslatableString("undoableAction", "Move lyrics"));
4033 0 : score()->cmdMoveLyrics(toLyrics(el), toDirection(d));
4034 0 : apply();
4035 : }
4036 :
4037 0 : void NotationInteraction::nudge(MoveDirection d, bool quickly)
4038 : {
4039 0 : EngravingItem* el = score()->selection().element();
4040 0 : IF_ASSERT_FAILED(el && (el->isTextBase() || el->isArticulationFamily())) {
4041 0 : return;
4042 : }
4043 :
4044 0 : startEdit(TranslatableString("undoableAction", "Nudge element"));
4045 :
4046 0 : qreal step = quickly ? mu::engraving::MScore::nudgeStep10 : mu::engraving::MScore::nudgeStep;
4047 0 : step = step * el->spatium();
4048 :
4049 0 : switch (d) {
4050 0 : case MoveDirection::Undefined: {
4051 0 : IF_ASSERT_FAILED(d != MoveDirection::Undefined) {
4052 0 : return;
4053 : }
4054 0 : } break;
4055 0 : case MoveDirection::Left:
4056 0 : el->undoChangeProperty(mu::engraving::Pid::OFFSET, el->offset() - PointF(step, 0.0), mu::engraving::PropertyFlags::UNSTYLED);
4057 0 : break;
4058 0 : case MoveDirection::Right:
4059 0 : el->undoChangeProperty(mu::engraving::Pid::OFFSET, el->offset() + PointF(step, 0.0), mu::engraving::PropertyFlags::UNSTYLED);
4060 0 : break;
4061 0 : case MoveDirection::Up:
4062 0 : el->undoChangeProperty(mu::engraving::Pid::OFFSET, el->offset() - PointF(0.0, step), mu::engraving::PropertyFlags::UNSTYLED);
4063 0 : break;
4064 0 : case MoveDirection::Down:
4065 0 : el->undoChangeProperty(mu::engraving::Pid::OFFSET, el->offset() + PointF(0.0, step), mu::engraving::PropertyFlags::UNSTYLED);
4066 0 : break;
4067 : }
4068 :
4069 0 : apply();
4070 :
4071 0 : notifyAboutDragChanged();
4072 : }
4073 :
4074 0 : void NotationInteraction::nudgeAnchors(MoveDirection d)
4075 : {
4076 0 : IF_ASSERT_FAILED(m_editData.element) {
4077 0 : return;
4078 : }
4079 :
4080 0 : startEdit(TranslatableString("undoableAction", "Nudge"));
4081 0 : double vRaster = getVRaster();
4082 0 : double hRaster = getHRaster();
4083 :
4084 0 : switch (d) {
4085 0 : case MoveDirection::Left:
4086 0 : m_editData.delta = PointF(-nudgeDistance(m_editData, hRaster), 0);
4087 0 : break;
4088 0 : case MoveDirection::Right:
4089 0 : m_editData.delta = PointF(nudgeDistance(m_editData, hRaster), 0);
4090 0 : break;
4091 0 : case MoveDirection::Up:
4092 0 : m_editData.delta = PointF(0, -nudgeDistance(m_editData, vRaster));
4093 0 : break;
4094 0 : case MoveDirection::Down:
4095 0 : m_editData.delta = PointF(0, nudgeDistance(m_editData, vRaster));
4096 0 : break;
4097 0 : default:
4098 0 : rollback();
4099 0 : return;
4100 : }
4101 :
4102 0 : m_editData.evtDelta = m_editData.moveDelta = m_editData.delta;
4103 0 : m_editData.hRaster = hRaster;
4104 0 : m_editData.vRaster = vRaster;
4105 :
4106 0 : if (m_editData.curGrip != mu::engraving::Grip::NO_GRIP && int(m_editData.curGrip) < m_editData.grips) {
4107 0 : m_editData.pos = m_editData.grip[int(m_editData.curGrip)].center() + m_editData.delta;
4108 :
4109 0 : m_editData.element->startDragGrip(m_editData);
4110 0 : m_editData.element->dragGrip(m_editData);
4111 0 : m_editData.element->endDragGrip(m_editData);
4112 : } else {
4113 0 : m_editData.element->startDrag(m_editData);
4114 0 : m_editData.element->drag(m_editData);
4115 0 : m_editData.element->endDrag(m_editData);
4116 : }
4117 :
4118 0 : apply();
4119 : }
4120 :
4121 0 : bool NotationInteraction::isTextSelected() const
4122 : {
4123 0 : EngravingItem* selectedElement = m_selection->element();
4124 0 : if (!selectedElement) {
4125 0 : return false;
4126 : }
4127 :
4128 0 : if (!selectedElement->isTextBase()) {
4129 0 : return false;
4130 : }
4131 :
4132 0 : return true;
4133 : }
4134 :
4135 0 : bool NotationInteraction::isTextEditingStarted() const
4136 : {
4137 0 : EngravingItem* element = m_editData.element;
4138 0 : return element && element->isTextBase() && toTextBase(element)->cursor() && toTextBase(element)->cursor()->editing();
4139 : }
4140 :
4141 0 : bool NotationInteraction::textEditingAllowed(const EngravingItem* element) const
4142 : {
4143 0 : return element && element->isEditable() && (element->isTextBase() || element->isTBox());
4144 : }
4145 :
4146 0 : void NotationInteraction::startEditText(EngravingItem* element, const PointF& cursorPos)
4147 : {
4148 0 : if (!element) {
4149 0 : return;
4150 : }
4151 :
4152 0 : if (element->isTBox()) {
4153 0 : element = toTBox(element)->text();
4154 : }
4155 :
4156 0 : if (element != m_selection->element()) {
4157 0 : select({ element }, SelectType::SINGLE);
4158 : }
4159 :
4160 0 : m_editData.clear();
4161 0 : m_editData.element = element;
4162 0 : m_editData.startMove = bindCursorPosToText(cursorPos, m_editData.element);
4163 :
4164 0 : m_editData.element->startEdit(m_editData);
4165 0 : startTextCursorBlinkTimer();
4166 :
4167 0 : m_isEditingElementChanged.notify();
4168 0 : notifyAboutTextEditingStarted();
4169 0 : notifyAboutTextEditingChanged();
4170 : }
4171 :
4172 : //! NOTE: Copied from TextBase::inputTransition
4173 0 : void NotationInteraction::editText(QInputMethodEvent* event)
4174 : {
4175 0 : if (!isTextEditingStarted()) {
4176 0 : return;
4177 : }
4178 :
4179 0 : mu::engraving::TextBase* text = mu::engraving::toTextBase(m_editData.element);
4180 0 : mu::engraving::TextCursor* cursor = text->cursor();
4181 0 : String& preeditString = m_editData.preeditString;
4182 :
4183 : // remove preedit string
4184 0 : size_t n = preeditString.size();
4185 0 : while (n--) {
4186 0 : if (cursor->movePosition(mu::engraving::TextCursor::MoveOperation::Left)) {
4187 0 : mu::engraving::TextBlock& curLine = cursor->curLine();
4188 0 : curLine.remove(static_cast<int>(cursor->column()), cursor);
4189 0 : text->triggerLayout();
4190 0 : text->setTextInvalid();
4191 : }
4192 : }
4193 :
4194 0 : if (!event->commitString().isEmpty()) {
4195 0 : score()->startCmd(TranslatableString("undoableAction", "Edit text"));
4196 0 : text->insertText(event->commitString());
4197 0 : score()->endCmd();
4198 0 : preeditString.clear();
4199 : } else {
4200 0 : preeditString = event->preeditString();
4201 :
4202 0 : if (!preeditString.isEmpty()) {
4203 0 : cursor->updateCursorFormat();
4204 0 : text->editInsertText(cursor, preeditString);
4205 0 : text->setTextInvalid();
4206 0 : score()->renderer()->layoutText1(text);
4207 0 : score()->update();
4208 : }
4209 : }
4210 :
4211 0 : event->accept();
4212 0 : notifyAboutTextEditingChanged();
4213 : }
4214 :
4215 0 : bool NotationInteraction::needStartEditGrip(QKeyEvent* event) const
4216 : {
4217 0 : if (!m_editData.element || !m_editData.element->hasGrips()) {
4218 0 : return false;
4219 : }
4220 :
4221 0 : if (isGripEditStarted()) {
4222 0 : return false;
4223 : }
4224 :
4225 : static const std::set<Qt::Key> arrows {
4226 : Qt::Key_Left,
4227 : Qt::Key_Right,
4228 : Qt::Key_Up,
4229 : Qt::Key_Down
4230 0 : };
4231 :
4232 0 : return muse::contains(arrows, static_cast<Qt::Key>(event->key()));
4233 : }
4234 :
4235 0 : bool NotationInteraction::handleKeyPress(QKeyEvent* event)
4236 : {
4237 0 : mu::engraving::EngravingItem* editElem = m_editData.element;
4238 0 : IF_ASSERT_FAILED(editElem) {
4239 0 : return false;
4240 : }
4241 :
4242 0 : if (editElem->isTextBase() && doTextEdit(event, toTextBase(editElem))) {
4243 0 : return true;
4244 : }
4245 :
4246 : //: Means: an editing operation triggered by a keystroke
4247 0 : startEdit(TranslatableString("undoableAction", "Keystroke edit"));
4248 :
4249 0 : if (editElem->edit(m_editData)) {
4250 0 : apply();
4251 0 : return true;
4252 : } else {
4253 0 : rollback();
4254 : }
4255 :
4256 0 : if (event->modifiers() & Qt::KeyboardModifier::AltModifier) {
4257 0 : return false;
4258 : }
4259 :
4260 0 : const double vRaster = editElem->isBeam() ? 4 : mu::engraving::MScore::vRaster();
4261 0 : const double hRaster = editElem->isBeam() ? 4 : mu::engraving::MScore::hRaster();
4262 :
4263 0 : switch (event->key()) {
4264 0 : case Qt::Key_Tab:
4265 0 : if (!editElem->hasGrips()) {
4266 0 : return false;
4267 : }
4268 0 : editElem->nextGrip(m_editData);
4269 0 : notifyAboutNotationChanged();
4270 0 : return true;
4271 0 : case Qt::Key_Backtab:
4272 0 : if (!editElem->hasGrips()) {
4273 0 : return false;
4274 : }
4275 0 : editElem->prevGrip(m_editData);
4276 0 : notifyAboutNotationChanged();
4277 0 : return true;
4278 0 : case Qt::Key_Left:
4279 0 : m_editData.delta = PointF(-nudgeDistance(m_editData, hRaster), 0);
4280 0 : break;
4281 0 : case Qt::Key_Right:
4282 0 : m_editData.delta = PointF(nudgeDistance(m_editData, hRaster), 0);
4283 0 : break;
4284 0 : case Qt::Key_Up:
4285 0 : m_editData.delta = PointF(0, -nudgeDistance(m_editData, vRaster));
4286 0 : break;
4287 0 : case Qt::Key_Down:
4288 0 : m_editData.delta = PointF(0, nudgeDistance(m_editData, vRaster));
4289 0 : break;
4290 0 : default:
4291 0 : return false;
4292 : }
4293 :
4294 0 : m_editData.evtDelta = m_editData.moveDelta = m_editData.delta;
4295 0 : m_editData.hRaster = hRaster;
4296 0 : m_editData.vRaster = vRaster;
4297 :
4298 0 : m_editData.isEditMode = isEditingElement();
4299 :
4300 : //: Means: an editing operation triggered by a keystroke
4301 0 : startEdit(TranslatableString("undoableAction", "Keystroke edit"));
4302 :
4303 0 : if (m_editData.curGrip != mu::engraving::Grip::NO_GRIP && int(m_editData.curGrip) < m_editData.grips) {
4304 0 : m_editData.pos = m_editData.grip[int(m_editData.curGrip)].center() + m_editData.delta;
4305 :
4306 0 : editElem->startDragGrip(m_editData);
4307 0 : editElem->dragGrip(m_editData);
4308 0 : editElem->endDragGrip(m_editData);
4309 : } else {
4310 0 : editElem->startDrag(m_editData);
4311 0 : editElem->drag(m_editData);
4312 0 : editElem->endDrag(m_editData);
4313 : }
4314 :
4315 0 : apply();
4316 0 : return true;
4317 : }
4318 :
4319 0 : bool NotationInteraction::doTextEdit(QKeyEvent* event, TextBase* tb)
4320 : {
4321 0 : IF_ASSERT_FAILED(event && tb) {
4322 0 : return false;
4323 : }
4324 :
4325 : //: Means: an editing operation triggered by a keystroke
4326 0 : startEdit(TranslatableString("undoableAction", "Keystroke edit"));
4327 :
4328 0 : if (!tb->edit(m_editData)) {
4329 0 : rollback();
4330 0 : return false;
4331 : }
4332 :
4333 0 : apply();
4334 :
4335 : // Replace newly added straight quotes with curly ones in a separate undo action...
4336 0 : const bool isSingleQuoteInput = event->text() == u"\'";
4337 0 : const bool isDoubleQuoteInput = event->text() == u"\"";
4338 0 : if (!isSingleQuoteInput && !isDoubleQuoteInput) {
4339 0 : return true;
4340 : }
4341 :
4342 0 : TextCursor* cursor = tb->cursor();
4343 :
4344 0 : bool useCloseQuote = false; // Use close if there's a non-space before the newly inputted quote
4345 :
4346 0 : const int row = static_cast<int>(cursor->row());
4347 0 : const int col = static_cast<int>(cursor->column());
4348 0 : if (col > 1) {
4349 0 : const String prev = cursor->extractText(row, col - 2, row, col - 1);
4350 0 : useCloseQuote = !prev.isEmpty() && !prev.front().isSpace();
4351 0 : }
4352 :
4353 : //: Means: an editing operation triggered by a keystroke
4354 0 : startEdit(TranslatableString("undoableAction", "Keystroke edit"));
4355 :
4356 0 : cursor->movePosition(TextCursor::MoveOperation::Left);
4357 0 : score()->undo(new RemoveText(cursor, event->text()));
4358 :
4359 : //: Single open quotation mark
4360 0 : const String singleOpenQuote = muse::mtrc("notation", u"‘");
4361 : //: Single close quotation mark
4362 0 : const String singleCloseQuote = muse::mtrc("notation", u"’");
4363 : //: Double open quotation mark
4364 0 : const String doubleOpenQuote = muse::mtrc("notation", u"“");
4365 : //: Double close quotation mark
4366 0 : const String doubleCloseQuote = muse::mtrc("notation", u"”");
4367 :
4368 : const String replacement = isSingleQuoteInput
4369 : ? (useCloseQuote ? singleCloseQuote : singleOpenQuote)
4370 0 : : (useCloseQuote ? doubleCloseQuote : doubleOpenQuote);
4371 :
4372 0 : tb->insertText(replacement);
4373 0 : apply();
4374 :
4375 0 : return true;
4376 0 : }
4377 :
4378 0 : void NotationInteraction::endEditText()
4379 : {
4380 0 : if (!isTextEditingStarted()) {
4381 0 : return;
4382 : }
4383 :
4384 0 : stopTextCursorBlinkTimer();
4385 :
4386 0 : TextBase* editedElement = toTextBase(m_editData.element);
4387 0 : doEndEditElement();
4388 :
4389 0 : m_isEditingElementChanged.notify();
4390 0 : notifyAboutTextEditingEnded(editedElement);
4391 :
4392 0 : notifyAboutTextEditingChanged();
4393 0 : m_selection->notifyAboutSelectionChangedIfNeed();
4394 : }
4395 :
4396 0 : void NotationInteraction::blinkTextCursor()
4397 : {
4398 0 : if (!isTextEditingStarted()) {
4399 0 : stopTextCursorBlinkTimer();
4400 0 : return;
4401 : }
4402 :
4403 0 : const TextBase* editedElement = editedText();
4404 0 : IF_ASSERT_FAILED(editedElement) {
4405 0 : return;
4406 : }
4407 0 : TextCursor* cursor = editedElement->cursor();
4408 0 : IF_ASSERT_FAILED(cursor) {
4409 0 : return;
4410 : }
4411 0 : cursor->toggleVisible();
4412 :
4413 0 : muse::RectF cursorCanvasRect = cursor->cursorCanvasRect();
4414 0 : if (cursorCanvasRect.isValid()) {
4415 0 : notifyAboutNotationChanged(cursorCanvasRect);
4416 : }
4417 : }
4418 :
4419 0 : void NotationInteraction::startTextCursorBlinkTimer()
4420 : {
4421 0 : const int cursorFlashTime = QApplication::cursorFlashTime() / 2;
4422 0 : if (cursorFlashTime > 0) {
4423 0 : m_textCursorBlinkTimer.start(cursorFlashTime);
4424 : } else {
4425 : // text cursor flashing is disabled
4426 0 : stopTextCursorBlinkTimer();
4427 : }
4428 0 : }
4429 :
4430 0 : void NotationInteraction::stopTextCursorBlinkTimer()
4431 : {
4432 0 : if (m_textCursorBlinkTimer.isActive()) {
4433 0 : m_textCursorBlinkTimer.stop();
4434 : }
4435 0 : }
4436 :
4437 0 : void NotationInteraction::onTextEditingChanged()
4438 : {
4439 0 : updateTextCursorVisibility();
4440 0 : }
4441 :
4442 0 : void NotationInteraction::updateTextCursorVisibility()
4443 : {
4444 0 : if (!isTextEditingStarted()) {
4445 0 : return;
4446 : }
4447 :
4448 0 : const TextBase* editedElement = editedText();
4449 0 : IF_ASSERT_FAILED(editedElement) {
4450 0 : return;
4451 : }
4452 0 : TextCursor* cursor = editedElement->cursor();
4453 0 : IF_ASSERT_FAILED(cursor) {
4454 0 : return;
4455 : }
4456 :
4457 : // Whenever the cursor moves, we update its visibility.
4458 : // If it has to be visible, we display it immediately
4459 : // regardless of the blinking, and restart the blinking.
4460 0 : bool showCursor = !cursor->hasSelection();
4461 0 : cursor->setVisible(showCursor);
4462 0 : if (showCursor) {
4463 0 : startTextCursorBlinkTimer();
4464 : } else {
4465 0 : stopTextCursorBlinkTimer();
4466 : }
4467 : }
4468 :
4469 0 : void NotationInteraction::changeTextCursorPosition(const PointF& newCursorPos)
4470 : {
4471 0 : IF_ASSERT_FAILED(isTextEditingStarted() && m_editData.element) {
4472 0 : return;
4473 : }
4474 :
4475 0 : m_editData.startMove = bindCursorPosToText(newCursorPos, m_editData.element);
4476 :
4477 0 : mu::engraving::TextBase* textEl = mu::engraving::toTextBase(m_editData.element);
4478 :
4479 0 : textEl->mousePress(m_editData);
4480 0 : if (m_editData.buttons == mu::engraving::MiddleButton) {
4481 0 : QString txt = QGuiApplication::clipboard()->text();
4482 0 : textEl->paste(txt);
4483 0 : }
4484 :
4485 0 : notifyAboutTextEditingChanged();
4486 : }
4487 :
4488 0 : void NotationInteraction::selectText(mu::engraving::SelectTextType type)
4489 : {
4490 0 : if (!isTextEditingStarted()) {
4491 0 : return;
4492 : }
4493 :
4494 0 : mu::engraving::TextBase* text = mu::engraving::toTextBase(m_editData.element);
4495 0 : text->select(type);
4496 0 : text->endHexState();
4497 0 : text->setPrimed(false);
4498 :
4499 0 : notifyAboutTextEditingChanged();
4500 : }
4501 :
4502 0 : void NotationInteraction::selectAllText()
4503 : {
4504 0 : if (!isTextEditingStarted()) {
4505 0 : return;
4506 : }
4507 :
4508 0 : mu::engraving::TextBase* text = mu::engraving::toTextBase(m_editData.element);
4509 0 : text->selectAll();
4510 : }
4511 :
4512 0 : const TextBase* NotationInteraction::editedText() const
4513 : {
4514 0 : return mu::engraving::toTextBase(m_editData.element);
4515 : }
4516 :
4517 0 : void NotationInteraction::undo()
4518 : {
4519 0 : m_undoStack->undo(&m_editData);
4520 0 : }
4521 :
4522 0 : void NotationInteraction::redo()
4523 : {
4524 0 : m_undoStack->redo(&m_editData);
4525 0 : }
4526 :
4527 0 : void NotationInteraction::undoRedoToIndex(size_t idx)
4528 : {
4529 0 : endEditElement();
4530 0 : m_undoStack->undoRedoToIndex(idx, &m_editData);
4531 0 : }
4532 :
4533 0 : muse::async::Notification NotationInteraction::textEditingStarted() const
4534 : {
4535 0 : return m_textEditingStarted;
4536 : }
4537 :
4538 0 : muse::async::Notification NotationInteraction::textEditingChanged() const
4539 : {
4540 0 : return m_textEditingChanged;
4541 : }
4542 :
4543 0 : muse::async::Channel<TextBase*> NotationInteraction::textEditingEnded() const
4544 : {
4545 0 : return m_textEditingEnded;
4546 : }
4547 :
4548 0 : bool NotationInteraction::isGripEditStarted() const
4549 : {
4550 0 : return m_editData.element && m_editData.curGrip != mu::engraving::Grip::NO_GRIP;
4551 : }
4552 :
4553 0 : bool NotationInteraction::isStartEndGripSelected() const
4554 : {
4555 0 : return m_editData.element && (m_editData.curGrip == mu::engraving::Grip::START || m_editData.curGrip == mu::engraving::Grip::END);
4556 : }
4557 :
4558 0 : static int findGrip(const std::vector<muse::RectF>& grips, const muse::PointF& canvasPos)
4559 : {
4560 0 : if (grips.empty()) {
4561 0 : return -1;
4562 : }
4563 0 : qreal align = grips[0].width() / 2;
4564 0 : for (size_t i = 0; i < grips.size(); ++i) {
4565 0 : if (grips[i].adjusted(-align, -align, align, align).contains(canvasPos)) {
4566 0 : return static_cast<int>(i);
4567 : }
4568 : }
4569 0 : return -1;
4570 : }
4571 :
4572 0 : bool NotationInteraction::isHitGrip(const PointF& pos) const
4573 : {
4574 0 : return selection()->element() && findGrip(m_editData.grip, pos) != -1;
4575 : }
4576 :
4577 0 : void NotationInteraction::startEditGrip(const PointF& pos)
4578 : {
4579 0 : int grip = findGrip(m_editData.grip, pos);
4580 0 : if (grip == -1) {
4581 0 : return;
4582 : }
4583 0 : startEditGrip(selection()->element(), mu::engraving::Grip(grip));
4584 : }
4585 :
4586 0 : void NotationInteraction::startEditGrip(EngravingItem* element, mu::engraving::Grip grip)
4587 : {
4588 0 : if (m_editData.element == element && m_editData.curGrip == grip) {
4589 0 : return;
4590 : }
4591 :
4592 0 : m_editData.element = element;
4593 0 : m_editData.curGrip = grip;
4594 :
4595 0 : updateGripAnchorLines();
4596 0 : m_editData.element->startEdit(m_editData);
4597 :
4598 0 : notifyAboutNotationChanged();
4599 : }
4600 :
4601 0 : void NotationInteraction::endEditGrip()
4602 : {
4603 0 : if (m_editData.curGrip == Grip::NO_GRIP) {
4604 0 : return;
4605 : }
4606 :
4607 0 : m_editData.curGrip = Grip::NO_GRIP;
4608 0 : notifyAboutNotationChanged();
4609 : }
4610 :
4611 0 : void NotationInteraction::updateGripAnchorLines()
4612 : {
4613 0 : if (!m_editData.element) {
4614 0 : return;
4615 : }
4616 :
4617 0 : std::vector<LineF> lines;
4618 0 : mu::engraving::Grip anchorLinesGrip = m_editData.curGrip == mu::engraving::Grip::NO_GRIP
4619 0 : ? m_editData.element->defaultGrip() : m_editData.curGrip;
4620 0 : std::vector<LineF> anchorLines = m_editData.element->gripAnchorLines(anchorLinesGrip);
4621 :
4622 0 : if (!anchorLines.empty()) {
4623 0 : for (LineF& line : anchorLines) {
4624 0 : if (line.p1() != line.p2()) {
4625 0 : lines.push_back(line);
4626 : }
4627 : }
4628 : }
4629 :
4630 0 : setAnchorLines(lines);
4631 0 : }
4632 :
4633 0 : void NotationInteraction::updateDragAnchorLines()
4634 : {
4635 0 : std::vector<LineF> anchorLines;
4636 0 : for (const EngravingItem* e : selection()->elements()) {
4637 0 : std::vector<LineF> elAnchorLines = e->dragAnchorLines();
4638 0 : if (!elAnchorLines.empty()) {
4639 0 : for (LineF& l : elAnchorLines) {
4640 0 : anchorLines.push_back(l);
4641 : }
4642 : }
4643 0 : }
4644 :
4645 0 : setAnchorLines(anchorLines);
4646 0 : }
4647 :
4648 0 : bool NotationInteraction::isEditingElement() const
4649 : {
4650 0 : return m_editData.element != nullptr;
4651 : }
4652 :
4653 0 : void NotationInteraction::startEditElement(EngravingItem* element)
4654 : {
4655 0 : if (!element || !element->isEditable()) {
4656 0 : return;
4657 : }
4658 :
4659 0 : if (isEditingElement()) {
4660 0 : return;
4661 : }
4662 :
4663 0 : if (element->isTextBase()) {
4664 0 : startEditText(element);
4665 0 : } else if (element->hasGrips() && !element->isImage()) {
4666 0 : startEditGrip(element, element->defaultGrip());
4667 : } else {
4668 0 : element->startEdit(m_editData);
4669 0 : m_editData.element = element;
4670 : }
4671 :
4672 0 : m_isEditingElementChanged.notify();
4673 : }
4674 :
4675 0 : void NotationInteraction::changeEditElement(EngravingItem* newElement)
4676 : {
4677 0 : IF_ASSERT_FAILED(newElement) {
4678 0 : return;
4679 : }
4680 :
4681 0 : if (m_editData.element == newElement) {
4682 0 : return;
4683 : }
4684 :
4685 0 : mu::engraving::Grip currentGrip = m_editData.curGrip;
4686 0 : bool gripEditStarted = isGripEditStarted();
4687 :
4688 0 : doEndEditElement();
4689 :
4690 0 : if (gripEditStarted) {
4691 0 : startEditGrip(newElement, currentGrip);
4692 : } else {
4693 0 : startEditElement(newElement);
4694 : }
4695 : }
4696 :
4697 0 : bool NotationInteraction::isEditAllowed(QKeyEvent* event)
4698 : {
4699 0 : if (!m_editData.element) {
4700 0 : return false;
4701 : }
4702 :
4703 0 : mu::engraving::EditData editData = m_editData;
4704 0 : editData.modifiers = keyboardModifier(event->modifiers());
4705 0 : editData.key = event->key();
4706 0 : editData.s = event->text();
4707 :
4708 0 : if (editData.element->isEditAllowed(editData)) {
4709 0 : return true;
4710 : }
4711 :
4712 0 : if (event->modifiers() & Qt::KeyboardModifier::AltModifier) {
4713 0 : return false;
4714 : }
4715 :
4716 0 : if (editData.element->isTextBase()) {
4717 0 : return false;
4718 : }
4719 :
4720 : static QSet<int> navigationKeys = {
4721 : Qt::Key_Left,
4722 : Qt::Key_Right,
4723 : Qt::Key_Up,
4724 : Qt::Key_Down
4725 0 : };
4726 :
4727 0 : if (editData.element->hasGrips()) {
4728 0 : navigationKeys += { Qt::Key_Tab, Qt::Key_Backtab };
4729 : }
4730 :
4731 0 : return navigationKeys.contains(event->key());
4732 0 : }
4733 :
4734 0 : void NotationInteraction::editElement(QKeyEvent* event)
4735 : {
4736 : #if 0
4737 : static int index = 0;
4738 : LOGDA() << "[" << ++index << "] " << QKeySequence(event->keyCombination()).toString();
4739 : #endif
4740 :
4741 0 : if (!m_editData.element) {
4742 0 : return;
4743 : }
4744 :
4745 0 : m_editData.modifiers = keyboardModifier(event->modifiers());
4746 :
4747 0 : if (isDragStarted()) {
4748 0 : return; // ignore all key strokes while dragging
4749 : }
4750 :
4751 0 : m_editData.key = event->key();
4752 0 : m_editData.s = event->text();
4753 :
4754 : // Brackets get deleted during layout with their system, so the best we can do to maintain
4755 : // the bracket selected is to store their indices and use them to find the new ones
4756 0 : bool isBracket = m_editData.element->isBracket();
4757 0 : size_t bracketIndex = muse::nidx;
4758 0 : size_t systemIndex = muse::nidx;
4759 :
4760 0 : if (isBracket) {
4761 0 : const Bracket* bracket = toBracket(m_editData.element);
4762 0 : System* system = bracket->system();
4763 :
4764 0 : if (system) {
4765 0 : bracketIndex = muse::indexOf(system->brackets(), bracket);
4766 0 : systemIndex = muse::indexOf(score()->systems(), system);
4767 : }
4768 0 : } else if (m_editData.element->isHarmony()) {
4769 0 : if (isTextEditingStarted() && (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)) {
4770 0 : endEditText();
4771 0 : return;
4772 : }
4773 : }
4774 :
4775 0 : if (needStartEditGrip(event)) {
4776 0 : m_editData.curGrip = m_editData.element->defaultGrip();
4777 : }
4778 :
4779 0 : if (handleKeyPress(event)) {
4780 0 : if (isBracket && bracketIndex != muse::nidx && systemIndex != muse::nidx) {
4781 : // Try to restore selected bracket
4782 0 : System* system = systemIndex < score()->systems().size() ? score()->systems().at(systemIndex) : nullptr;
4783 0 : EngravingItem* bracket = system && bracketIndex < system->brackets().size() ? system->brackets().at(bracketIndex) : nullptr;
4784 0 : if (bracket) {
4785 0 : m_editData.element = bracket;
4786 0 : select({ bracket }, SelectType::SINGLE);
4787 : }
4788 : }
4789 :
4790 0 : if (isGripEditStarted()) {
4791 0 : updateGripAnchorLines();
4792 : }
4793 :
4794 0 : event->accept();
4795 : }
4796 :
4797 0 : if (isTextEditingStarted()) {
4798 0 : notifyAboutTextEditingChanged();
4799 : }
4800 : }
4801 :
4802 0 : void NotationInteraction::endEditElement()
4803 : {
4804 0 : if (!m_editData.element) {
4805 0 : return;
4806 : }
4807 :
4808 0 : if (isTextEditingStarted()) {
4809 0 : endEditText();
4810 0 : return;
4811 : }
4812 :
4813 0 : if (isDragStarted()) {
4814 0 : doEndDrag();
4815 0 : rollback();
4816 : }
4817 :
4818 0 : doEndEditElement();
4819 0 : resetAnchorLines();
4820 :
4821 0 : m_isEditingElementChanged.notify();
4822 0 : notifyAboutNotationChanged();
4823 : }
4824 :
4825 0 : muse::async::Notification NotationInteraction::isEditingElementChanged() const
4826 : {
4827 0 : return m_isEditingElementChanged;
4828 : }
4829 :
4830 0 : void NotationInteraction::updateTimeTickAnchors(QKeyEvent* event)
4831 : {
4832 0 : EngravingItem* selectedElement = m_selection->element();
4833 0 : if (selectedElement && selectedElement->allowTimeAnchor() && event->type() == QKeyEvent::Type::KeyPress && !isTextEditingStarted()) {
4834 0 : EditTimeTickAnchors::updateAnchors(selectedElement);
4835 0 : updateDragAnchorLines();
4836 0 : score()->update();
4837 : } else {
4838 0 : score()->hideAnchors();
4839 0 : resetAnchorLines();
4840 : }
4841 :
4842 0 : notifyAboutNotationChanged();
4843 0 : }
4844 :
4845 0 : void NotationInteraction::moveElementAnchors(QKeyEvent* event)
4846 : {
4847 0 : EngravingItem* element = selection()->element();
4848 0 : IF_ASSERT_FAILED(element) {
4849 0 : return;
4850 : }
4851 :
4852 0 : startEdit(TranslatableString("undoableAction", "Move element anchors"));
4853 :
4854 0 : MoveElementAnchors::moveElementAnchors(element, KeyboardKey(event->key()), keyboardModifier(event->modifiers()));
4855 :
4856 0 : apply();
4857 :
4858 0 : updateDragAnchorLines();
4859 : }
4860 :
4861 0 : void NotationInteraction::doEndEditElement()
4862 : {
4863 0 : if (m_editData.element) {
4864 0 : m_editData.element->endEdit(m_editData);
4865 : }
4866 0 : m_editData.clear();
4867 0 : }
4868 :
4869 0 : void NotationInteraction::onElementDestroyed(EngravingItem* element)
4870 : {
4871 0 : if (m_editData.element == element) {
4872 0 : m_editData.element = nullptr;
4873 : }
4874 :
4875 0 : if (m_hitElementContext.element == element) {
4876 0 : m_hitElementContext.element = nullptr;
4877 0 : m_hitElementContext.staff = nullptr;
4878 : }
4879 0 : }
4880 :
4881 0 : void NotationInteraction::splitSelectedMeasure()
4882 : {
4883 0 : EngravingItem* selectedElement = m_selection->element();
4884 0 : if (!selectedElement) {
4885 0 : return;
4886 : }
4887 :
4888 0 : if (!selectedElement->isNote() && !selectedElement->isRest()) {
4889 0 : return;
4890 : }
4891 :
4892 0 : if (selectedElement->isNote()) {
4893 0 : selectedElement = toNote(selectedElement)->chord();
4894 : }
4895 :
4896 0 : ChordRest* chordRest = toChordRest(selectedElement);
4897 :
4898 0 : transaction(TranslatableString("undoableAction", "Split measure"), [&](engraving::Transaction& tx) {
4899 0 : SplitJoinMeasure::splitMeasure(tx, score()->masterScore(), chordRest->tick());
4900 0 : });
4901 :
4902 0 : checkAndShowError();
4903 : }
4904 :
4905 0 : void NotationInteraction::joinSelectedMeasures()
4906 : {
4907 0 : if (!m_selection->isRange()) {
4908 0 : return;
4909 : }
4910 :
4911 0 : const INotationSelectionRange::MeasureBaseRange measureBaseRange = m_selection->range()->measureBaseRange();
4912 0 : const MeasureBase* startMeasureBase = measureBaseRange.startMeasureBase;
4913 0 : const MeasureBase* endMeasureBase = measureBaseRange.endMeasureBase;
4914 0 : if (!startMeasureBase || !endMeasureBase) {
4915 0 : return;
4916 : }
4917 :
4918 0 : const Measure* startMeasure = startMeasureBase->isMeasure() ? toMeasure(startMeasureBase) : startMeasureBase->nextMeasure();
4919 0 : const Measure* endMeasure = endMeasureBase->isMeasure() ? toMeasure(endMeasureBase) : endMeasureBase->prevMeasure();
4920 0 : if (!startMeasure || !endMeasure || endMeasure->tick() < startMeasure->tick()) {
4921 0 : return;
4922 : }
4923 :
4924 0 : transaction(TranslatableString("undoableAction", "Join measures"), [&](engraving::Transaction& tx) {
4925 0 : SplitJoinMeasure::joinMeasures(tx, score()->masterScore(), startMeasure->tick(), endMeasure->tick());
4926 0 : });
4927 :
4928 0 : checkAndShowError();
4929 : }
4930 :
4931 0 : void NotationInteraction::addBoxes(BoxType boxType, int count, AddBoxesTarget target)
4932 : {
4933 0 : int beforeBoxIndex = -1;
4934 0 : bool moveSignaturesClefs = (target != AddBoxesTarget::AfterSelection);
4935 0 : const EngravingItem* selectedItem = nullptr;
4936 0 : const MeasureBase* selectedItemMeasure = nullptr;
4937 :
4938 0 : switch (target) {
4939 0 : case AddBoxesTarget::AfterSelection:
4940 : case AddBoxesTarget::BeforeSelection: {
4941 0 : if (selection()->isNone()) {
4942 0 : MScore::setError(MsError::NO_MEASURE_SELECTED);
4943 0 : checkAndShowError();
4944 0 : return;
4945 : }
4946 :
4947 0 : if (selection()->isRange()) {
4948 0 : INotationSelectionRange::MeasureBaseRange range = selection()->range()->measureBaseRange();
4949 :
4950 0 : const int startMeasureIndex = range.startMeasureBase ? range.startMeasureBase->index() : 0;
4951 0 : const int endMeasureIndex = range.endMeasureBase ? range.endMeasureBase->index() + 1 : 0;
4952 :
4953 0 : beforeBoxIndex = target == AddBoxesTarget::BeforeSelection
4954 0 : ? startMeasureIndex
4955 : : endMeasureIndex;
4956 0 : break;
4957 : }
4958 :
4959 0 : const std::vector<EngravingItem*>& elements = selection()->elements();
4960 0 : IF_ASSERT_FAILED(!elements.empty()) {
4961 : // This would contradict the fact that selection()->isNone() == false at this point
4962 0 : MScore::setError(MsError::NO_MEASURE_SELECTED);
4963 0 : checkAndShowError();
4964 0 : return;
4965 : }
4966 :
4967 0 : for (const EngravingItem* item : elements) {
4968 0 : const MeasureBase* itemMeasure = item->findMeasureBase();
4969 0 : if (!itemMeasure) {
4970 0 : continue;
4971 : }
4972 :
4973 0 : int itemMeasureIndex = itemMeasure->index();
4974 0 : if (itemMeasureIndex < 0) {
4975 0 : continue;
4976 : }
4977 :
4978 0 : if (target == AddBoxesTarget::BeforeSelection) {
4979 0 : if (beforeBoxIndex < 0 || itemMeasureIndex < beforeBoxIndex) {
4980 0 : beforeBoxIndex = itemMeasureIndex;
4981 0 : selectedItem = item;
4982 0 : selectedItemMeasure = itemMeasure;
4983 : }
4984 : } else {
4985 0 : if (itemMeasureIndex + 1 > beforeBoxIndex) {
4986 0 : beforeBoxIndex = itemMeasureIndex + 1;
4987 0 : selectedItem = item;
4988 0 : selectedItemMeasure = itemMeasure;
4989 : }
4990 : }
4991 : }
4992 :
4993 0 : const bool selectedIsValid = selectedItem && selectedItemMeasure;
4994 0 : const ElementType selectedItemType = selectedIsValid ? selectedItem->type() : ElementType::INVALID;
4995 :
4996 : // special cases for "between measures elements"
4997 0 : switch (selectedItemType) {
4998 0 : case ElementType::CLEF:
4999 : case ElementType::BAR_LINE:
5000 : case ElementType::TIMESIG:
5001 : case ElementType::KEYSIG: {
5002 0 : const Fraction itemTick = selectedItem->tick();
5003 0 : const Fraction measureTick = selectedItemMeasure->tick();
5004 0 : const Fraction measureLastTick = measureTick + selectedItemMeasure->ticks();
5005 :
5006 0 : if (itemTick == measureTick) {
5007 0 : if (target == AddBoxesTarget::AfterSelection) {
5008 0 : beforeBoxIndex -= 1;
5009 : }
5010 0 : moveSignaturesClefs = (target == AddBoxesTarget::AfterSelection);
5011 0 : } else if (itemTick == measureLastTick) {
5012 0 : if (target == AddBoxesTarget::BeforeSelection) {
5013 0 : beforeBoxIndex += 1;
5014 : }
5015 0 : moveSignaturesClefs = (target == AddBoxesTarget::AfterSelection);
5016 : }
5017 : }
5018 0 : default: break;
5019 : }
5020 :
5021 0 : if (beforeBoxIndex < 0) {
5022 0 : MScore::setError(MsError::NO_MEASURE_SELECTED);
5023 0 : checkAndShowError();
5024 0 : return;
5025 : }
5026 0 : } break;
5027 0 : case AddBoxesTarget::AtStartOfScore: {
5028 0 : Measure* firstMeasure = score()->firstMeasure();
5029 0 : beforeBoxIndex = firstMeasure ? firstMeasure->index() : -1;
5030 0 : } break;
5031 0 : case AddBoxesTarget::AtEndOfScore:
5032 0 : beforeBoxIndex = -1;
5033 0 : break;
5034 : }
5035 :
5036 0 : addBoxes(boxType, count, beforeBoxIndex, moveSignaturesClefs);
5037 : }
5038 :
5039 0 : void NotationInteraction::addBoxes(BoxType boxType, int count, int beforeBoxIndex, bool moveSignaturesClef)
5040 : {
5041 0 : if (count < 1) {
5042 0 : return;
5043 : }
5044 :
5045 0 : auto boxTypeToElementType = [](BoxType boxType) {
5046 0 : switch (boxType) {
5047 0 : case BoxType::Horizontal: return mu::engraving::ElementType::HBOX;
5048 0 : case BoxType::Vertical: return mu::engraving::ElementType::VBOX;
5049 0 : case BoxType::Text: return mu::engraving::ElementType::TBOX;
5050 0 : case BoxType::Fret: return mu::engraving::ElementType::FBOX;
5051 0 : case BoxType::Measure: return mu::engraving::ElementType::MEASURE;
5052 0 : case BoxType::Unknown: return mu::engraving::ElementType::INVALID;
5053 : }
5054 :
5055 0 : return ElementType::INVALID;
5056 : };
5057 :
5058 0 : mu::engraving::ElementType elementType = boxTypeToElementType(boxType);
5059 0 : if (elementType == mu::engraving::ElementType::INVALID) {
5060 0 : return;
5061 : }
5062 :
5063 0 : auto boxTypeDescription = [count](BoxType boxType) -> TranslatableString {
5064 0 : switch (boxType) {
5065 0 : case BoxType::Horizontal: return TranslatableString("undoableAction", "Add horizontal frame");
5066 0 : case BoxType::Vertical: return TranslatableString("undoableAction", "Add vertical frame");
5067 0 : case BoxType::Text: return TranslatableString("undoableAction", "Add text frame");
5068 0 : case BoxType::Fret: return TranslatableString("undoableAction", "Add fretboard diagram legend");
5069 0 : case BoxType::Measure: return TranslatableString("undoableAction", "Add %Ln measure(s)", nullptr, count);
5070 0 : case BoxType::Unknown: break;
5071 : }
5072 :
5073 0 : return {};
5074 0 : };
5075 :
5076 0 : startEdit(boxTypeDescription(boxType));
5077 :
5078 0 : mu::engraving::MeasureBase* beforeBox = beforeBoxIndex >= 0 ? score()->measure(beforeBoxIndex) : nullptr;
5079 :
5080 0 : mu::engraving::Score::InsertMeasureOptions options;
5081 0 : options.createMeasureRests = false;
5082 0 : options.moveSignaturesClef = moveSignaturesClef;
5083 0 : options.needDeselectAll = false;
5084 0 : options.cloneBoxToAllParts = boxType != BoxType::Fret;
5085 :
5086 0 : for (int i = 0; i < count; ++i) {
5087 0 : score()->insertMeasure(elementType, beforeBox, options);
5088 : }
5089 :
5090 0 : apply();
5091 :
5092 0 : int indexOfFirstAddedMeasure = beforeBoxIndex >= 0 ? beforeBoxIndex : score()->measures()->size() - count;
5093 0 : MeasureBase* firstAddedMeasure = score()->measure(indexOfFirstAddedMeasure);
5094 0 : m_selection->select({ firstAddedMeasure }, SelectType::REPLACE);
5095 0 : showItem(firstAddedMeasure);
5096 :
5097 : // For other box types, it makes little sense to select them all
5098 0 : if (boxType == BoxType::Measure) {
5099 0 : m_selection->select({ score()->measure(indexOfFirstAddedMeasure + count - 1) }, SelectType::RANGE);
5100 : }
5101 :
5102 0 : m_selection->notifyAboutSelectionChangedIfNeed();
5103 : }
5104 :
5105 0 : void NotationInteraction::copySelection()
5106 : {
5107 0 : if (!selection()->canCopy()) {
5108 0 : checkAndShowError();
5109 0 : return;
5110 : }
5111 :
5112 0 : if (isTextEditingStarted()) {
5113 0 : m_editData.element->editCopy(m_editData);
5114 0 : mu::engraving::TextEditData* ted = static_cast<mu::engraving::TextEditData*>(m_editData.getData(m_editData.element).get());
5115 0 : if (!ted->selectedText.isEmpty()) {
5116 0 : QMimeData* mimeData = new QMimeData();
5117 0 : mimeData->setData(TextEditData::mimeRichTextFormat, ted->selectedText.toQString().toUtf8());
5118 0 : mimeData->setText(ted->selectedPlainText);
5119 0 : QGuiApplication::clipboard()->setMimeData(mimeData);
5120 : }
5121 : } else {
5122 0 : QMimeData* mimeData = selection()->qMimeData();
5123 0 : if (!mimeData) {
5124 0 : return;
5125 : }
5126 0 : QGuiApplication::clipboard()->setMimeData(mimeData);
5127 : }
5128 : }
5129 :
5130 0 : void NotationInteraction::repeatSelection()
5131 : {
5132 0 : const Selection& selection = score()->selection();
5133 0 : if (score()->noteEntryMode() && selection.isSingle()) {
5134 : // Single selections require special handling in note entry mode...
5135 0 : EngravingItem* el = selection.element();
5136 0 : if (!el) {
5137 0 : return;
5138 : }
5139 0 : Chord* c = nullptr;
5140 0 : if (el->isNote()) {
5141 0 : c = toNote(el)->chord();
5142 0 : } else if (el->isRest()) {
5143 0 : Segment* prevSegment = toRest(el)->segment()->prev1WithElemsOnTrack(el->track());
5144 :
5145 : // Looking for the previous Chord
5146 0 : while (prevSegment) {
5147 0 : if (prevSegment->element(el->track())->isChord()) {
5148 0 : c = toChord(prevSegment->element(el->track()));
5149 0 : break;
5150 : }
5151 0 : prevSegment = prevSegment->prev1WithElemsOnTrack(el->track());
5152 : }
5153 : }
5154 0 : if (c) {
5155 0 : transaction(TranslatableString("undoableAction", "Repeat selection"), [&](engraving::Transaction& tx) {
5156 0 : for (Note* note : c->notes()) {
5157 0 : NoteVal nval = note->noteVal();
5158 0 : NoteInput::addPitch(tx, score(), nval, note != c->notes()[0]);
5159 : }
5160 0 : });
5161 : }
5162 0 : return;
5163 : }
5164 :
5165 : //! NOTE: Ideally we would use our copy-paste logic for this case, but this isn't
5166 : //! fully compatible with list selections right now...
5167 0 : if (selection.isList() && !selection.noteList().empty()) {
5168 0 : const Fraction& firstTick = selection.tickStart();
5169 0 : const Fraction& lastTick = selection.tickEnd();
5170 : // Only "single-tick" list selections are currently supported...
5171 0 : if (firstTick != lastTick) {
5172 0 : MScore::setError(MsError::CANNOT_REPEAT_SELECTION);
5173 0 : checkAndShowError();
5174 0 : return;
5175 : }
5176 0 : startEdit(TranslatableString("undoableAction", "Repeat selection"));
5177 0 : if (!Paste::repeatListSelection(score()->transactionManager()->currentOrDummyTransaction(), score())) {
5178 0 : rollback();
5179 0 : MScore::setError(MsError::CANNOT_REPEAT_SELECTION);
5180 0 : return;
5181 : }
5182 0 : apply();
5183 0 : return;
5184 : }
5185 :
5186 : // If a list selection with no notes is a rest, convert to a range selection
5187 0 : if (selection.isList()) {
5188 0 : ChordRest* cr = score()->getSelectedChordRest();
5189 0 : if (!cr) {
5190 0 : MScore::setError(MsError::CANNOT_REPEAT_SELECTION);
5191 0 : checkAndShowError();
5192 0 : return;
5193 : }
5194 0 : score()->select(cr, SelectType::RANGE);
5195 : }
5196 :
5197 : // Use copy-paste logic for range selections...
5198 0 : if (!selection.isRange() || !m_selection->canCopy()) {
5199 0 : MScore::setError(MsError::CANNOT_REPEAT_SELECTION);
5200 0 : checkAndShowError();
5201 0 : return;
5202 : }
5203 :
5204 0 : XmlReader xml(selection.mimeData());
5205 0 : staff_idx_t dStaff = selection.staffStart();
5206 0 : mu::engraving::Segment* endSegment = selection.endSegment();
5207 :
5208 0 : startEdit(TranslatableString("undoableAction", "Repeat selection"));
5209 0 : if (endSegment && endSegment->segmentType() != SegmentType::ChordRest) {
5210 0 : if (!endSegment->next1(SegmentType::ChordRest)) {
5211 0 : score()->appendMeasures(1);
5212 : }
5213 0 : endSegment = endSegment->next1(SegmentType::ChordRest);
5214 : }
5215 0 : if (!endSegment) {
5216 0 : return;
5217 : }
5218 0 : for (track_idx_t track = staff2track(dStaff); track < staff2track(dStaff + 1); ++track) {
5219 0 : EngravingItem* e = endSegment->element(track);
5220 0 : if (!e) {
5221 0 : continue;
5222 : }
5223 :
5224 0 : ChordRest* cr = toChordRest(e);
5225 0 : if (!Paste::pasteStaff(score()->transactionManager()->currentOrDummyTransaction(), score(), xml, cr->segment(), cr->staffIdx())) {
5226 0 : rollback();
5227 0 : checkAndShowError();
5228 0 : return;
5229 : }
5230 0 : apply();
5231 :
5232 0 : showItem(cr);
5233 0 : break;
5234 : }
5235 0 : }
5236 :
5237 0 : void NotationInteraction::copyLyrics()
5238 : {
5239 0 : QString text = score()->extractLyrics();
5240 0 : QGuiApplication::clipboard()->setText(text);
5241 0 : }
5242 :
5243 0 : void NotationInteraction::pasteSelection(const Fraction& scale)
5244 : {
5245 0 : startEdit(TranslatableString("undoableAction", "Paste"));
5246 :
5247 0 : bool succeeded = true;
5248 0 : if (isTextEditingStarted()) {
5249 0 : pasteIntoTextEdit();
5250 : } else {
5251 0 : const QMimeData* mimeData = QGuiApplication::clipboard()->mimeData();
5252 0 : QMimeDataAdapter ma(mimeData);
5253 0 : succeeded = Paste::paste(score()->transactionManager()->currentOrDummyTransaction(), score(), &ma, nullptr, scale);
5254 0 : m_editData.element = nullptr;
5255 0 : }
5256 :
5257 0 : if (succeeded) {
5258 0 : apply();
5259 0 : if (EngravingItem* pastedElement = selection()->element()) {
5260 0 : selectAndStartEditIfNeeded(pastedElement);
5261 : }
5262 : } else {
5263 0 : rollback();
5264 : }
5265 :
5266 0 : checkAndShowError();
5267 0 : }
5268 :
5269 0 : void NotationInteraction::pasteIntoTextEdit()
5270 : {
5271 0 : const QMimeData* mimeData = QGuiApplication::clipboard()->mimeData();
5272 0 : if (mimeData->hasFormat(TextEditData::mimeRichTextFormat)) {
5273 0 : const QString txt = QString::fromUtf8(mimeData->data(TextEditData::mimeRichTextFormat));
5274 0 : toTextBase(m_editData.element)->paste(txt);
5275 0 : notifyAboutTextEditingChanged();
5276 0 : return;
5277 0 : }
5278 :
5279 0 : QString clipboardText = mimeData->text();
5280 0 : QString textForPaste = clipboardText;
5281 0 : if ((!clipboardText.startsWith('<') || !clipboardText.contains('>')) && m_editData.element->isLyrics()) {
5282 0 : textForPaste = extractSyllable(clipboardText);
5283 : }
5284 :
5285 0 : toTextBase(m_editData.element)->paste(textForPaste);
5286 0 : notifyAboutTextEditingChanged();
5287 :
5288 0 : if (textForPaste.isEmpty() || !m_editData.element->isLyrics()) {
5289 0 : return;
5290 : }
5291 :
5292 0 : if (textForPaste.endsWith('-')) {
5293 0 : navigateToNextSyllable();
5294 0 : } else if (textForPaste.endsWith('_')) {
5295 0 : addMelisma();
5296 : } else {
5297 0 : navigateToLyrics(false, false, false);
5298 : }
5299 :
5300 0 : const QString textForNextPaste = clipboardText.remove(0, clipboardText.indexOf(textForPaste) + textForPaste.size());
5301 0 : QGuiApplication::clipboard()->setText(textForNextPaste);
5302 0 : }
5303 :
5304 0 : void NotationInteraction::swapSelection()
5305 : {
5306 0 : if (!selection()->canCopy()) {
5307 0 : return;
5308 : }
5309 :
5310 : // Store the old selection...
5311 0 : mu::engraving::Selection& selection = score()->selection();
5312 0 : QByteArray oldSelection = selection.mimeData().toQByteArray();
5313 :
5314 : // Paste using contents of clipboard...
5315 0 : pasteSelection();
5316 :
5317 : // Save old selection to clipboard...
5318 0 : QMimeData* mimeData = new QMimeData();
5319 0 : mimeData->setData(selection.mimeType(), oldSelection);
5320 0 : QGuiApplication::clipboard()->setMimeData(mimeData);
5321 0 : }
5322 :
5323 0 : void NotationInteraction::deleteSelection()
5324 : {
5325 0 : if (selection()->isNone()) {
5326 0 : return;
5327 : }
5328 :
5329 0 : startEdit(TranslatableString("undoableAction", "Delete"));
5330 :
5331 0 : if (isTextEditingStarted()) {
5332 0 : mu::engraving::TextBase* textBase = toTextBase(m_editData.element);
5333 0 : if (!textBase->deleteSelectedText()) {
5334 0 : m_editData.key = Qt::Key_Backspace;
5335 0 : m_editData.modifiers = {};
5336 0 : if (textBase->edit(m_editData)) {
5337 0 : notifyAboutTextEditingChanged();
5338 : }
5339 : } else {
5340 0 : notifyAboutTextEditingChanged();
5341 : }
5342 : } else {
5343 0 : doEndEditElement();
5344 0 : resetGripEdit();
5345 0 : score()->cmdDeleteSelection();
5346 : }
5347 :
5348 0 : checkAndShowError();
5349 0 : apply();
5350 0 : resetHitElementContext();
5351 : }
5352 :
5353 0 : void NotationInteraction::flipSelection()
5354 : {
5355 0 : if (selection()->isNone()) {
5356 0 : return;
5357 : }
5358 :
5359 0 : transaction(TranslatableString("undoableAction", "Flip direction"), [&](auto& tx) {
5360 0 : Flip::flip(tx, score());
5361 0 : });
5362 :
5363 0 : updateGripAnchorLines();
5364 : }
5365 :
5366 0 : void NotationInteraction::flipSelectionHorizontally()
5367 : {
5368 0 : if (selection()->isNone()) {
5369 0 : return;
5370 : }
5371 :
5372 0 : transaction(TranslatableString("undoableAction", "Flip horizontally"), [&](auto& tx) {
5373 0 : Flip::flipHorizontally(tx, score());
5374 0 : });
5375 : }
5376 :
5377 0 : void NotationInteraction::mirrorNotes()
5378 : {
5379 0 : if (selection()->isNone()) {
5380 0 : return;
5381 : }
5382 :
5383 0 : transaction(TranslatableString("undoableAction", "Mirror notehead"), [&](auto& tx) {
5384 0 : Flip::mirrorNoteHead(tx, score());
5385 0 : });
5386 :
5387 0 : notifyAboutNotationChanged();
5388 : }
5389 :
5390 0 : void NotationInteraction::toggleTieForSelection()
5391 : {
5392 : // Calls `startEdit` internally
5393 0 : Tie* newTie = mu::engraving::EditTie::cmdToggleTie(score());
5394 :
5395 0 : notifyAboutNotationChanged();
5396 0 : m_selection->selectionChanged().notify();
5397 :
5398 0 : if (newTie && newTie->tieJumpPoints() && newTie->tieJumpPoints()->size() > 1) {
5399 0 : selectAndStartEditIfNeeded(newTie);
5400 : }
5401 0 : }
5402 :
5403 0 : void NotationInteraction::addLaissezVibToSelection()
5404 : {
5405 : // Calls `startEdit` internally
5406 0 : mu::engraving::EditTie::cmdToggleLaissezVib(score());
5407 :
5408 0 : notifyAboutNotationChanged();
5409 0 : m_selection->selectionChanged().notify();
5410 0 : }
5411 :
5412 0 : void NotationInteraction::addSlurToSelection()
5413 : {
5414 0 : if (selection()->isNone()) {
5415 0 : return;
5416 : }
5417 :
5418 : // Calls `startEdit` internally
5419 0 : doAddSlur();
5420 0 : m_selection->selectionChanged().notify();
5421 : }
5422 :
5423 0 : void NotationInteraction::addHammerOnPullOffToSelection()
5424 : {
5425 0 : if (selection()->isNone()) {
5426 0 : return;
5427 : }
5428 :
5429 0 : HammerOnPullOff* hopo = Factory::createHammerOnPullOff(score()->dummy());
5430 : // Calls `startEdit` internally
5431 0 : doAddSlur(hopo);
5432 : }
5433 :
5434 0 : void NotationInteraction::addOttavaToSelection(OttavaType type)
5435 : {
5436 0 : if (selection()->isNone()) {
5437 0 : return;
5438 : }
5439 :
5440 0 : transaction(TranslatableString("undoableAction", "Add ottava"), [&](auto&) {
5441 0 : score()->cmdAddOttava(type);
5442 0 : });
5443 : }
5444 :
5445 0 : void NotationInteraction::addHairpinOnGripDrag(EditData& ed, bool isLeftGrip)
5446 : {
5447 0 : startEdit(TranslatableString("undoableAction", "Add hairpin"));
5448 :
5449 0 : Dynamic* dynamic = toDynamic(ed.element);
5450 :
5451 0 : const PointF pos = m_dragData.ed.pos;
5452 0 : Transaction& tx = score()->transactionManager()->currentOrDummyTransaction();
5453 0 : Hairpin* hairpin = EditHairpin::addHairpinToDynamicOnGripDrag(tx, score(), dynamic, isLeftGrip, pos);
5454 :
5455 0 : if (!hairpin) {
5456 0 : rollback();
5457 0 : return;
5458 : }
5459 :
5460 0 : apply();
5461 :
5462 : // Reset grip offset to zero after drawing the hairpin
5463 0 : dynamic->resetRightDragOffset();
5464 :
5465 0 : IF_ASSERT_FAILED(!hairpin->segmentsEmpty()) {
5466 0 : return;
5467 : }
5468 :
5469 0 : if (isLeftGrip) {
5470 0 : LineSegment* segment = hairpin->frontSegment();
5471 0 : select({ segment });
5472 0 : startEditGrip(segment, Grip::START);
5473 : } else {
5474 0 : LineSegment* segment = hairpin->backSegment();
5475 0 : select({ segment });
5476 0 : startEditGrip(segment, Grip::END);
5477 : }
5478 :
5479 0 : ed.isHairpinDragCreatedFromDynamic = true;
5480 : }
5481 :
5482 0 : void NotationInteraction::addHairpinsToSelection(HairpinType type)
5483 : {
5484 0 : if (selection()->isNone()) {
5485 0 : return;
5486 : }
5487 :
5488 0 : std::vector<mu::engraving::Hairpin*> hairpins;
5489 0 : transaction(TranslatableString("undoableAction", "Add hairpin"), [&](Transaction& tx) {
5490 0 : hairpins = EditHairpin::addHairpins(tx, score(), type);
5491 0 : });
5492 :
5493 0 : if (!noteInput()->isNoteInputMode() && hairpins.size() == 1) {
5494 0 : mu::engraving::LineSegment* segment = hairpins.front()->frontSegment();
5495 0 : select({ segment });
5496 0 : startEditGrip(segment, mu::engraving::Grip::END);
5497 : }
5498 0 : }
5499 :
5500 0 : void NotationInteraction::putRestToSelection()
5501 : {
5502 0 : mu::engraving::InputState& is = score()->inputState();
5503 0 : if (!is.duration().isValid() || is.duration().isZero() || is.duration().isMeasure()) {
5504 0 : is.setDuration(DurationType::V_QUARTER);
5505 : }
5506 :
5507 0 : if (!m_noteInput->isNoteInputMode()) {
5508 0 : m_noteInput->startNoteInput(configuration()->defaultNoteInputMethod());
5509 : }
5510 :
5511 0 : if (is.usingNoteEntryMethod(NoteEntryMethod::BY_DURATION) || is.usingNoteEntryMethod(NoteEntryMethod::RHYTHM)) {
5512 0 : m_noteInput->toggleRest();
5513 0 : } else if (is.usingNoteEntryMethod(NoteEntryMethod::REPITCH)) {
5514 0 : ChordRest* cr = is.cr();
5515 0 : if (cr) {
5516 0 : TDuration duration = cr->durationType();
5517 0 : if (duration.isValid() && !duration.isZero()) {
5518 0 : if (duration.isMeasure()) {
5519 0 : is.moveToNextInputPos();
5520 : } else {
5521 0 : putRest(duration);
5522 : }
5523 : }
5524 : }
5525 : } else {
5526 0 : putRest(is.duration());
5527 : }
5528 0 : }
5529 :
5530 0 : void NotationInteraction::putRest(Duration duration)
5531 : {
5532 0 : if (selection()->isNone()) {
5533 0 : return;
5534 : }
5535 :
5536 : // Calls `startEdit` internally
5537 0 : score()->cmdEnterRest(duration);
5538 :
5539 0 : notifyAboutNotationChanged();
5540 : }
5541 :
5542 0 : void NotationInteraction::addBracketsToSelection(BracketsType type)
5543 : {
5544 0 : if (selection()->isNone()) {
5545 0 : return;
5546 : }
5547 :
5548 0 : switch (type) {
5549 0 : case BracketsType::Brackets:
5550 0 : transaction(TranslatableString("undoableAction", "Add brackets"), [&](auto& tx) {
5551 0 : EditAccidentalBrackets::addBracket(tx, score());
5552 0 : });
5553 0 : break;
5554 0 : case BracketsType::Braces:
5555 0 : transaction(TranslatableString("undoableAction", "Add braces"), [&](auto& tx) {
5556 0 : EditAccidentalBrackets::addBraces(tx, score());
5557 0 : });
5558 0 : break;
5559 0 : case BracketsType::Parentheses:
5560 0 : transaction(TranslatableString("undoableAction", "Add parentheses"), [&](auto& tx) {
5561 0 : EditParentheses::toggleParentheses(tx, score());
5562 0 : });
5563 0 : break;
5564 : }
5565 : }
5566 :
5567 0 : void NotationInteraction::toggleAccidentalForSelection(AccidentalType type)
5568 : {
5569 0 : if (selection()->isNone()) {
5570 0 : return;
5571 : }
5572 :
5573 0 : bool accidentalAlreadyAdded = false;
5574 0 : for (const EngravingItem* item : score()->selection().elements()) {
5575 0 : if (!item->isNote()) {
5576 0 : continue;
5577 : }
5578 :
5579 0 : if (toNote(item)->accidentalType() != type) {
5580 0 : accidentalAlreadyAdded = false;
5581 0 : break;
5582 : }
5583 :
5584 0 : accidentalAlreadyAdded = true;
5585 : }
5586 :
5587 0 : transaction(TranslatableString("undoableAction", "Toggle accidental"), [&](auto&) {
5588 0 : if (accidentalAlreadyAdded) {
5589 0 : mu::engraving::EditNote::changeAccidental(score(), AccidentalType::NONE);
5590 : } else {
5591 0 : mu::engraving::EditNote::changeAccidental(score(), type);
5592 : }
5593 0 : });
5594 : }
5595 :
5596 0 : void NotationInteraction::toggleArticulationForSelection(SymbolId articulationSymbolId)
5597 : {
5598 0 : if (selection()->isNone()) {
5599 0 : return;
5600 : }
5601 :
5602 0 : std::vector<mu::engraving::Note*> notes = score()->selection().noteList();
5603 0 : if (notes.empty()) {
5604 : // no notes, but maybe they have an articulation selected. we should use that chord
5605 0 : EngravingItem* e = score()->selection().element();
5606 0 : if (e && e->isArticulationFamily()) {
5607 0 : Chord* c = toChord(toArticulation(e)->ownershipParent());
5608 0 : if (c) {
5609 0 : notes.insert(notes.begin(), c->notes().begin(), c->notes().end());
5610 : }
5611 : }
5612 : }
5613 :
5614 0 : auto updateMode = notesHaveActiculation(notes, articulationSymbolId)
5615 0 : ? mu::engraving::ArticulationsUpdateMode::Remove : mu::engraving::ArticulationsUpdateMode::Insert;
5616 :
5617 0 : std::set<Chord*> chords;
5618 0 : for (Note* note: notes) {
5619 0 : if (note->isTrillCueNote()) {
5620 0 : return;
5621 : }
5622 0 : Chord* chord = note->chord();
5623 0 : if (chords.find(chord) == chords.end()) {
5624 0 : chords.insert(chord);
5625 : }
5626 : }
5627 :
5628 0 : startEdit(TranslatableString("undoableAction", "Toggle articulation"));
5629 0 : for (Chord* chord: chords) {
5630 0 : chord->updateArticulations({ articulationSymbolId }, updateMode);
5631 : }
5632 0 : apply();
5633 0 : }
5634 :
5635 0 : void NotationInteraction::toggleDotsForSelection(int dots)
5636 : {
5637 0 : IF_ASSERT_FAILED(dots >= 1 && dots <= 4) {
5638 0 : return;
5639 : }
5640 :
5641 0 : if (selection()->isNone()) {
5642 0 : return;
5643 : }
5644 :
5645 0 : transaction(TranslatableString("undoableAction", "Toggle augmentation dots"), [&](auto& tx) {
5646 0 : NoteInput::toggleDots(tx, score(), dots, true /*toggleForSelectionOnly*/);
5647 0 : });
5648 :
5649 0 : notifyAboutNotationChanged();
5650 : }
5651 :
5652 0 : void NotationInteraction::addGraceNotesToSelectedNotes(GraceNoteType type)
5653 : {
5654 0 : if (selection()->isNone()) {
5655 0 : return;
5656 : }
5657 :
5658 0 : int denominator = 1;
5659 :
5660 0 : switch (type) {
5661 0 : case GraceNoteType::GRACE4:
5662 : case GraceNoteType::INVALID:
5663 : case GraceNoteType::NORMAL:
5664 0 : denominator = 1;
5665 0 : break;
5666 0 : case GraceNoteType::ACCIACCATURA:
5667 : case GraceNoteType::APPOGGIATURA:
5668 : case GraceNoteType::GRACE8_AFTER:
5669 0 : denominator = 2;
5670 0 : break;
5671 0 : case GraceNoteType::GRACE16:
5672 : case GraceNoteType::GRACE16_AFTER:
5673 0 : denominator = 4;
5674 0 : break;
5675 0 : case GraceNoteType::GRACE32:
5676 : case GraceNoteType::GRACE32_AFTER:
5677 0 : denominator = 8;
5678 0 : break;
5679 : }
5680 :
5681 0 : startEdit(TranslatableString("undoableAction", "Add grace note"));
5682 0 : score()->cmdAddGrace(type, mu::engraving::Constants::DIVISION / denominator);
5683 0 : apply();
5684 : }
5685 :
5686 0 : bool NotationInteraction::canAddTupletToSelectedChordRests() const
5687 : {
5688 0 : for (ChordRest* chordRest : score()->getSelectedChordRests()) {
5689 0 : if (chordRest->isGrace() || (chordRest->isChord() && toChord(chordRest)->isTrillCueNote())) {
5690 0 : continue;
5691 : }
5692 :
5693 0 : if (chordRest->durationType() < mu::engraving::TDuration(mu::engraving::DurationType::V_512TH)
5694 0 : && chordRest->durationType() != mu::engraving::TDuration(mu::engraving::DurationType::V_MEASURE)) {
5695 0 : return false;
5696 : }
5697 0 : }
5698 :
5699 0 : return true;
5700 : }
5701 :
5702 0 : void NotationInteraction::addTupletToSelectedChordRests(const TupletOptions& options)
5703 : {
5704 0 : if (selection()->isNone()) {
5705 0 : return;
5706 : }
5707 :
5708 0 : if (options.selectedDurationAs == TupletSelectedDuration::SUBDIVISION) {
5709 0 : if (!EditTuplet::checkAddTupletsWithSelectedDurations(score(), options.ratio)) {
5710 0 : checkAndShowError();
5711 0 : return;
5712 : }
5713 0 : } else if (!canAddTupletToSelectedChordRests()) {
5714 0 : MScore::setError(MsError::TUPLET_NOTE_VALUE_TOO_SHORT);
5715 0 : checkAndShowError();
5716 0 : return;
5717 : }
5718 :
5719 0 : transaction(TranslatableString("undoableAction", "Add tuplet"), [&](mu::engraving::Transaction& tx) {
5720 0 : EditTuplet::addTuplets(tx, score(), options.ratio, options.numberType, options.bracketType, options.autoBaseLen,
5721 0 : options.selectedDurationAs);
5722 0 : });
5723 0 : notifyAboutNotationChanged();
5724 : }
5725 :
5726 0 : void NotationInteraction::addBeamToSelectedChordRests(BeamMode mode)
5727 : {
5728 0 : if (selection()->isNone()) {
5729 0 : return;
5730 : }
5731 :
5732 0 : transaction(TranslatableString("undoableAction", "Set beam type"), [&](Transaction& tx) {
5733 0 : EditBeam::setBeamMode(tx, score(), mode);
5734 0 : });
5735 0 : notifyAboutNotationChanged();
5736 : }
5737 :
5738 0 : void NotationInteraction::beamSelectedRange()
5739 : {
5740 0 : if (selection()->isNone()) {
5741 0 : return;
5742 : }
5743 :
5744 0 : transaction(TranslatableString("undoableAction", "Beam selected"), [&](Transaction& tx) {
5745 0 : EditBeam::beamSelectedRange(tx, score());
5746 0 : });
5747 0 : notifyAboutNotationChanged();
5748 : }
5749 :
5750 0 : void NotationInteraction::increaseDecreaseDuration(int steps, bool stepByDots)
5751 : {
5752 0 : if (selection()->isNone()) {
5753 0 : return;
5754 : }
5755 :
5756 0 : m_undoStack->transaction(steps > 0 // negative: increase, positive: decrease
5757 0 : ? TranslatableString("undoableAction", "Decrease duration")
5758 : : TranslatableString("undoableAction", "Increase duration"),
5759 0 : [&](Transaction& tx) {
5760 0 : EditDuration::incDecDuration(tx, score(), steps, stepByDots);
5761 0 : });
5762 0 : notifyAboutNotationChanged();
5763 : }
5764 :
5765 0 : void NotationInteraction::increaseDecreaseSelectedDynamicsValues(int delta)
5766 : {
5767 0 : if (selection()->isNone()) {
5768 0 : return;
5769 : }
5770 :
5771 0 : const auto inRange = [](DynamicType type) {
5772 0 : return type >= DynamicType::PPPPP && type <= DynamicType::FFFFF;
5773 : };
5774 :
5775 0 : std::map<Dynamic*, /*newType*/ DynamicType> dynamicsMap;
5776 :
5777 : // Collect valid dynamics before trying anything (avoid unnecessary calls to startEdit etc)...
5778 0 : for (EngravingItem* item : selection()->elements()) {
5779 0 : if (!item->isDynamic()) {
5780 0 : continue;
5781 : }
5782 0 : Dynamic* dynamic = toDynamic(item);
5783 0 : DynamicType newType = static_cast<DynamicType>(static_cast<int>(dynamic->dynamicType()) + delta);
5784 0 : if (inRange(newType)) {
5785 0 : dynamicsMap.emplace(dynamic, newType);
5786 : }
5787 : }
5788 :
5789 0 : if (dynamicsMap.empty()) {
5790 0 : return;
5791 : }
5792 :
5793 0 : startEdit(delta > 0 // positive: increase dynamics, negative: decrease dynamics
5794 0 : ? TranslatableString("undoableAction", "Increase dynamics")
5795 : : TranslatableString("undoableAction", "Decrease dynamics"));
5796 :
5797 0 : for (const auto& [dynamic, newType] : dynamicsMap) {
5798 0 : dynamic->undoChangeProperty(Pid::DYNAMIC_TYPE, newType);
5799 0 : dynamic->undoChangeProperty(Pid::TEXT, Dynamic::dynamicText(newType));
5800 0 : doAutoFlipHairpinsType(dynamic);
5801 : }
5802 :
5803 0 : apply();
5804 0 : }
5805 :
5806 0 : void NotationInteraction::autoFlipHairpinsType(Dynamic* dynamic)
5807 : {
5808 0 : if (!dynamic || dynamic->dynamicType() == DynamicType::OTHER || dynamic->dynamicType() >= DynamicType::FP) {
5809 0 : return;
5810 : }
5811 0 : startEdit(TranslatableString("undoableAction", "Change hairpin type"));
5812 0 : doAutoFlipHairpinsType(dynamic);
5813 0 : apply();
5814 : }
5815 :
5816 0 : void NotationInteraction::doAutoFlipHairpinsType(Dynamic* dynamic)
5817 : {
5818 0 : dynamic->findAdjacentHairpins();
5819 0 : if (Hairpin* leftHp = dynamic->leftHairpin()) {
5820 0 : const Dynamic* startDyn = leftHp->dynamicSnappedBefore();
5821 0 : if (startDyn
5822 0 : && !(startDyn->dynamicType() == DynamicType::OTHER || startDyn->dynamicType() >= DynamicType::FP)
5823 0 : && !leftHp->isLineType()) {
5824 0 : if (int(startDyn->dynamicType()) > int(dynamic->dynamicType())) {
5825 0 : leftHp->undoChangeProperty(Pid::HAIRPIN_TYPE, int(HairpinType::DIM_HAIRPIN));
5826 : } else {
5827 0 : leftHp->undoChangeProperty(Pid::HAIRPIN_TYPE, int(HairpinType::CRESC_HAIRPIN));
5828 : }
5829 : }
5830 : }
5831 0 : if (Hairpin* rightHp = dynamic->rightHairpin()) {
5832 0 : const Dynamic* endDyn = rightHp->dynamicSnappedAfter();
5833 0 : if (endDyn
5834 0 : && !(endDyn->dynamicType() == DynamicType::OTHER || endDyn->dynamicType() >= DynamicType::FP)
5835 0 : && !rightHp->isLineType()) {
5836 0 : if (int(endDyn->dynamicType()) > int(dynamic->dynamicType())) {
5837 0 : rightHp->undoChangeProperty(Pid::HAIRPIN_TYPE, int(HairpinType::CRESC_HAIRPIN));
5838 : } else {
5839 0 : rightHp->undoChangeProperty(Pid::HAIRPIN_TYPE, int(HairpinType::DIM_HAIRPIN));
5840 : }
5841 : }
5842 : }
5843 0 : }
5844 :
5845 0 : void NotationInteraction::toggleDynamicPopup()
5846 : {
5847 0 : EngravingItem* el = selection()->element();
5848 0 : if (!el) {
5849 0 : return;
5850 : }
5851 0 : if (!el->isHairpinSegment()) {
5852 0 : addTextToItem(TextStyleType::DYNAMICS, el);
5853 0 : return;
5854 : }
5855 :
5856 0 : HairpinSegment* hairpinSeg = toHairpinSegment(el);
5857 0 : Hairpin* hairpin = hairpinSeg->hairpin();
5858 :
5859 0 : auto addDynamic = [this](Fraction tick, track_idx_t track, VoiceAssignment voiceAssignment) {
5860 0 : endEditElement();
5861 0 : startEdit(TranslatableString("undoableAction", "Add dynamic"));
5862 0 : Measure* measure = score()->tick2measure(tick);
5863 0 : Segment* segment = measure->undoGetChordRestOrTimeTickSegment(tick);
5864 0 : Dynamic* dynamic = Factory::createDynamic(segment);
5865 0 : dynamic->setOwnershipParent(segment);
5866 0 : dynamic->setTrack(track);
5867 0 : dynamic->setVoiceAssignment(voiceAssignment);
5868 0 : score()->undoAddElement(dynamic);
5869 0 : apply();
5870 0 : startEditText(dynamic);
5871 0 : };
5872 : // if the selected grip is at the left end, place the dynamic at the start
5873 0 : if (m_editData.curGrip == Grip::START || (m_editData.curGrip == Grip::APERTURE && hairpin->isDecrescendo())) {
5874 0 : if (EngravingItem* startDynOrExp = hairpinSeg->findElementToSnapBefore()) {
5875 : // If there is already a dynamic, select it instead of opening an empty popup
5876 0 : select({ startDynOrExp });
5877 0 : if (startDynOrExp->isDynamic()) {
5878 0 : autoFlipHairpinsType(toDynamic(startDynOrExp));
5879 : }
5880 : } else {
5881 0 : addDynamic(hairpin->tick(), hairpin->track(), hairpin->voiceAssignment());
5882 : }
5883 : } else {
5884 0 : if (EngravingItem* endDynOrExp = hairpinSeg->findElementToSnapAfter()) {
5885 : // If there is already a dynamic, select it instead of opening an empty popup
5886 0 : select({ endDynOrExp });
5887 0 : if (endDynOrExp->isDynamic()) {
5888 0 : autoFlipHairpinsType(toDynamic(endDynOrExp));
5889 : }
5890 : } else {
5891 0 : addDynamic(hairpin->tick2(), hairpin->track2(), hairpin->voiceAssignment());
5892 : }
5893 : }
5894 : }
5895 :
5896 0 : bool NotationInteraction::toggleLayoutBreakAvailable() const
5897 : {
5898 0 : return !selection()->isNone() && !isTextEditingStarted();
5899 : }
5900 :
5901 0 : void NotationInteraction::toggleLayoutBreak(LayoutBreakType breakType)
5902 : {
5903 0 : transaction(TranslatableString("undoableAction", "Toggle layout break"), [&](auto&) {
5904 0 : score()->cmdToggleLayoutBreak(breakType);
5905 0 : });
5906 0 : }
5907 :
5908 0 : void NotationInteraction::moveMeasureToPrevSystem()
5909 : {
5910 0 : MeasureBase* m = score()->selection().endMeasureBase();
5911 0 : if (!m) {
5912 0 : return;
5913 : }
5914 0 : transaction(TranslatableString("undoableAction", "Move measure to previous system"), [&](auto& tx) {
5915 0 : EditSystemLocks::moveMeasureToPrevSystem(tx, score(), m);
5916 0 : });
5917 : }
5918 :
5919 0 : void NotationInteraction::moveMeasureToNextSystem()
5920 : {
5921 0 : MeasureBase* m = score()->selection().startMeasureBase();
5922 0 : if (!m) {
5923 0 : return;
5924 : }
5925 0 : transaction(TranslatableString("undoableAction", "Move measure to next system"), [&](auto& tx) {
5926 0 : EditSystemLocks::moveMeasureToNextSystem(tx, score(), m);
5927 0 : });
5928 : }
5929 :
5930 0 : void NotationInteraction::toggleSystemLock()
5931 : {
5932 0 : transaction(TranslatableString("undoableAction", "Lock/unlock selected system(s)"), [&](auto& tx) {
5933 0 : EditSystemLocks::toggleSystemLock(tx, score(), selection()->selectedSystems());
5934 0 : });
5935 0 : }
5936 :
5937 0 : void NotationInteraction::toggleScoreLock()
5938 : {
5939 0 : transaction(TranslatableString("undoableAction", "Lock/unlock all systems"), [&](auto& tx) {
5940 0 : EditSystemLocks::toggleScoreLock(tx, score());
5941 0 : });
5942 0 : }
5943 :
5944 0 : void NotationInteraction::makeIntoSystem()
5945 : {
5946 0 : MeasureBase* first = score()->selection().startMeasureBase();
5947 0 : MeasureBase* last = score()->selection().endMeasureBase();
5948 0 : if (!first || !last) {
5949 0 : return;
5950 : }
5951 :
5952 0 : transaction(TranslatableString("undoableAction", "Create system from selection"), [&](auto& tx) {
5953 0 : EditSystemLocks::makeIntoSystem(tx, score(), first, last);
5954 0 : });
5955 : }
5956 :
5957 0 : void NotationInteraction::applySystemLock()
5958 : {
5959 0 : transaction(TranslatableString("undoableAction", "Apply system lock to selection"), [&](auto& tx) {
5960 0 : EditSystemLocks::applyLockToSelection(tx, score());
5961 0 : });
5962 0 : }
5963 :
5964 0 : void NotationInteraction::moveSystemToPrevPage()
5965 : {
5966 0 : MeasureBase* firstMeas = score()->selection().startMeasureBase();
5967 0 : MeasureBase* endMeas = score()->selection().endMeasureBase();
5968 0 : System* startSys = firstMeas ? firstMeas->system() : nullptr;
5969 0 : System* endSys = endMeas ? endMeas->system() : nullptr;
5970 0 : if (!startSys || !endSys) {
5971 0 : return;
5972 : }
5973 0 : MeasureBase* firstMb = startSys->first();
5974 0 : MeasureBase* lastMb = endSys->last();
5975 0 : transaction(TranslatableString("undoableAction", "Move system to previous page"), [&](auto& tx) {
5976 0 : EditPageLocks::moveMeasuresToPrevPage(tx, score(), firstMb, lastMb);
5977 0 : });
5978 : }
5979 :
5980 0 : void NotationInteraction::moveSystemToNextPage()
5981 : {
5982 0 : MeasureBase* firstMeas = score()->selection().startMeasureBase();
5983 0 : MeasureBase* endMeas = score()->selection().endMeasureBase();
5984 0 : System* startSys = firstMeas ? firstMeas->system() : nullptr;
5985 0 : System* endSys = endMeas ? endMeas->system() : nullptr;
5986 0 : if (!startSys || !endSys) {
5987 0 : return;
5988 : }
5989 0 : MeasureBase* firstMb = startSys->first();
5990 0 : MeasureBase* lastMb = endSys->last();
5991 0 : transaction(TranslatableString("undoableAction", "Move system to next page"), [&](auto& tx) {
5992 0 : EditPageLocks::moveMeasuresToNextPage(tx, score(), firstMb, lastMb);
5993 0 : });
5994 : }
5995 :
5996 0 : void NotationInteraction::togglePageLock()
5997 : {
5998 0 : transaction(TranslatableString("undoableAction", "Lock/unlock selected page(s)"), [&](auto& tx) {
5999 0 : EditPageLocks::togglePageLock(tx, score(), selection()->pagesContainingSelection());
6000 0 : });
6001 0 : }
6002 :
6003 0 : void NotationInteraction::makeIntoPage()
6004 : {
6005 0 : MeasureBase* first = score()->selection().startMeasureBase();
6006 0 : MeasureBase* last = score()->selection().endMeasureBase();
6007 0 : if (!first || !last) {
6008 0 : return;
6009 : }
6010 :
6011 0 : transaction(TranslatableString("undoableAction", "Create page from selection"), [&](auto& tx) {
6012 0 : EditPageLocks::makeIntoPage(tx, score(), first, last);
6013 0 : });
6014 : }
6015 :
6016 0 : void NotationInteraction::applyPageLock()
6017 : {
6018 0 : transaction(TranslatableString("undoableAction", "Apply page lock to selection"), [&](auto& tx) {
6019 0 : EditPageLocks::applyLockToSelection(tx, score());
6020 0 : });
6021 0 : }
6022 :
6023 0 : void NotationInteraction::addRemoveSystemLocks(AddRemoveSystemLockType intervalType, int interval)
6024 : {
6025 0 : interval = intervalType == AddRemoveSystemLockType::MeasuresInterval ? interval : 0;
6026 0 : bool afterEachSystem = intervalType == AddRemoveSystemLockType::AfterEachSystem;
6027 :
6028 0 : transaction(TranslatableString("undoableAction", "Measures per system"), [&](auto& tx) {
6029 0 : EditSystemLocks::addRemoveSystemLocks(tx, score(), interval, afterEachSystem);
6030 0 : });
6031 0 : }
6032 :
6033 0 : bool NotationInteraction::transpose(const TransposeOptions& options)
6034 : {
6035 0 : bool ok = false;
6036 :
6037 0 : transaction(TranslatableString("undoableAction", "Transposition"), [&](auto& tx) {
6038 0 : ok = Transpose::transpose(tx, score(), options.mode, options.direction, options.key, options.interval,
6039 0 : options.needTransposeKeys, options.needTransposeChordNames, options.needTransposeDoubleSharpsFlats);
6040 0 : });
6041 :
6042 0 : return ok;
6043 : }
6044 :
6045 0 : void NotationInteraction::swapVoices(voice_idx_t voiceIndex1, voice_idx_t voiceIndex2)
6046 : {
6047 0 : if (selection()->isNone()) {
6048 0 : return;
6049 : }
6050 :
6051 0 : if (voiceIndex1 == voiceIndex2) {
6052 0 : return;
6053 : }
6054 :
6055 0 : if (!isVoiceIndexValid(voiceIndex1) || !isVoiceIndexValid(voiceIndex2)) {
6056 0 : return;
6057 : }
6058 :
6059 0 : startEdit(TranslatableString("undoableAction", "Swap voices"));
6060 0 : ExchangeVoices::exchangeVoicesInSelection(score(), voiceIndex1, voiceIndex2);
6061 0 : apply();
6062 : }
6063 :
6064 0 : void NotationInteraction::addIntervalToSelectedNotes(int interval)
6065 : {
6066 0 : if (!isNotesIntervalValid(interval)) {
6067 0 : return;
6068 : }
6069 :
6070 0 : std::vector<Note*> notes;
6071 :
6072 0 : if (score()->selection().isRange()) {
6073 0 : const bool hasMultiNoteChords = score()->selection().rangeContainsMultiNoteChords();
6074 0 : for (const ChordRest* chordRest : score()->getSelectedChordRests()) {
6075 0 : if (!chordRest->isChord()) {
6076 0 : continue;
6077 : }
6078 0 : const Chord* chord = toChord(chordRest);
6079 0 : const std::vector<Note*> nl = chord->notes();
6080 0 : if (interval > 0) {
6081 0 : for (size_t noteIdx = nl.size(); noteIdx > 0; --noteIdx) {
6082 0 : if (score()->selectionFilter().canSelectNoteIdx(noteIdx - 1, nl.size(), hasMultiNoteChords)) {
6083 0 : notes.push_back(nl.at(noteIdx - 1));
6084 0 : break;
6085 : }
6086 : }
6087 : } else {
6088 0 : for (size_t noteIdx = 0; noteIdx < nl.size(); ++noteIdx) {
6089 0 : if (score()->selectionFilter().canSelectNoteIdx(noteIdx, nl.size(), hasMultiNoteChords)) {
6090 0 : notes.push_back(nl.at(noteIdx));
6091 0 : break;
6092 : }
6093 : }
6094 : }
6095 0 : }
6096 : } else {
6097 0 : notes = score()->selection().noteList();
6098 : }
6099 :
6100 0 : if (notes.empty()) {
6101 0 : MScore::setError(MsError::NO_NOTE_SELECTED);
6102 0 : checkAndShowError();
6103 0 : return;
6104 : }
6105 :
6106 0 : startEdit(TranslatableString("undoableAction", "Add interval"));
6107 0 : score()->addInterval(interval, notes);
6108 0 : apply();
6109 0 : }
6110 :
6111 0 : void NotationInteraction::addFret(int fretIndex)
6112 : {
6113 0 : transaction(TranslatableString("undoableAction", "Enter note at fret %1").arg(fretIndex), [&](auto& tx) {
6114 0 : NoteInput::addFret(tx, score(), fretIndex);
6115 0 : });
6116 :
6117 0 : notifyAboutNotationChanged();
6118 0 : }
6119 :
6120 0 : void NotationInteraction::changeSelectedElementsVoice(voice_idx_t voiceIndex)
6121 : {
6122 0 : if (selection()->isNone()) {
6123 0 : return;
6124 : }
6125 :
6126 0 : if (!isVoiceIndexValid(voiceIndex)) {
6127 0 : return;
6128 : }
6129 :
6130 0 : startEdit(TranslatableString("undoableAction", "Change voice"));
6131 0 : EditVoice::changeSelectedElementsVoice(score()->transactionManager()->currentOrDummyTransaction(), score(), voiceIndex);
6132 0 : apply();
6133 : }
6134 :
6135 0 : void NotationInteraction::changeSelectedElementsVoiceAssignment(VoiceAssignment voiceAssignment)
6136 : {
6137 0 : if (selection()->isNone()) {
6138 0 : return;
6139 : }
6140 :
6141 0 : startEdit(TranslatableString("undoableAction", "Change voice assignment"));
6142 0 : EditVoice::changeSelectedElementsVoiceAssignment(score()->transactionManager()->currentOrDummyTransaction(), score(),
6143 : voiceAssignment);
6144 0 : apply();
6145 : }
6146 :
6147 0 : void NotationInteraction::addAnchoredLineToSelectedNotes()
6148 : {
6149 0 : if (selection()->isNone()) {
6150 0 : return;
6151 : }
6152 :
6153 0 : startEdit(TranslatableString("undoableAction", "Add note-anchored line"));
6154 0 : score()->addNoteLine();
6155 0 : apply();
6156 : }
6157 :
6158 0 : void NotationInteraction::addTextToTopFrame(TextStyleType type)
6159 : {
6160 0 : addText(type);
6161 0 : }
6162 :
6163 0 : bool NotationInteraction::canAddTextToItem(TextStyleType type, const EngravingItem* item) const
6164 : {
6165 0 : if (isVerticalBoxTextStyle(type)) {
6166 0 : return item && item->isVBox();
6167 : }
6168 :
6169 0 : if (type == TextStyleType::FRAME) {
6170 0 : return item && item->isBox() ? muse::make_ok() : make_ret(Err::EmptySelection);
6171 : }
6172 :
6173 : static const std::set<TextStyleType> harmonyTypes {
6174 : TextStyleType::HARMONY_A,
6175 : TextStyleType::HARMONY_ROMAN,
6176 : TextStyleType::HARMONY_NASHVILLE,
6177 0 : };
6178 :
6179 0 : const bool isHarmony = muse::contains(harmonyTypes, type);
6180 0 : if (isHarmony && item && item->isFretDiagram()) {
6181 0 : return true;
6182 : }
6183 :
6184 : static const std::set<TextStyleType> needSelectNoteOrRestTypes {
6185 : TextStyleType::SYSTEM,
6186 : TextStyleType::STAFF,
6187 : TextStyleType::DYNAMICS,
6188 : TextStyleType::EXPRESSION,
6189 : TextStyleType::REHEARSAL_MARK,
6190 : TextStyleType::INSTRUMENT_CHANGE,
6191 : TextStyleType::FINGERING,
6192 : TextStyleType::LH_GUITAR_FINGERING,
6193 : TextStyleType::RH_GUITAR_FINGERING,
6194 : TextStyleType::STRING_NUMBER,
6195 : TextStyleType::STICKING,
6196 : TextStyleType::HARMONY_A,
6197 : TextStyleType::HARMONY_ROMAN,
6198 : TextStyleType::HARMONY_NASHVILLE,
6199 : TextStyleType::LYRICS_ODD,
6200 : TextStyleType::TEMPO,
6201 0 : };
6202 :
6203 0 : if (muse::contains(needSelectNoteOrRestTypes, type)) {
6204 : static const std::set<ElementType> requiredElementTypes {
6205 : ElementType::NOTE,
6206 : ElementType::REST,
6207 : ElementType::MMREST,
6208 : ElementType::MEASURE_REPEAT,
6209 : ElementType::CHORD,
6210 0 : };
6211 :
6212 0 : return item && muse::contains(requiredElementTypes, item->type());
6213 : }
6214 :
6215 0 : return true;
6216 : }
6217 :
6218 0 : void NotationInteraction::addTextToItem(TextStyleType type, EngravingItem* item)
6219 : {
6220 0 : if (!scoreHasMeasure()) {
6221 0 : LOGE() << "Need to create measure";
6222 0 : return;
6223 : }
6224 :
6225 0 : if (!canAddTextToItem(type, item)) {
6226 0 : MScore::setError(MsError::NO_NOTE_REST_SELECTED);
6227 0 : checkAndShowError();
6228 0 : return;
6229 : }
6230 :
6231 0 : addText(type, item);
6232 : }
6233 :
6234 0 : void NotationInteraction::addText(TextStyleType type, EngravingItem* item)
6235 : {
6236 0 : if (m_noteInput->isNoteInputMode()) {
6237 0 : m_noteInput->endNoteInput();
6238 : }
6239 :
6240 0 : startEdit(TranslatableString("undoableAction", "Add text"));
6241 0 : mu::engraving::TextBase* text = score()->addText(type, item);
6242 :
6243 0 : if (!text) {
6244 0 : rollback();
6245 0 : return;
6246 : }
6247 :
6248 0 : if (text->isInstrumentChange()) {
6249 0 : if (!selectInstrument(toInstrumentChange(text))) {
6250 0 : rollback();
6251 0 : return;
6252 : }
6253 : }
6254 :
6255 0 : if (text->hasVoiceAssignmentProperties()) {
6256 0 : text->setInitialTrackAndVoiceAssignment(item->track(), false);
6257 : }
6258 :
6259 0 : apply();
6260 0 : showItem(text);
6261 :
6262 0 : if (!text->isInstrumentChange()) {
6263 0 : startEditText(text);
6264 : }
6265 :
6266 0 : if (text->isRehearsalMark() || text->isTempoText()) {
6267 0 : text->cursor()->selectWord();
6268 : }
6269 : }
6270 :
6271 0 : Ret NotationInteraction::canAddImageToItem(const EngravingItem* item) const
6272 : {
6273 0 : return item && item->isMeasureBase();
6274 : }
6275 :
6276 0 : void NotationInteraction::addImageToItem(const muse::io::path_t& imagePath, EngravingItem* item)
6277 : {
6278 0 : if (imagePath.empty() || !item) {
6279 0 : return;
6280 : }
6281 :
6282 : static const std::map<muse::io::path_t, ImageType> suffixToType {
6283 0 : { "svg", ImageType::SVG },
6284 0 : { "svgz", ImageType::SVG },
6285 0 : { "jpg", ImageType::RASTER },
6286 0 : { "jpeg", ImageType::RASTER },
6287 0 : { "png", ImageType::RASTER },
6288 0 : { "bmp", ImageType::RASTER },
6289 0 : { "tif", ImageType::RASTER },
6290 0 : { "tiff", ImageType::RASTER },
6291 0 : };
6292 :
6293 0 : muse::io::path_t suffix = io::suffix(imagePath);
6294 :
6295 0 : ImageType type = muse::value(suffixToType, suffix, ImageType::NONE);
6296 0 : if (type == ImageType::NONE) {
6297 0 : return;
6298 : }
6299 :
6300 0 : Image* image = Factory::createImage(item);
6301 0 : image->setImageType(type);
6302 :
6303 0 : if (!image->loadFromFile(imagePath)) {
6304 0 : delete image;
6305 0 : return;
6306 : }
6307 :
6308 0 : startEdit(TranslatableString("undoableAction", "Add image"));
6309 0 : score()->undoAddElement(image);
6310 0 : apply();
6311 0 : }
6312 :
6313 0 : void NotationInteraction::addFiguredBass()
6314 : {
6315 : static const ElementTypeSet REQUIRED_TYPES {
6316 : ElementType::NOTE,
6317 : ElementType::FIGURED_BASS,
6318 : ElementType::REST
6319 0 : };
6320 :
6321 0 : if (!m_selection->elementsSelected(REQUIRED_TYPES)) {
6322 0 : MScore::setError(MsError::NO_NOTE_FIGUREDBASS_SELECTED);
6323 0 : checkAndShowError();
6324 0 : return;
6325 : }
6326 :
6327 0 : startEdit(TranslatableString("undoableAction", "Add figured bass"));
6328 0 : mu::engraving::FiguredBass* figuredBass = score()->addFiguredBass();
6329 :
6330 0 : if (figuredBass) {
6331 0 : apply();
6332 0 : startEditText(figuredBass, PointF());
6333 0 : m_selection->notifyAboutSelectionChangedIfNeed();
6334 : } else {
6335 0 : rollback();
6336 : }
6337 : }
6338 :
6339 0 : void NotationInteraction::addStretch(qreal value)
6340 : {
6341 0 : TranslatableString name = value >= 0
6342 0 : ? TranslatableString("undoableAction", "Increase layout stretch")
6343 0 : : TranslatableString("undoableAction", "Decrease layout stretch");
6344 0 : transaction(name, [&](Transaction& tx) {
6345 0 : mu::engraving::EditStretch::addStretch(tx, score(), value);
6346 0 : });
6347 0 : }
6348 :
6349 0 : Measure* NotationInteraction::selectedMeasure() const
6350 : {
6351 0 : INotationInteraction::HitElementContext context = hitElementContext();
6352 0 : mu::engraving::Measure* measure = context.element && context.element->isMeasure() ? mu::engraving::toMeasure(context.element) : nullptr;
6353 0 : if (measure) {
6354 0 : return measure;
6355 : }
6356 :
6357 0 : INotationSelectionPtr selection = this->selection();
6358 0 : if (selection->isRange()) {
6359 0 : MeasureBase* mb = selection->range()->measureBaseRange().endMeasureBase;
6360 0 : if (!mb) {
6361 0 : return nullptr;
6362 : }
6363 0 : return mb->isMeasure() ? toMeasure(mb) : mb->prevMeasure();
6364 : }
6365 :
6366 0 : if (selection->element()) {
6367 0 : return selection->element()->findMeasure();
6368 : }
6369 :
6370 0 : return nullptr;
6371 0 : }
6372 :
6373 0 : void NotationInteraction::addTimeSignature(Measure* measure, staff_idx_t staffIndex, TimeSignature* timeSignature)
6374 : {
6375 0 : startEdit(TranslatableString("undoableAction", "Add time signature"));
6376 0 : Transaction& tx = score()->transactionManager()->currentOrDummyTransaction();
6377 0 : EditTimeSig::addTimeSig(tx, score(), measure, staffIndex, timeSignature, true);
6378 0 : apply();
6379 0 : }
6380 :
6381 0 : void NotationInteraction::explodeSelectedStaff()
6382 : {
6383 0 : if (!selection()->isRange()) {
6384 0 : return;
6385 : }
6386 :
6387 0 : startEdit(TranslatableString("undoableAction", "Explode"));
6388 0 : if (ImplodeExplode::explode(score())) {
6389 0 : apply();
6390 : } else {
6391 0 : rollback();
6392 : }
6393 :
6394 0 : checkAndShowError();
6395 : }
6396 :
6397 0 : void NotationInteraction::implodeSelectedStaff()
6398 : {
6399 0 : if (!selection()->isRange()) {
6400 0 : return;
6401 : }
6402 :
6403 0 : startEdit(TranslatableString("undoableAction", "Implode"));
6404 0 : if (ImplodeExplode::implode(score())) {
6405 0 : apply();
6406 : } else {
6407 0 : rollback();
6408 : }
6409 :
6410 0 : checkAndShowError();
6411 : }
6412 :
6413 0 : void NotationInteraction::realizeSelectedChordSymbols(bool literal, Voicing voicing, HDuration durationType)
6414 : {
6415 0 : if (selection()->isNone()) {
6416 0 : return;
6417 : }
6418 :
6419 0 : startEdit(TranslatableString("undoableAction", "Realize chord symbols"));
6420 0 : RealizeChordSymbols::realizeChordSymbols(score()->transactionManager()->currentOrDummyTransaction(), score(), literal, voicing,
6421 : durationType);
6422 0 : apply();
6423 : }
6424 :
6425 0 : void NotationInteraction::extendToNextNote()
6426 : {
6427 0 : if (selection()->isNone()) {
6428 0 : return;
6429 : }
6430 :
6431 0 : m_undoStack->transaction(TranslatableString("undoableAction", "Extend to next note"), [&](mu::engraving::Transaction& tx) {
6432 0 : EditDuration::extendToNextNote(tx, score());
6433 0 : });
6434 0 : notifyAboutNotationChanged();
6435 : }
6436 :
6437 0 : void NotationInteraction::removeSelectedMeasures()
6438 : {
6439 0 : if (selection()->isNone()) {
6440 0 : return;
6441 : }
6442 :
6443 0 : mu::engraving::MeasureBase* firstMeasureBase = nullptr;
6444 0 : mu::engraving::MeasureBase* lastMeasureBase = nullptr;
6445 :
6446 0 : if (selection()->isRange()) {
6447 0 : INotationSelectionRange::MeasureBaseRange measureBaseRange = selection()->range()->measureBaseRange();
6448 0 : firstMeasureBase = measureBaseRange.startMeasureBase;
6449 0 : lastMeasureBase = measureBaseRange.endMeasureBase;
6450 : } else {
6451 0 : const std::vector<EngravingItem*>& elements = selection()->elements();
6452 0 : if (elements.empty()) {
6453 0 : return;
6454 : }
6455 :
6456 0 : for (EngravingItem* element : elements) {
6457 0 : mu::engraving::MeasureBase* elementMeasure = element->findMeasureBase();
6458 :
6459 0 : if (!firstMeasureBase || firstMeasureBase->index() > elementMeasure->index()) {
6460 0 : firstMeasureBase = elementMeasure;
6461 : }
6462 :
6463 0 : if (!lastMeasureBase || lastMeasureBase->index() < elementMeasure->index()) {
6464 0 : lastMeasureBase = elementMeasure;
6465 : }
6466 : }
6467 : }
6468 :
6469 0 : IF_ASSERT_FAILED(firstMeasureBase && lastMeasureBase) {
6470 0 : return;
6471 : }
6472 :
6473 0 : m_selection->select({ firstMeasureBase }, SelectType::REPLACE);
6474 0 : m_selection->select({ lastMeasureBase }, SelectType::RANGE);
6475 :
6476 : // Slight hack because measureIndex is only meaningful for Measure types (perhaps this property shouldn't
6477 : // exist on MeasureBase in the first place)...
6478 0 : const Measure* firstMeasure = firstMeasureBase->isMeasure() ? toMeasure(firstMeasureBase) : firstMeasureBase->nextMeasure();
6479 0 : const Measure* lastMeasure = lastMeasureBase->isMeasure() ? toMeasure(lastMeasureBase) : lastMeasureBase->prevMeasure();
6480 :
6481 0 : int numDeletedMeasures = 0;
6482 0 : if (firstMeasure && lastMeasure && firstMeasure->measureIndex() <= lastMeasure->measureIndex()) {
6483 0 : numDeletedMeasures = 1 + lastMeasure->measureIndex() - firstMeasure->measureIndex();
6484 : }
6485 :
6486 0 : startEdit(TranslatableString("undoableAction", "Delete %Ln measure(s)", nullptr, numDeletedMeasures));
6487 0 : score()->cmdTimeDelete();
6488 0 : apply();
6489 : }
6490 :
6491 0 : void NotationInteraction::removeSelectedRange()
6492 : {
6493 0 : if (selection()->isNone()) {
6494 0 : return;
6495 : }
6496 :
6497 0 : startEdit(TranslatableString("undoableAction", "Delete range"));
6498 0 : score()->cmdTimeDelete();
6499 0 : apply();
6500 : }
6501 :
6502 0 : void NotationInteraction::removeEmptyTrailingMeasures()
6503 : {
6504 0 : startEdit(TranslatableString("undoableAction", "Remove empty trailing measures"));
6505 0 : score()->cmdRemoveEmptyTrailingMeasures();
6506 0 : apply();
6507 0 : }
6508 :
6509 0 : void NotationInteraction::fillSelectionWithSlashes()
6510 : {
6511 0 : if (selection()->isNone()) {
6512 0 : return;
6513 : }
6514 :
6515 0 : transaction(TranslatableString("undoableAction", "Fill with slashes"), [&](auto& tx) {
6516 0 : EditSlashNotation::slashFill(tx, score());
6517 0 : });
6518 :
6519 0 : notifyAboutNotationChanged();
6520 : }
6521 :
6522 0 : void NotationInteraction::replaceSelectedNotesWithSlashes()
6523 : {
6524 0 : if (selection()->isNone()) {
6525 0 : return;
6526 : }
6527 :
6528 0 : transaction(TranslatableString("undoableAction", "Toggle rhythmic slash notation"), [&](auto& tx) {
6529 0 : EditSlashNotation::slashRhythm(tx, score());
6530 0 : });
6531 :
6532 0 : notifyAboutNotationChanged();
6533 : }
6534 :
6535 0 : void NotationInteraction::changeEnharmonicSpelling(bool both)
6536 : {
6537 0 : transaction(TranslatableString("undoableAction", "Change enharmonic spelling"), [&](auto&) {
6538 0 : EditEnharmonicSpelling::changeEnharmonicSpelling(score(), both);
6539 0 : });
6540 :
6541 0 : notifyAboutNotationChanged();
6542 0 : }
6543 :
6544 0 : void NotationInteraction::spellPitches()
6545 : {
6546 0 : transaction(TranslatableString("undoableAction", "Optimize enharmonic spelling"), [&](auto&) {
6547 0 : EditEnharmonicSpelling::spell(score());
6548 0 : });
6549 :
6550 0 : notifyAboutNotationChanged();
6551 0 : }
6552 :
6553 0 : void NotationInteraction::spellPitchesWithSharps()
6554 : {
6555 0 : transaction(TranslatableString("undoableAction", "Respell pitches with sharps"), [&](auto&) {
6556 0 : EditEnharmonicSpelling::spellWithSharpsOrFlats(score(), Prefer::SHARPS);
6557 0 : });
6558 :
6559 0 : notifyAboutNotationChanged();
6560 0 : }
6561 :
6562 0 : void NotationInteraction::spellPitchesWithFlats()
6563 : {
6564 0 : transaction(TranslatableString("undoableAction", "Respell pitches with flats"), [&](auto&) {
6565 0 : EditEnharmonicSpelling::spellWithSharpsOrFlats(score(), Prefer::FLATS);
6566 0 : });
6567 :
6568 0 : notifyAboutNotationChanged();
6569 0 : }
6570 :
6571 0 : void NotationInteraction::regroupNotesAndRests()
6572 : {
6573 0 : transaction(TranslatableString("undoableAction", "Regroup rhythms"), [&](Transaction& tx) {
6574 0 : Regroup::regroupNotesAndRestsInSelection(tx, score());
6575 0 : });
6576 :
6577 0 : notifyAboutNotationChanged();
6578 0 : }
6579 :
6580 0 : void NotationInteraction::resequenceRehearsalMarks()
6581 : {
6582 0 : transaction(TranslatableString("undoableAction", "Resequence rehearsal marks"), [&](Transaction& tx) {
6583 0 : EditRehearsalMark::resequenceRehearsalMarks(tx, score());
6584 0 : });
6585 :
6586 0 : notifyAboutNotationChanged();
6587 0 : }
6588 :
6589 0 : void NotationInteraction::resetStretch()
6590 : {
6591 0 : transaction(TranslatableString("undoableAction", "Reset layout stretch"), [&](Transaction& tx) {
6592 0 : mu::engraving::EditStretch::resetUserStretch(tx, score());
6593 0 : });
6594 0 : }
6595 :
6596 0 : void NotationInteraction::resetTextStyleOverrides()
6597 : {
6598 0 : startEdit(TranslatableString("undoableAction", "Reset text style overrides"));
6599 0 : Reset::resetTextStyleOverrides(score());
6600 0 : apply();
6601 0 : }
6602 :
6603 0 : void NotationInteraction::resetBeamMode()
6604 : {
6605 0 : transaction(TranslatableString("undoableAction", "Reset beams"), [&](Transaction& tx) {
6606 0 : EditBeam::resetBeamMode(tx, score());
6607 0 : });
6608 0 : notifyAboutNotationChanged();
6609 0 : }
6610 :
6611 0 : void NotationInteraction::resetShapesAndPosition()
6612 : {
6613 0 : auto resetItem = [](EngravingItem* item) {
6614 0 : item->reset();
6615 :
6616 0 : if (item->isSpanner()) {
6617 0 : for (mu::engraving::SpannerSegment* spannerSegment : toSpanner(item)->spannerSegments()) {
6618 0 : spannerSegment->reset();
6619 : }
6620 : }
6621 0 : };
6622 :
6623 0 : startEdit(TranslatableString("undoableAction", "Reset shapes and positions"));
6624 :
6625 0 : DEFER {
6626 0 : apply();
6627 0 : };
6628 :
6629 0 : if (selection()->element()) {
6630 0 : resetItem(selection()->element());
6631 0 : return;
6632 : }
6633 :
6634 0 : for (EngravingItem* item : selection()->elements()) {
6635 0 : resetItem(item);
6636 : }
6637 0 : }
6638 :
6639 0 : void NotationInteraction::freezeSelectionPosition()
6640 : {
6641 0 : std::vector<EngravingItem*> items = selection()->elements();
6642 0 : if (items.empty()) {
6643 0 : return;
6644 : }
6645 :
6646 0 : transaction(TranslatableString("undoableAction", "Freeze current placement"), [&](mu::engraving::Transaction& tx) {
6647 0 : EditPosition::freezeItemsPositions(tx, items);
6648 0 : });
6649 :
6650 0 : notifyAboutNotationChanged();
6651 0 : }
6652 :
6653 0 : void NotationInteraction::resetToDefaultLayout()
6654 : {
6655 0 : TRACEFUNC;
6656 :
6657 0 : startEdit(TranslatableString("undoableAction", "Reset to default layout"));
6658 0 : Reset::resetToDefaultLayout(score()->transactionManager()->currentOrDummyTransaction(), score());
6659 0 : apply();
6660 0 : }
6661 :
6662 0 : ScoreConfig NotationInteraction::scoreConfig() const
6663 : {
6664 0 : ScoreConfig config;
6665 0 : config.setShown(ScoreConfigType::ShowInvisibleElements, score()->isShowInvisible());
6666 0 : config.setShown(ScoreConfigType::ShowUnprintableElements, score()->showUnprintable());
6667 0 : config.setShown(ScoreConfigType::ShowFrames, score()->showFrames());
6668 0 : config.setShown(ScoreConfigType::ShowPageMargins, score()->showPageborders());
6669 0 : config.setShown(ScoreConfigType::ShowSoundFlags, score()->showSoundFlags());
6670 0 : config.setShown(ScoreConfigType::MarkIrregularMeasures, score()->markIrregularMeasures());
6671 0 : return config;
6672 0 : }
6673 :
6674 0 : void NotationInteraction::setScoreConfig(const ScoreConfig& config)
6675 : {
6676 0 : const ScoreConfig oldConfig = scoreConfig();
6677 0 : if (oldConfig == config) {
6678 0 : return;
6679 : }
6680 :
6681 : // TODO: refactor together with `NotationActionController::toggleScoreConfig`
6682 : // to be able to give a more precise name
6683 0 : startEdit(TranslatableString("undoableAction", "Set score view settings"));
6684 0 : score()->setShowInvisible(config.isShown(ScoreConfigType::ShowInvisibleElements));
6685 0 : score()->setShowUnprintable(config.isShown(ScoreConfigType::ShowUnprintableElements));
6686 0 : score()->setShowFrames(config.isShown(ScoreConfigType::ShowFrames));
6687 0 : score()->setShowPageborders(config.isShown(ScoreConfigType::ShowPageMargins));
6688 0 : score()->setShowSoundFlags(config.isShown(ScoreConfigType::ShowSoundFlags));
6689 0 : score()->setMarkIrregularMeasures(config.isShown(ScoreConfigType::MarkIrregularMeasures));
6690 :
6691 0 : EngravingItem* selectedElement = selection()->element();
6692 0 : if (selectedElement && !selectedElement->isInteractionAvailable()) {
6693 0 : clearSelection();
6694 : }
6695 :
6696 0 : apply();
6697 :
6698 0 : for (const auto& p : oldConfig.config) {
6699 0 : if (p.second != config.isShown(p.first)) {
6700 0 : m_scoreConfigChanged.send(p.first);
6701 : }
6702 : }
6703 0 : }
6704 :
6705 0 : muse::async::Channel<ScoreConfigType> NotationInteraction::scoreConfigChanged() const
6706 : {
6707 0 : return m_scoreConfigChanged;
6708 : }
6709 :
6710 0 : bool NotationInteraction::needEndTextEditing(const std::vector<EngravingItem*>& newSelectedElements) const
6711 : {
6712 0 : if (!isTextEditingStarted()) {
6713 0 : return false;
6714 : }
6715 :
6716 0 : if (newSelectedElements.empty()) {
6717 0 : return false;
6718 : }
6719 :
6720 0 : if (newSelectedElements.size() > 1) {
6721 0 : return true;
6722 : }
6723 :
6724 0 : if (m_editData.element && m_editData.element->isStaffText()) {
6725 0 : EngravingItem* element = newSelectedElements.front();
6726 0 : if (element && element->isSoundFlag() && element->ownershipParent() == m_editData.element) {
6727 0 : return false;
6728 : }
6729 : }
6730 :
6731 0 : return newSelectedElements.front() != m_editData.element;
6732 : }
6733 :
6734 0 : bool NotationInteraction::needEndElementEditing(const std::vector<EngravingItem*>& newSelectedElements) const
6735 : {
6736 0 : if (!isEditingElement()) {
6737 0 : return false;
6738 : }
6739 :
6740 0 : if (newSelectedElements.size() != 1) {
6741 0 : return true;
6742 : }
6743 :
6744 0 : if (m_editData.element && m_editData.element->isStaffText()) {
6745 0 : EngravingItem* element = newSelectedElements.front();
6746 0 : if (element && element->isSoundFlag() && element->ownershipParent() == m_editData.element) {
6747 0 : return false;
6748 : }
6749 : }
6750 :
6751 0 : return newSelectedElements.front() != score()->selection().element();
6752 : }
6753 :
6754 0 : void NotationInteraction::resetGripEdit()
6755 : {
6756 0 : m_editData.grips = 0;
6757 0 : m_editData.curGrip = mu::engraving::Grip::NO_GRIP;
6758 0 : m_editData.grip.clear();
6759 :
6760 0 : resetAnchorLines();
6761 0 : }
6762 :
6763 0 : void NotationInteraction::resetHitElementContext()
6764 : {
6765 0 : setHitElementContext(HitElementContext());
6766 0 : }
6767 :
6768 : //! NOTE: Copied from ScoreView::lyricsTab
6769 0 : void NotationInteraction::navigateToLyrics(bool back, bool moveOnly, bool end)
6770 : {
6771 0 : if (!m_editData.element || !m_editData.element->isLyrics()) {
6772 0 : LOGW("nextLyric called with invalid current element");
6773 0 : return;
6774 : }
6775 0 : mu::engraving::Lyrics* lyrics = toLyrics(m_editData.element);
6776 0 : track_idx_t track = lyrics->track();
6777 0 : mu::engraving::Segment* segment = lyrics->segment();
6778 0 : int verse = lyrics->verse();
6779 0 : mu::engraving::PlacementV placement = lyrics->placement();
6780 0 : mu::engraving::PropertyFlags pFlags = lyrics->propertyFlags(mu::engraving::Pid::PLACEMENT);
6781 0 : mu::engraving::FontStyle fStyle = lyrics->fontStyle();
6782 0 : mu::engraving::PropertyFlags fFlags = lyrics->propertyFlags(mu::engraving::Pid::FONT_STYLE);
6783 0 : mu::engraving::AutoOnOff centering = lyrics->centerBetweenStaves();
6784 0 : mu::engraving::TextStyleType styleType = lyrics->textStyleType();
6785 0 : double yOffset = lyrics->offset().y();
6786 :
6787 0 : mu::engraving::Segment* nextSegment = segment;
6788 0 : if (back) {
6789 : // search prev chord
6790 0 : while ((nextSegment = nextSegment->prev1(mu::engraving::SegmentType::ChordRest))) {
6791 0 : EngravingItem* el = nextSegment->element(track);
6792 0 : if (!el) {
6793 0 : continue;
6794 : }
6795 0 : if (el->isChord()) {
6796 0 : break;
6797 0 : } else if (el->isRest()) {
6798 0 : mu::engraving::Lyrics* nextLyrics = toChordRest(el)->lyrics(verse, placement);
6799 0 : if (nextLyrics) {
6800 0 : break;
6801 : }
6802 : }
6803 : }
6804 : } else {
6805 : // search next chord
6806 0 : while ((nextSegment = nextSegment->next1(mu::engraving::SegmentType::ChordRest))) {
6807 0 : EngravingItem* el = nextSegment->element(track);
6808 0 : if (!el) {
6809 0 : continue;
6810 : }
6811 0 : if (el->isChord()) {
6812 0 : break;
6813 0 : } else if (el->isRest()) {
6814 0 : mu::engraving::Lyrics* nextLyrics = toChordRest(el)->lyrics(verse, placement);
6815 0 : if (nextLyrics) {
6816 0 : break;
6817 : }
6818 : }
6819 : }
6820 : }
6821 0 : if (!nextSegment) {
6822 0 : return;
6823 : }
6824 :
6825 0 : endEditText();
6826 :
6827 : // look for the lyrics we are moving from; may be the current lyrics or a previous one
6828 : // if we are skipping several chords with spaces
6829 0 : mu::engraving::Lyrics* fromLyrics = 0;
6830 0 : if (!back) {
6831 0 : while (segment) {
6832 0 : ChordRest* cr = toChordRest(segment->element(track));
6833 0 : if (cr) {
6834 0 : fromLyrics = cr->lyrics(verse, placement);
6835 0 : if (fromLyrics) {
6836 0 : break;
6837 : }
6838 : }
6839 0 : segment = segment->prev1(mu::engraving::SegmentType::ChordRest);
6840 : }
6841 : }
6842 :
6843 0 : ChordRest* cr = toChordRest(nextSegment->element(track));
6844 0 : if (!cr) {
6845 0 : LOGD("no next lyrics list: %s", nextSegment->element(track)->typeName());
6846 0 : return;
6847 : }
6848 0 : mu::engraving::Lyrics* nextLyrics = cr->lyrics(verse, placement);
6849 :
6850 0 : bool newLyrics = false;
6851 0 : if (!nextLyrics) {
6852 0 : nextLyrics = Factory::createLyrics(cr);
6853 0 : nextLyrics->setTrack(track);
6854 0 : cr = toChordRest(nextSegment->element(track));
6855 0 : nextLyrics->setOwnershipParent(cr);
6856 0 : nextLyrics->setVerse(verse);
6857 0 : nextLyrics->setTextStyleType(styleType);
6858 0 : nextLyrics->setPlacement(placement);
6859 0 : nextLyrics->setPropertyFlags(mu::engraving::Pid::PLACEMENT, pFlags);
6860 0 : nextLyrics->setCenterBetweenStaves(centering);
6861 0 : nextLyrics->setSyllabic(mu::engraving::LyricsSyllabic::SINGLE);
6862 0 : nextLyrics->setFontStyle(fStyle);
6863 0 : nextLyrics->setPropertyFlags(mu::engraving::Pid::FONT_STYLE, fFlags);
6864 0 : nextLyrics->setOffset(PointF(0.0, yOffset));
6865 0 : newLyrics = true;
6866 : }
6867 :
6868 0 : score()->startCmd(TranslatableString("undoableAction", "Navigate to lyrics"));
6869 0 : if (fromLyrics && !moveOnly) {
6870 0 : switch (nextLyrics->syllabic()) {
6871 : // as we arrived at nextLyrics by a [Space], it can be the beginning
6872 : // of a multi-syllable, but cannot have syllabic dashes before
6873 0 : case mu::engraving::LyricsSyllabic::SINGLE:
6874 : case mu::engraving::LyricsSyllabic::BEGIN:
6875 0 : break;
6876 0 : case mu::engraving::LyricsSyllabic::END:
6877 0 : nextLyrics->undoChangeProperty(mu::engraving::Pid::SYLLABIC, int(mu::engraving::LyricsSyllabic::SINGLE));
6878 0 : break;
6879 0 : case mu::engraving::LyricsSyllabic::MIDDLE:
6880 0 : nextLyrics->undoChangeProperty(mu::engraving::Pid::SYLLABIC, int(mu::engraving::LyricsSyllabic::BEGIN));
6881 0 : break;
6882 : }
6883 : // as we moved away from fromLyrics by a [Space], it can be
6884 : // the end of a multi-syllable, but cannot have syllabic dashes after
6885 0 : switch (fromLyrics->syllabic()) {
6886 0 : case mu::engraving::LyricsSyllabic::SINGLE:
6887 : case mu::engraving::LyricsSyllabic::END:
6888 0 : break;
6889 0 : case mu::engraving::LyricsSyllabic::BEGIN:
6890 0 : fromLyrics->undoChangeProperty(mu::engraving::Pid::SYLLABIC, int(mu::engraving::LyricsSyllabic::SINGLE));
6891 0 : break;
6892 0 : case mu::engraving::LyricsSyllabic::MIDDLE:
6893 0 : fromLyrics->undoChangeProperty(mu::engraving::Pid::SYLLABIC, int(mu::engraving::LyricsSyllabic::END));
6894 0 : break;
6895 : }
6896 : // for the same reason, it cannot have a melisma
6897 0 : if (fromLyrics->separator() && !fromLyrics->separator()->isEndMelisma()) {
6898 0 : fromLyrics->undoChangeProperty(mu::engraving::Pid::LYRIC_TICKS, Fraction::fromTicks(0));
6899 : }
6900 : }
6901 :
6902 0 : if (newLyrics) {
6903 0 : score()->undoAddElement(nextLyrics);
6904 : }
6905 0 : score()->endCmd();
6906 0 : score()->select(nextLyrics, SelectType::SINGLE, 0);
6907 0 : score()->setLayoutAll();
6908 :
6909 0 : startEditText(nextLyrics, PointF());
6910 :
6911 0 : if (end) {
6912 0 : nextLyrics->selectAll();
6913 0 : } else if (!newLyrics) {
6914 0 : mu::engraving::TextCursor* cursor = nextLyrics->cursor();
6915 0 : cursor->movePosition(mu::engraving::TextCursor::MoveOperation::End, mu::engraving::TextCursor::MoveMode::MoveAnchor);
6916 0 : cursor->movePosition(mu::engraving::TextCursor::MoveOperation::Start, mu::engraving::TextCursor::MoveMode::KeepAnchor);
6917 : }
6918 :
6919 0 : showItem(nextLyrics);
6920 : }
6921 :
6922 0 : void NotationInteraction::navigateToLyrics(MoveDirection direction, bool moveOnly)
6923 : {
6924 0 : navigateToLyrics(direction == MoveDirection::Left, moveOnly, false);
6925 0 : }
6926 :
6927 : //! NOTE: Copied from ScoreView::lyricsTab
6928 0 : void NotationInteraction::navigateToNextSyllable()
6929 : {
6930 0 : if (!m_editData.element || !m_editData.element->isLyrics()) {
6931 0 : LOGW("nextSyllable called with invalid current element");
6932 0 : return;
6933 : }
6934 0 : Lyrics* lyrics = toLyrics(m_editData.element);
6935 0 : ChordRest* initialCR = lyrics->chordRest();
6936 0 : const bool hasPrecedingRepeat = initialCR->hasPrecedingJumpItem();
6937 0 : const bool hasFollowingRepeat = initialCR->hasFollowingJumpItem();
6938 0 : track_idx_t track = lyrics->track();
6939 0 : track_idx_t toLyricTrack = track;
6940 0 : Segment* segment = lyrics->segment();
6941 0 : int verse = lyrics->verse();
6942 0 : PlacementV placement = lyrics->placement();
6943 0 : PropertyFlags pFlags = lyrics->propertyFlags(Pid::PLACEMENT);
6944 0 : FontStyle fStyle = lyrics->fontStyle();
6945 0 : PropertyFlags fFlags = lyrics->propertyFlags(Pid::FONT_STYLE);
6946 0 : AutoOnOff centering = lyrics->centerBetweenStaves();
6947 0 : mu::engraving::TextStyleType styleType = lyrics->textStyleType();
6948 0 : double yOffset = lyrics->offset().y();
6949 :
6950 : // search next chord
6951 0 : Segment* nextSegment = segment;
6952 0 : while ((nextSegment = nextSegment->next1(SegmentType::ChordRest))) {
6953 0 : EngravingItem* el = nextSegment->element(track);
6954 0 : if (!el || !el->isChord()) {
6955 0 : const track_idx_t strack = trackZeroVoice(track);
6956 0 : const track_idx_t etrack = strack + VOICES;
6957 0 : for (track_idx_t t = strack; t < etrack; ++t) {
6958 0 : el = nextSegment->element(t);
6959 0 : if (el && el->isChord() && toChord(el)->lyrics(verse, placement)) {
6960 0 : toLyricTrack = t;
6961 0 : break;
6962 : }
6963 : }
6964 : }
6965 :
6966 0 : if (el && el->isChord()) {
6967 0 : break;
6968 : }
6969 : }
6970 :
6971 0 : if (!segmentsAreAdjacent(segment, nextSegment)) {
6972 0 : nextSegment = nullptr;
6973 : }
6974 :
6975 0 : if (!nextSegment && !hasFollowingRepeat && !hasPrecedingRepeat) {
6976 0 : return;
6977 : }
6978 :
6979 0 : PartialLyricsLine* prevPartialLyricsLine = findPrevPartialLyricsLineDash(lyrics);
6980 :
6981 0 : endEditText();
6982 :
6983 : // look for the lyrics we are moving from; may be the current lyrics or a previous one
6984 : // we are extending with several dashes
6985 0 : Lyrics* fromLyrics = nullptr;
6986 0 : Segment* curSeg = segment;
6987 0 : while (segment && segmentsAreAdjacent(segment, curSeg)) {
6988 0 : ChordRest* cr = toChordRest(segment->element(track));
6989 0 : if (!cr) {
6990 0 : segment = segment->prev1(SegmentType::ChordRest);
6991 0 : continue;
6992 : }
6993 0 : fromLyrics = cr->lyrics(verse, placement);
6994 0 : if (fromLyrics) {
6995 0 : break;
6996 : }
6997 0 : segment = segment->prev1(SegmentType::ChordRest);
6998 : }
6999 :
7000 0 : if (!nextSegment) {
7001 0 : score()->startCmd(TranslatableString("undoableAction", "Navigate to next syllable"));
7002 0 : if (fromLyrics && hasFollowingRepeat) {
7003 : // Allow dash with no end syllable if there is a repeat
7004 0 : switch (fromLyrics->syllabic()) {
7005 0 : case LyricsSyllabic::BEGIN:
7006 : case LyricsSyllabic::MIDDLE:
7007 0 : break;
7008 0 : case LyricsSyllabic::SINGLE:
7009 0 : fromLyrics->undoChangeProperty(Pid::SYLLABIC, int(LyricsSyllabic::BEGIN));
7010 0 : break;
7011 0 : case LyricsSyllabic::END:
7012 0 : fromLyrics->undoChangeProperty(Pid::SYLLABIC, int(LyricsSyllabic::MIDDLE));
7013 0 : break;
7014 : }
7015 0 : fromLyrics->undoChangeProperty(Pid::LYRIC_TICKS, Fraction(0, 1));
7016 0 : score()->setLayoutAll();
7017 0 : score()->endCmd();
7018 0 : return;
7019 : }
7020 :
7021 0 : if (hasPrecedingRepeat) {
7022 0 : score()->endCmd();
7023 0 : score()->startCmd(TranslatableString("undoableAction", "Add partial lyrics dash"));
7024 : // No from lyrics - create incoming partial dash
7025 0 : PartialLyricsLine* dash = Factory::createPartialLyricsLine(score()->dummy());
7026 0 : dash->setIsEndMelisma(false);
7027 0 : dash->setVerse(verse);
7028 0 : dash->setPlacement(placement);
7029 0 : dash->setPropertyFlags(Pid::PLACEMENT, pFlags);
7030 0 : dash->setCenterBetweenStaves(centering);
7031 0 : dash->setTick(initialCR->tick());
7032 0 : dash->setTicks(Fraction(0, 1));
7033 0 : dash->setTrack(initialCR->track());
7034 0 : dash->setTrack2(initialCR->track());
7035 :
7036 0 : score()->undoAddElement(dash);
7037 :
7038 : // Don't advance cursor
7039 :
7040 0 : Lyrics* toLyrics = Factory::createLyrics(initialCR);
7041 0 : toLyrics->setTrack(track);
7042 0 : toLyrics->setVerse(verse);
7043 0 : toLyrics->setTextStyleType(styleType);
7044 0 : toLyrics->setPlacement(placement);
7045 0 : toLyrics->setPropertyFlags(Pid::PLACEMENT, pFlags);
7046 0 : toLyrics->setCenterBetweenStaves(centering);
7047 0 : toLyrics->setSyllabic(LyricsSyllabic::END);
7048 0 : toLyrics->setFontStyle(fStyle);
7049 0 : toLyrics->setPropertyFlags(Pid::FONT_STYLE, fFlags);
7050 0 : toLyrics->setOffset(PointF(0.0, yOffset));
7051 :
7052 0 : score()->undoAddElement(toLyrics);
7053 0 : score()->endCmd();
7054 0 : score()->select(toLyrics, SelectType::SINGLE, 0);
7055 0 : score()->setLayoutAll();
7056 0 : startEditText(toLyrics, PointF());
7057 0 : toLyrics->selectAll();
7058 0 : showItem(toLyrics);
7059 :
7060 0 : return;
7061 : }
7062 : }
7063 :
7064 0 : score()->startCmd(TranslatableString("undoableAction", "Navigate to next syllable"));
7065 0 : ChordRest* cr = nextSegment ? toChordRest(nextSegment->element(toLyricTrack)) : nullptr;
7066 0 : Lyrics* toLyrics = cr ? cr->lyrics(verse, placement) : nullptr;
7067 :
7068 0 : if (!toLyrics && !cr) {
7069 0 : return;
7070 : }
7071 :
7072 : // If no lyrics in current track, check others
7073 0 : if (!toLyrics) {
7074 0 : const track_idx_t strack = trackZeroVoice(track);
7075 0 : const track_idx_t etrack = strack + VOICES;
7076 0 : for (track_idx_t t = strack; t < etrack; ++t) {
7077 0 : if (t == track) {
7078 0 : continue;
7079 : }
7080 0 : cr = toChordRest(nextSegment->element(t));
7081 0 : if (cr) {
7082 0 : toLyrics = cr->lyrics(verse, placement);
7083 0 : if (toLyrics) {
7084 0 : break;
7085 : }
7086 : }
7087 : }
7088 : }
7089 :
7090 : // This will be a partial dash at the start of a measure after a repeat
7091 : // We don't want toLyrics
7092 0 : toLyrics = hasPrecedingRepeat && !fromLyrics ? nullptr : toLyrics;
7093 :
7094 : // Make sure we end up with either the cr of toLyrics or cr on correct track
7095 0 : cr = !toLyrics ? toChordRest(nextSegment->element(track)) : cr;
7096 :
7097 : // Disallow dashes between non-adjacent repeat sections eg. 1st volta -> 2nd volta
7098 : // Instead, try to add partial dashes
7099 0 : if (cr && fromLyrics) {
7100 0 : Measure* toLyricsMeasure = cr->measure();
7101 0 : Measure* fromLyricsMeasure = fromLyrics->measure();
7102 :
7103 0 : if (toLyricsMeasure != fromLyricsMeasure && fromLyricsMeasure->lastChordRest(track)->hasFollowingJumpItem()) {
7104 0 : const std::vector<Measure*> previousRepeats = findPreviousRepeatMeasures(toLyricsMeasure);
7105 0 : const bool inPrecedingRepeatSeg = muse::contains(previousRepeats, fromLyricsMeasure);
7106 0 : if (!previousRepeats.empty() && !inPrecedingRepeatSeg) {
7107 0 : fromLyrics = nullptr;
7108 : }
7109 0 : }
7110 : }
7111 :
7112 0 : const bool startIncomingPartialDash = hasPrecedingRepeat && !fromLyrics
7113 0 : && !prevPartialLyricsLine && lyrics->xmlText().empty();
7114 :
7115 0 : const bool newLyrics = !toLyrics || startIncomingPartialDash;
7116 0 : if (newLyrics) {
7117 : /* Don't advance the cursor when starting an incoming partial dash after a repeat,
7118 : * i.e. when there is no adjacent preceding syllable to dash from: */
7119 0 : ChordRest* toLyricsChord = startIncomingPartialDash ? initialCR : cr;
7120 :
7121 0 : toLyrics = Factory::createLyrics(toLyricsChord);
7122 0 : toLyrics->setTrack(track);
7123 :
7124 0 : toLyrics->setVerse(verse);
7125 0 : toLyrics->setTextStyleType(styleType);
7126 :
7127 0 : toLyrics->setPlacement(placement);
7128 0 : toLyrics->setPropertyFlags(Pid::PLACEMENT, pFlags);
7129 0 : toLyrics->setCenterBetweenStaves(centering);
7130 0 : toLyrics->setSyllabic(LyricsSyllabic::END);
7131 0 : toLyrics->setFontStyle(fStyle);
7132 0 : toLyrics->setPropertyFlags(Pid::FONT_STYLE, fFlags);
7133 0 : toLyrics->setOffset(PointF(0.0, yOffset));
7134 : } else {
7135 : // as we arrived at toLyrics by a dash, it cannot be initial or isolated
7136 0 : if (toLyrics->syllabic() == LyricsSyllabic::BEGIN) {
7137 0 : toLyrics->undoChangeProperty(Pid::SYLLABIC, int(LyricsSyllabic::MIDDLE));
7138 0 : } else if (toLyrics->syllabic() == LyricsSyllabic::SINGLE) {
7139 0 : toLyrics->undoChangeProperty(Pid::SYLLABIC, int(LyricsSyllabic::END));
7140 : }
7141 : }
7142 :
7143 0 : if (fromLyrics) {
7144 : // as we moved away from fromLyrics by a dash,
7145 : // it can have syll. dashes before and after but cannot be isolated or terminal
7146 0 : switch (fromLyrics->syllabic()) {
7147 0 : case LyricsSyllabic::BEGIN:
7148 : case LyricsSyllabic::MIDDLE:
7149 0 : break;
7150 0 : case LyricsSyllabic::SINGLE:
7151 0 : fromLyrics->undoChangeProperty(Pid::SYLLABIC, int(LyricsSyllabic::BEGIN));
7152 0 : break;
7153 0 : case LyricsSyllabic::END:
7154 0 : fromLyrics->undoChangeProperty(Pid::SYLLABIC, int(LyricsSyllabic::MIDDLE));
7155 0 : break;
7156 : }
7157 : // for the same reason, it cannot have a melisma
7158 0 : fromLyrics->undoChangeProperty(Pid::LYRIC_TICKS, Fraction(0, 1));
7159 0 : } else if (hasPrecedingRepeat && !prevPartialLyricsLine) {
7160 : // No from lyrics - create incoming partial dash
7161 0 : PartialLyricsLine* dash = Factory::createPartialLyricsLine(score()->dummy());
7162 0 : dash->setIsEndMelisma(false);
7163 0 : dash->setVerse(verse);
7164 0 : dash->setPlacement(placement);
7165 0 : dash->setPropertyFlags(Pid::PLACEMENT, pFlags);
7166 0 : dash->setCenterBetweenStaves(centering);
7167 0 : dash->setTick(initialCR->tick());
7168 0 : dash->setTicks(hasPrecedingRepeat ? Fraction(0, 1) : initialCR->ticks());
7169 0 : dash->setTrack(initialCR->track());
7170 0 : dash->setTrack2(initialCR->track());
7171 :
7172 0 : score()->undoAddElement(dash);
7173 0 : } else if (prevPartialLyricsLine) {
7174 0 : const Fraction tickDiff = cr->tick() - prevPartialLyricsLine->tick2();
7175 0 : prevPartialLyricsLine->undoMoveEnd(tickDiff);
7176 0 : score()->undoAddElement(prevPartialLyricsLine);
7177 0 : prevPartialLyricsLine->triggerLayout();
7178 : }
7179 :
7180 0 : if (newLyrics) {
7181 0 : score()->undoAddElement(toLyrics);
7182 : }
7183 :
7184 0 : score()->endCmd();
7185 0 : score()->select(toLyrics, SelectType::SINGLE, 0);
7186 0 : score()->setLayoutAll();
7187 :
7188 0 : startEditText(toLyrics, PointF());
7189 :
7190 0 : toLyrics->selectAll();
7191 0 : showItem(toLyrics);
7192 : }
7193 :
7194 : //! NOTE: Copied from ScoreView::lyricsUpDown
7195 0 : void NotationInteraction::navigateToLyricsVerse(MoveDirection direction)
7196 : {
7197 0 : if (!m_editData.element || !m_editData.element->isLyrics()) {
7198 0 : LOGW("nextLyricVerse called with invalid current element");
7199 0 : return;
7200 : }
7201 0 : mu::engraving::Lyrics* lyrics = toLyrics(m_editData.element);
7202 0 : engraving::track_idx_t track = lyrics->track();
7203 0 : ChordRest* cr = lyrics->chordRest();
7204 0 : int verse = lyrics->verse();
7205 0 : mu::engraving::PlacementV placement = lyrics->placement();
7206 0 : mu::engraving::PropertyFlags pFlags = lyrics->propertyFlags(mu::engraving::Pid::PLACEMENT);
7207 0 : mu::engraving::FontStyle fStyle = lyrics->fontStyle();
7208 0 : mu::engraving::PropertyFlags fFlags = lyrics->propertyFlags(mu::engraving::Pid::FONT_STYLE);
7209 0 : mu::engraving::AutoOnOff centering = lyrics->centerBetweenStaves();
7210 0 : mu::engraving::TextStyleType styleType = lyrics->textStyleType();
7211 :
7212 0 : if (direction == MoveDirection::Up) {
7213 0 : if (verse == 0) {
7214 0 : return;
7215 : }
7216 0 : --verse;
7217 : } else {
7218 0 : ++verse;
7219 0 : if (verse > cr->lastVerse(placement)) {
7220 0 : return;
7221 : }
7222 : }
7223 :
7224 0 : endEditText();
7225 :
7226 0 : lyrics = cr->lyrics(verse, placement);
7227 0 : if (!lyrics) {
7228 0 : lyrics = Factory::createLyrics(cr);
7229 0 : lyrics->setTrack(track);
7230 0 : lyrics->setVerse(verse);
7231 0 : lyrics->setTextStyleType(styleType);
7232 0 : lyrics->setPlacement(placement);
7233 0 : lyrics->setPropertyFlags(mu::engraving::Pid::PLACEMENT, pFlags);
7234 0 : lyrics->setCenterBetweenStaves(centering);
7235 0 : lyrics->setFontStyle(fStyle);
7236 0 : lyrics->setPropertyFlags(mu::engraving::Pid::FONT_STYLE, fFlags);
7237 :
7238 0 : score()->startCmd(TranslatableString("undoableAction", "Navigate to verse"));
7239 0 : score()->undoAddElement(lyrics);
7240 0 : score()->endCmd();
7241 : }
7242 :
7243 0 : score()->select(lyrics, SelectType::SINGLE, 0);
7244 0 : startEditText(lyrics, PointF());
7245 :
7246 0 : lyrics = toLyrics(m_editData.element);
7247 :
7248 0 : score()->setLayoutAll();
7249 0 : score()->update();
7250 :
7251 0 : lyrics->selectAll();
7252 0 : showItem(lyrics);
7253 : }
7254 :
7255 : //! NOTE: Copied from ScoreView::harmonyBeatsTab
7256 0 : void NotationInteraction::navigateToNearHarmony(MoveDirection direction, bool nearNoteOrRest)
7257 : {
7258 0 : Harmony* harmony = editedHarmony();
7259 0 : if (!harmony) {
7260 0 : return;
7261 : }
7262 :
7263 0 : Segment* segment = harmonySegment(harmony);
7264 0 : if (!segment) {
7265 0 : LOGD("no segment");
7266 0 : return;
7267 : }
7268 :
7269 0 : Measure* measure = segment->measure();
7270 0 : Fraction tick = segment->tick();
7271 0 : engraving::track_idx_t track = harmony->track();
7272 0 : bool backDirection = direction == MoveDirection::Left;
7273 :
7274 0 : if (backDirection && tick == measure->tick()) {
7275 : // previous bar, if any
7276 0 : measure = measure->prevMeasure();
7277 0 : if (!measure) {
7278 0 : LOGD("no previous measure");
7279 0 : return;
7280 : }
7281 : }
7282 :
7283 0 : Fraction f = measure->ticks();
7284 0 : int ticksPerBeat = f.ticks()
7285 0 : / ((f.numerator() > 3 && (f.numerator() % 3) == 0 && f.denominator() > 4) ? f.numerator() / 3 : f.numerator());
7286 0 : Fraction tickInBar = tick - measure->tick();
7287 0 : Fraction newTick = measure->tick()
7288 0 : + Fraction::fromTicks((
7289 0 : (tickInBar.ticks() + (backDirection ? -1 : ticksPerBeat)) / ticksPerBeat
7290 : )
7291 0 : * ticksPerBeat);
7292 :
7293 0 : endEditText();
7294 :
7295 0 : startEdit(TranslatableString("undoableAction", "Navigate to next chord symbol"));
7296 :
7297 : // look for next/prev beat, note, rest or chord
7298 : for (;;) {
7299 0 : segment = backDirection ? segment->prev1(mu::engraving::SegmentType::ChordRest) : segment->next1(
7300 : mu::engraving::SegmentType::ChordRest);
7301 :
7302 0 : if (!segment || (backDirection ? (segment->tick() < newTick) : (segment->tick() > newTick))) {
7303 : // no segment or moved past the beat - create new segment
7304 0 : if (!backDirection && newTick >= measure->tick() + f) {
7305 : // next bar, if any
7306 0 : measure = measure->nextMeasure();
7307 0 : if (!measure) {
7308 0 : LOGD("no next measure");
7309 0 : rollback();
7310 0 : return;
7311 : }
7312 : }
7313 :
7314 0 : segment = measure->undoGetSegment(SegmentType::ChordRest, newTick);
7315 0 : break;
7316 : }
7317 :
7318 0 : if (segment->tick() == newTick) {
7319 0 : break;
7320 : }
7321 :
7322 0 : if (nearNoteOrRest) {
7323 0 : track_idx_t minTrack = trackZeroVoice(track);
7324 0 : track_idx_t maxTrack = minTrack + VOICES - 1;
7325 0 : if (segment->hasAnnotationOrElement(ElementType::HARMONY, minTrack, maxTrack)) {
7326 0 : break;
7327 : }
7328 : }
7329 0 : }
7330 :
7331 0 : mu::engraving::Harmony* nextHarmony = findHarmonyInSegment(segment, track, harmony->textStyleType());
7332 0 : if (!nextHarmony) {
7333 0 : nextHarmony = createHarmony(segment, track, harmony->harmonyType());
7334 0 : score()->undoAddElement(nextHarmony);
7335 : }
7336 :
7337 0 : apply();
7338 0 : startEditText(nextHarmony);
7339 0 : showItem(nextHarmony);
7340 : }
7341 :
7342 : //! NOTE: Copied from ScoreView::harmonyTab
7343 0 : void NotationInteraction::navigateToHarmonyInNearMeasure(MoveDirection direction)
7344 : {
7345 0 : Harmony* harmony = editedHarmony();
7346 0 : if (!harmony) {
7347 0 : return;
7348 : }
7349 :
7350 0 : Segment* segment = harmonySegment(harmony);
7351 0 : if (!segment) {
7352 0 : LOGD("harmonyTicksTab: no segment");
7353 0 : return;
7354 : }
7355 :
7356 : // moving to next/prev measure
7357 0 : Measure* measure = segment->measure();
7358 0 : if (measure) {
7359 0 : if (direction == MoveDirection::Left) {
7360 0 : measure = measure->prevMeasure();
7361 : } else {
7362 0 : measure = measure->nextMeasure();
7363 : }
7364 : }
7365 :
7366 0 : if (!measure) {
7367 0 : LOGD("no prev/next measure");
7368 0 : return;
7369 : }
7370 :
7371 0 : segment = measure->findSegment(mu::engraving::SegmentType::ChordRest, measure->tick());
7372 0 : if (!segment) {
7373 0 : LOGD("no ChordRest segment as measure");
7374 0 : return;
7375 : }
7376 :
7377 0 : track_idx_t track = harmony->track();
7378 :
7379 0 : doEndEditElement();
7380 :
7381 0 : mu::engraving::Harmony* nextHarmony = findHarmonyInSegment(segment, track, harmony->textStyleType());
7382 0 : if (!nextHarmony) {
7383 0 : nextHarmony = createHarmony(segment, track, harmony->harmonyType());
7384 :
7385 0 : startEdit(TranslatableString("undoableAction", "Navigate to next chord symbol"));
7386 0 : score()->undoAddElement(nextHarmony);
7387 0 : apply();
7388 : }
7389 :
7390 0 : startEditText(nextHarmony);
7391 0 : showItem(nextHarmony);
7392 : }
7393 :
7394 : //! NOTE: Copied from ScoreView::harmonyBeatsTab
7395 0 : void NotationInteraction::navigateToHarmony(const Fraction& ticks)
7396 : {
7397 0 : Harmony* harmony = editedHarmony();
7398 0 : if (!harmony) {
7399 0 : return;
7400 : }
7401 :
7402 0 : Segment* segment = harmonySegment(harmony);
7403 0 : if (!segment) {
7404 0 : LOGD("no segment");
7405 0 : return;
7406 : }
7407 :
7408 0 : Measure* measure = segment->measure();
7409 :
7410 0 : Fraction newTick = segment->tick() + ticks;
7411 :
7412 : // find the measure containing the target tick
7413 0 : while (newTick >= measure->endTick()) {
7414 0 : measure = measure->nextMeasure();
7415 0 : if (!measure) {
7416 0 : LOGD("no next measure");
7417 0 : return;
7418 : }
7419 : }
7420 :
7421 : // look for a segment at this tick; if none, create one
7422 0 : while (segment && segment->tick() < newTick) {
7423 0 : segment = segment->next1(mu::engraving::SegmentType::ChordRest);
7424 : }
7425 :
7426 0 : doEndEditElement();
7427 :
7428 0 : startEdit(TranslatableString("undoableAction", "Navigate to chord symbol"));
7429 :
7430 0 : if (!segment || segment->tick() > newTick) { // no ChordRest segment at this tick
7431 0 : segment = measure->undoGetSegment(SegmentType::ChordRest, newTick);
7432 : }
7433 :
7434 0 : engraving::track_idx_t track = harmony->track();
7435 :
7436 0 : mu::engraving::Harmony* nextHarmony = findHarmonyInSegment(segment, track, harmony->textStyleType());
7437 0 : if (!nextHarmony) {
7438 0 : nextHarmony = createHarmony(segment, track, harmony->harmonyType());
7439 0 : score()->undoAddElement(nextHarmony);
7440 : }
7441 :
7442 0 : apply();
7443 0 : startEditText(nextHarmony);
7444 0 : showItem(nextHarmony);
7445 : }
7446 :
7447 : //! NOTE: Copied from ScoreView::figuredBassTab
7448 0 : void NotationInteraction::navigateToNearFiguredBass(MoveDirection direction)
7449 : {
7450 0 : mu::engraving::FiguredBass* fb = mu::engraving::toFiguredBass(m_editData.element);
7451 0 : mu::engraving::Segment* segm = fb->segment();
7452 0 : track_idx_t track = fb->track();
7453 0 : bool backDirection = direction == MoveDirection::Left;
7454 :
7455 0 : if (!segm) {
7456 0 : LOGD("figuredBassTab: no segment");
7457 0 : return;
7458 : }
7459 :
7460 : // search next chord segment in same staff
7461 0 : mu::engraving::Segment* nextSegm = backDirection ? segm->prev1(mu::engraving::SegmentType::ChordRest) : segm->next1(
7462 0 : mu::engraving::SegmentType::ChordRest);
7463 0 : track_idx_t minTrack = trackZeroVoice(track);
7464 0 : track_idx_t maxTrack = minTrack + VOICES - 1;
7465 :
7466 0 : while (nextSegm) { // look for a ChordRest in the compatible track range
7467 0 : if (nextSegm->hasAnnotationOrElement(ElementType::FIGURED_BASS, minTrack, maxTrack)) {
7468 0 : break;
7469 : }
7470 0 : nextSegm = backDirection ? nextSegm->prev1(mu::engraving::SegmentType::ChordRest) : nextSegm->next1(
7471 : mu::engraving::SegmentType::ChordRest);
7472 : }
7473 :
7474 0 : if (!nextSegm) {
7475 0 : LOGD("figuredBassTab: no prev/next segment");
7476 0 : return;
7477 : }
7478 :
7479 0 : doEndEditElement();
7480 :
7481 0 : bool bNew = false;
7482 : // add a (new) FB element, using chord duration as default duration
7483 0 : mu::engraving::FiguredBass* fbNew = mu::engraving::FiguredBass::addFiguredBassToSegment(nextSegm, track, Fraction(0, 1), &bNew);
7484 0 : if (bNew) {
7485 0 : startEdit(TranslatableString("undoableAction", "Navigate to next figured bass"));
7486 0 : score()->undoAddElement(fbNew);
7487 0 : apply();
7488 : }
7489 :
7490 0 : startEditText(fbNew);
7491 0 : showItem(fbNew);
7492 : }
7493 :
7494 : //! NOTE: Copied from ScoreView::figuredBassTab
7495 0 : void NotationInteraction::navigateToFiguredBassInNearMeasure(MoveDirection direction)
7496 : {
7497 0 : mu::engraving::FiguredBass* fb = mu::engraving::toFiguredBass(m_editData.element);
7498 0 : mu::engraving::Segment* segm = fb->segment();
7499 :
7500 0 : if (!segm) {
7501 0 : LOGD("figuredBassTab: no segment");
7502 0 : return;
7503 : }
7504 :
7505 : // if moving to next/prev measure
7506 0 : Measure* meas = segm->measure();
7507 0 : if (meas) {
7508 0 : if (direction == MoveDirection::Left) {
7509 0 : meas = meas->prevMeasure();
7510 : } else {
7511 0 : meas = meas->nextMeasure();
7512 : }
7513 : }
7514 0 : if (!meas) {
7515 0 : LOGD("figuredBassTab: no prev/next measure");
7516 0 : return;
7517 : }
7518 : // find initial ChordRest segment
7519 0 : mu::engraving::Segment* nextSegm = meas->findSegment(mu::engraving::SegmentType::ChordRest, meas->tick());
7520 0 : if (!nextSegm) {
7521 0 : LOGD("figuredBassTab: no ChordRest segment at measure");
7522 0 : return;
7523 : }
7524 :
7525 0 : doEndEditElement();
7526 :
7527 0 : bool bNew = false;
7528 : // add a (new) FB element, using chord duration as default duration
7529 0 : mu::engraving::FiguredBass* fbNew = mu::engraving::FiguredBass::addFiguredBassToSegment(nextSegm, fb->track(), Fraction(0, 1), &bNew);
7530 0 : if (bNew) {
7531 0 : startEdit(TranslatableString("undoableAction", "Navigate to next figured bass"));
7532 0 : score()->undoAddElement(fbNew);
7533 0 : apply();
7534 : }
7535 :
7536 0 : startEditText(fbNew);
7537 0 : showItem(fbNew);
7538 : }
7539 :
7540 : //! NOTE: Copied from ScoreView::figuredBassTicksTab
7541 0 : void NotationInteraction::navigateToFiguredBass(const Fraction& ticks)
7542 : {
7543 0 : mu::engraving::FiguredBass* fb = mu::engraving::toFiguredBass(m_editData.element);
7544 0 : track_idx_t track = fb->track();
7545 0 : mu::engraving::Segment* segm = fb->segment();
7546 0 : if (!segm) {
7547 0 : LOGD("figuredBassTicksTab: no segment");
7548 0 : return;
7549 : }
7550 0 : Measure* measure = segm->measure();
7551 :
7552 0 : Fraction nextSegTick = segm->tick() + ticks;
7553 :
7554 : // find the measure containing the target tick
7555 0 : while (nextSegTick >= measure->endTick()) {
7556 0 : measure = measure->nextMeasure();
7557 0 : if (!measure) {
7558 0 : LOGD("figuredBassTicksTab: no next measure");
7559 0 : return;
7560 : }
7561 : }
7562 :
7563 : // look for a segment at this tick; if none, create one
7564 0 : mu::engraving::Segment* nextSegm = segm;
7565 0 : while (nextSegm && nextSegm->tick() < nextSegTick) {
7566 0 : nextSegm = nextSegm->next1(mu::engraving::SegmentType::ChordRest);
7567 : }
7568 :
7569 0 : if (!nextSegm || nextSegm->tick() > nextSegTick) { // no ChordRest segm at this tick
7570 0 : nextSegm = measure->undoGetSegment(SegmentType::ChordRest, nextSegTick);
7571 0 : if (!nextSegm) {
7572 0 : LOGD("figuredBassTicksTab: no next segment");
7573 0 : return;
7574 : }
7575 : }
7576 :
7577 0 : doEndEditElement();
7578 :
7579 0 : startEdit(TranslatableString("undoableAction", "Navigate to figured bass"));
7580 :
7581 0 : bool bNew = false;
7582 0 : mu::engraving::FiguredBass* fbNew = mu::engraving::FiguredBass::addFiguredBassToSegment(nextSegm, track, ticks, &bNew);
7583 0 : if (bNew) {
7584 0 : score()->undoAddElement(fbNew);
7585 : }
7586 :
7587 0 : apply();
7588 0 : startEditText(fbNew);
7589 0 : showItem(fbNew);
7590 : }
7591 :
7592 : //! NOTE: Copied from ScoreView::textTab
7593 0 : void NotationInteraction::navigateToNearText(MoveDirection direction)
7594 : {
7595 0 : mu::engraving::EngravingItem* oe = m_editData.element;
7596 0 : if (!oe || !oe->isTextBase()) {
7597 0 : return;
7598 : }
7599 :
7600 0 : mu::engraving::EngravingItem* op = toEngravingItem(oe->parent());
7601 0 : if (!op || !(op->isSegment() || op->isNote())) {
7602 0 : LOGD("navigateToNearText: parent not note or segment.");
7603 0 : return;
7604 : }
7605 :
7606 0 : TextBase* ot = mu::engraving::toTextBase(oe);
7607 0 : mu::engraving::TextStyleType textStyleType = ot->textStyleType();
7608 0 : ElementType type = ot->type();
7609 0 : mu::engraving::staff_idx_t staffIdx = ot->staffIdx();
7610 0 : bool back = direction == MoveDirection::Left;
7611 0 : int minTrack = static_cast<int>(trackZeroVoice(oe->track()));
7612 0 : int maxTrack = minTrack + VOICES - 1;
7613 :
7614 0 : mu::engraving::EngravingItem* el = nullptr;
7615 :
7616 0 : if (op->isNote()) {
7617 : // go to next/prev note in same chord, or go to next/prev chord, which may be in another voice
7618 0 : Note* origNote = toNote(op);
7619 0 : Chord* ch = origNote->chord();
7620 0 : const std::vector<Note*>& notes = ch->notes();
7621 :
7622 : // first, try going to prev/next note in the current chord
7623 0 : if (origNote != (back ? notes.back() : notes.front())) {
7624 0 : auto it = std::find(notes.begin(), notes.end(), origNote);
7625 0 : if (it != notes.end()) {
7626 0 : el = back ? *std::next(it) : *std::prev(it);
7627 : }
7628 : }
7629 :
7630 : // next, try going to next/prev grace note chord in the same group as the current
7631 0 : const std::vector<Chord*> chordList = ch->allGraceChordsOfMainChord();
7632 :
7633 0 : if (!el && ch != (back ? chordList.front() : chordList.back())) {
7634 0 : auto it = std::find(chordList.begin(), chordList.end(), ch);
7635 0 : if (it != chordList.end()) {
7636 0 : if (back) {
7637 0 : const Chord* targetChord = *std::prev(it);
7638 0 : el = targetChord->notes().front();
7639 : } else {
7640 0 : const Chord* targetChord = *std::next(it);
7641 0 : el = targetChord->notes().back();
7642 : }
7643 : }
7644 : }
7645 :
7646 : // next, try going to prev/next chord in another voice
7647 0 : if (!el) {
7648 0 : Segment* seg = ch->segment();
7649 0 : if (!seg) {
7650 0 : LOGD("navigateToNearText: no segment");
7651 0 : return;
7652 : }
7653 0 : int inc = back ? -1 : 1;
7654 0 : int sTrack = int(oe->track()) + inc;
7655 0 : int eTrack = back ? minTrack : maxTrack;
7656 0 : for (int track = sTrack; back ? (track >= eTrack) : (track <= eTrack); track += inc) {
7657 0 : EngravingItem* e = seg->element(track);
7658 0 : if (e && e->isChord()) {
7659 0 : const std::vector<Chord*> targetChordList = toChord(e)->allGraceChordsOfMainChord();
7660 0 : if (back) {
7661 0 : Chord* targetChord = targetChordList.back();
7662 0 : el = targetChord->notes().front();
7663 : } else {
7664 0 : Chord* targetChord = targetChordList.front();
7665 0 : el = targetChord->notes().back();
7666 : }
7667 0 : break;
7668 0 : }
7669 : }
7670 : }
7671 :
7672 : // finally, try going to chord in prev/next segments
7673 0 : if (!el) {
7674 0 : Segment* seg = ch->segment();
7675 0 : seg = back ? seg->prev1(SegmentType::ChordRest) : seg->next1(SegmentType::ChordRest);
7676 0 : int sTrack = back ? maxTrack : minTrack;
7677 0 : int eTrack = back ? minTrack : maxTrack;
7678 0 : int inc = back ? -1 : 1;
7679 0 : while (seg) {
7680 0 : for (int track = sTrack; back ? (track >= eTrack) : (track <= eTrack); track += inc) {
7681 0 : EngravingItem* e = seg->element(track);
7682 0 : if (e && e->isChord()) {
7683 0 : const std::vector<Chord*> targetChordList = toChord(e)->allGraceChordsOfMainChord();
7684 0 : if (back) {
7685 0 : Chord* targetChord = targetChordList.back();
7686 0 : el = targetChord->notes().front();
7687 : } else {
7688 0 : Chord* targetChord = targetChordList.front();
7689 0 : el = targetChord->notes().back();
7690 : }
7691 0 : break;
7692 0 : }
7693 : }
7694 :
7695 0 : if (el) {
7696 0 : break;
7697 : }
7698 :
7699 0 : seg = back ? seg->prev1(SegmentType::ChordRest) : seg->next1(SegmentType::ChordRest);
7700 : }
7701 : }
7702 0 : } else if (op->isSegment()) {
7703 0 : Segment* seg = toSegment(op);
7704 0 : seg = back ? seg->prev1(SegmentType::ChordRest) : seg->next1(SegmentType::ChordRest);
7705 :
7706 : // go to first segment with a chord or an existing text
7707 0 : while (seg) {
7708 0 : for (int track = minTrack; track <= maxTrack; ++track) {
7709 0 : EngravingItem* e = seg->element(track);
7710 0 : if (e && e->isChord()) {
7711 0 : el = e;
7712 0 : break;
7713 : }
7714 : }
7715 0 : if (el) {
7716 0 : break;
7717 : }
7718 :
7719 : // this segment only contains rests, check for existing text
7720 0 : for (EngravingItem* e : seg->annotations()) {
7721 0 : if (e->staffIdx() != staffIdx || e->type() != type) {
7722 0 : continue;
7723 : }
7724 0 : TextBase* nt = mu::engraving::toTextBase(e);
7725 0 : if (nt->textStyleType() == textStyleType) {
7726 0 : el = seg->firstElement(staffIdx);
7727 0 : break;
7728 : }
7729 : }
7730 0 : if (el) {
7731 0 : break;
7732 : }
7733 :
7734 0 : seg = back ? seg->prev1(SegmentType::ChordRest) : seg->next1(SegmentType::ChordRest);
7735 : }
7736 : }
7737 :
7738 0 : if (!el) {
7739 0 : return;
7740 : }
7741 :
7742 : // get existing text to edit
7743 0 : EngravingItem* textEl = nullptr;
7744 0 : if (op->isNote()) {
7745 0 : if (!el->isNote()) {
7746 0 : LOGD("navigateToNearText: new element is not Note.");
7747 0 : return;
7748 : }
7749 : // check element list of new note
7750 0 : for (mu::engraving::EngravingItem* e : toNote(el)->el()) {
7751 0 : if (e->type() != type) {
7752 0 : continue;
7753 : }
7754 0 : TextBase* nt = mu::engraving::toTextBase(e);
7755 0 : if (nt->textStyleType() == textStyleType) {
7756 0 : textEl = e;
7757 0 : break;
7758 : }
7759 : }
7760 0 : } else if (op->isSegment()) {
7761 0 : if (!el->isChordRest()) {
7762 0 : LOGD("navigateToNearText: new element is not ChordRest.");
7763 0 : return;
7764 : }
7765 : // check annotation list of new segment
7766 0 : mu::engraving::Segment* ns = toChordRest(el)->segment();
7767 0 : for (mu::engraving::EngravingItem* e : ns->annotations()) {
7768 0 : if (e->staffIdx() != staffIdx || e->type() != type) {
7769 0 : continue;
7770 : }
7771 0 : TextBase* nt = mu::engraving::toTextBase(e);
7772 0 : if (nt->textStyleType() == textStyleType) {
7773 0 : textEl = e;
7774 0 : break;
7775 : }
7776 : }
7777 : }
7778 :
7779 0 : doEndEditElement();
7780 :
7781 0 : if (textEl) {
7782 : // edit existing text
7783 0 : TextBase* text = toTextBase(textEl);
7784 :
7785 0 : if (text) {
7786 0 : startEditText(text);
7787 0 : text->selectAll();
7788 0 : showItem(text);
7789 : }
7790 : } else {
7791 : // add new text if no existing element to edit
7792 : // TODO: for tempo text, would be better to create empty tempo element
7793 0 : if (type != ElementType::TEMPO_TEXT) {
7794 0 : addTextToItem(textStyleType, el);
7795 : }
7796 : }
7797 : }
7798 :
7799 : //! NOTE: Copied from ScoreView::lyricsUnderscore
7800 0 : void NotationInteraction::addMelisma()
7801 : {
7802 0 : if (!m_editData.element || !m_editData.element->isLyrics()) {
7803 0 : LOGW("addMelisma called with invalid current element");
7804 0 : return;
7805 : }
7806 0 : Lyrics* lyrics = toLyrics(m_editData.element);
7807 0 : ChordRest* initialCR = lyrics->chordRest();
7808 0 : const bool hasPrecedingRepeat = initialCR->hasPrecedingJumpItem();
7809 0 : track_idx_t track = lyrics->track();
7810 0 : Segment* segment = lyrics->segment();
7811 0 : int verse = lyrics->verse();
7812 0 : PlacementV placement = lyrics->placement();
7813 0 : PropertyFlags pFlags = lyrics->propertyFlags(Pid::PLACEMENT);
7814 0 : FontStyle fStyle = lyrics->fontStyle();
7815 0 : PropertyFlags fFlags = lyrics->propertyFlags(Pid::FONT_STYLE);
7816 0 : double yOffset = lyrics->offset().y();
7817 0 : AutoOnOff centering = lyrics->centerBetweenStaves();
7818 0 : Fraction endTick = segment->tick(); // a previous melisma cannot extend beyond this point
7819 0 : endEditText();
7820 :
7821 : // search next chord
7822 0 : Segment* nextSegment = segment;
7823 0 : while ((nextSegment = nextSegment->next1(SegmentType::ChordRest))) {
7824 0 : EngravingItem* el = nextSegment->element(track);
7825 0 : if (el && el->isChord()) {
7826 0 : break;
7827 : }
7828 : }
7829 :
7830 0 : if (!segmentsAreAdjacent(segment, nextSegment)) {
7831 0 : nextSegment = nullptr;
7832 : }
7833 :
7834 : // look for the lyrics we are moving from; may be the current lyrics or a previous one
7835 : // we are extending with several underscores
7836 0 : Segment* curSeg = segment;
7837 0 : Lyrics* fromLyrics = nullptr;
7838 0 : PartialLyricsLine* prevPartialLyricsLine = nullptr;
7839 0 : while (segment && segmentsAreAdjacent(segment, curSeg)) {
7840 0 : ChordRest* cr = toChordRest(segment->element(track));
7841 0 : if (cr) {
7842 0 : fromLyrics = cr->lyrics(verse, placement);
7843 0 : if (fromLyrics) {
7844 0 : break;
7845 : }
7846 : // Check if there is a partial melisma to extend
7847 0 : auto spanners = score()->spannerMap().findOverlapping(cr->tick().ticks(), cr->endTick().ticks());
7848 0 : for (auto& spanner : spanners) {
7849 0 : if (!spanner.value->isPartialLyricsLine() || spanner.value->staffIdx() != cr->staffIdx()) {
7850 0 : continue;
7851 : }
7852 0 : PartialLyricsLine* lyricsLine = toPartialLyricsLine(spanner.value);
7853 0 : if (lyricsLine->verse() != verse || lyricsLine->placement() != placement || !lyricsLine->isEndMelisma()) {
7854 0 : continue;
7855 : }
7856 :
7857 0 : prevPartialLyricsLine = lyricsLine;
7858 0 : break;
7859 : }
7860 :
7861 0 : if (prevPartialLyricsLine) {
7862 0 : break;
7863 : }
7864 0 : }
7865 0 : segment = segment->prev1(SegmentType::ChordRest);
7866 : }
7867 :
7868 : // one-chord melisma?
7869 : // if still at melisma initial chord and there is a valid next chord (if not,
7870 : // there will be no melisma anyway), set a temporary melisma duration
7871 0 : if (fromLyrics == lyrics && nextSegment) {
7872 0 : score()->startCmd(TranslatableString("undoableAction", "Enter lyrics extension line"));
7873 0 : lyrics->undoChangeProperty(Pid::LYRIC_TICKS, Lyrics::TEMP_MELISMA_TICKS);
7874 0 : score()->setLayoutAll();
7875 0 : score()->endCmd();
7876 : }
7877 :
7878 0 : auto getPartialLyricLineEndSegment = [&]() -> Fraction {
7879 0 : Segment* endSegment = segment;
7880 0 : while (endSegment && endSegment->tick() < endTick) {
7881 0 : endSegment = endSegment->nextCR(track, true);
7882 : }
7883 0 : EngravingItem* endSegmentElement = endSegment->element(track);
7884 0 : if (endSegment->tick() == endTick && endSegmentElement && endSegmentElement->isChord()) {
7885 0 : Segment* endChordSeg = endSegment;
7886 0 : Chord* endChord = toChord(endSegmentElement);
7887 :
7888 0 : endSegment = endChordSeg->nextCR(track, false);
7889 :
7890 0 : if (!endSegment || endSegment->tick() > endChord->endTick()) {
7891 0 : endSegment = endChordSeg;
7892 0 : while (endSegment && endSegment->tick() < endChord->endTick()) {
7893 0 : endSegment = endSegment->nextCR(muse::nidx, true);
7894 : }
7895 : }
7896 : }
7897 0 : return endSegment ? endSegment->tick() : score()->endTick();
7898 0 : };
7899 :
7900 0 : if (!nextSegment) {
7901 0 : score()->startCmd(TranslatableString("undoableAction", "Enter lyrics extension line"));
7902 0 : if (fromLyrics) {
7903 0 : switch (fromLyrics->syllabic()) {
7904 0 : case LyricsSyllabic::SINGLE:
7905 : case LyricsSyllabic::END:
7906 0 : break;
7907 0 : default:
7908 0 : fromLyrics->undoChangeProperty(Pid::SYLLABIC, int(LyricsSyllabic::END));
7909 0 : break;
7910 : }
7911 0 : if (fromLyrics->segment()->tick() < endTick
7912 0 : || (endTick + initialCR->actualTicks() == segment->measure()->endTick() && initialCR->hasFollowingJumpItem())) {
7913 0 : Fraction ticks = std::max(endTick - fromLyrics->segment()->tick(), Lyrics::TEMP_MELISMA_TICKS);
7914 0 : fromLyrics->undoChangeProperty(Pid::LYRIC_TICKS, ticks);
7915 : }
7916 : }
7917 :
7918 0 : if (hasPrecedingRepeat && !prevPartialLyricsLine) {
7919 : // No from lyrics - create incoming partial melisma
7920 0 : PartialLyricsLine* melisma = Factory::createPartialLyricsLine(score()->dummy());
7921 0 : melisma->setIsEndMelisma(true);
7922 0 : melisma->setVerse(verse);
7923 0 : melisma->setPlacement(placement);
7924 0 : melisma->setPropertyFlags(Pid::PLACEMENT, pFlags);
7925 0 : melisma->setCenterBetweenStaves(centering);
7926 0 : melisma->setTick(initialCR->tick());
7927 0 : melisma->setTicks(initialCR->ticks());
7928 0 : melisma->setTrack(initialCR->track());
7929 0 : melisma->setTrack2(initialCR->track());
7930 :
7931 0 : score()->undoAddElement(melisma);
7932 0 : } else if (prevPartialLyricsLine) {
7933 0 : const Fraction segEndTick = getPartialLyricLineEndSegment();
7934 0 : const Fraction tickDiff = segEndTick - prevPartialLyricsLine->tick2();
7935 0 : prevPartialLyricsLine->undoMoveEnd(tickDiff);
7936 : }
7937 :
7938 0 : if (fromLyrics) {
7939 0 : score()->select(fromLyrics, SelectType::SINGLE, 0);
7940 : }
7941 0 : score()->setLayoutAll();
7942 0 : score()->endCmd();
7943 0 : return;
7944 : }
7945 :
7946 : // if a place for a new lyrics has been found, create a lyrics there
7947 0 : ChordRest* nextCR = toChordRest(nextSegment->element(track));
7948 :
7949 : // Disallow melisma lines between non-adjacent repeat sections eg. 1st volta -> 2nd volta
7950 : // Instead, try to add partial melisma line
7951 0 : if (nextCR && fromLyrics) {
7952 0 : Measure* toLyricsMeasure = nextCR->measure();
7953 0 : Measure* fromLyricsMeasure = fromLyrics->measure();
7954 :
7955 0 : if (toLyricsMeasure != fromLyricsMeasure && fromLyricsMeasure->lastChordRest(track)->hasFollowingJumpItem()) {
7956 0 : const std::vector<Measure*> previousRepeats = findPreviousRepeatMeasures(toLyricsMeasure);
7957 0 : const bool inPrecedingRepeatSeg = muse::contains(previousRepeats, fromLyricsMeasure);
7958 0 : if (!previousRepeats.empty() && !inPrecedingRepeatSeg) {
7959 0 : fromLyrics = nullptr;
7960 : }
7961 0 : }
7962 : }
7963 :
7964 0 : score()->startCmd(TranslatableString("undoableAction", "Enter lyrics extension line"));
7965 0 : Lyrics* toLyrics = nextCR->lyrics(verse, placement);
7966 0 : bool newLyrics = (toLyrics == nullptr);
7967 0 : if (!toLyrics) {
7968 0 : toLyrics = Factory::createLyrics(nextCR);
7969 0 : toLyrics->setTrack(track);
7970 0 : toLyrics->setVerse(verse);
7971 0 : const TextStyleType styleType(toLyrics->isEven() ? TextStyleType::LYRICS_EVEN : TextStyleType::LYRICS_ODD);
7972 0 : toLyrics->setTextStyleType(styleType);
7973 :
7974 0 : toLyrics->setPlacement(placement);
7975 0 : toLyrics->setPropertyFlags(Pid::PLACEMENT, pFlags);
7976 0 : toLyrics->setCenterBetweenStaves(centering);
7977 0 : toLyrics->setSyllabic(LyricsSyllabic::SINGLE);
7978 0 : toLyrics->setFontStyle(fStyle);
7979 0 : toLyrics->setPropertyFlags(Pid::FONT_STYLE, fFlags);
7980 0 : toLyrics->setOffset(PointF(0.0, yOffset));
7981 : }
7982 : // as we arrived at toLyrics by an underscore, it cannot have syllabic dashes before
7983 0 : else if (toLyrics->syllabic() == LyricsSyllabic::MIDDLE) {
7984 0 : toLyrics->undoChangeProperty(Pid::SYLLABIC, int(LyricsSyllabic::BEGIN));
7985 0 : } else if (toLyrics->syllabic() == LyricsSyllabic::END) {
7986 0 : toLyrics->undoChangeProperty(Pid::SYLLABIC, int(LyricsSyllabic::SINGLE));
7987 : }
7988 :
7989 0 : if (fromLyrics) {
7990 : // as we moved away from fromLyrics by an underscore,
7991 : // it can be isolated or terminal but cannot have dashes after
7992 0 : switch (fromLyrics->syllabic()) {
7993 0 : case LyricsSyllabic::SINGLE:
7994 : case LyricsSyllabic::END:
7995 0 : break;
7996 0 : default:
7997 0 : fromLyrics->undoChangeProperty(Pid::SYLLABIC, int(LyricsSyllabic::END));
7998 0 : break;
7999 : }
8000 : // for the same reason, if it has a melisma, this cannot extend beyond toLyrics
8001 0 : if (fromLyrics->segment()->tick() < endTick) {
8002 0 : fromLyrics->undoChangeProperty(Pid::LYRIC_TICKS, endTick - fromLyrics->segment()->tick());
8003 : }
8004 0 : } else if (hasPrecedingRepeat && !prevPartialLyricsLine) {
8005 : // No from lyrics - create incoming partial melisma
8006 0 : PartialLyricsLine* melisma = Factory::createPartialLyricsLine(score()->dummy());
8007 0 : melisma->setIsEndMelisma(true);
8008 0 : melisma->setVerse(verse);
8009 0 : melisma->setPlacement(placement);
8010 0 : melisma->setPropertyFlags(Pid::PLACEMENT, pFlags);
8011 0 : melisma->setCenterBetweenStaves(centering);
8012 0 : melisma->setTick(initialCR->tick());
8013 0 : melisma->setTicks(initialCR->ticks());
8014 0 : melisma->setTrack(initialCR->track());
8015 0 : melisma->setTrack2(initialCR->track());
8016 :
8017 0 : score()->undoAddElement(melisma);
8018 0 : } else if (prevPartialLyricsLine) {
8019 0 : const Fraction segEndTick = getPartialLyricLineEndSegment();
8020 0 : const Fraction tickDiff = segEndTick - prevPartialLyricsLine->tick2();
8021 0 : prevPartialLyricsLine->undoMoveEnd(tickDiff);
8022 0 : prevPartialLyricsLine->triggerLayout();
8023 : }
8024 0 : score()->endCmd();
8025 :
8026 0 : score()->startCmd(TranslatableString("undoableAction", "Enter lyrics extension line"));
8027 0 : if (newLyrics) {
8028 0 : score()->undoAddElement(toLyrics);
8029 : }
8030 0 : score()->endCmd();
8031 :
8032 0 : score()->select(toLyrics, SelectType::SINGLE, 0);
8033 0 : startEditText(toLyrics, PointF());
8034 :
8035 0 : toLyrics->selectAll();
8036 : }
8037 :
8038 : //! NOTE: Copied from ScoreView::lyricsReturn
8039 0 : void NotationInteraction::addLyricsVerse()
8040 : {
8041 0 : if (!m_editData.element || !m_editData.element->isLyrics()) {
8042 0 : LOGW("nextLyricVerse called with invalid current element");
8043 0 : return;
8044 : }
8045 0 : mu::engraving::Lyrics* oldLyrics = toLyrics(m_editData.element);
8046 0 : mu::engraving::FontStyle fStyle = oldLyrics->fontStyle();
8047 0 : mu::engraving::PropertyFlags fFlags = oldLyrics->propertyFlags(mu::engraving::Pid::FONT_STYLE);
8048 0 : mu::engraving::AutoOnOff centering = oldLyrics->centerBetweenStaves();
8049 :
8050 0 : endEditText();
8051 :
8052 0 : score()->startCmd(TranslatableString("undoableAction", "Add lyrics verse"));
8053 0 : int newVerse = oldLyrics->verse() + 1;
8054 :
8055 0 : mu::engraving::Lyrics* lyrics = Factory::createLyrics(oldLyrics->chordRest());
8056 0 : lyrics->setTrack(oldLyrics->track());
8057 0 : lyrics->setPlacement(oldLyrics->placement());
8058 0 : lyrics->setPropertyFlags(mu::engraving::Pid::PLACEMENT, oldLyrics->propertyFlags(mu::engraving::Pid::PLACEMENT));
8059 0 : lyrics->setCenterBetweenStaves(centering);
8060 :
8061 0 : lyrics->setVerse(newVerse);
8062 0 : const mu::engraving::TextStyleType styleType(lyrics->isEven() ? TextStyleType::LYRICS_EVEN : TextStyleType::LYRICS_ODD);
8063 0 : lyrics->setTextStyleType(styleType);
8064 :
8065 0 : lyrics->setFontStyle(fStyle);
8066 0 : lyrics->setPropertyFlags(mu::engraving::Pid::FONT_STYLE, fFlags);
8067 :
8068 0 : score()->undoAddElement(lyrics);
8069 0 : score()->endCmd();
8070 :
8071 0 : score()->select(lyrics, SelectType::SINGLE, 0);
8072 0 : startEditText(lyrics, PointF());
8073 : }
8074 :
8075 0 : void NotationInteraction::addGuitarBend(GuitarBendType bendType)
8076 : {
8077 0 : Score* score = this->score();
8078 0 : IF_ASSERT_FAILED(score) {
8079 0 : MScore::setError(MsError::NO_NOTE_SELECTED);
8080 0 : checkAndShowError();
8081 0 : return;
8082 : }
8083 :
8084 0 : const Selection& selection = score->selection();
8085 0 : const std::vector<Note*>& noteList = selection.noteList();
8086 0 : if (selection.isNone() || noteList.empty()) {
8087 0 : MScore::setError(MsError::NO_NOTE_SELECTED);
8088 0 : checkAndShowError();
8089 0 : return;
8090 : }
8091 :
8092 0 : startEdit(TranslatableString("undoableAction", "Enter guitar bend"));
8093 :
8094 0 : Note* startNote = nullptr;
8095 0 : Note* endNote = nullptr;
8096 0 : bool noteToNote = false;
8097 0 : if (selection.isList() && noteList.size() == 2) {
8098 0 : startNote = noteList.front();
8099 0 : endNote = noteList.back();
8100 0 : if (endNote->tick() > startNote->tick()) {
8101 0 : noteToNote = true;
8102 : }
8103 : }
8104 :
8105 0 : mu::engraving::GuitarBend* guitarBend = nullptr;
8106 0 : if (noteToNote) {
8107 0 : guitarBend = score->addGuitarBend(bendType, startNote, endNote);
8108 : } else {
8109 0 : for (Note* note : noteList) {
8110 : // (will select the last one)
8111 0 : guitarBend = score->addGuitarBend(bendType, note, nullptr);
8112 : }
8113 : }
8114 :
8115 0 : if (guitarBend) {
8116 0 : apply();
8117 0 : select({ guitarBend });
8118 : } else {
8119 0 : rollback();
8120 : }
8121 0 : }
8122 :
8123 0 : void NotationInteraction::addFretboardDiagram()
8124 : {
8125 0 : Score* score = this->score();
8126 0 : IF_ASSERT_FAILED(score) {
8127 0 : MScore::setError(MsError::NO_NOTE_REST_HARMONY_SELECTED);
8128 0 : checkAndShowError();
8129 0 : return;
8130 : };
8131 :
8132 0 : auto selection = this->selection();
8133 0 : std::vector<EngravingItem*> filteredElements;
8134 :
8135 0 : if (selection && !selection->isNone()) {
8136 0 : std::vector<EngravingItem*> selectedElements;
8137 :
8138 0 : if (EngravingItem* element = selection->element()) {
8139 0 : selectedElements = { element };
8140 : } else {
8141 0 : selectedElements = selection->elements();
8142 : }
8143 :
8144 0 : for (EngravingItem* element : selectedElements) {
8145 0 : if (!element || !element->isHarmony()) {
8146 0 : continue;
8147 : }
8148 :
8149 0 : if (!element->ownershipParent()->isFretDiagram()) {
8150 0 : filteredElements.emplace_back(element);
8151 : }
8152 : }
8153 0 : }
8154 :
8155 0 : if (filteredElements.empty()) {
8156 0 : MScore::setError(MsError::NO_NOTE_REST_HARMONY_SELECTED);
8157 0 : checkAndShowError();
8158 0 : return;
8159 : }
8160 :
8161 0 : startEdit(TranslatableString("undoableAction", "Add fretboard diagram"));
8162 :
8163 0 : engraving::FretDiagram* lastAddedDiagram = nullptr;
8164 0 : std::vector<engraving::FretDiagram*> created;
8165 :
8166 0 : for (EngravingItem* element : filteredElements) {
8167 0 : engraving::FretDiagram* diagram = engraving::Factory::createFretDiagram(score->dummy());
8168 0 : diagram->setTrack(element->track());
8169 :
8170 0 : Harmony* harmony = toHarmony(element);
8171 0 : diagram->updateDiagram(harmony->harmonyName());
8172 :
8173 0 : diagram->setOwnershipParent(harmony->parent());
8174 0 : score->undoAddElement(diagram);
8175 0 : created.push_back(diagram);
8176 0 : lastAddedDiagram = diagram;
8177 : }
8178 :
8179 0 : for (int i = int(created.size()) - 1; i >= 0; --i) {
8180 0 : FretDiagram* diagram = created[i];
8181 0 : Harmony* harmony = toHarmony(filteredElements[i]);
8182 :
8183 0 : harmony->undoResetProperty(Pid::OFFSET);
8184 0 : harmony->undoChangeProperty(Pid::ALIGN, Align(AlignH::HCENTER, AlignV::BASELINE));
8185 0 : if (harmony->propertyFlags(Pid::ALIGN) == PropertyFlags::STYLED) {
8186 0 : harmony->setPropertyFlags(Pid::ALIGN, PropertyFlags::UNSTYLED);
8187 : }
8188 :
8189 0 : score->undoChangeParent(harmony, diagram,
8190 0 : track2staff(filteredElements[i]->track()));
8191 : }
8192 :
8193 0 : apply();
8194 :
8195 0 : if (lastAddedDiagram) {
8196 0 : select({ lastAddedDiagram });
8197 : }
8198 0 : }
8199 :
8200 0 : Harmony* NotationInteraction::editedHarmony() const
8201 : {
8202 0 : Harmony* harmony = toHarmony(m_editData.element);
8203 0 : if (!harmony) {
8204 0 : return nullptr;
8205 : }
8206 :
8207 0 : if (!harmony->parent() || (!harmony->parent()->isSegment()
8208 0 : && !harmony->parent()->isFretDiagram())) {
8209 0 : LOGD("no segment parent");
8210 0 : return nullptr;
8211 : }
8212 :
8213 0 : return harmony;
8214 : }
8215 :
8216 0 : Segment* NotationInteraction::harmonySegment(const Harmony* harmony) const
8217 : {
8218 0 : if (!harmony || !harmony->parent()) {
8219 0 : return nullptr;
8220 : }
8221 :
8222 0 : EngravingObject* parent = harmony->parent();
8223 0 : Segment* segment = parent->isSegment() ? toSegment(parent) : nullptr;
8224 0 : if (!segment && parent->isFretDiagram()) {
8225 0 : segment = toFretDiagram(parent)->segment();
8226 : }
8227 :
8228 0 : return segment;
8229 : }
8230 :
8231 0 : mu::engraving::Harmony* NotationInteraction::findHarmonyInSegment(const mu::engraving::Segment* segment, track_idx_t track,
8232 : mu::engraving::TextStyleType textStyleType) const
8233 : {
8234 0 : for (mu::engraving::EngravingItem* e : segment->annotations()) {
8235 0 : if (!e->isHarmony() && !e->isFretDiagram()) {
8236 0 : continue;
8237 : }
8238 :
8239 0 : Harmony* harmony = e->isHarmony() ? toHarmony(e) : nullptr;
8240 0 : if (e->isFretDiagram()) {
8241 0 : harmony = toFretDiagram(e)->harmony();
8242 : }
8243 :
8244 0 : if (!harmony) {
8245 0 : continue;
8246 : }
8247 :
8248 0 : if (harmony->track() == track && harmony->textStyleType() == textStyleType) {
8249 0 : return harmony;
8250 : }
8251 : }
8252 :
8253 0 : return nullptr;
8254 : }
8255 :
8256 0 : mu::engraving::Harmony* NotationInteraction::createHarmony(mu::engraving::Segment* segment, track_idx_t track,
8257 : mu::engraving::HarmonyType type) const
8258 : {
8259 0 : mu::engraving::Harmony* harmony = Factory::createHarmony(segment);
8260 0 : harmony->setScore(score());
8261 0 : harmony->setTrack(track);
8262 0 : harmony->setHarmonyType(type);
8263 :
8264 0 : return harmony;
8265 : }
8266 :
8267 0 : void NotationInteraction::startEditText(mu::engraving::TextBase* text)
8268 : {
8269 0 : doEndEditElement();
8270 0 : select({ text }, SelectType::SINGLE);
8271 :
8272 : //! NOTE: Copied from ScoreView::cmdAddText
8273 0 : Measure* measure = text->findMeasure();
8274 0 : if (measure && measure->hasMMRest() && text->links()) {
8275 0 : Measure* mmRest = measure->mmRest();
8276 0 : for (EngravingObject* link : *text->links()) {
8277 0 : TextBase* linkedText = toTextBase(link);
8278 0 : if (text != linkedText && linkedText->findMeasure() == mmRest) {
8279 0 : text = linkedText;
8280 0 : break;
8281 : }
8282 : }
8283 : }
8284 :
8285 0 : startEditText(text, PointF());
8286 0 : text->cursor()->moveCursorToEnd();
8287 0 : }
8288 :
8289 0 : bool NotationInteraction::needEndTextEdit() const
8290 : {
8291 0 : if (isTextEditingStarted()) {
8292 0 : const mu::engraving::TextBase* text = mu::engraving::toTextBase(m_editData.element);
8293 0 : return !text || !text->cursor()->editing();
8294 : }
8295 :
8296 0 : return false;
8297 : }
8298 :
8299 0 : void NotationInteraction::toggleFontStyle(mu::engraving::FontStyle style)
8300 : {
8301 0 : if (!m_editData.element || !m_editData.element->isTextBase()) {
8302 0 : LOGW("toggleFontStyle called with invalid current element");
8303 0 : return;
8304 : }
8305 0 : mu::engraving::TextBase* text = toTextBase(m_editData.element);
8306 0 : int currentStyle = text->getProperty(mu::engraving::Pid::FONT_STYLE).toInt();
8307 0 : score()->startCmd(TranslatableString("undoableAction", "Format text"));
8308 0 : text->undoChangeProperty(mu::engraving::Pid::FONT_STYLE, PropertyValue::fromValue(
8309 0 : currentStyle ^ static_cast<int>(style)), mu::engraving::PropertyFlags::UNSTYLED);
8310 0 : score()->endCmd();
8311 0 : notifyAboutTextEditingChanged();
8312 : }
8313 :
8314 0 : void NotationInteraction::toggleVerticalAlignment(VerticalAlignment align)
8315 : {
8316 0 : if (!m_editData.element || !m_editData.element->isTextBase()) {
8317 0 : LOGW("toggleVerticalAlignment called with invalid current element");
8318 0 : return;
8319 : }
8320 0 : mu::engraving::TextBase* text = toTextBase(m_editData.element);
8321 0 : int ialign = static_cast<int>(align);
8322 0 : int currentAlign = text->getProperty(mu::engraving::Pid::TEXT_SCRIPT_ALIGN).toInt();
8323 :
8324 0 : TranslatableString actionName = [align]() {
8325 0 : switch (align) {
8326 0 : case VerticalAlignment::AlignSubScript:
8327 0 : return TranslatableString("undoableAction", "Toggle subscript");
8328 0 : case VerticalAlignment::AlignSuperScript:
8329 0 : return TranslatableString("undoableAction", "Toggle superscript");
8330 0 : default:
8331 0 : return TranslatableString("undoableAction", "Toggle subscript/superscript");
8332 : }
8333 0 : }();
8334 :
8335 0 : score()->startCmd(actionName);
8336 0 : text->undoChangeProperty(mu::engraving::Pid::TEXT_SCRIPT_ALIGN, PropertyValue::fromValue(
8337 0 : (currentAlign == ialign) ? static_cast<int>(VerticalAlignment::AlignNormal) : ialign),
8338 : mu::engraving::PropertyFlags::UNSTYLED);
8339 0 : score()->endCmd();
8340 0 : notifyAboutTextEditingChanged();
8341 0 : }
8342 :
8343 0 : void NotationInteraction::toggleBold()
8344 : {
8345 0 : toggleFontStyle(mu::engraving::FontStyle::Bold);
8346 0 : }
8347 :
8348 0 : void NotationInteraction::toggleItalic()
8349 : {
8350 0 : toggleFontStyle(mu::engraving::FontStyle::Italic);
8351 0 : }
8352 :
8353 0 : void NotationInteraction::toggleUnderline()
8354 : {
8355 0 : toggleFontStyle(mu::engraving::FontStyle::Underline);
8356 0 : }
8357 :
8358 0 : void NotationInteraction::toggleStrike()
8359 : {
8360 0 : toggleFontStyle(mu::engraving::FontStyle::Strike);
8361 0 : }
8362 :
8363 0 : void NotationInteraction::toggleSubScript()
8364 : {
8365 0 : toggleVerticalAlignment(VerticalAlignment::AlignSubScript);
8366 0 : }
8367 :
8368 0 : void NotationInteraction::toggleSuperScript()
8369 : {
8370 0 : toggleVerticalAlignment(VerticalAlignment::AlignSuperScript);
8371 0 : }
8372 :
8373 : template<typename P>
8374 0 : void NotationInteraction::execute(void (mu::engraving::Score::* function)(P), P param, const TranslatableString& actionName)
8375 : {
8376 0 : startEdit(actionName);
8377 0 : (score()->*function)(param);
8378 0 : apply();
8379 0 : }
8380 :
8381 0 : void NotationInteraction::toggleArticulation(mu::engraving::SymId symId)
8382 : {
8383 0 : startEdit(TranslatableString("undoableAction", "Toggle articulation"));
8384 0 : mu::engraving::EditChord::toggleArticulation(score(), symId);
8385 0 : apply();
8386 0 : }
8387 :
8388 0 : void NotationInteraction::toggleOrnament(mu::engraving::SymId symId)
8389 : {
8390 0 : startEdit(TranslatableString("undoableAction", "Toggle ornament"));
8391 0 : mu::engraving::EditNote::toggleOrnament(score(), symId);
8392 0 : apply();
8393 0 : }
8394 :
8395 0 : void NotationInteraction::toggleAutoplace(bool all)
8396 : {
8397 0 : execute(&mu::engraving::Score::cmdToggleAutoplace, all, TranslatableString("undoableAction", "Toggle automatic placement"));
8398 0 : }
8399 :
8400 0 : bool NotationInteraction::canInsertClef(ClefType type) const
8401 : {
8402 0 : const Score* score = this->score();
8403 0 : return score && mu::engraving::EditClef::canInsertClef(score, type);
8404 : }
8405 :
8406 0 : void NotationInteraction::insertClef(ClefType type)
8407 : {
8408 0 : transaction(TranslatableString("undoableAction", "Add clef"), [&](Transaction& tx) {
8409 0 : mu::engraving::EditClef::insertClef(tx, score(), type);
8410 0 : });
8411 0 : }
8412 :
8413 0 : void NotationInteraction::changeAccidental(mu::engraving::AccidentalType accidental)
8414 : {
8415 0 : transaction(TranslatableString("undoableAction", "Add accidental"), [&](auto& ) {
8416 0 : mu::engraving::EditNote::changeAccidental(score(), accidental);
8417 0 : });
8418 0 : }
8419 :
8420 0 : void NotationInteraction::transposeSemitone(int steps)
8421 : {
8422 0 : transaction(TranslatableString("undoableAction", "Transpose semitone"), [&](auto& tx) {
8423 0 : Transpose::transposeSemitone(tx, score(), steps);
8424 0 : });
8425 0 : }
8426 :
8427 0 : void NotationInteraction::transposeDiatonicAlterations(mu::engraving::TransposeDirection direction)
8428 : {
8429 0 : transaction(TranslatableString("undoableAction", "Transpose diatonically"), [&](auto& tx) {
8430 0 : Transpose::transposeDiatonicAlterations(tx, score(), direction);
8431 0 : });
8432 0 : }
8433 :
8434 0 : void NotationInteraction::getLocation()
8435 : {
8436 0 : auto* e = score()->selection().element();
8437 0 : if (!e) {
8438 : // no current selection - restore lost selection
8439 0 : e = score()->selection().currentCR();
8440 0 : if (e && e->isChord()) {
8441 0 : e = toChord(e)->upNote();
8442 : }
8443 : }
8444 0 : if (!e) {
8445 0 : e = Navigation::firstElement(score(), false);
8446 : }
8447 0 : if (e) {
8448 0 : if (e->isNote() || e->isHarmony()) {
8449 0 : score()->setPlayNote(true);
8450 : }
8451 0 : select({ e }, SelectType::SINGLE);
8452 0 : showItem(e);
8453 : }
8454 0 : }
8455 :
8456 0 : void NotationInteraction::execute(void (mu::engraving::Score::* function)(), const TranslatableString& actionName)
8457 : {
8458 0 : startEdit(actionName);
8459 0 : (score()->*function)();
8460 0 : apply();
8461 0 : }
8462 :
8463 : //! NOTE: Copied from ScoreView::adjustCanvasPosition
8464 0 : void NotationInteraction::showItem(const mu::engraving::EngravingItem* el, int staffIndex)
8465 : {
8466 0 : if (!el) {
8467 0 : return;
8468 : }
8469 :
8470 0 : if (!configuration()->isAutomaticallyPanEnabled()) {
8471 0 : return;
8472 : }
8473 :
8474 0 : const mu::engraving::MeasureBase* m = nullptr;
8475 :
8476 0 : if (el->isNote()) {
8477 0 : m = toNote(el)->chord()->measure();
8478 0 : } else if (el->isRest()) {
8479 0 : m = toRest(el)->measure();
8480 0 : } else if (el->isChord()) {
8481 0 : m = toChord(el)->measure();
8482 0 : } else if (el->isSegment()) {
8483 0 : m = toSegment(el)->measure();
8484 0 : } else if (el->isLyrics()) {
8485 0 : m = toLyrics(el)->measure();
8486 0 : } else if ((el->isHarmony() || el->isFiguredBass()) && el->parent()->isSegment()) {
8487 0 : m = toSegment(el->parent())->measure();
8488 0 : } else if (el->isHarmony() && el->parent()->isFretDiagram() && el->parent()->parent()->isSegment()) {
8489 0 : m = toSegment(el->parent()->parent())->measure();
8490 0 : } else if (el->isMeasureBase()) {
8491 0 : m = toMeasureBase(el);
8492 0 : } else if (el->isSpannerSegment()) {
8493 0 : EngravingItem* se = toSpannerSegment(el)->spanner()->startElement();
8494 0 : m = se->findMeasure();
8495 0 : } else if (el->isSpanner()) {
8496 0 : EngravingItem* se = toSpanner(el)->startElement();
8497 0 : m = se->findMeasure();
8498 0 : } else if (el->isPage()) {
8499 0 : const mu::engraving::Page* p = toPage(el);
8500 0 : mu::engraving::System* s = !p->systems().empty() ? p->systems().front() : nullptr;
8501 0 : m = s && !s->measures().empty() ? s->measures().front() : nullptr;
8502 : } else {
8503 : // attempt to find measure
8504 0 : mu::engraving::EngravingObject* e = el->parent();
8505 0 : while (e && !e->isMeasureBase()) {
8506 0 : e = e->parent();
8507 : }
8508 0 : if (e) {
8509 0 : m = toMeasureBase(e);
8510 : } else {
8511 0 : return;
8512 : }
8513 : }
8514 0 : if (!m) {
8515 0 : return;
8516 : }
8517 :
8518 0 : mu::engraving::System* sys = m->system();
8519 0 : if (!sys) {
8520 0 : return;
8521 : }
8522 :
8523 0 : RectF mRect(m->canvasBoundingRect());
8524 0 : RectF sysRect = mRect;
8525 :
8526 0 : double _spatium = score()->style().spatium();
8527 0 : const qreal border = _spatium * 3;
8528 0 : RectF showRect;
8529 0 : if (staffIndex == -1) {
8530 0 : showRect = RectF(mRect.x(), sysRect.y(), mRect.width(), sysRect.height())
8531 0 : .adjusted(-border, -border, border, border);
8532 : } else {
8533 : // find a box for the individual stave in a system
8534 0 : RectF stave = RectF(sys->canvasBoundingRect().left(),
8535 : sys->staffCanvasYpage(staffIndex),
8536 : sys->width(),
8537 0 : sys->staff(staffIndex)->bbox().height());
8538 0 : showRect = mRect.intersected(stave).adjusted(-border, -border, border, border);
8539 : }
8540 :
8541 0 : ShowItemRequest request;
8542 0 : request.item = el;
8543 0 : request.showRect = showRect;
8544 :
8545 0 : m_showItemRequested.send(request);
8546 : }
8547 :
8548 0 : void NotationInteraction::toggleDebugShowGapRests()
8549 : {
8550 : // Doesn't perform any action on the score, just triggers the necessary relayout
8551 0 : startEdit(TranslatableString("debugOption", "Toggle show gap rests"));
8552 0 : score()->setLayoutAll();
8553 0 : apply();
8554 0 : }
8555 :
8556 0 : muse::async::Channel<NotationInteraction::ShowItemRequest> NotationInteraction::showItemRequested() const
8557 : {
8558 0 : return m_showItemRequested;
8559 : }
8560 :
8561 0 : void NotationInteraction::setGetViewRectFunc(const std::function<RectF()>& func)
8562 : {
8563 0 : static_cast<NotationNoteInput*>(m_noteInput.get())->setGetViewRectFunc(func);
8564 0 : }
8565 :
8566 0 : void NotationInteraction::checkAndShowError()
8567 : {
8568 0 : IF_ASSERT_FAILED(m_errorsController) {
8569 0 : return;
8570 : }
8571 0 : m_errorsController->checkAndShowMScoreError();
8572 : }
|