From bd979cf23bd6478330990ed103df3fa85bd89c21 Mon Sep 17 00:00:00 2001 From: Dmitri Ovodok Date: Mon, 27 Jan 2020 09:43:58 +0200 Subject: [PATCH 1/6] Allow plugins choose if Cursor is synchronised with score input state --- libmscore/noteentry.cpp | 70 ++++++++------- libmscore/score.h | 2 +- mscore/plugin/api/cursor.cpp | 167 ++++++++++++++++++++++++----------- mscore/plugin/api/cursor.h | 37 ++++++-- 4 files changed, 183 insertions(+), 93 deletions(-) diff --git a/libmscore/noteentry.cpp b/libmscore/noteentry.cpp index b1c88f3f11e80..3bb300fb90d99 100644 --- a/libmscore/noteentry.cpp +++ b/libmscore/noteentry.cpp @@ -121,20 +121,22 @@ NoteVal Score::noteValForPosition(Position pos, AccidentalType at, bool &error) // addPitch //--------------------------------------------------------- -Note* Score::addPitch(NoteVal& nval, bool addFlag) +Note* Score::addPitch(NoteVal& nval, bool addFlag, InputState* externalInputState) { + InputState& is = externalInputState ? (*externalInputState) : _is; + if (addFlag) { - Chord* c = toChord(_is.lastSegment()->element(_is.track())); + Chord* c = toChord(is.lastSegment()->element(is.track())); if (c == 0 || !c->isChord()) { qDebug("Score::addPitch: cr %s", c ? c->name() : "zero"); return 0; } Note* note = addNote(c, nval); - if (_is.lastSegment() == _is.segment()) { - NoteEntryMethod entryMethod = _is.noteEntryMethod(); + if (is.lastSegment() == is.segment()) { + NoteEntryMethod entryMethod = is.noteEntryMethod(); if (entryMethod != NoteEntryMethod::REALTIME_AUTO && entryMethod != NoteEntryMethod::REALTIME_MANUAL) - _is.moveToNextInputPos(); + is.moveToNextInputPos(); } return note; } @@ -142,41 +144,41 @@ Note* Score::addPitch(NoteVal& nval, bool addFlag) // insert note Direction stemDirection = Direction::AUTO; - int track = _is.track(); - if (_is.drumNote() != -1) { - nval.pitch = _is.drumNote(); - const Drumset* ds = _is.drumset(); + int track = is.track(); + if (is.drumNote() != -1) { + nval.pitch = is.drumNote(); + const Drumset* ds = is.drumset(); nval.headGroup = ds->noteHead(nval.pitch); stemDirection = ds->stemDirection(nval.pitch); - track = ds->voice(nval.pitch) + (_is.track() / VOICES) * VOICES; - _is.setTrack(track); + track = ds->voice(nval.pitch) + (is.track() / VOICES) * VOICES; + is.setTrack(track); expandVoice(); } - if (!_is.cr()) + if (!is.cr()) return 0; Fraction duration; - if (_is.usingNoteEntryMethod(NoteEntryMethod::REPITCH)) { - duration = _is.cr()->ticks(); + if (is.usingNoteEntryMethod(NoteEntryMethod::REPITCH)) { + duration = is.cr()->ticks(); } - else if (_is.usingNoteEntryMethod(NoteEntryMethod::REALTIME_AUTO) || _is.usingNoteEntryMethod(NoteEntryMethod::REALTIME_MANUAL)) { + else if (is.usingNoteEntryMethod(NoteEntryMethod::REALTIME_AUTO) || is.usingNoteEntryMethod(NoteEntryMethod::REALTIME_MANUAL)) { // FIXME: truncate duration at barline in real-time modes. // The user might try to enter a duration that is too long to fit in the remaining space in the measure. // We could split the duration at the barline and continue into the next bar, but this would create extra // notes, extra ties, and extra pain. Instead, we simply truncate the duration at the barline. - Fraction ticks2measureEnd = _is.segment()->measure()->ticks() - _is.segment()->rtick(); - duration = _is.duration() > ticks2measureEnd ? ticks2measureEnd : _is.duration().fraction(); + Fraction ticks2measureEnd = is.segment()->measure()->ticks() - is.segment()->rtick(); + duration = is.duration() > ticks2measureEnd ? ticks2measureEnd : is.duration().fraction(); } else { - duration = _is.duration().fraction(); + duration = is.duration().fraction(); } Note* note = 0; Note* firstTiedNote = 0; Note* lastTiedNote = 0; - if (_is.usingNoteEntryMethod(NoteEntryMethod::REPITCH) && _is.cr()->isChord()) { + if (is.usingNoteEntryMethod(NoteEntryMethod::REPITCH) && is.cr()->isChord()) { // repitch mode for MIDI input (where we are given a pitch) is handled here // for keyboard input (where we are given a staff position), there is a separate function Score::repitchNote() // the code is similar enough that it could possibly be refactored - Chord* chord = toChord(_is.cr()); + Chord* chord = toChord(is.cr()); note = new Note(this); note->setParent(chord); note->setTrack(chord->track()); @@ -232,49 +234,49 @@ Note* Score::addPitch(NoteVal& nval, bool addFlag) } select(lastTiedNote); } - else if (!_is.usingNoteEntryMethod(NoteEntryMethod::REPITCH)) { - Segment* seg = setNoteRest(_is.segment(), track, nval, duration, stemDirection); + else if (!is.usingNoteEntryMethod(NoteEntryMethod::REPITCH)) { + Segment* seg = setNoteRest(is.segment(), track, nval, duration, stemDirection); if (seg) { note = toChord(seg->element(track))->upNote(); } } - if (_is.slur()) { + if (is.slur()) { // // extend slur // - ChordRest* e = searchNote(_is.tick(), _is.track()); + ChordRest* e = searchNote(is.tick(), is.track()); if (e) { Fraction stick = Fraction(0, 1); - Element* ee = _is.slur()->startElement(); + Element* ee = is.slur()->startElement(); if (ee->isChordRest()) stick = toChordRest(ee)->tick(); else if (ee->isNote()) stick = toNote(ee)->chord()->tick(); if (stick == e->tick()) { - _is.slur()->setTick(stick); - _is.slur()->setStartElement(e); + is.slur()->setTick(stick); + is.slur()->setStartElement(e); } else { - _is.slur()->setTick2(e->tick()); - _is.slur()->setEndElement(e); + is.slur()->setTick2(e->tick()); + is.slur()->setEndElement(e); } } else qDebug("addPitch: cannot find slur note"); } - if (_is.usingNoteEntryMethod(NoteEntryMethod::REPITCH)) { + if (is.usingNoteEntryMethod(NoteEntryMethod::REPITCH)) { // move cursor to next note, but skip tied notes (they were already repitched above) - ChordRest* next = lastTiedNote ? nextChordRest(lastTiedNote->chord()) : nextChordRest(_is.cr()); + ChordRest* next = lastTiedNote ? nextChordRest(lastTiedNote->chord()) : nextChordRest(is.cr()); while (next && !next->isChord()) next = nextChordRest(next); if (next) - _is.moveInputPos(next->segment()); + is.moveInputPos(next->segment()); } else { - NoteEntryMethod entryMethod = _is.noteEntryMethod(); + NoteEntryMethod entryMethod = is.noteEntryMethod(); if (entryMethod != NoteEntryMethod::REALTIME_AUTO && entryMethod != NoteEntryMethod::REALTIME_MANUAL) - _is.moveToNextInputPos(); + is.moveToNextInputPos(); } return note; } diff --git a/libmscore/score.h b/libmscore/score.h index 857c9eb22cf1d..70a0f42ce6607 100644 --- a/libmscore/score.h +++ b/libmscore/score.h @@ -716,7 +716,7 @@ class Score : public QObject, public ScoreElement { void addElement(Element*); void removeElement(Element*); - Note* addPitch(NoteVal&, bool addFlag); + Note* addPitch(NoteVal&, bool addFlag, InputState* externalInputState = nullptr); void addPitch(int pitch, bool addFlag, bool insert); Note* addTiedMidiPitch(int pitch, bool addFlag, Chord* prevChord); Note* addMidiPitch(int pitch, bool addFlag); diff --git a/mscore/plugin/api/cursor.cpp b/mscore/plugin/api/cursor.cpp index ebd1dcd620d24..4e45cbb1fb272 100644 --- a/mscore/plugin/api/cursor.cpp +++ b/mscore/plugin/api/cursor.cpp @@ -54,11 +54,18 @@ Score* Cursor::score() const void Cursor::setScore(Ms::Score* s) { + if (_score == s) + return; + _score = s; - if (_score) { - _score->inputState().setTrack(_track); - _score->inputState().setSegment(_segment); - } + + switch (_inputStateMode) { + case INPUT_STATE_INDEPENDENT: + is.reset(new InputState); + break; + case INPUT_STATE_SYNC_WITH_SCORE: + break; + }; } //--------------------------------------------------------- @@ -70,6 +77,29 @@ void Cursor::setScore(Score* s) setScore(s ? s->score() : nullptr); } +//--------------------------------------------------------- +// inputState +//--------------------------------------------------------- + +InputState& Cursor::inputState() + { + return is ? *is.get() : _score->inputState(); + } + +//--------------------------------------------------------- +// setInputStateMode +//--------------------------------------------------------- + +void Cursor::setInputStateMode(InputStateMode val) + { + if (val == INPUT_STATE_SYNC_WITH_SCORE) + is.reset(); + else + is.reset(new InputState); + + _inputStateMode = val; + } + //--------------------------------------------------------- // rewind /// Rewind cursor to a certain position. @@ -88,12 +118,14 @@ void Cursor::rewind(RewindMode mode) // rewind to start of score // if (mode == SCORE_START) { - _segment = nullptr; Ms::Measure* m = _score->firstMeasure(); if (m) { - _segment = m->first(_filter); + setSegment(m->first(_filter)); nextInTrack(); } + else { + setSegment(nullptr); + } } // // rewind to start of selection @@ -101,8 +133,8 @@ void Cursor::rewind(RewindMode mode) else if (mode == SELECTION_START) { if (!_score->selection().isRange()) return; - _segment = _score->selection().startSegment(); - _track = _score->selection().staffStart() * VOICES; + setSegment(_score->selection().startSegment()); + setTrack(_score->selection().staffStart() * VOICES); nextInTrack(); } // @@ -111,11 +143,9 @@ void Cursor::rewind(RewindMode mode) else if (mode == SELECTION_END) { if (!_score->selection().isRange()) return; - _segment = _score->selection().endSegment(); - _track = (_score->selection().staffEnd() * VOICES) - 1; // be sure _track exists + setSegment(_score->selection().endSegment()); + setTrack((_score->selection().staffEnd() * VOICES) - 1); // be sure _track exists } - _score->inputState().setTrack(_track); - _score->inputState().setSegment(_segment); } //--------------------------------------------------------- @@ -128,12 +158,10 @@ void Cursor::rewind(RewindMode mode) bool Cursor::prev() { - if (!_segment) + if (!segment()) return false; prevInTrack(); - _score->inputState().setTrack(_track); - _score->inputState().setSegment(_segment); - return _segment != 0; + return segment(); } //--------------------------------------------------------- @@ -145,13 +173,11 @@ bool Cursor::prev() bool Cursor::next() { - if (!_segment) + if (!segment()) return false; - _segment = _segment->next1(_filter); + setSegment(segment()->next1(_filter)); nextInTrack(); - _score->inputState().setTrack(_track); - _score->inputState().setSegment(_segment); - return _segment != 0; + return segment(); } //--------------------------------------------------------- @@ -164,16 +190,16 @@ bool Cursor::next() bool Cursor::nextMeasure() { - if (_segment == 0) + if (!segment()) return false; - Ms::Measure* m = _segment->measure()->nextMeasure(); + Ms::Measure* m = segment()->measure()->nextMeasure(); if (m == 0) { - _segment = 0; + setSegment(nullptr); return false; } - _segment = m->first(_filter); + setSegment(m->first(_filter)); nextInTrack(); - return _segment != 0; + return segment(); } //--------------------------------------------------------- @@ -185,7 +211,7 @@ bool Cursor::nextMeasure() void Cursor::add(Element* wrapped) { Ms::Element* s = wrapped ? wrapped->element() : nullptr; - if (!_segment || !s) + if (!segment() || !s) return; // Ensure that the object has the expected ownership @@ -194,6 +220,9 @@ void Cursor::add(Element* wrapped) return; // Don't allow operation. } + const int _track = track(); + Ms::Segment* _segment = segment(); + wrapped->setOwnership(Ownership::SCORE); s->setScore(_score); s->setTrack(_track); @@ -325,11 +354,10 @@ void Cursor::addNote(int pitch, bool addToChord) qWarning("Cursor::addNote: invalid pitch: %d", pitch); return; } - if (!_score->inputState().duration().isValid()) + if (!inputState().duration().isValid()) setDuration(1, 4); NoteVal nval(pitch); - _score->addPitch(nval, addToChord); - _segment = _score->inputState().segment(); + _score->addPitch(nval, addToChord, is.get()); } //--------------------------------------------------------- @@ -346,7 +374,7 @@ void Cursor::setDuration(int z, int n) TDuration d(Fraction(z, n)); if (!d.isValid()) d = TDuration(TDuration::DurationType::V_QUARTER); - _score->inputState().setDuration(d); + inputState().setDuration(d); } //--------------------------------------------------------- @@ -355,7 +383,8 @@ void Cursor::setDuration(int z, int n) int Cursor::tick() { - return (_segment) ? _segment->tick().ticks() : 0; + const Ms::Segment* seg = segment(); + return seg ? seg->tick().ticks() : 0; } //--------------------------------------------------------- @@ -382,16 +411,19 @@ qreal Cursor::tempo() Ms::Element* Cursor::currentElement() const { - return _segment && _segment->element(_track) ? _segment->element(_track) : nullptr; + const int t = track(); + Ms::Segment* seg = segment(); + return seg && seg->element(t) ? seg->element(t) : nullptr; } //--------------------------------------------------------- // segment //--------------------------------------------------------- -Segment* Cursor::segment() const +Segment* Cursor::qmlSegment() const { - return _segment ? wrap(_segment, Ownership::SCORE) : nullptr; + Ms::Segment* seg = segment(); + return seg ? wrap(seg, Ownership::SCORE) : nullptr; } //--------------------------------------------------------- @@ -412,22 +444,31 @@ Element* Cursor::element() const Measure* Cursor::measure() const { - return _segment ? wrap(_segment->measure(), Ownership::SCORE) : nullptr; + Ms::Segment* seg = segment(); + return seg ? wrap(seg->measure(), Ownership::SCORE) : nullptr; + } + +//--------------------------------------------------------- +// track +//--------------------------------------------------------- + +int Cursor::track() const + { + return inputState().track(); } //--------------------------------------------------------- // setTrack //--------------------------------------------------------- -void Cursor::setTrack(int v) +void Cursor::setTrack(int _track) { - _track = v; int tracks = _score->nstaves() * VOICES; if (_track < 0) _track = 0; else if (_track >= tracks) _track = tracks - 1; - _score->inputState().setTrack(_track); + inputState().setTrack(_track); } //--------------------------------------------------------- @@ -436,13 +477,13 @@ void Cursor::setTrack(int v) void Cursor::setStaffIdx(int v) { - _track = v * VOICES + _track % VOICES; + int _track = v * VOICES + track() % VOICES; int tracks = _score->nstaves() * VOICES; if (_track < 0) _track = 0; else if (_track >= tracks) _track = tracks - 1; - _score->inputState().setTrack(_track); + inputState().setTrack(_track); } //--------------------------------------------------------- @@ -451,13 +492,31 @@ void Cursor::setStaffIdx(int v) void Cursor::setVoice(int v) { - _track = (_track / VOICES) * VOICES + v; + int _track = (track() / VOICES) * VOICES + v; int tracks = _score->nstaves() * VOICES; if (_track < 0) _track = 0; else if (_track >= tracks) _track = tracks - 1; - _score->inputState().setTrack(_track); + inputState().setTrack(_track); + } + +//--------------------------------------------------------- +// segment +//--------------------------------------------------------- + +Ms::Segment* Cursor::segment() const + { + return inputState().segment(); + } + +//--------------------------------------------------------- +// setSegment +//--------------------------------------------------------- + +void Cursor::setSegment(Ms::Segment* seg) + { + inputState().setSegment(seg); } //--------------------------------------------------------- @@ -466,7 +525,7 @@ void Cursor::setVoice(int v) int Cursor::staffIdx() const { - return _track / VOICES; + return track() / VOICES; } //--------------------------------------------------------- @@ -475,7 +534,7 @@ int Cursor::staffIdx() const int Cursor::voice() const { - return _track % VOICES; + return track() % VOICES; } //--------------------------------------------------------- @@ -485,10 +544,13 @@ int Cursor::voice() const void Cursor::prevInTrack() { - if (_segment) - _segment = _segment->prev1(_filter); - while (_segment && !_segment->element(_track)) - _segment = _segment->prev1(_filter); + const int t = track(); + Ms::Segment* seg = segment(); + if (seg) + seg = seg->prev1(_filter); + while (seg && !seg->element(t)) + seg = seg->prev1(_filter); + setSegment(seg); } //--------------------------------------------------------- @@ -498,8 +560,11 @@ void Cursor::prevInTrack() void Cursor::nextInTrack() { - while (_segment && _segment->element(_track) == 0) - _segment = _segment->next1(_filter); + const int t = track(); + Ms::Segment* seg = segment(); + while (seg && seg->element(t) == 0) + seg = seg->next1(_filter); + setSegment(seg); } //--------------------------------------------------------- diff --git a/mscore/plugin/api/cursor.h b/mscore/plugin/api/cursor.h index 30955a08cb795..f951c6f8c9f58 100644 --- a/mscore/plugin/api/cursor.h +++ b/mscore/plugin/api/cursor.h @@ -16,6 +16,7 @@ namespace Ms { class Element; +class InputState; class Score; class Chord; class Rest; @@ -74,7 +75,7 @@ class Cursor : public QObject { /** Current element at track, read only */ Q_PROPERTY(Ms::PluginAPI::Element* element READ element) /** Current segment, read only */ - Q_PROPERTY(Ms::PluginAPI::Segment* segment READ segment) + Q_PROPERTY(Ms::PluginAPI::Segment* segment READ qmlSegment) /** Current measure, read only */ Q_PROPERTY(Ms::PluginAPI::Measure* measure READ measure) @@ -86,14 +87,27 @@ class Cursor : public QObject { }; Q_ENUM(RewindMode); + /** \since MuseScore 3.5 */ + enum InputStateMode { + INPUT_STATE_INDEPENDENT, ///< Input state of cursor is independent of score input state (default) + INPUT_STATE_SYNC_WITH_SCORE ///< Input state of cursor is synchronized with score input state + }; + Q_ENUM(InputStateMode); + private: + /** + * Behavior of input state (position, notes duration etc.) of this cursor + * with respect to input state of the score. By default any changes in + * score and in this Cursor are not synchronized. + * \since MuseScore 3.5 + */ + Q_PROPERTY(InputStateMode inputStateMode READ inputStateMode WRITE setInputStateMode) + Ms::Score* _score = nullptr; - int _track = 0; // bool _expandRepeats; // used? - - //state - Ms::Segment* _segment = nullptr; SegmentType _filter; + std::unique_ptr is; + InputStateMode _inputStateMode = INPUT_STATE_INDEPENDENT; // utility methods void prevInTrack(); @@ -101,6 +115,12 @@ class Cursor : public QObject { void setScore(Ms::Score* s); Ms::Element* currentElement() const; + InputState& inputState(); + const InputState& inputState() const { return const_cast(this)->inputState(); } + + Ms::Segment* segment() const; + void setSegment(Ms::Segment* seg); + public: /// \cond MS_INTERNAL Cursor(Ms::Score* s = nullptr); @@ -109,7 +129,7 @@ class Cursor : public QObject { Score* score() const; void setScore(Score* s); - int track() const { return _track; } + int track() const; void setTrack(int v); int staffIdx() const; @@ -121,8 +141,11 @@ class Cursor : public QObject { int filter() const { return int(_filter); } void setFilter(int f) { _filter = SegmentType(f); } + InputStateMode inputStateMode() const { return _inputStateMode; } + void setInputStateMode(InputStateMode val); + Element* element() const; - Segment* segment() const; + Segment* qmlSegment() const; Measure* measure() const; int tick(); From 8e79b16fb99f77284333e8098cfad6bdbd1edf6c Mon Sep 17 00:00:00 2001 From: Dmitri Ovodok Date: Wed, 29 Jan 2020 01:39:26 +0200 Subject: [PATCH 2/6] Add Cursor.rewindToTick() function --- mscore/plugin/api/cursor.cpp | 24 ++++++++++++++++++++++++ mscore/plugin/api/cursor.h | 1 + 2 files changed, 25 insertions(+) diff --git a/mscore/plugin/api/cursor.cpp b/mscore/plugin/api/cursor.cpp index 4e45cbb1fb272..ec031cc789da7 100644 --- a/mscore/plugin/api/cursor.cpp +++ b/mscore/plugin/api/cursor.cpp @@ -148,6 +148,30 @@ void Cursor::rewind(RewindMode mode) } } +//--------------------------------------------------------- +// rewindToTick +/// Rewind cursor to a position defined by tick. +/// \param tick Determines the position where to move +/// this cursor. +/// \see \ref Ms::PluginAPI::Segment::tick "Segment.tick" +/// \since MuseScore 3.5 +//--------------------------------------------------------- + +void Cursor::rewindToTick(int tick) + { + // integer ticks may contain numeric errors so it is + // better to search not precisely if possible + Ms::Fraction fTick = Ms::Fraction::fromTicks(tick + 1); + Ms::Segment* seg = _score->tick2leftSegment(fTick); + if (!(seg->segmentType() & _filter)) { + // we need another segment type, search by known tick + seg = _score->tick2segment(seg->tick(), /* first */ true, _filter); + } + + setSegment(seg); + nextInTrack(); + } + //--------------------------------------------------------- // prev /// Move the cursor to the previous segment. diff --git a/mscore/plugin/api/cursor.h b/mscore/plugin/api/cursor.h index f951c6f8c9f58..f14b1cd9eab0d 100644 --- a/mscore/plugin/api/cursor.h +++ b/mscore/plugin/api/cursor.h @@ -156,6 +156,7 @@ class Cursor : public QObject { /// \endcond Q_INVOKABLE void rewind(RewindMode mode); + Q_INVOKABLE void rewindToTick(int tick); Q_INVOKABLE bool next(); Q_INVOKABLE bool nextMeasure(); From 0966d0fe4ecb319e4a2bf2939b38eb45a336c657 Mon Sep 17 00:00:00 2001 From: Dmitri Ovodok Date: Wed, 29 Jan 2020 01:39:44 +0200 Subject: [PATCH 3/6] Add Cursor.stringNumber property to use with tablature scores --- mscore/plugin/api/cursor.cpp | 17 +++++++++++++++++ mscore/plugin/api/cursor.h | 10 ++++++++++ 2 files changed, 27 insertions(+) diff --git a/mscore/plugin/api/cursor.cpp b/mscore/plugin/api/cursor.cpp index ec031cc789da7..ffd17c9ae180f 100644 --- a/mscore/plugin/api/cursor.cpp +++ b/mscore/plugin/api/cursor.cpp @@ -602,5 +602,22 @@ int Cursor::qmlKeySignature() Staff* staff = _score->staves()[staffIdx()]; return static_cast(staff->key(Fraction::fromTicks(tick()))); } + +//--------------------------------------------------------- +// inputStateString +//--------------------------------------------------------- + +int Cursor::inputStateString() const + { + const InputState& istate = inputState(); + return _score->staff(staffIdx())->staffType(istate.tick())->visualStringToPhys(istate.string()); + } + +void Cursor::setInputStateString(int string) + { + InputState& istate = inputState(); + const int visString = _score->staff(staffIdx())->staffType(istate.tick())->visualStringToPhys(string); + istate.setString(visString); + } } } diff --git a/mscore/plugin/api/cursor.h b/mscore/plugin/api/cursor.h index f14b1cd9eab0d..67d7f3c31b39e 100644 --- a/mscore/plugin/api/cursor.h +++ b/mscore/plugin/api/cursor.h @@ -78,6 +78,13 @@ class Cursor : public QObject { Q_PROPERTY(Ms::PluginAPI::Segment* segment READ qmlSegment) /** Current measure, read only */ Q_PROPERTY(Ms::PluginAPI::Measure* measure READ measure) + /** + * A physical string number where this cursor currently at. This is useful + * in conjunction with \ref InputStateMode.INPUT_STATE_SYNC_WITH_SCORE + * cursor mode. + * \since MuseScore 3.5 + */ + Q_PROPERTY(int stringNumber READ inputStateString WRITE setInputStateString) public: enum RewindMode { @@ -121,6 +128,9 @@ class Cursor : public QObject { Ms::Segment* segment() const; void setSegment(Ms::Segment* seg); + int inputStateString() const; + void setInputStateString(int); + public: /// \cond MS_INTERNAL Cursor(Ms::Score* s = nullptr); From 84abc52414369495b75b5d06cd9714b49a3239e1 Mon Sep 17 00:00:00 2001 From: Dmitri Ovodok Date: Thu, 30 Jan 2020 13:38:58 +0200 Subject: [PATCH 4/6] Don't call endCmd() twice on undo/redo commands --- mscore/musescore.cpp | 2 +- mscore/shortcut.cpp | 6 ++++-- mscore/shortcut.h | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mscore/musescore.cpp b/mscore/musescore.cpp index 5002598cbe3d9..c0cc983ebb504 100644 --- a/mscore/musescore.cpp +++ b/mscore/musescore.cpp @@ -5914,7 +5914,7 @@ void MuseScore::cmd(QAction* a) cmd(a, cmdn); if (lastShortcut->isCmd()) cs->endCmd(); - else + else if (!lastShortcut->isUndoRedo()) // undoRedo() calls endCmd() itself endCmd(); TourHandler::startTour(cmdn); } diff --git a/mscore/shortcut.cpp b/mscore/shortcut.cpp index 09098f16db6b6..7bba982a95ec2 100644 --- a/mscore/shortcut.cpp +++ b/mscore/shortcut.cpp @@ -170,7 +170,8 @@ Shortcut Shortcut::_sc[] = { QT_TRANSLATE_NOOP("action","Undo"), QT_TRANSLATE_NOOP("action","Undo last change"), Icons::undo_ICON, - Qt::ApplicationShortcut + Qt::ApplicationShortcut, + ShortcutFlags::A_UNDO_REDO }, { MsWidget::MAIN_WINDOW, @@ -180,7 +181,8 @@ Shortcut Shortcut::_sc[] = { QT_TRANSLATE_NOOP("action","Redo"), QT_TRANSLATE_NOOP("action","Redo last undo"), Icons::redo_ICON, - Qt::ApplicationShortcut + Qt::ApplicationShortcut, + ShortcutFlags::A_UNDO_REDO }, { MsWidget::SCORE_TAB, diff --git a/mscore/shortcut.h b/mscore/shortcut.h index 0fadb729a4eeb..36e46e027c35d 100644 --- a/mscore/shortcut.h +++ b/mscore/shortcut.h @@ -77,7 +77,8 @@ enum class ShortcutFlags : char { A_SCORE = 1, A_CMD = 1 << 1, A_CHECKABLE = 1 << 2, - A_CHECKED = 1 << 3 + A_CHECKED = 1 << 3, + A_UNDO_REDO = 1 << 4, }; constexpr ShortcutFlags operator| (ShortcutFlags t1, ShortcutFlags t2) { @@ -150,6 +151,7 @@ class Shortcut { void setState(int v) { _state = v; } bool needsScore() const { return _flags & ShortcutFlags::A_SCORE; } bool isCmd() const { return _flags & ShortcutFlags::A_CMD; } + bool isUndoRedo() const { return _flags & ShortcutFlags::A_UNDO_REDO; } bool isCheckable() const { return _flags & ShortcutFlags::A_CHECKABLE; } bool isChecked() const { return _flags & ShortcutFlags::A_CHECKED; } Icons icon() const { return _icon; } From 81a272a7781c6d063ef4e0d6ac1942ede48653ce Mon Sep 17 00:00:00 2001 From: Dmitri Ovodok Date: Thu, 30 Jan 2020 13:55:13 +0200 Subject: [PATCH 5/6] Add documentation for state.undoRedo in onScoreStateChanged handler in plugins --- mscore/plugin/api/qmlpluginapi.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mscore/plugin/api/qmlpluginapi.h b/mscore/plugin/api/qmlpluginapi.h index 58235ea05c3ae..d4cdb57162adf 100644 --- a/mscore/plugin/api/qmlpluginapi.h +++ b/mscore/plugin/api/qmlpluginapi.h @@ -173,6 +173,9 @@ class PluginAPI : public Ms::QmlPlugin { * - \p instrumentsChanged * - \p startLayoutTick * - \p endLayoutTick + * - \p undoRedo - whether this onScoreStateChanged invokation results + * from user undo/redo action. It is usualy not recommended to modify + * score from plugins in this case. Available since MuseScore 3.5. * * If a plugin modifies score in this handler, then it should: * 1. enclose all modifications within Score::startCmd() / Score::endCmd() From 2c1a21fe9894b489f3d6899bbf3ce7903e45028e Mon Sep 17 00:00:00 2001 From: Dmitri Ovodok Date: Thu, 30 Jan 2020 13:47:47 +0200 Subject: [PATCH 6/6] Add interactive version of Note Names plugin --- share/plugins/notenames-interactive.qml | 370 ++++++++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 share/plugins/notenames-interactive.qml diff --git a/share/plugins/notenames-interactive.qml b/share/plugins/notenames-interactive.qml new file mode 100644 index 0000000000000..0745c2f85e9c6 --- /dev/null +++ b/share/plugins/notenames-interactive.qml @@ -0,0 +1,370 @@ +//============================================================================= +// MuseScore +// Music Composition & Notation +// +// Note Names Plugin +// +// Copyright (C) 2012 Werner Schweer +// Copyright (C) 2013 - 2019 Joachim Schmitz +// Copyright (C) 2014 Jörn Eichler +// Copyright (C) 2020 MuseScore BVBA +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 +// as published by the Free Software Foundation and appearing in +// the file LICENCE.GPL +//============================================================================= + +import QtQuick 2.2 +import QtQuick.Controls 2.0 +import MuseScore 3.0 + +MuseScore { + version: "3.4" + description: qsTr("This plugin names notes as per your language setting") + menuPath: "Plugins.Notes." + qsTr("Note Names (Interactive)") + pluginType: "dock" + + implicitHeight: controls.implicitHeight * 1.5 + implicitWidth: controls.implicitWidth + + // Small note name size is fraction of the full font size. + property var defaultFontSize + property var fontSizeMini: 0.7; + + property int nstaves: 0 // for validators in staff number inputs + + property bool inCmd: false + + function ensureCmdStarted() { + if (!inCmd) { + curScore.startCmd(); + inCmd = true; + } + } + + function ensureCmdEnded() { + if (inCmd) { + curScore.endCmd(); + inCmd = false; + } + } + + function findSegment(el) { + while (el && el.type != Element.SEGMENT) + el = el.parent; + return el; + } + + function getChordName(chord) { + var text = ""; + var notes = chord.notes; + for (var i = 0; i < notes.length; i++) { + var sep = "\n"; // change to "," if you want them horizontally (anybody?) + if ( i > 0 ) + text = sep + text; // any but top note + if (typeof notes[i].tpc === "undefined") // like for grace notes ?!? + return; + switch (notes[i].tpc) { + case -1: text = qsTranslate("InspectorAmbitus", "F♭♭") + text; break; + case 0: text = qsTranslate("InspectorAmbitus", "C♭♭") + text; break; + case 1: text = qsTranslate("InspectorAmbitus", "G♭♭") + text; break; + case 2: text = qsTranslate("InspectorAmbitus", "D♭♭") + text; break; + case 3: text = qsTranslate("InspectorAmbitus", "A♭♭") + text; break; + case 4: text = qsTranslate("InspectorAmbitus", "E♭♭") + text; break; + case 5: text = qsTranslate("InspectorAmbitus", "B♭♭") + text; break; + case 6: text = qsTranslate("InspectorAmbitus", "F♭") + text; break; + case 7: text = qsTranslate("InspectorAmbitus", "C♭") + text; break; + + case 8: text = qsTranslate("InspectorAmbitus", "G♭") + text; break; + case 9: text = qsTranslate("InspectorAmbitus", "D♭") + text; break; + case 10: text = qsTranslate("InspectorAmbitus", "A♭") + text; break; + case 11: text = qsTranslate("InspectorAmbitus", "E♭") + text; break; + case 12: text = qsTranslate("InspectorAmbitus", "B♭") + text; break; + case 13: text = qsTranslate("InspectorAmbitus", "F") + text; break; + case 14: text = qsTranslate("InspectorAmbitus", "C") + text; break; + case 15: text = qsTranslate("InspectorAmbitus", "G") + text; break; + case 16: text = qsTranslate("InspectorAmbitus", "D") + text; break; + case 17: text = qsTranslate("InspectorAmbitus", "A") + text; break; + case 18: text = qsTranslate("InspectorAmbitus", "E") + text; break; + case 19: text = qsTranslate("InspectorAmbitus", "B") + text; break; + + case 20: text = qsTranslate("InspectorAmbitus", "F♯") + text; break; + case 21: text = qsTranslate("InspectorAmbitus", "C♯") + text; break; + case 22: text = qsTranslate("InspectorAmbitus", "G♯") + text; break; + case 23: text = qsTranslate("InspectorAmbitus", "D♯") + text; break; + case 24: text = qsTranslate("InspectorAmbitus", "A♯") + text; break; + case 25: text = qsTranslate("InspectorAmbitus", "E♯") + text; break; + case 26: text = qsTranslate("InspectorAmbitus", "B♯") + text; break; + case 27: text = qsTranslate("InspectorAmbitus", "F♯♯") + text; break; + case 28: text = qsTranslate("InspectorAmbitus", "C♯♯") + text; break; + case 29: text = qsTranslate("InspectorAmbitus", "G♯♯") + text; break; + case 30: text = qsTranslate("InspectorAmbitus", "D♯♯") + text; break; + case 31: text = qsTranslate("InspectorAmbitus", "A♯♯") + text; break; + case 32: text = qsTranslate("InspectorAmbitus", "E♯♯") + text; break; + case 33: text = qsTranslate("InspectorAmbitus", "B♯♯") + text; break; + default: text = qsTr("?") + text; break; + } // end switch tpc + + // octave, middle C being C4 + //text += (Math.floor(notes[i].pitch / 12) - 1) + // or + //text += (Math.floor(notes[i].ppitch / 12) - 1) + + // change below false to true for courtesy- and microtonal accidentals + // you might need to come up with suitable translations + // only #, b, natural and possibly also ## seem to be available in UNICODE + if (false) { + switch (notes[i].userAccidental) { + case 0: break; + case 1: text = qsTranslate("accidental", "Sharp") + text; break; + case 2: text = qsTranslate("accidental", "Flat") + text; break; + case 3: text = qsTranslate("accidental", "Double sharp") + text; break; + case 4: text = qsTranslate("accidental", "Double flat") + text; break; + case 5: text = qsTranslate("accidental", "Natural") + text; break; + case 6: text = qsTranslate("accidental", "Flat-slash") + text; break; + case 7: text = qsTranslate("accidental", "Flat-slash2") + text; break; + case 8: text = qsTranslate("accidental", "Mirrored-flat2") + text; break; + case 9: text = qsTranslate("accidental", "Mirrored-flat") + text; break; + case 10: text = qsTranslate("accidental", "Mirrored-flat-slash") + text; break; + case 11: text = qsTranslate("accidental", "Flat-flat-slash") + text; break; + case 12: text = qsTranslate("accidental", "Sharp-slash") + text; break; + case 13: text = qsTranslate("accidental", "Sharp-slash2") + text; break; + case 14: text = qsTranslate("accidental", "Sharp-slash3") + text; break; + case 15: text = qsTranslate("accidental", "Sharp-slash4") + text; break; + case 16: text = qsTranslate("accidental", "Sharp arrow up") + text; break; + case 17: text = qsTranslate("accidental", "Sharp arrow down") + text; break; + case 18: text = qsTranslate("accidental", "Sharp arrow both") + text; break; + case 19: text = qsTranslate("accidental", "Flat arrow up") + text; break; + case 20: text = qsTranslate("accidental", "Flat arrow down") + text; break; + case 21: text = qsTranslate("accidental", "Flat arrow both") + text; break; + case 22: text = qsTranslate("accidental", "Natural arrow down") + text; break; + case 23: text = qsTranslate("accidental", "Natural arrow up") + text; break; + case 24: text = qsTranslate("accidental", "Natural arrow both") + text; break; + case 25: text = qsTranslate("accidental", "Sori") + text; break; + case 26: text = qsTranslate("accidental", "Koron") + text; break; + default: text = qsTr("?") + text; break; + } // end switch userAccidental + } // end if courtesy- and microtonal accidentals + } // end for note + + return text; + } + + function getGraceNoteNames(graceChordsList) { + var names = []; + // iterate through all grace chords + for (var chordNum = 0; chordNum < graceChordsList.length; chordNum++) { + var chord = graceChordsList[chordNum]; + var chordName = getChordName(chord); + // append the name to the list of names + names.push(chordName); + + } + return names; + } + + function getAllChords(el) { + // List chords in the following order: + // 1) Leading grace notes; + // 2) Chord itself; + // 3) Trailing grace notes. + + if (!el || el.type != Element.CHORD) + return []; + + var chord = el; + var allChords = [ chord ]; + + // Search for grace notes + var graceChords = chord.graceNotes; + for (var chordNum = 0; chordNum < graceChords.length; chordNum++) { + var graceChord = graceChords[chordNum]; + var noteType = graceChord.noteType; + + switch (noteType) { + case NoteType.GRACE8_AFTER: + case NoteType.GRACE16_AFTER: + case NoteType.GRACE32_AFTER: + leadingLifo.push(graceChord); // append trailing grace chord to list + break; + default: + allChords.unshift(graceChord); // prepend leading grace chord to list + break; + } + } + + return allChords; + } + + function isNoteName(el) { + return el.type == Element.STAFF_TEXT; // TODO: how to distinguish note names from all staff texts? + } + + function getExistingNoteNames(segment, track) { + var annotations = segment.annotations; + var noteNames = []; + + for (var i = 0; i < annotations.length; ++i) { + var a = annotations[i]; + if (a.track != track) + continue; + if (isNoteName(a)) + noteNames.push(a); + } + + return noteNames; + } + + function handleChordAtCursor(cursor) { + var allNoteNames = getExistingNoteNames(cursor.segment, cursor.track); + var allChords = getAllChords(cursor.element); + var chordIdx = 0; + + for (; chordIdx < allChords.length; ++chordIdx) { + var chord = allChords[chordIdx]; + var noteName = allNoteNames[chordIdx]; + + var chordProperties = { + "offsetX": chord.posX, + "fontSize" : chord.noteType == NoteType.NORMAL ? defaultFontSize : (defaultFontSize * fontSizeMini), + "placement": (chord.voice & 1) ? Placement.BELOW : Placement.ABOVE, // place below for voice 1 and voice 3 (numbered as 2 and 4 in user interface) + "text": getChordName(chord) + } + + if (!noteName) { + // Note name does not exist, add a new one + ensureCmdStarted(); + var nameText = newElement(Element.STAFF_TEXT); + for (var prop in chordProperties) { + if (nameText[prop] != chordProperties[prop]) + nameText[prop] = chordProperties[prop]; + } + cursor.add(nameText); + } else { + // Note name exists, ensure it is up to date + for (var prop in chordProperties) { + if (noteName[prop] != chordProperties[prop]) { + ensureCmdStarted(); + noteName[prop] = chordProperties[prop]; + } + } + } // end if/else noteName + } // end for allChords + + // Remove the remaining redundant note names, if any + for (; chordIdx < allNoteNames.length; ++chordIdx) { + ensureCmdStarted(); + var noteName = allNoteNames[chordIdx]; + removeElement(noteName); + } + } // end handleChordAtCursor() + + function processRange(startTick, endTick, firstStaff, lastStaff) { + if (startTick < 0) + startTick = 0; + if (endTick < 0) + endTick = Infinity; // process the entire score + + var cursor = curScore.newCursor(); + + for (var staff = firstStaff; staff <= lastStaff; staff++) { + for (var voice = 0; voice < 4; voice++) { + cursor.voice = voice; + cursor.staffIdx = staff; + cursor.rewindToTick(startTick); + + while (cursor.segment && cursor.tick <= endTick) { + handleChordAtCursor(cursor); + cursor.next(); + } // end while segment + + } // end for voice + } // end for staff + + ensureCmdEnded(); + } + + function getStavesRange() { + if (allStavesCheckBox.checked) + return [0, curScore.nstaves]; + + var firstStaff = firstStaffInput.acceptableInput ? +firstStaffInput.text : curScore.nstaves; + var lastStaff = lastStaffInput.acceptableInput ? +lastStaffInput.text : -1; + + return [firstStaff, lastStaff]; + } + + onScoreStateChanged: { + if (inCmd) // prevent recursion from own changes + return; + if (state.undoRedo) // try not to interfere with undo/redo commands + return; + + nstaves = curScore.nstaves; // needed for validators in staff number inputs + + if (!noteNamesEnabledCheckBox.checked) + return; + if (!curScore || state.startLayoutTick < 0) // nothing to process? + return; + + var stavesRange = getStavesRange(); + + processRange(state.startLayoutTick, state.endLayoutTick, stavesRange[0], stavesRange[1]); + } + + onRun: { + defaultFontSize = newElement(Element.STAFF_TEXT).fontSize; + } + + Column { + id: controls + + CheckBox { + id: noteNamesEnabledCheckBox + text: "Enable notes naming" + } + CheckBox { + id: allStavesCheckBox + checked: true + text: "All staves" + } + + Grid { + id: staffRangeControls + columns: 2 + spacing: 4 + enabled: !allStavesCheckBox.checked + + Text { + height: firstStaffInput.height + verticalAlignment: Text.AlignVCenter + text: "first staff:" + } + TextField { + id: firstStaffInput + text: "0" + validator: IntValidator { bottom: 0; top: nstaves - 1 } + onTextChanged: { + if (+lastStaffInput.text < +text) + lastStaffInput.text = text + } + } + + Text { + height: lastStaffInput.height + verticalAlignment: Text.AlignVCenter + text: "last staff:" + } + TextField { + id: lastStaffInput + text: "0" + validator: IntValidator { bottom: 0; top: nstaves - 1 } + onTextChanged: { + if (text !== "" && (+firstStaffInput.text > +text)) + firstStaffInput.text = text + } + } + } + } +}