Merge pull request #5657 from dmitrio95/plugin-cursor-modes

Plugin API: alter Cursor API for better usage in interactive plugins
This commit is contained in:
anatoly-os 2020-04-22 20:30:28 +03:00 committed by GitHub
commit 4703ec3b2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 618 additions and 99 deletions

View file

@ -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;
}

View file

@ -720,7 +720,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);

View file

@ -5941,7 +5941,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);
}

View file

@ -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,33 @@ 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);
}
//---------------------------------------------------------
// 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();
}
//---------------------------------------------------------
@ -128,12 +182,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 +197,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 +214,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 +235,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 +244,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 +378,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 +398,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 +407,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 +435,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>(_segment, Ownership::SCORE) : nullptr;
Ms::Segment* seg = segment();
return seg ? wrap<Segment>(seg, Ownership::SCORE) : nullptr;
}
//---------------------------------------------------------
@ -412,22 +468,31 @@ Element* Cursor::element() const
Measure* Cursor::measure() const
{
return _segment ? wrap<Measure>(_segment->measure(), Ownership::SCORE) : nullptr;
Ms::Segment* seg = segment();
return seg ? wrap<Measure>(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 +501,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 +516,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 +549,7 @@ void Cursor::setVoice(int v)
int Cursor::staffIdx() const
{
return _track / VOICES;
return track() / VOICES;
}
//---------------------------------------------------------
@ -475,7 +558,7 @@ int Cursor::staffIdx() const
int Cursor::voice() const
{
return _track % VOICES;
return track() % VOICES;
}
//---------------------------------------------------------
@ -485,10 +568,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 +584,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);
}
//---------------------------------------------------------
@ -513,5 +602,22 @@ int Cursor::qmlKeySignature()
Staff* staff = _score->staves()[staffIdx()];
return static_cast<int>(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);
}
}
}

View file

@ -16,6 +16,7 @@
namespace Ms {
class Element;
class InputState;
class Score;
class Chord;
class Rest;
@ -74,9 +75,16 @@ 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)
/**
* 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 {
@ -86,14 +94,27 @@ class Cursor : public QObject {
};
Q_ENUM(RewindMode);
private:
Ms::Score* _score = nullptr;
int _track = 0;
// bool _expandRepeats; // used?
/** \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);
//state
Ms::Segment* _segment = nullptr;
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;
// bool _expandRepeats; // used?
SegmentType _filter;
std::unique_ptr<InputState> is;
InputStateMode _inputStateMode = INPUT_STATE_INDEPENDENT;
// utility methods
void prevInTrack();
@ -101,6 +122,15 @@ class Cursor : public QObject {
void setScore(Ms::Score* s);
Ms::Element* currentElement() const;
InputState& inputState();
const InputState& inputState() const { return const_cast<Cursor*>(this)->inputState(); }
Ms::Segment* segment() const;
void setSegment(Ms::Segment* seg);
int inputStateString() const;
void setInputStateString(int);
public:
/// \cond MS_INTERNAL
Cursor(Ms::Score* s = nullptr);
@ -109,7 +139,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 +151,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();
@ -133,6 +166,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();

View file

@ -175,6 +175,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()

View file

@ -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,

View file

@ -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; }

View file

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