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 : #include "notationnoteinput.h"
23 :
24 : #include "engraving/dom/articulation.h"
25 : #include "engraving/dom/chord.h"
26 : #include "engraving/dom/drumset.h"
27 : #include "engraving/dom/input.h"
28 : #include "engraving/dom/masterscore.h"
29 : #include "engraving/dom/mscore.h"
30 : #include "engraving/dom/note.h"
31 : #include "engraving/dom/part.h"
32 : #include "engraving/dom/segment.h"
33 : #include "engraving/dom/slur.h"
34 : #include "engraving/dom/staff.h"
35 : #include "engraving/dom/stafftype.h"
36 : #include "engraving/dom/system.h"
37 : #include "engraving/dom/tuplet.h"
38 : #include "engraving/dom/utils.h"
39 :
40 : #include "engraving/editing/editnote.h"
41 : #include "engraving/editing/edittie.h"
42 : #include "engraving/editing/noteinput.h"
43 : #include "engraving/editing/transaction/transaction.h"
44 :
45 : #include "log.h"
46 :
47 : using namespace mu::notation;
48 : using namespace mu::engraving;
49 : using namespace muse;
50 : using namespace muse::async;
51 :
52 0 : TranslatableString nameOfNoteInputMethod(NoteInputMethod method)
53 : {
54 0 : switch (method) {
55 0 : case NoteInputMethod::UNKNOWN: break;
56 0 : case NoteInputMethod::BY_NOTE_NAME: return TranslatableString("noteInputMethod", "Input by note name mode");
57 0 : case NoteInputMethod::BY_DURATION: return TranslatableString("noteInputMethod", "Input by duration mode");
58 0 : case NoteInputMethod::REPITCH: return TranslatableString("noteInputMethod", "Re-pitch existing notes mode");
59 0 : case NoteInputMethod::RHYTHM: return TranslatableString("noteInputMethod", "Rhythm-only input mode");
60 0 : case NoteInputMethod::REALTIME_AUTO: return TranslatableString("noteInputMethod", "Metronome real-time input mode");
61 0 : case NoteInputMethod::REALTIME_MANUAL: return TranslatableString("noteInputMethod", "Pedal real-time input mode");
62 0 : case NoteInputMethod::TIMEWISE: return TranslatableString("noteInputMethod", "Insert mode (grow measures)");
63 : // No default case. We want a compiler warning if an enum value is not handled here.
64 : }
65 :
66 0 : UNREACHABLE;
67 : return TranslatableString("noteInputMethod", "Unknown note input mode");
68 : }
69 :
70 0 : static bool noteInputMethodAvailable(NoteInputMethod method, const Staff* staff, const Fraction& tick)
71 : {
72 0 : if (method == NoteInputMethod::BY_DURATION) {
73 0 : return staff && !staff->isTabStaff(tick);
74 : }
75 :
76 0 : return true;
77 : }
78 :
79 0 : NotationNoteInput::NotationNoteInput(const IGetScore* getScore, INotationInteraction* interaction, INotationUndoStackPtr undoStack
80 0 : , const modularity::ContextPtr& iocCtx)
81 0 : : muse::Contextable(iocCtx), m_getScore(getScore), m_interaction(interaction), m_undoStack(undoStack)
82 : {
83 0 : m_interaction->selectionChanged().onNotify(this, [this]() {
84 0 : if (!isNoteInputMode()) {
85 0 : updateInputState();
86 0 : } else if (shouldSetupInputNote()) {
87 0 : const NoteInputState& is = state();
88 0 : const staff_idx_t prevStaffIdx = track2staff(is.prevTrack());
89 :
90 0 : if (prevStaffIdx != is.staffIdx()) {
91 0 : setupInputNote();
92 : }
93 : }
94 0 : });
95 0 : }
96 :
97 0 : bool NotationNoteInput::isNoteInputMode() const
98 : {
99 0 : return score()->inputState().noteEntryMode();
100 : }
101 :
102 0 : const NoteInputState& NotationNoteInput::state() const
103 : {
104 0 : return score()->inputState();
105 : }
106 :
107 : //! NOTE Copied from `void ScoreView::startNoteEntry()`
108 0 : void NotationNoteInput::startNoteInput(NoteInputMethod method, bool focusNotation)
109 : {
110 0 : TRACEFUNC;
111 :
112 0 : if (isNoteInputMode()) {
113 0 : return;
114 : }
115 :
116 0 : EngravingItem* el = resolveNoteInputStartPosition();
117 0 : if (!el) {
118 0 : return;
119 : }
120 :
121 0 : if (Part* part = el->part(); part && part->isSharedPart()) {
122 0 : return;
123 : }
124 :
125 0 : m_interaction->select({ el }, SelectType::SINGLE, 0);
126 :
127 0 : InputState& is = score()->inputState();
128 :
129 0 : Duration d(is.duration());
130 0 : if (!d.isValid() || d.isZero() || d.type() == DurationType::V_MEASURE) {
131 0 : is.setDuration(Duration(DurationType::V_QUARTER));
132 : }
133 0 : is.setAccidentalType(AccidentalType::NONE);
134 :
135 0 : is.setRest(false);
136 0 : is.setNoteEntryMode(true);
137 :
138 0 : const Staff* staff = score()->staff(is.track() / VOICES);
139 :
140 0 : if (noteInputMethodAvailable(method, staff, is.tick())) {
141 0 : is.setNoteEntryMethod(method);
142 : } else {
143 0 : is.setNoteEntryMethod(NoteInputMethod::BY_NOTE_NAME); // fallback
144 : }
145 :
146 0 : if (shouldSetupInputNote()) {
147 0 : setupInputNote();
148 : }
149 :
150 0 : switch (staff->staffType(is.tick())->group()) {
151 0 : case StaffGroup::STANDARD:
152 0 : break;
153 0 : case StaffGroup::TAB: {
154 0 : int strg = 0; // assume topmost string as current string
155 : // if entering note entry with a note selected and the note has a string
156 : // set InputState::_string to note physical string
157 0 : if (el->isNote()) {
158 0 : strg = (toNote(el))->string();
159 : }
160 0 : is.setString(strg);
161 0 : break;
162 : }
163 0 : case StaffGroup::PERCUSSION:
164 0 : break;
165 : }
166 :
167 0 : notifyAboutNoteInputStarted(focusNotation);
168 0 : notifyAboutStateChanged();
169 :
170 0 : m_interaction->showItem(el);
171 0 : accessibilityController()->announce(nameOfNoteInputMethod(method).translated());
172 0 : }
173 :
174 0 : EngravingItem* NotationNoteInput::resolveNoteInputStartPosition() const
175 : {
176 0 : ChordRest* topLeftChordRest = nullptr;
177 :
178 0 : if (score()->selection().isNone() && m_getViewRectFunc) {
179 : // no selection
180 : // choose page in current view (favor top left quadrant if possible)
181 : // select first (top/left) chordrest of that page in current view
182 : // or, CR at last selected position if that is in view
183 0 : RectF viewRect = m_getViewRectFunc();
184 0 : PointF topLeft = viewRect.topLeft();
185 :
186 0 : std::vector<PointF> points;
187 0 : points.push_back({ topLeft.x() + viewRect.width() * 0.25, topLeft.y() + viewRect.height() * 0.25 });
188 0 : points.push_back(topLeft);
189 0 : points.push_back(viewRect.bottomLeft());
190 0 : points.push_back(viewRect.topRight());
191 0 : points.push_back(viewRect.bottomRight());
192 :
193 0 : Page* page = nullptr;
194 0 : for (const PointF& point : points) {
195 0 : page = score()->searchPage(point);
196 0 : if (page) {
197 0 : break;
198 : }
199 : }
200 :
201 0 : if (page) {
202 0 : qreal tlY = 0.0;
203 0 : Fraction tlTick = Fraction(0, 1);
204 0 : RectF pageRect = page->ldata()->bbox().translated(page->x(), page->y());
205 0 : RectF intersect = viewRect & pageRect;
206 0 : intersect.translate(-page->x(), -page->y());
207 0 : std::vector<EngravingItem*> el = page->items(intersect);
208 :
209 0 : const ChordRest* lastSelected = score()->selection().currentCR();
210 :
211 0 : if (lastSelected && lastSelected->voice()) {
212 : // if last selected CR was not in voice 1,
213 : // find CR in voice 1 instead
214 0 : track_idx_t track = trackZeroVoice(lastSelected->track());
215 0 : const Segment* s = lastSelected->segment();
216 0 : if (s) {
217 0 : lastSelected = s->nextChordRest(track, true);
218 : }
219 : }
220 :
221 0 : for (EngravingItem* e : el) {
222 : // loop through visible elements
223 : // looking for the CR in voice 1 with earliest tick and highest staff position
224 : // but stop if we find the last selected CR
225 0 : ElementType et = e->type();
226 0 : if (et == ElementType::NOTE || et == ElementType::REST) {
227 0 : if (e->voice()) {
228 0 : continue;
229 : }
230 : ChordRest* cr;
231 0 : if (et == ElementType::NOTE) {
232 0 : cr = toChordRest(e->parent());
233 0 : if (!cr) {
234 0 : continue;
235 : }
236 : } else {
237 0 : cr = toChordRest(e);
238 : }
239 0 : if (cr == lastSelected) {
240 0 : topLeftChordRest = cr;
241 0 : break;
242 : }
243 : // compare ticks rather than x position
244 : // to make sure we favor earlier rather than later systems
245 : // even though later system might have note farther to left
246 0 : Fraction crTick = Fraction(0, 1);
247 0 : if (cr->segment()) {
248 0 : crTick = cr->segment()->tick();
249 : } else {
250 0 : continue;
251 : }
252 : // compare staff Y position rather than note Y position
253 : // to be sure we do not reject earliest note
254 : // just because it is lower in pitch than subsequent notes
255 0 : qreal crY = 0.0;
256 0 : if (cr->measure() && cr->measure()->system()) {
257 0 : crY = cr->measure()->system()->staffYpage(cr->staffIdx());
258 : } else {
259 0 : continue;
260 : }
261 0 : if (topLeftChordRest) {
262 0 : if (crTick <= tlTick && crY <= tlY) {
263 0 : topLeftChordRest = cr;
264 0 : tlTick = crTick;
265 0 : tlY = crY;
266 : }
267 : } else {
268 0 : topLeftChordRest = cr;
269 0 : tlTick = crTick;
270 0 : tlY = crY;
271 : }
272 : }
273 : }
274 0 : }
275 0 : }
276 :
277 0 : EngravingItem* el = topLeftChordRest ? topLeftChordRest : score()->selection().element();
278 0 : if (!el) {
279 0 : el = score()->selection().firstChordRest();
280 : }
281 :
282 0 : const InputState& is = score()->inputState();
283 :
284 0 : if (!el) {
285 0 : if (const Segment* segment = is.lastSegment()) {
286 0 : el = segment->element(is.track());
287 : }
288 : }
289 :
290 0 : if (!el || (!el->isChordRest() && !el->isNote())) {
291 : // if no note/rest is selected, start with voice 0
292 0 : engraving::track_idx_t track = is.track() == muse::nidx ? 0 : (is.track() / VOICES) * VOICES;
293 : // try to find an appropriate measure to start in
294 0 : Fraction tick = el ? el->tick() : Fraction(0, 1);
295 0 : el = score()->searchNote(tick, track);
296 0 : if (!el) {
297 0 : el = score()->searchNote(Fraction(0, 1), track);
298 : }
299 : }
300 :
301 0 : if (!el) {
302 0 : return nullptr;
303 : }
304 :
305 0 : if (el->isChord()) {
306 0 : Chord* c = toChord(el);
307 0 : Note* note = c->selectedNote();
308 0 : if (note == 0) {
309 0 : note = c->upNote();
310 : }
311 0 : el = note;
312 : }
313 :
314 0 : return el;
315 : }
316 :
317 0 : bool NotationNoteInput::shouldSetupInputNote() const
318 : {
319 0 : if (!score()->inputState().isValid()) {
320 0 : return false;
321 : }
322 0 : return usingNoteInputMethod(NoteInputMethod::BY_DURATION)
323 0 : || usingNoteInputMethod(NoteInputMethod::RHYTHM);
324 : }
325 :
326 0 : void NotationNoteInput::setupInputNote()
327 : {
328 0 : InputState& is = score()->inputState();
329 0 : const EngravingItem* selectedItem = score()->selection().element();
330 :
331 0 : if (selectedItem && selectedItem->isNote()) {
332 0 : is.setNotes({ toNote(selectedItem)->noteVal() });
333 0 : return;
334 : }
335 :
336 0 : Staff* staff = is.staff();
337 0 : IF_ASSERT_FAILED(staff) {
338 0 : return;
339 : }
340 :
341 0 : const Fraction tick = is.tick();
342 0 : NoteVal nval;
343 :
344 0 : if (staff->isTabStaff(tick)) {
345 0 : if (const StringData* stringData = staff->part()->stringData(tick, is.staffIdx())) {
346 0 : nval.fret = 0;
347 0 : nval.string = is.string();
348 0 : nval.pitch = stringData->getPitch(nval.string, nval.fret, staff);
349 : }
350 0 : } else if (staff->isDrumStaff(tick)) {
351 0 : if (const Drumset* drumset = is.drumset()) {
352 0 : nval.pitch = is.drumNote();
353 :
354 0 : if (nval.pitch < 0) {
355 0 : nval.pitch = drumset->nextPitch(nval.pitch);
356 : }
357 :
358 0 : if (drumset->isValid(nval.pitch)) {
359 0 : nval.headGroup = drumset->noteHead(nval.pitch);
360 : }
361 : }
362 : } else {
363 0 : nval = noteValForLine(staff->middleLine(tick));
364 : }
365 :
366 0 : if (nval.pitch > 0) {
367 0 : is.setNotes({ nval });
368 : }
369 : }
370 :
371 0 : NoteVal NotationNoteInput::noteValForLine(int line) const
372 : {
373 0 : const InputState& is = score()->inputState();
374 :
375 0 : Position pos;
376 0 : pos.segment = is.segment();
377 0 : pos.staffIdx = is.staffIdx();
378 0 : pos.line = line;
379 :
380 0 : bool error = false;
381 0 : const NoteVal nval = NoteInput::noteValForPosition(score(), pos, is.accidentalType(), error);
382 0 : if (error) {
383 0 : LOGE() << "Could not find note val for position, staffIdx: " << pos.staffIdx << ", line: " << pos.line;
384 : }
385 :
386 0 : return nval;
387 : }
388 :
389 0 : void NotationNoteInput::endNoteInput(bool resetState)
390 : {
391 0 : TRACEFUNC;
392 :
393 0 : if (!isNoteInputMode()) {
394 0 : return;
395 : }
396 :
397 0 : InputState& is = score()->inputState();
398 0 : is.setNoteEntryMode(false);
399 :
400 0 : if (is.slur()) {
401 0 : const std::vector<SpannerSegment*>& el = is.slur()->spannerSegments();
402 0 : if (!el.empty()) {
403 0 : el.front()->setSelected(false);
404 : }
405 0 : is.setSlur(0);
406 : }
407 :
408 0 : if (resetState) {
409 0 : is.setTrack(muse::nidx);
410 0 : is.setString(-1);
411 0 : is.setSegment(nullptr);
412 0 : is.setNotes({});
413 : }
414 :
415 0 : notifyAboutNoteInputEnded();
416 0 : updateInputState();
417 0 : accessibilityController()->announce(TranslatableString("noteInputMethod", "Normal mode").translated());
418 0 : }
419 :
420 0 : Channel</*focusNotation*/ bool> NotationNoteInput::noteInputStarted() const
421 : {
422 0 : return m_noteInputStarted;
423 : }
424 :
425 0 : Notification NotationNoteInput::noteInputEnded() const
426 : {
427 0 : return m_noteInputEnded;
428 : }
429 :
430 0 : bool NotationNoteInput::usingNoteInputMethod(NoteInputMethod method) const
431 : {
432 0 : return score()->usingNoteEntryMethod(method);
433 : }
434 :
435 0 : void NotationNoteInput::setNoteInputMethod(NoteInputMethod method)
436 : {
437 0 : TRACEFUNC;
438 :
439 0 : NoteInputState& is = score()->inputState();
440 0 : if (is.usingNoteEntryMethod(method)) {
441 0 : return;
442 : }
443 :
444 0 : if (!noteInputMethodAvailable(method, is.staff(), is.tick())) {
445 0 : return;
446 : }
447 :
448 0 : is.setNoteEntryMethod(method);
449 0 : if (shouldSetupInputNote()) {
450 0 : setupInputNote();
451 : }
452 :
453 0 : notifyAboutStateChanged();
454 0 : }
455 :
456 0 : void NotationNoteInput::addNote(const NoteInputParams& params, NoteAddingMode addingMode)
457 : {
458 0 : TRACEFUNC;
459 :
460 0 : m_interaction->hideShadowNote();
461 :
462 0 : bool addToUpOnCurrentChord = addingMode == NoteAddingMode::CurrentChord;
463 0 : bool insertNewChord = addingMode == NoteAddingMode::InsertChord;
464 :
465 0 : if (addToUpOnCurrentChord) {
466 0 : startEdit(TranslatableString("undoableAction", "Add note to chord"));
467 0 : } else if (insertNewChord) {
468 0 : startEdit(TranslatableString("undoableAction", "Insert note"));
469 : } else {
470 0 : startEdit(TranslatableString("undoableAction", "Enter note"));
471 : }
472 0 : NoteInput::addPitch(score()->transactionManager()->currentOrDummyTransaction(), score(), params, addToUpOnCurrentChord,
473 : insertNewChord);
474 :
475 0 : apply();
476 :
477 0 : if (shouldSetupInputNote()) {
478 0 : setupInputNote();
479 : }
480 :
481 0 : notifyNoteAddedChanged();
482 0 : notifyAboutStateChanged();
483 :
484 0 : m_interaction->checkAndShowError();
485 0 : m_interaction->showItem(state().cr());
486 0 : }
487 :
488 0 : void NotationNoteInput::applyNoteValueChange(const muse::TranslatableString& actionName,
489 : const std::function<void(mu::engraving::Transaction&)>& change)
490 : {
491 0 : TRACEFUNC;
492 :
493 0 : m_interaction->hideShadowNote();
494 :
495 0 : startEdit(actionName);
496 0 : change(score()->transactionManager()->currentOrDummyTransaction());
497 0 : apply();
498 :
499 0 : notifyAboutStateChanged();
500 :
501 0 : m_interaction->checkAndShowError();
502 0 : }
503 :
504 0 : void NotationNoteInput::setDuration(DurationType duration)
505 : {
506 0 : applyNoteValueChange(muse::TranslatableString("undoableAction", "Set duration"), [this, duration](mu::engraving::Transaction& tx) {
507 0 : NoteInput::setDuration(tx, score(), duration);
508 0 : });
509 0 : }
510 :
511 0 : void NotationNoteInput::toggleRest()
512 : {
513 0 : applyNoteValueChange(muse::TranslatableString("undoableAction", "Toggle rest"), [this](mu::engraving::Transaction& tx) {
514 0 : NoteInput::toggleRest(tx, score());
515 0 : });
516 0 : }
517 :
518 0 : void NotationNoteInput::toggleDots(int dots)
519 : {
520 0 : applyNoteValueChange(muse::TranslatableString("undoableAction", "Toggle dots"), [this, dots](mu::engraving::Transaction& tx) {
521 0 : NoteInput::toggleDots(tx, score(), dots);
522 0 : });
523 0 : }
524 :
525 0 : Ret NotationNoteInput::putNote(const PointF& pos, bool replace, bool insert)
526 : {
527 0 : TRACEFUNC;
528 :
529 0 : startEdit(TranslatableString("undoableAction", "Enter note"));
530 0 : Ret ret = NoteInput::putNote(score()->transactionManager()->currentOrDummyTransaction(), score(), pos, replace, insert);
531 0 : apply();
532 :
533 0 : if (ret) {
534 0 : if (shouldSetupInputNote()) {
535 0 : setupInputNote();
536 : }
537 : }
538 :
539 0 : notifyNoteAddedChanged();
540 0 : notifyAboutStateChanged();
541 :
542 0 : m_interaction->checkAndShowError();
543 :
544 0 : return ret;
545 0 : }
546 :
547 0 : void NotationNoteInput::removeNote(const PointF& pos)
548 : {
549 0 : TRACEFUNC;
550 :
551 0 : InputState& inputState = score()->inputState();
552 0 : bool restMode = inputState.rest();
553 :
554 0 : startEdit(TranslatableString("undoableAction", "Delete note"));
555 0 : inputState.setRest(!restMode);
556 0 : NoteInput::putNote(score()->transactionManager()->currentOrDummyTransaction(), score(), pos, false, false);
557 0 : inputState.setRest(restMode);
558 0 : apply();
559 :
560 0 : notifyAboutStateChanged();
561 :
562 0 : m_interaction->checkAndShowError();
563 0 : }
564 :
565 0 : void NotationNoteInput::setInputNote(const NoteInputParams& params)
566 : {
567 0 : TRACEFUNC;
568 :
569 0 : NoteInputState& is = score()->inputState();
570 0 : IF_ASSERT_FAILED(is.isValid()) {
571 0 : return;
572 : }
573 0 : const Staff* staff = is.staff();
574 0 : const Fraction tick = is.tick();
575 :
576 0 : NoteVal nval;
577 :
578 0 : if (staff->isDrumStaff(tick)) {
579 0 : const Drumset* drumset = is.drumset();
580 0 : if (drumset && drumset->isValid(params.drumPitch)) {
581 0 : nval.pitch = params.drumPitch;
582 0 : nval.headGroup = drumset->noteHead(params.drumPitch);
583 : }
584 : } else {
585 0 : nval = noteValForLine(relStep(params.step, staff->clef(tick)));
586 : }
587 :
588 0 : if (nval.pitch > 0) {
589 0 : setInputNotes({ nval });
590 : }
591 0 : }
592 :
593 0 : void NotationNoteInput::setInputNotes(const NoteValList& notes)
594 : {
595 0 : TRACEFUNC;
596 :
597 0 : NoteInputState& is = score()->inputState();
598 :
599 0 : if (is.notes() == notes) {
600 0 : return;
601 : }
602 :
603 0 : if (!notes.empty()) {
604 0 : if (const Drumset* drumset = is.drumset()) {
605 0 : const int pitch = notes.front().pitch;
606 0 : is.setDrumNote(pitch);
607 0 : is.setVoice(drumset->voice(pitch));
608 : }
609 : }
610 :
611 0 : is.setNotes(notes);
612 0 : notifyAboutStateChanged();
613 0 : }
614 :
615 0 : void NotationNoteInput::moveInputNotes(bool up, PitchMode mode)
616 : {
617 0 : TRACEFUNC;
618 :
619 0 : InputState& is = score()->inputState();
620 0 : IF_ASSERT_FAILED(is.isValid()) {
621 0 : return;
622 : }
623 :
624 0 : is.setAccidentalType(AccidentalType::NONE);
625 :
626 0 : const Staff* staff = is.staff();
627 0 : const Fraction tick = is.tick();
628 :
629 0 : NoteValList notes;
630 :
631 0 : for (const NoteVal& val : is.notes()) {
632 0 : NoteVal newVal;
633 :
634 0 : if (staff->isDrumStaff(tick)) {
635 0 : if (const Drumset* drumset = is.drumset()) {
636 0 : newVal.pitch = up ? drumset->nextPitch(val.pitch) : drumset->prevPitch(val.pitch);
637 :
638 0 : if (drumset->isValid(newVal.pitch)) {
639 0 : newVal.headGroup = drumset->noteHead(newVal.pitch);
640 0 : notes.push_back(newVal);
641 : }
642 : }
643 0 : continue;
644 0 : }
645 :
646 0 : switch (mode) {
647 0 : case PitchMode::CHROMATIC:
648 0 : newVal.pitch = val.pitch + (up ? 1 : -1);
649 0 : break;
650 0 : case PitchMode::DIATONIC: {
651 0 : const int oldLine = noteValToLine(val, staff, tick);
652 0 : const int newLine = oldLine + (up ? -1 : 1);
653 0 : newVal = noteValForLine(newLine);
654 0 : } break;
655 0 : case PitchMode::OCTAVE:
656 0 : newVal = val;
657 0 : newVal.pitch += up ? PITCH_DELTA_OCTAVE : -PITCH_DELTA_OCTAVE;
658 0 : break;
659 : }
660 :
661 0 : newVal.pitch = clampPitch(newVal.pitch);
662 0 : notes.push_back(newVal);
663 : }
664 :
665 0 : setInputNotes(notes);
666 0 : }
667 :
668 0 : void NotationNoteInput::setRestMode(bool rest)
669 : {
670 0 : InputState& is = score()->inputState();
671 0 : if (is.rest() == rest) {
672 0 : return;
673 : }
674 :
675 0 : is.setRest(rest);
676 0 : notifyAboutStateChanged();
677 : }
678 :
679 0 : void NotationNoteInput::setAccidental(AccidentalType accidentalType)
680 : {
681 0 : TRACEFUNC;
682 :
683 0 : EditNote::toggleAccidental(score(), accidentalType);
684 :
685 0 : notifyAboutStateChanged();
686 :
687 0 : m_interaction->checkAndShowError();
688 0 : }
689 :
690 0 : void NotationNoteInput::setArticulation(SymbolId articulationSymbolId)
691 : {
692 0 : TRACEFUNC;
693 :
694 0 : InputState& inputState = score()->inputState();
695 :
696 : std::set<SymbolId> articulations = updateArticulations(
697 0 : inputState.articulationIds(), articulationSymbolId, ArticulationsUpdateMode::Remove);
698 0 : inputState.setArticulationIds(articulations);
699 :
700 0 : notifyAboutStateChanged();
701 :
702 0 : m_interaction->checkAndShowError();
703 0 : }
704 :
705 0 : void NotationNoteInput::setDrumNote(int note)
706 : {
707 0 : TRACEFUNC;
708 :
709 0 : InputState& is = score()->inputState();
710 0 : if (is.drumNote() == note) {
711 0 : return;
712 : }
713 :
714 0 : is.setDrumNote(note);
715 0 : if (shouldSetupInputNote()) {
716 0 : setupInputNote();
717 : }
718 :
719 0 : notifyAboutStateChanged();
720 0 : }
721 :
722 0 : void NotationNoteInput::setCurrentVoice(voice_idx_t voiceIndex)
723 : {
724 0 : TRACEFUNC;
725 :
726 0 : InputState& inputState = score()->inputState();
727 0 : if (!isVoiceIndexValid(voiceIndex) || voiceIndex == inputState.voice()) {
728 0 : return;
729 : }
730 :
731 0 : inputState.setVoice(voiceIndex);
732 0 : notifyAboutStateChanged();
733 0 : }
734 :
735 0 : void NotationNoteInput::setCurrentTrack(track_idx_t trackIndex)
736 : {
737 0 : TRACEFUNC;
738 :
739 0 : score()->inputState().setTrack(trackIndex);
740 0 : notifyAboutStateChanged();
741 0 : }
742 :
743 0 : void NotationNoteInput::addTuplet(const TupletOptions& options)
744 : {
745 0 : TRACEFUNC;
746 :
747 0 : const InputState& inputState = score()->inputState();
748 :
749 0 : const bool durationAsSubdivision = options.selectedDurationAs == TupletSelectedDuration::SUBDIVISION;
750 0 : if (durationAsSubdivision
751 0 : && !score()->canAddTuplet(inputState.segment(), inputState.track(), options.ratio, inputState.duration())) {
752 0 : MScore::setError(MsError::NO_SPACE_FOR_TUPLET);
753 0 : m_interaction->checkAndShowError();
754 0 : return;
755 : }
756 :
757 0 : if (!durationAsSubdivision && !m_interaction->canAddTupletToSelectedChordRests()) {
758 0 : MScore::setError(MsError::TUPLET_NOTE_VALUE_TOO_SHORT);
759 0 : m_interaction->checkAndShowError();
760 0 : return;
761 : }
762 :
763 0 : startEdit(TranslatableString("undoableAction", "Add tuplet"));
764 0 : score()->expandVoice();
765 0 : ChordRest* chordRest = inputState.cr();
766 0 : if (chordRest) {
767 0 : Tuplet* tuplet = nullptr;
768 0 : if (durationAsSubdivision) {
769 0 : tuplet = score()->addTuplet(chordRest, options.ratio, inputState.duration(), options.numberType, options.bracketType);
770 : } else {
771 0 : Fraction ratio = options.ratio;
772 0 : if (options.autoBaseLen) {
773 0 : ratio.setDenominator(Tuplet::computeTupletDenominator(ratio.numerator(), inputState.ticks()));
774 : }
775 0 : score()->changeCRlen(chordRest, inputState.duration());
776 0 : tuplet = score()->addTuplet(chordRest, ratio, options.numberType, options.bracketType);
777 : }
778 0 : if (tuplet) {
779 0 : score()->selectNewTuplet(tuplet);
780 : }
781 : }
782 0 : apply();
783 :
784 0 : notifyAboutStateChanged();
785 0 : }
786 :
787 0 : static muse::RectF segmentContentRect(const Segment* segment, track_idx_t track)
788 : {
789 0 : RectF result;
790 :
791 0 : const EngravingItem* el = segment->element(track);
792 0 : if (!el) {
793 0 : return result;
794 : }
795 :
796 0 : if (el->isChord()) {
797 0 : const Chord* chord = toChord(el);
798 0 : for (const Note* note: chord->notes()) {
799 0 : result.unite(note->ldata()->bbox().translated(note->pos()));
800 : }
801 :
802 0 : if (Hook* hook = chord->hook()) {
803 0 : result.unite(hook->ldata()->bbox().translated(hook->pos()));
804 : }
805 0 : } else if (el->isRest() && !toRest(el)->isFullMeasureRest()) {
806 0 : result.unite(el->ldata()->bbox());
807 : }
808 :
809 0 : return result;
810 : }
811 :
812 0 : muse::RectF NotationNoteInput::cursorRect() const
813 : {
814 0 : TRACEFUNC;
815 :
816 0 : if (!isNoteInputMode()) {
817 0 : return {};
818 : }
819 :
820 0 : const InputState& inputState = score()->inputState();
821 0 : const Segment* segment = inputState.segment();
822 0 : if (!segment) {
823 0 : return {};
824 : }
825 :
826 0 : const System* system = segment->measure()->system();
827 0 : if (!system) {
828 0 : return {};
829 : }
830 :
831 0 : const track_idx_t track = inputState.track() == muse::nidx ? 0 : inputState.track();
832 0 : const staff_idx_t staffIdx = track2staff(track);
833 :
834 0 : const Staff* staff = score()->staff(staffIdx);
835 0 : if (!staff) {
836 0 : return {};
837 : }
838 :
839 0 : const double globalSpatium = score()->style().spatium();
840 :
841 0 : const StaffType* staffType = staff->staffType(inputState.tick());
842 0 : const bool isTabStaff = staffType->isTabStaff();
843 0 : const double localSpatium = staffType->spatium();
844 :
845 0 : const double defaultWidth = score()->noteHeadWidth() * localSpatium / globalSpatium;
846 :
847 0 : const RectF segmentContentRect = ::segmentContentRect(segment, track);
848 0 : double x = segmentContentRect.x() + segment->pagePos().x();
849 :
850 0 : const double barNoteDist = score()->style().styleAbsolute(Sid::barNoteDistance);
851 0 : if (segment->x() < barNoteDist) {
852 0 : x += barNoteDist - segment->x();
853 : }
854 :
855 0 : double y = system->staffCanvasYpage(staffIdx);
856 0 : double w = segmentContentRect.width() > 0 ? segmentContentRect.width() : defaultWidth;
857 0 : double h = 0.0;
858 :
859 0 : if (inputState.beyondScore()) {
860 0 : const Measure* lastMeasure = score()->lastMeasure();
861 0 : x = lastMeasure->pageBoundingRect().right() + globalSpatium;
862 0 : w = defaultWidth;
863 : }
864 :
865 0 : double sideMargin = 0.0;
866 :
867 0 : if (isTabStaff) {
868 0 : const double minWidth = 2.0 * globalSpatium;
869 0 : sideMargin = std::max(0.3 * globalSpatium, 0.5 * (minWidth - w));
870 : } else {
871 0 : sideMargin = 0.5 * globalSpatium;
872 : }
873 :
874 : // Don't extend further to the left than the center between the current and previous segment
875 0 : const engraving::Segment* prevSeg = segment->prev1WithElemsOnTrack(track, engraving::SegmentType::ChordRest);
876 0 : if (prevSeg && prevSeg->measure() == segment->measure()) {
877 0 : const RectF prevSegContentRect = ::segmentContentRect(prevSeg, track);
878 0 : if (prevSegContentRect.width() > 0) {
879 0 : const double centerBetweenPrevSegRightAndCurrSegLeft = (prevSeg->pagePos().x() + prevSegContentRect.right() + x) * 0.5;
880 0 : sideMargin = std::min(sideMargin, x - centerBetweenPrevSegRightAndCurrSegLeft);
881 : }
882 : }
883 :
884 0 : x -= sideMargin;
885 0 : w += 2 * sideMargin;
886 :
887 0 : const double yOffset = staffType->yoffset().val() * localSpatium;
888 0 : y += yOffset;
889 :
890 0 : const int inputStateStringsCount = inputState.string();
891 0 : const int instrumentStringsCount = static_cast<int>(staff->part()->instrument()->stringData()->strings());
892 :
893 0 : const double lineDist = staffType->lineDistance().val() * localSpatium;
894 :
895 0 : if (isTabStaff && inputStateStringsCount >= 0 && inputStateStringsCount <= instrumentStringsCount) {
896 0 : h = lineDist;
897 0 : y += staffType->physStringToYOffset(inputStateStringsCount).toAbsolute(localSpatium);
898 0 : y -= (staffType->onLines() ? lineDist * 0.5 : lineDist);
899 : } else {
900 0 : const double skylineMargin = 0.75 * localSpatium;
901 0 : const int lines = staffType->lines();
902 :
903 0 : h = (lines - 1) * lineDist + 2 * skylineMargin;
904 0 : y -= skylineMargin;
905 : }
906 :
907 0 : RectF result { x, y, w, h };
908 :
909 0 : if (configuration()->canvasOrientation().val == muse::Orientation::Horizontal) {
910 0 : result.translate(system->page()->pos());
911 : }
912 :
913 0 : return result;
914 0 : }
915 :
916 0 : void NotationNoteInput::addSlur(Slur* slur)
917 : {
918 0 : TRACEFUNC;
919 :
920 0 : InputState& inputState = score()->inputState();
921 0 : inputState.setSlur(slur);
922 :
923 0 : if (slur) {
924 0 : std::vector<SpannerSegment*> slurSpannerSegments = slur->spannerSegments();
925 0 : if (!slurSpannerSegments.empty()) {
926 0 : slurSpannerSegments.front()->setSelected(true);
927 : }
928 0 : }
929 :
930 0 : notifyAboutStateChanged();
931 0 : }
932 :
933 0 : void NotationNoteInput::resetSlur()
934 : {
935 0 : TRACEFUNC;
936 :
937 0 : InputState& inputState = score()->inputState();
938 0 : Slur* slur = inputState.slur();
939 0 : if (!slur) {
940 0 : return;
941 : }
942 :
943 0 : score()->deselect(slur);
944 :
945 0 : addSlur(nullptr);
946 0 : }
947 :
948 0 : void NotationNoteInput::addTie()
949 : {
950 0 : TRACEFUNC;
951 :
952 : // Calls `startEdit` internally
953 0 : EditTie::cmdAddTie(score());
954 :
955 0 : notifyAboutStateChanged();
956 0 : m_interaction->checkAndShowError();
957 0 : }
958 :
959 0 : void NotationNoteInput::addLaissezVib()
960 : {
961 0 : TRACEFUNC;
962 :
963 : // Calls `startEdit` internally
964 0 : EditTie::cmdToggleLaissezVib(score());
965 :
966 0 : notifyAboutStateChanged();
967 0 : m_interaction->checkAndShowError();
968 0 : }
969 :
970 0 : Notification NotationNoteInput::noteAdded() const
971 : {
972 0 : return m_noteAdded;
973 : }
974 :
975 0 : Notification NotationNoteInput::stateChanged() const
976 : {
977 0 : return m_stateChanged;
978 : }
979 :
980 0 : void NotationNoteInput::setGetViewRectFunc(const std::function<RectF()>& func)
981 : {
982 0 : m_getViewRectFunc = func;
983 0 : }
984 :
985 0 : Score* NotationNoteInput::score() const
986 : {
987 0 : return m_getScore->score();
988 : }
989 :
990 0 : void NotationNoteInput::startEdit(const muse::TranslatableString& actionName)
991 : {
992 0 : m_undoStack->prepareChanges(actionName);
993 0 : }
994 :
995 0 : void NotationNoteInput::apply()
996 : {
997 0 : m_undoStack->commitChanges();
998 :
999 0 : if (ChordRest* chordRest = score()->inputState().cr()) {
1000 0 : m_interaction->showItem(chordRest);
1001 : }
1002 0 : }
1003 :
1004 0 : void NotationNoteInput::updateInputState()
1005 : {
1006 0 : TRACEFUNC;
1007 :
1008 0 : NoteInputState& is = score()->inputState();
1009 0 : is.update(score()->selection());
1010 :
1011 0 : if (!configuration()->addAccidentalDotsArticulationsToNextNoteEntered()) {
1012 0 : is.setAccidentalType(AccidentalType::NONE);
1013 0 : is.setDots(0);
1014 0 : is.setArticulationIds({});
1015 : }
1016 :
1017 0 : notifyAboutStateChanged();
1018 0 : }
1019 :
1020 0 : void NotationNoteInput::notifyAboutStateChanged()
1021 : {
1022 0 : m_stateChanged.notify();
1023 0 : }
1024 :
1025 0 : void NotationNoteInput::notifyNoteAddedChanged()
1026 : {
1027 0 : m_noteAdded.notify();
1028 0 : }
1029 :
1030 0 : void NotationNoteInput::notifyAboutNoteInputStarted(bool focusNotation)
1031 : {
1032 0 : m_noteInputStarted.send(focusNotation);
1033 0 : }
1034 :
1035 0 : void NotationNoteInput::notifyAboutNoteInputEnded()
1036 : {
1037 0 : m_noteInputEnded.notify();
1038 0 : }
1039 :
1040 0 : void NotationNoteInput::doubleNoteInputDuration()
1041 : {
1042 0 : TRACEFUNC;
1043 :
1044 0 : startEdit(TranslatableString("undoableAction", "Double note input duration"));
1045 0 : NoteInput::increaseDuration(score()->transactionManager()->currentOrDummyTransaction(), score());
1046 0 : apply();
1047 :
1048 0 : notifyAboutStateChanged();
1049 0 : m_interaction->checkAndShowError();
1050 0 : }
1051 :
1052 0 : void NotationNoteInput::halveNoteInputDuration()
1053 : {
1054 0 : TRACEFUNC;
1055 :
1056 0 : startEdit(TranslatableString("undoableAction", "Halve note input duration"));
1057 0 : NoteInput::decreaseDuration(score()->transactionManager()->currentOrDummyTransaction(), score());
1058 0 : apply();
1059 :
1060 0 : notifyAboutStateChanged();
1061 0 : m_interaction->checkAndShowError();
1062 0 : }
|