From af662bc2e123e025127dbb9ae1f637f21ccf77c1 Mon Sep 17 00:00:00 2001 From: "J.G" Date: Mon, 9 Sep 2024 07:15:47 +0000 Subject: [PATCH] [Cmd: Paste Clone] insert cloned measure bases via shortcut or right-click score menu Frames work with regular copy paste (as insertion, not appending) [Keysig/Timesig/Clef] Copied before performing [Score to score] V/H/T Boxes to allow copy-drag to clone. Also work as regular copy/paste since they didn't do anything beforehand --- libmscore/box.cpp | 18 +++ libmscore/measure.cpp | 137 ++++++++++++++++ libmscore/measure.h | 1 + libmscore/measurebase.cpp | 4 +- libmscore/score.cpp | 321 ++++++++++++++++++++++++++++++++++++++ libmscore/score.h | 3 + libmscore/segment.cpp | 2 + libmscore/textframe.cpp | 6 +- mscore/dragdrop.cpp | 7 +- mscore/musescore.cpp | 1 + mscore/musescore.h | 8 + mscore/scoreview.cpp | 97 +++++++++++- mscore/scoreview.h | 1 + mscore/shortcut.cpp | 10 ++ 14 files changed, 610 insertions(+), 6 deletions(-) diff --git a/libmscore/box.cpp b/libmscore/box.cpp index 8524ffb4bf6b2..9c7a6fb7f4c97 100644 --- a/libmscore/box.cpp +++ b/libmscore/box.cpp @@ -515,6 +515,11 @@ bool Box::acceptDrop(EditData& data) const case ElementType::STAFF_TEXT: case ElementType::IMAGE: case ElementType::SYMBOL: + case ElementType::VBOX: + case ElementType::HBOX: + case ElementType::TBOX: + case ElementType::MEASURE_LIST: + case ElementType::MEASURE: return true; case ElementType::ICON: switch (toIcon(data.dropElement)->iconType()) { @@ -587,6 +592,19 @@ Element* Box::drop(EditData& data) return text; } + case ElementType::HBOX: + case ElementType::VBOX: + case ElementType::TBOX: + { + if (auto mbSource = e->findMeasureBase()) { + auto mbDestination = this->findMeasureBase(); + auto boxClone = mbSource->clone(); + boxClone->setPrev(this->prevMM()); + boxClone->setNext(mbDestination); + score()->undo(new InsertMeasures(boxClone, boxClone)); + } + break; + } case ElementType::ICON: switch (toIcon(e)->iconType()) { case IconType::VFRAME: diff --git a/libmscore/measure.cpp b/libmscore/measure.cpp index 5c7a82c3f00b1..9d733d696ffc9 100644 --- a/libmscore/measure.cpp +++ b/libmscore/measure.cpp @@ -1410,6 +1410,10 @@ bool Measure::acceptDrop(EditData& data) const case ElementType::SYMBOL: case ElementType::CLEF: case ElementType::STAFFTYPE_CHANGE: + case ElementType::VBOX: + case ElementType::HBOX: + case ElementType::TBOX: + viewer->setDropRectangle(staffR); return true; @@ -1697,6 +1701,19 @@ Element* Measure::drop(EditData& data) delete e; return cmdInsertRepeatMeasure(staffIdx); } + case ElementType::HBOX: + case ElementType::VBOX: + case ElementType::TBOX: + { + if (auto mbSource = e->findMeasureBase()) { + auto mbDestination = this->findMeasureBase(); + auto boxClone = mbSource->clone(); + boxClone->setPrev(this->prevMM()); + boxClone->setNext(mbDestination); + score()->undo(new InsertMeasures(boxClone, boxClone)); + } + break; + } case ElementType::ICON: switch(toIcon(e)->iconType()) { case IconType::VFRAME: @@ -3197,6 +3214,126 @@ Measure* Measure::cloneMeasure(Score* sc, const Fraction& tick, TieMap* tieMap) return m; } +//--------------------------------------------------------- +// cloneMeasure +//--------------------------------------------------------- + +Measure* Measure::cloneMeasureLimited(Score* scoreDest, const Fraction& tick, TieMap* tieMap, int startStaff, int endStaff) + { + Measure* m = new Measure(scoreDest); + m->_timesig = _timesig; + m->_len = _len; + m->_repeatCount = _repeatCount; + + // Limited edition: Allow discrepancy of staves amount, to be limited by start/endStaff during cloning + // Code reuse: nearly identical to above cloneMeasure() + + m->setNo(no()); + m->setNoOffset(noOffset()); + m->setIrregular(irregular()); + m->_userStretch = _userStretch; + m->_breakMultiMeasureRest = _breakMultiMeasureRest; + m->_playbackCount = _playbackCount; + + m->setTick(tick); + m->setLineBreak(lineBreak()); + m->setPageBreak(pageBreak()); + m->setSectionBreak(sectionBreak() ? new LayoutBreak(*sectionBreakElement()) : 0); + + m->setHeader(header()); m->setTrailer(trailer()); + TupletMap tupletMap; + + auto fcr = scoreDest->selection().firstChordRest(); + auto destStaffStart = fcr ? fcr->staffIdx() : 0; + int initTrack = (startStaff * VOICES); + int limitTrack = (endStaff * VOICES); + int destTrack = (destStaffStart * VOICES); + + for (Segment* oseg = first(); oseg; oseg = oseg->next()) { + Segment* s = new Segment(m, oseg->segmentType(), oseg->rtick()); + s->setEnabled(oseg->enabled()); s->setVisible(oseg->visible()); + s->setHeader(oseg->header()); s->setTrailer(oseg->trailer()); + + m->_segments.push_back(s); + for (int track = initTrack; track < limitTrack; ++track) { + int newTrack = track - initTrack + destTrack; + Element* oe = oseg->element(track); + for (Element* e : oseg->annotations()) { + if (e->generated() || e->track() != track) + continue; + Element* ne = e->clone(); + ne->setTrack(newTrack); + ne->setOffset(e->offset()); + ne->setScore(scoreDest); + s->add(ne); + } + if (!oe) + continue; + Element* ne = oe->clone(); + ne->setTrack(newTrack); + if (oe->isChordRest()) { + ChordRest* ocr = toChordRest(oe); + ChordRest* ncr = toChordRest(ne); + Tuplet* ot = ocr->tuplet(); + ncr->setTrack(newTrack); + if (ot) { + Tuplet* nt = tupletMap.findNew(ot); + if (nt == 0) { + nt = new Tuplet(*ot); + nt->clear(); + nt->setTrack(newTrack); + nt->setScore(scoreDest); + nt->setParent(m); + nt->setTick(m->tick() + ot->rtick()); + tupletMap.add(ot, nt); + } + ncr->setTuplet(nt); + nt->add(ncr); + } + if (oe->isChord()) { + Chord* och = toChord(ocr); + Chord* nch = toChord(ncr); + nch->setTrack(newTrack); + size_t n = och->notes().size(); + for (size_t i = 0; i < n; ++i) { + Note* on = och->notes().at(i); + Note* nn = nch->notes().at(i); + if (on->tieFor()) { + Tie* tie = on->tieFor()->clone(); + tie->setScore(scoreDest); + tie->setTrack(newTrack); + nn->setTieFor(tie); + tie->setStartNote(nn); + tieMap->add(on->tieFor(), tie); + } + if (on->tieBack()) { + Tie* tie = tieMap->findNew(on->tieBack()); + if (tie) { + tie->setTrack(nch->track()); + nn->setTieBack(tie); + tie->setEndNote(nn); + } + else { + qDebug("cloneMeasure: cannot find tie, track %d", newTrack); + } + } + } + } + } + ne->setOffset(oe->offset()); + ne->setScore(scoreDest); + s->add(ne); + } + } + for (Element* e : el()) { + Element* ne = e->clone(); + ne->setScore(scoreDest); + ne->setOffset(e->offset()); + m->add(ne); + } + return m; + } + //--------------------------------------------------------- // snap //--------------------------------------------------------- diff --git a/libmscore/measure.h b/libmscore/measure.h index 5c9e9a1ef3822..d99a9cb1bbe01 100644 --- a/libmscore/measure.h +++ b/libmscore/measure.h @@ -101,6 +101,7 @@ class Measure final : public MeasureBase { ElementType type() const override { return ElementType::MEASURE; } void setScore(Score* s) override; Measure* cloneMeasure(Score*, const Fraction& tick, TieMap*); + Measure* cloneMeasureLimited(Score*, const Fraction& tick, TieMap*, int startStaff, int endStaff); void read(XmlReader&, int idx); void read(XmlReader& d) override { read(d, 0); } diff --git a/libmscore/measurebase.cpp b/libmscore/measurebase.cpp index 8108c2735a1e2..8100c5685b8f4 100644 --- a/libmscore/measurebase.cpp +++ b/libmscore/measurebase.cpp @@ -155,7 +155,9 @@ void MeasureBase::add(Element* e) if (next()) next()->triggerLayout(); } - triggerLayout(); + // Observation: Cloning a MeasureBase has an issue with triggerlayout here, + // Will keep comment in case there's a problem later to refer back here + // triggerLayout() _el.push_back(e); } diff --git a/libmscore/score.cpp b/libmscore/score.cpp index 7964f25c8e1ad..62f9b35d1b93d 100644 --- a/libmscore/score.cpp +++ b/libmscore/score.cpp @@ -2221,6 +2221,327 @@ bool Score::appendMeasuresFromScore(Score* score, const Fraction& startTick, con return true; } +//--------------------------------------------------------- +// insertMeasuresFromScore +// Objective: clone measures from any score between a +// copied range selection, to be inserted [before mbInsert] +// (not appended). This provides a means of "deep copying" +// Time signatures, barlines, key signatures, etc. +// - Supports only entire measures +// - Frames included "within" range selection +// - Single frame will copy/paste via insertion +// - Instead of deep cloning entire staves, destination selection +// and source range is considered +// - Spanners don't keep node customizations during cloning, so +// resorting to a regular paste afterwards as a workaround +//--------------------------------------------------------- + +MeasureBase* Score::insertMeasuresFromScore(Score* scoreSource, const Selection& selectionSource, MeasureBase& mbInsert) { + auto scoreDest = this; + Measure* mFirst; + Measure* mLast; + selectionSource.measureRange(&mFirst, &mLast); + MeasureBase* mbSingle = + selectionSource.element() ? selectionSource.element()->findMeasureBase() : nullptr; + if (!mFirst && !mbSingle) + return nullptr; + + // staffEnd here is not inclusive - e.g. one grand staff provides [2] as staffEnd + auto staffStart = selectionSource.staffStart(); + auto staffEnd = selectionSource.staffEnd(); + auto szStavesSelected = staffEnd - staffStart; + auto fcr = scoreDest->selection().firstChordRest(); + auto destStaffStart = fcr ? fcr->staffIdx() : scoreDest->selection().staffStart(); + auto szDestStaves = scoreDest->nstaves() - destStaffStart; + auto destStaffEnd = (destStaffStart + szStavesSelected); + + if ( !(szDestStaves >= szStavesSelected) ) { + QMessageBox::warning(0, "MuseScore", + tr("Clone paste error: insufficient staves available at given position")); + return nullptr; + } + + auto mbStart = mbSingle ? mbSingle : mFirst->findMeasureBase(); + auto mbEnd = mbSingle ? mbSingle : mLast->findMeasureBase(); + + Fraction tickOfInsert = mbInsert.tick(); + + auto mbPrevious = mbInsert.prevMM(); + auto mInsert = mbInsert.findMeasure(); + + TieMap tieMap; + + // Store current sigs at entry point before cloning + auto tickCurrent = tickOfInsert; + std::vector oldKeySigs, originalKeySigs; + std::vector oldClefTypes, originalClefTypes; + std::vector oldTimeSigs, originalTimeSigs; + + // Store start of range selection status: + auto tickStartOfRange = mbStart->tick(); + int index = 0; + for (int staffIdx = selectionSource.staffStart(); staffIdx < selectionSource.staffEnd(); ++staffIdx) { + auto staffSource = scoreSource->staff(staffIdx); + KeySigEvent nkse = + staffSource->keySigEvent(tickStartOfRange); + TimeSig* timeSig = + staffSource->timeSig(tickStartOfRange); + ClefType clef = + staffSource->clef(tickStartOfRange); + originalKeySigs.emplace_back(nkse); + originalClefTypes.emplace_back(clef); + originalTimeSigs.emplace_back(timeSig->clone()); + + index++; + } + + // Store insertion point status: + index = 0; + for (int staffIdx = destStaffStart; staffIdx < destStaffEnd; ++staffIdx) { + if (!mInsert || mbSingle) + break; + + auto staffDest = scoreDest->staff(staffIdx); + auto tickInsertion = mInsert->tick(); + KeySigEvent nkse = staffDest->keySigEvent(tickInsertion); + TimeSig* timeSig = staffDest->timeSig(tickInsertion); + ClefType clef = staffDest->clef(tickInsertion); + oldKeySigs.emplace_back(nkse); + oldClefTypes.emplace_back(clef); + oldTimeSigs.emplace_back(timeSig->clone()); + index++; + } + + // Clone & Insert measures: + std::vector insertedMeasures; + int safeGuard = 0; + const int maxIterations = 1888; + for (auto mbCurrent = mbStart; mbCurrent && (mbCurrent->no() <= mbEnd->no()); mbCurrent = mbCurrent->next()) { + bool firstIteration = insertedMeasures.empty(); + MeasureBase* mbNext; + + // Sanity Safeguards - these should never occur + if (++safeGuard > maxIterations) { + qDebug() << "error:" << "hit max iterations" << maxIterations << "while cloning measures"; + return nullptr; + } + else if (mbCurrent == mbCurrent->next()) { + qDebug() << "error:" << "next measure = current measure"; + return nullptr; + } + if (!firstIteration) { + if ((mbCurrent->no() == mbStart->no()) && !mbCurrent->isBox()) { + qDebug() << "error:" << "restart or invalid interaction with insertion point."; + break; + } + else if (!mbCurrent->no()) { + qDebug() << "error:" << "reached a measure #0 mid-way" ; + break; + } + else if (mbCurrent == mbStart) { + qDebug() << "error:" << "reached first measure more than once"; + return nullptr; + } + } + + if (mbCurrent->isMeasure()) { + auto mNext = + toMeasure(mbCurrent) + ->cloneMeasureLimited + (scoreDest, + tickCurrent, + &tieMap, + staffStart, + staffEnd); + + tickCurrent += mNext->ticks(); + mbNext = toMeasureBase(mNext); + } + else { // frames don't contribute to tick counter + mbNext = mbCurrent->clone(); + } + + mbNext->setScore(scoreDest); + mbNext->setPrev(mbPrevious); + mbNext->setNext(&mbInsert); + mbPrevious = mbNext; + + scoreDest->undo(new InsertMeasures(mbNext, mbNext)); + + if (mbSingle) + return mbSingle; + + insertedMeasures.emplace_back(mbNext); + } + + if (insertedMeasures.empty()) + return nullptr; + + auto mbStartInsertion = insertedMeasures.front(); + auto mbEndInsertion = insertedMeasures.back(); + auto firstInsertedMeasure = mbStartInsertion->findMeasure(); + auto lastInsertedMeasure = mbEndInsertion->findMeasure(); + if (!firstInsertedMeasure || !lastInsertedMeasure) { + // insanity check + return nullptr; + } + + // Update full measure rests: + for (auto m = firstInsertedMeasure; m; m = m->nextMeasure()) { + if (m->nextMeasure() == m) { + // insanity check + break; + } + + for (int staffIdx = 0; staffIdx < nstaves(); ++staffIdx) { + Fraction f; + for (auto s = m->first(SegmentType::ChordRest); s; s = s->next(SegmentType::ChordRest)) { + for (int v = 0; v < VOICES; ++v) { + auto cr = toChordRest(s->element(staffIdx * VOICES + v)); + if (cr == 0) { + continue; + } + f += cr->actualTicks(); + } + } + if (f.isZero()) { + addRest(m->tick(), staffIdx*VOICES, TDuration(TDuration::DurationType::V_MEASURE), 0); + } + } + if (m->findMeasureBase() == mbEndInsertion) + break; + } + + // Iterate destination staves and re-apply information at end of insertion if appropriate + auto tickInsertionPoint = mInsert->tick(); + auto tickFirstInsertion = firstInsertedMeasure->tick(); + index = 0; + for (int staffIdx = destStaffStart; staffIdx < destStaffEnd; ++staffIdx) { + if (!mInsert) + break; + + int trackIdx = staff2track(staffIdx); + auto staffDest = scoreDest->staff(staffIdx); + + // Apply measure information if not in accord with score + + // [After insertion point]: + KeySigEvent nkse = oldKeySigs.at(index); + KeySigEvent oNkse = staffDest->keySigEvent(tickInsertionPoint); + if (nkse.key() != oNkse.key()) + undoChangeKeySig(staffDest, tickInsertionPoint, nkse); + + TimeSig* newTimeSig = oldTimeSigs.at(index); + TimeSig* oldTimeSig = staffDest->timeSig(tickInsertionPoint); + if (newTimeSig->sig() != oldTimeSig->sig()) { + newTimeSig->setScore(scoreDest); + newTimeSig->setTrack(trackIdx); + cmdAddTimeSig(mInsert, staffIdx, newTimeSig, false); + } + + ClefType clefType = oldClefTypes.at(index); + ClefType oldClefType = staffDest->clef(tickInsertionPoint); + if (clefType != oldClefType) + undoChangeClef(staffDest, mInsert, clefType); + + // [Beginning of inserted measures] + nkse = originalKeySigs.at(index); + oNkse = staffDest->keySigEvent(tickFirstInsertion); + if (nkse.key() != oNkse.key()) + undoChangeKeySig(staffDest, tickFirstInsertion, nkse); + + newTimeSig = originalTimeSigs.at(index); + oldTimeSig = staffDest->timeSig(tickFirstInsertion); + if (newTimeSig->sig() != oldTimeSig->sig()) { + newTimeSig->setScore(scoreDest); + newTimeSig->setTrack(trackIdx); + cmdAddTimeSig(firstInsertedMeasure, staffIdx, newTimeSig, false); + } + + clefType = originalClefTypes.at(index); + oldClefType = staffDest->clef(tickFirstInsertion); + if (clefType != oldClefType) + undoChangeClef(staffDest, firstInsertedMeasure, clefType); + + // Code to convert header clefs to regular measure clefs + #if 0 + Finally, convert header clefs to regular measure clefs if need be: + if (auto clefSeg = firstInsertedMeasure->undoGetSegment(SegmentType::HeaderClef, firstInsertedMeasure->tick())) { + qDebug() << "Got header clef @ track: " << trackIdx; + if (auto e = clefSeg->element(trackIdx)) { + if (e->isClef()) { + auto clef = toClef(e); + qDebug() << "header clef @ staff: " << clef->staffIdx(); + auto clefType = clef->clefType(); + bool explicitClef = (clef->tick() == firstInsertedMeasure->tick()); + if (explicitClef) { + if (firstInsertedMeasure->system() && firstInsertedMeasure->isFirstInSystem()) + ; + else { + clefSeg->remove(clef); + undoChangeClef(staffDest, firstInsertedMeasure, clefType); + } + } + } + } + } + #endif + + index++; + } + + // Anacrusis + auto beforeInsertedMeasures = firstInsertedMeasure->prevMeasureMM(); + if (beforeInsertedMeasures && beforeInsertedMeasures->timesig() == firstInsertedMeasure->timesig()) { + auto combinedTicks = beforeInsertedMeasures->ticks() + firstInsertedMeasure->ticks(); + if (combinedTicks == firstInsertedMeasure->timesig()) { + cmdJoinMeasure(beforeInsertedMeasures, firstInsertedMeasure); + } + } + + // Spanners: Clone within range + // This currently doesn't clone grip-node alterations - resorting to regular copy/paste after cloning + auto tickStart = mFirst->tick(); + auto tickEnd = mLast->tick() + mLast->ticks(); + auto spannersSource = scoreSource->spanner(); + auto lb = spannersSource.lower_bound(tickStart.ticks()); + auto ub = spannersSource.upper_bound(tickEnd.ticks()); + for (auto sp = lb; sp != ub; sp++) { + auto spanner = sp->second; + if (spanner->tick2() > tickEnd) { + // Spanner map is by tick(), so this can theoretically happen + continue; + } + auto ns = toSpanner(spanner->clone()); + + ns->setScore(scoreDest); + ns->setParent(nullptr); + + auto resultingTick = (spanner->tick() - tickStart) + tickOfInsert; + auto cr1 = findCR(resultingTick, ns->track()); + ns->setTick(resultingTick); + + resultingTick = (spanner->tick2() - tickStart) + tickOfInsert; + auto cr2 = findCR(resultingTick, ns->track()); + ns->setTick2(resultingTick); + + if (cr1 && cr2) { + ns->setStartElement(cr1); + ns->setEndElement(cr2); + } + else { + ns->computeStartElement(); + ns->computeEndElement(); + } + undoAddElement(ns); + } + + fixTicks(); + setLayoutAll(); + doLayout(); + return firstInsertedMeasure; + } + //--------------------------------------------------------- // splitStaff //--------------------------------------------------------- diff --git a/libmscore/score.h b/libmscore/score.h index 27a137df5af5b..6cc3704fef944 100644 --- a/libmscore/score.h +++ b/libmscore/score.h @@ -644,6 +644,9 @@ class Score : public QObject, public ScoreElement { bool trKeys, bool transposeChordNames, bool useDoubleSharpsFlats); bool appendMeasuresFromScore(Score* score, const Fraction& startTick, const Fraction& endTick); + + MeasureBase* insertMeasuresFromScore (Score* scoreSource, const Selection& selectionSource, MeasureBase& mbInsert); + bool appendScore(Score*, bool addPageBreak = false, bool addSectionBreak = true); void write(XmlWriter&, bool onlySelection); diff --git a/libmscore/segment.cpp b/libmscore/segment.cpp index b38780a485539..78b4b60c1e7ad 100644 --- a/libmscore/segment.cpp +++ b/libmscore/segment.cpp @@ -641,6 +641,8 @@ void Segment::remove(Element* el) { // qDebug("%p Segment::remove %s %p", this, el->name(), el); + if (!el) return; + int track = el->track(); switch(el->type()) { diff --git a/libmscore/textframe.cpp b/libmscore/textframe.cpp index e01441e404f19..545df7a1e1875 100644 --- a/libmscore/textframe.cpp +++ b/libmscore/textframe.cpp @@ -43,6 +43,7 @@ TBox::TBox(const TBox& tbox) : VBox(tbox) { _text = new Text(*(tbox._text)); + _text->setParent(this); } TBox::~TBox() @@ -59,7 +60,8 @@ TBox::~TBox() void TBox::layout() { setPos(QPointF()); // !? - bbox().setRect(0.0, 0.0, system()->width(), 0); + auto w = system() ? system()->width() : 0; + bbox().setRect(0.0, 0.0, w, 0); _text->layout(); qreal h = _text->height(); @@ -80,7 +82,7 @@ void TBox::layout() #endif _text->setPos(leftMargin() * DPMM, y); h += topMargin() * DPMM + bottomMargin() * DPMM; - bbox().setRect(0.0, 0.0, system()->width(), h); + bbox().setRect(0.0, 0.0, w, h); MeasureBase::layout(); // layout LayoutBreak's } diff --git a/mscore/dragdrop.cpp b/mscore/dragdrop.cpp index 76d578fcbdf9a..2c16649de1022 100644 --- a/mscore/dragdrop.cpp +++ b/mscore/dragdrop.cpp @@ -334,8 +334,9 @@ void ScoreView::dragMoveEvent(QDragMoveEvent* event) QPointF pos(imatrix.map(QPointF(event->pos()))); editData.pos = pos; editData.modifiers = event->keyboardModifiers(); + auto dropType = editData.dropElement->type(); - switch (editData.dropElement->type()) { + switch (dropType) { case ElementType::VOLTA: event->setAccepted(dragMeasureAnchorElement(pos)); break; @@ -394,6 +395,9 @@ void ScoreView::dragMoveEvent(QDragMoveEvent* event) case ElementType::LYRICS: case ElementType::FRET_DIAGRAM: case ElementType::STAFFTYPE_CHANGE: + case ElementType::VBOX: + case ElementType::TBOX: + case ElementType::HBOX: event->setAccepted(getDropTarget(editData)); break; default: @@ -496,6 +500,7 @@ void ScoreView::dropEvent(QDropEvent* event) break; case ElementType::HBOX: case ElementType::VBOX: + case ElementType::TBOX: case ElementType::KEYSIG: case ElementType::CLEF: case ElementType::TIMESIG: diff --git a/mscore/musescore.cpp b/mscore/musescore.cpp index e8d9b42bba6e1..feeb91679208f 100644 --- a/mscore/musescore.cpp +++ b/mscore/musescore.cpp @@ -4239,6 +4239,7 @@ void MuseScore::clipboardChanged() bool flag = true; getAction("paste")->setEnabled(flag); + getAction("paste-clone")->setEnabled(flag); getAction("swap")->setEnabled(flag); } diff --git a/mscore/musescore.h b/mscore/musescore.h index d67db976f7f3c..0ce0308c600ca 100644 --- a/mscore/musescore.h +++ b/mscore/musescore.h @@ -113,6 +113,7 @@ struct PaletteTree; class PaletteWidget; class PaletteWorkspace; class QmlDockWidget; +class Selection; struct PluginDescription; enum class SelState : char; @@ -203,6 +204,8 @@ class MuseScore : public QMainWindow, public MuseScoreCore { QSettings settings; ScoreView* cv { 0 }; ScoreTab* ctab { 0 }; + Score* copiedFromScore { 0 }; + Selection copiedSelection; QMap scoreWasShown; // whether each score in scoreList has ever been shown ScoreState _sstate; UpdateChecker* ucheck; @@ -679,6 +682,11 @@ class MuseScore : public QMainWindow, public MuseScoreCore { void updateInputState(Score*); void updateShadowNote(); + Score* getLastScoreCopiedFrom(void) { return copiedFromScore;} + void setLastScoreCopiedFrom(Score* s) { copiedFromScore = s; } + Selection& getLastScoreSelection(void) { return copiedSelection;} + void setLastScoreSelection(Selection& s) { copiedSelection = s; } + bool readLanguages(const QString& path); void setRevision(QString& r) {rev = r;} Q_INVOKABLE QString revision() {return rev;} diff --git a/mscore/scoreview.cpp b/mscore/scoreview.cpp index b346a6fd5ca2b..edaa12ba32e50 100644 --- a/mscore/scoreview.cpp +++ b/mscore/scoreview.cpp @@ -303,6 +303,7 @@ void ScoreView::objectPopup(const QPoint& pos, Element* obj) popup->addAction(getAction("cut")); popup->addAction(getAction("copy")); popup->addAction(getAction("paste")); + popup->addAction(getAction("paste-clone")); popup->addAction(getAction("swap")); popup->addAction(getAction("delete")); if (obj->isNote() || obj->isRest()) { @@ -339,7 +340,7 @@ void ScoreView::objectPopup(const QPoint& pos, Element* obj) if (a == 0) return; const QByteArray& cmd(a->data().toByteArray()); - if (cmd == "cut" || cmd =="copy" || cmd == "paste" || cmd == "swap" + if (cmd == "cut" || cmd =="copy" || cmd == "paste" || cmd == "paste-clone" || cmd == "swap" || cmd == "delete" || cmd == "time-delete") { // these actions are already activated return; @@ -427,6 +428,7 @@ void ScoreView::measurePopup(QContextMenuEvent* ev, Measure* obj) popup->addAction(getAction("cut")); popup->addAction(getAction("copy")); popup->addAction(getAction("paste")); + popup->addAction(getAction("paste-clone")); popup->addAction(getAction("swap")); popup->addAction(getAction("delete")); popup->addAction(getAction("time-delete")); @@ -1926,6 +1928,9 @@ void ScoreView::normalCopy() { if (!checkCopyOrCut()) return; + + mscore->setLastScoreSelection(_score->selection()); + QString mimeType = _score->selection().mimeType(); if (!mimeType.isEmpty()) { QMimeData* mimeData = new QMimeData; @@ -1944,6 +1949,9 @@ void ScoreView::normalCut() { if (!checkCopyOrCut()) return; + + mscore->setLastScoreSelection(_score->selection()); + _score->startCmd(); normalCopy(); _score->cmdDeleteSelection(); @@ -2032,6 +2040,32 @@ void ScoreView::normalSwap() bool ScoreView::normalPaste(Fraction scale) { _score->startCmd(); + + // Also allow for H/V/T boxes to be copied with their contents and be pasted (back inserted) + auto srcSelection = mscore->getLastScoreSelection(); + auto srcScore = srcSelection.score(); + MeasureBase* insertionMeasureBase = nullptr; + if (auto e = _score->selection().element()) { + if (auto mb = e->findMeasureBase()) { + insertionMeasureBase = mb; + } + } + else if (auto seg = _score->selection().startSegment()) { + if (auto mb = seg->findMeasureBase()) { + insertionMeasureBase = mb; + } + } + if (insertionMeasureBase) { + if (auto oe = srcSelection.element()) { + if (oe->isBox() && !oe->isFBox()) { + // Allow normal paste to insert clone of V/H/T boxes + _score->insertMeasuresFromScore(srcScore, srcSelection, *insertionMeasureBase); + _score->endCmd(); + return MScore::_error == MS_NO_ERROR; + } + } + } + const QMimeData* ms = QApplication::clipboard()->mimeData(); _score->cmdPaste(ms, this, scale); bool rv = MScore::_error == MS_NO_ERROR; @@ -2039,6 +2073,62 @@ bool ScoreView::normalPaste(Fraction scale) return rv; } +//--------------------------------------------------------- +// clonePaste +//--------------------------------------------------------- + +bool ScoreView::clonePaste() + { + MeasureBase* mbFirstInsertion = nullptr; + auto currentStaff = _score->selection().staffStart(); + _score->startCmd(); + + auto srcScore = mscore->getLastScoreSelection().score(); + auto copiedSel = mscore->getLastScoreSelection(); + if (!copiedSel.isRange() && !copiedSel.isSingle()) { + QMessageBox::warning(0, "MuseScore", + tr("A valid range or single selection required for measure position.")); + return false; + } + if (!srcScore) { + QMessageBox::warning(0, "MuseScore", + tr("Invalid source score.")); + return false; + } + + MeasureBase* insertionMeasureBase = nullptr; + bool rv; + + if (auto e = _score->selection().element()) { + if (auto mb = e->findMeasureBase()) { + insertionMeasureBase = mb; + } + } + else if (auto seg = _score->selection().startSegment()) { + if (auto mb = seg->findMeasureBase()) { + insertionMeasureBase = mb; + } + } + if (insertionMeasureBase) { + mbFirstInsertion = _score->insertMeasuresFromScore(srcScore, copiedSel, *insertionMeasureBase); + rv = MScore::_error == MS_NO_ERROR; + } + else rv = MScore::_error == NO_DEST; + + // Circumvent spanners not cloning node offsets by a work-around regular pasting after clone.... + if (mbFirstInsertion) { + if (auto mFirstInsertion = mbFirstInsertion->findMeasure()) { + _score->select(mFirstInsertion, SelectType::RANGE, currentStaff); + const QMimeData* ms = QApplication::clipboard()->mimeData(); + _score->cmdPaste(ms, this); + rv = MScore::_error == MS_NO_ERROR; + } + } + + _score->endCmd(); + return rv; + } + //--------------------------------------------------------- // cmdGotoElement //--------------------------------------------------------- @@ -2192,6 +2282,9 @@ void ScoreView::cmd(const char* s) else if (cv->state == ViewState::EDIT) cv->editPaste(); }}, + {{"paste-clone"}, [](ScoreView* cv, const QByteArray&) { + cv->clonePaste(); + }}, {{"paste-half"}, [](ScoreView* cv, const QByteArray&) { cv->normalPaste(Fraction(1, 2)); }}, @@ -4329,7 +4422,7 @@ void ScoreView::cmdChangeEnharmonic(bool both) void ScoreView::cloneElement(Element* e) { - if (e->isMeasure() || e->isNote() || e->isVBox()) + if (e->isMeasure() || e->isNote()) return; QDrag* drag = new QDrag(this); QMimeData* mimeData = new QMimeData; diff --git a/mscore/scoreview.h b/mscore/scoreview.h index 00d77c224fa1b..297ffdc4b68b0 100644 --- a/mscore/scoreview.h +++ b/mscore/scoreview.h @@ -362,6 +362,7 @@ class ScoreView : public QWidget, public MuseScoreView { void normalCopy(); void fotoModeCopy(bool includeLink = false); bool normalPaste(Fraction scale = Fraction(1, 1)); + bool clonePaste(); void normalSwap(); void setControlCursorVisible(bool v); diff --git a/mscore/shortcut.cpp b/mscore/shortcut.cpp index 466781aa1c2f1..b23c62e9f7088 100644 --- a/mscore/shortcut.cpp +++ b/mscore/shortcut.cpp @@ -204,6 +204,16 @@ Shortcut Shortcut::_sc[] = { Icons::paste_ICON, Qt::ApplicationShortcut }, + { + MsWidget::SCORE_TAB, + STATE_NORMAL, + "paste-clone", + QT_TRANSLATE_NOOP("action","Paste (Clone)"), + 0, + 0, + Icons::paste_ICON, + Qt::ApplicationShortcut + }, { MsWidget::SCORE_TAB, STATE_NORMAL,