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:
commit
4703ec3b2e
9 changed files with 618 additions and 99 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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; }
|
||||
|
|
370
share/plugins/notenames-interactive.qml
Normal file
370
share/plugins/notenames-interactive.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue