11namespace tracktion {
inline namespace engine
14Clipboard::Clipboard() {}
15Clipboard::~Clipboard() { clearSingletonInstance(); }
19void Clipboard::clear()
30 if (content != newContent)
32 content = std::move (newContent);
37const Clipboard::ContentType* Clipboard::getContent()
const {
return content.get(); }
38bool Clipboard::isEmpty()
const {
return content ==
nullptr; }
43Clipboard::ContentType::EditPastingOptions::EditPastingOptions (Edit& e, EditInsertPoint& ip, SelectionManager* sm)
44 : edit (e), insertPoint (ip), selectionManager (sm)
47Clipboard::ContentType::EditPastingOptions::EditPastingOptions (Edit& e, EditInsertPoint& ip)
48 : edit (e), insertPoint (ip)
52Clipboard::ContentType::~ContentType() {}
54bool Clipboard::ContentType::pasteIntoEdit (Edit& edit, EditInsertPoint& insertPoint, SelectionManager* sm)
const
57 Track::Ptr startTrack;
58 TimePosition startPos;
59 insertPoint.chooseInsertPoint (startTrack, startPos,
false, sm);
61 if (startTrack ==
nullptr)
67 Clipboard::ContentType::EditPastingOptions options (edit, insertPoint, sm);
68 options.startTrack = startTrack.get();
69 options.startTime = startPos;
71 return pasteIntoEdit (options);
74bool Clipboard::ContentType::pasteIntoEdit (
const EditPastingOptions&)
const {
return false; }
79Clipboard::ProjectItems::ProjectItems() {}
80Clipboard::ProjectItems::~ProjectItems() {}
82AudioTrack* getOrInsertAudioTrackNearestIndex (Edit& edit,
int trackIndex)
90 if (
auto at =
dynamic_cast<AudioTrack*
> (t))
96 return edit.insertNewAudioTrack (TrackInsertPoint (
nullptr, getAllTracks (edit).getLast()),
nullptr).get();
99static TimePosition pasteMIDIFileIntoEdit (Edit& edit,
const juce::File& midiFile,
100 int& targetTrackIndex,
int& targetSlotIndex,
101 TimePosition startTime,
bool importTempoChanges)
109 auto newClipEndTime = startTime;
111 bool importAsNoteExpression =
false;
113 if (MidiList::looksLikeMPEData (midiFile))
114 importAsNoteExpression = edit.engine.getUIBehaviour()
115 .showOkCancelAlertBox (
TRANS(
"Import as Note Expression?"),
116 TRANS(
"This MIDI file looks like it contains multi-channel MPE data. Do you want to convert this to note expression or import as multiple clips?"),
117 TRANS(
"Convert to Expression"),
118 TRANS(
"Separate Clips"));
120 if (MidiList::readSeparateTracksFromFile (midiFile, lists,
121 tempoChangeBeatNumbers, bpms,
122 numerators, denominators, len,
123 importAsNoteExpression))
125 auto& tempoSequence = edit.tempoSequence;
127 auto startBeat = tempoSequence.toBeats (startTime);
128 auto endBeat = startBeat + len;
130 for (
auto& list : lists)
131 endBeat =
std::
max (endBeat, startBeat + BeatDuration::fromBeats (list->getLastBeatNumber().inBeats()));
133 endBeat = startBeat + BeatDuration::fromBeats (
std::ceil ((endBeat - startBeat).inBeats()));
135 if (importTempoChanges)
137 if (tempoChangeBeatNumbers.
size() > 0)
138 tempoSequence.removeTemposBetween (TimeRange (startTime, tempoSequence.toTime (endBeat))
139 .expanded (0.001_td),
true);
141 for (
int i = 0; i < tempoChangeBeatNumbers.
size(); ++i)
143 auto insertTime = tempoSequence.toTime (startBeat + toDuration (tempoChangeBeatNumbers.
getUnchecked (i)));
144 auto& origTempo = tempoSequence.getTempoAt (insertTime);
146 if (std::abs (origTempo.getBpm() - bpms.
getUnchecked (i)) > 0.001)
147 if (
auto tempo = tempoSequence.insertTempo (insertTime))
150 auto& origTimeSig = tempoSequence.getTimeSigAt (insertTime);
152 if (origTimeSig.denominator != denominators.
getUnchecked (i)
153 || origTimeSig.numerator != numerators.
getUnchecked (i))
155 if (
auto timeSig = tempoSequence.insertTimeSig (insertTime))
162 auto lastTrackEndTime = BeatPosition::fromBeats (Edit::getMaximumEditEnd().inSeconds());
165 for (
auto list : lists)
167 auto listBeatStart = list->getFirstBeatNumber();
168 auto listBeatEnd =
std::max (listBeatStart + 1_bd,
169 std::max (list->getLastBeatNumber(),
172 if (lastTrackEndTime > listBeatStart)
175 lastTrackEndTime = listBeatEnd;
178 clipState.setProperty (IDs::channel, list->getMidiChannel(),
nullptr);
180 if (list->state.isValid())
181 clipState.addChild (list->state, -1,
nullptr);
183 if (
auto at = getOrInsertAudioTrackNearestIndex (edit, targetTrackIndex))
185 const auto timeRange = tempoSequence.toTime ({ startBeat, endBeat });
187 auto clipName = list->getImportedMidiTrackName();
188 if (
auto fn = list->getImportedFileName(); fn.isNotEmpty())
189 clipName +=
" - " + fn;
191 auto targetOwner = [&]() -> ClipOwner*
193 if (targetSlotIndex < 0)
196 auto& slotList = at->getClipSlotList();
197 auto slots = slotList.getClipSlots();
199 if (targetSlotIndex < slots.size())
200 return slots[targetSlotIndex];
202 if (! slots.isEmpty())
203 return slots.getFirst();
208 if (
auto newClip = insertClipWithState (*targetOwner,
209 clipState, clipName, TrackItem::Type::midi,
210 { timeRange, 0_td }, DeleteExistingClips::no,
false))
212 if (
auto mc =
dynamic_cast<MidiClip*
> (newClip))
214 if (mc->getClipSlot() !=
nullptr)
216 mc->setUsesProxy (
false);
217 mc->setStart (0_tp,
false,
true);
218 mc->setLoopRangeBeats (mc->getEditBeatRange());
221 if (importAsNoteExpression)
222 mc->setMPEMode(
true);
225 newClipEndTime =
std::max (newClipEndTime, newClip->getPosition().getEnd());
235 return newClipEndTime;
240 bool shouldImportTempoChangesFromMIDI =
false;
241 bool separateTracks =
false;
242 bool snapBWavsToOriginalTime =
false;
245static void askUserAboutProjectItemPastingOptions (
Engine& engine,
252 bool importedMIDIFilesHaveTempoChanges =
false;
253 int numAudioClips = 0;
254 int numAudioClipsWithBWAV = 0;
256 for (
auto& item : items.itemIDs)
258 if (
auto source = pm.getProjectItem (item.itemID))
260 auto file = source->getSourceFile();
264 if (source->isMidi())
266 if (! importedMIDIFilesHaveTempoChanges)
267 importedMIDIFilesHaveTempoChanges = MidiList::fileHasTempoChanges (file);
269 else if (source->isWave())
274 ++numAudioClipsWithBWAV;
280 if (importedMIDIFilesHaveTempoChanges)
281 options.shouldImportTempoChangesFromMIDI = ui.showOkCancelAlertBox (
TRANS(
"MIDI Clip"),
282 TRANS(
"Do you want to import tempo and time signature changes from the MIDI clip?"),
286 if (numAudioClips > 1)
288 if (numAudioClipsWithBWAV > 0 && ! engine.
getEngineBehaviour().ignoreBWavTimestamps())
290 #if JUCE_MODAL_LOOPS_PERMITTED
292 toggle.setSize (200, 20);
295 .createAlertWindow (
TRANS(
"Add multiple files"),
296 TRANS(
"Do you want to add multiple files to one track or to separate tracks?"),
297 {}, {}, {}, juce::AlertWindow::QuestionIcon,
300 aw->addCustomComponent (&toggle);
301 aw->addButton (
TRANS(
"One track"), 0);
302 aw->addButton (
TRANS(
"Separate tracks"), 1);
304 options.separateTracks = aw->runModalLoop() == 1;
305 options.snapBWavsToOriginalTime = toggle.getToggleState();
307 options.separateTracks =
false;
308 options.snapBWavsToOriginalTime =
false;
313 options.separateTracks = ! ui.showOkCancelAlertBox (
TRANS(
"Add multiple files"),
314 TRANS(
"Do you want to add multiple files to one track or to separate tracks?"),
316 TRANS(
"Separate tracks"));
317 options.snapBWavsToOriginalTime =
false;
320 else if (numAudioClips == 1 && numAudioClipsWithBWAV == 1)
323 options.snapBWavsToOriginalTime =
false;
325 options.snapBWavsToOriginalTime = ui.showOkCancelAlertBox (
TRANS(
"BWAV Clip"),
326 TRANS(
"Do you want clip placed at BWAV timestamp or cursor position?"),
327 TRANS(
"BWAV timestamp"),
328 TRANS(
"Cursor position"));
332inline bool isRecursiveEditClipPaste (
const Clipboard::ProjectItems& items, Edit& edit)
334 auto& pm = edit.engine.getProjectManager();
336 for (
auto& item : items.itemIDs)
337 if (auto source = pm.getProjectItem (item.itemID))
338 if (source->isEdit() && source->getID() == edit.getProjectItemID())
344bool Clipboard::ProjectItems::pasteIntoEdit (
const EditPastingOptions& options)
const
346 auto& e = options.edit.engine;
347 auto& pm = e.getProjectManager();
348 auto& ui = options.edit.engine.getUIBehaviour();
349 bool anythingPasted =
false;
351 ProjectItemPastingOptions pastingOptions;
353 pastingOptions.separateTracks = options.preferredLayout == FileDragList::consecutiveTracks;
355 if (! options.silent)
356 askUserAboutProjectItemPastingOptions (e, *
this, pastingOptions);
358 if (isRecursiveEditClipPaste (*
this, options.edit))
360 if (! options.silent)
361 ui.showWarningAlert (
TRANS(
"Can't Import Edit"),
362 TRANS(
"You can't paste an edit clip into itself!"));
367 auto [insertPointTrack, clipOwner,
time] = options.insertPoint.chooseInsertPoint (
false, options.selectionManager,
368 [] (
auto& t) { return t.isAudioTrack() || t.isFolderTrack(); });
369 auto startTime =
time.value_or (0_tp);
371 if (insertPointTrack ==
nullptr || clipOwner ==
nullptr)
377 const bool pastingInToClipLauncher =
dynamic_cast<ClipSlot*
> (clipOwner) !=
nullptr;
379 int targetTrackIndex = insertPointTrack->getIndexInEditTrackList();
380 SelectableList itemsAdded;
384 if (
auto sourceItem = pm.getProjectItem (item.itemID))
386 auto file = sourceItem->getSourceFile();
387 auto newClipEndTime = startTime;
391 if (sourceItem->isMidi())
393 int targetSlotIndex = -1;
395 if (
auto targetSlot =
dynamic_cast<ClipSlot*
> (clipOwner))
398 newClipEndTime = pasteMIDIFileIntoEdit (options.edit, file, targetTrackIndex, targetSlotIndex,
399 startTime, pastingOptions.shouldImportTempoChangesFromMIDI);
401 else if (sourceItem->isWave())
403 sourceItem->verifyLength();
404 jassert (sourceItem->getLength() > 0);
406 if (
auto clipSlot =
dynamic_cast<ClipSlot*
> (clipOwner->getClipOwnerSelectable()))
407 if (
auto existingClip = clipSlot->getClip())
408 existingClip->removeFromParent();
410 if (
auto newClip = insertWaveClip (*clipOwner,
411 sourceItem->getName(), sourceItem->getID(),
412 { { startTime, TimePosition::fromSeconds (startTime.inSeconds() + sourceItem->getLength()) }, 0_td },
413 DeleteExistingClips::no))
415 newClipEndTime = newClip->getPosition().getEnd();
416 itemsAdded.add (newClip.get());
418 if (pastingOptions.snapBWavsToOriginalTime)
419 newClip->snapToOriginalBWavTime();
422 if (newClip->getClipSlot())
424 if (newClip->effectsEnabled())
425 newClip->enableEffects (
false,
false);
427 newClip->setUsesProxy (
false);
428 newClip->setAutoTempo (
true);
429 newClip->setStart (0_tp,
false,
true);
430 newClip->setLoopRangeBeats ({ 0_bp, newClip->getLengthInBeats() });
435 else if (sourceItem->isEdit())
437 sourceItem->verifyLength();
438 jassert (sourceItem->getLength() > 0);
440 if (
auto newClip = insertEditClip (*clipOwner,
441 { startTime, startTime + TimeDuration::fromSeconds (sourceItem->getLength()) },
442 sourceItem->getID()))
444 newClipEndTime = newClip->getPosition().getEnd();
445 itemsAdded.add (newClip.get());
448 if (newClip->getClipSlot())
450 if (newClip->effectsEnabled())
451 newClip->enableEffects (
false,
false);
453 newClip->setUsesProxy (
false);
454 newClip->setAutoTempo (
true);
455 newClip->setLoopRangeBeats ({ 0_bp, newClip->getLengthInBeats() });
460 anythingPasted =
true;
462 if (
int (index) <
int (itemIDs.size() - 1))
464 if (pastingInToClipLauncher)
469 auto newTrack = getOrInsertAudioTrackNearestIndex (options.edit, targetTrackIndex);
471 auto slot =
dynamic_cast<ClipSlot*
> (clipOwner);
472 auto& at = *
dynamic_cast<AudioTrack*
> (&slot->track);
473 auto& list = at.getClipSlotList();
475 auto idx = list.getClipSlots().indexOf (slot);
477 options.edit.getSceneList().ensureNumberOfScenes (list.getClipSlots().size());
479 clipOwner = newTrack->getClipSlotList().getClipSlots()[idx];
483 auto slot =
dynamic_cast<ClipSlot*
> (clipOwner);
484 auto& at = *
dynamic_cast<AudioTrack*
> (&slot->track);
485 auto& list = at.getClipSlotList();
487 auto idx = list.getClipSlots().indexOf (slot) + 1;
489 options.edit.getSceneList().ensureNumberOfScenes (idx + 1);
491 clipOwner = list.getClipSlots()[idx];
496 if (pastingOptions.separateTracks)
499 startTime = newClipEndTime;
501 clipOwner = getOrInsertAudioTrackNearestIndex (options.edit, targetTrackIndex);
508 if (itemsAdded.isNotEmpty())
509 if (
auto sm = options.selectionManager)
510 sm->select (itemsAdded);
512 return anythingPasted;
515bool Clipboard::ProjectItems::pasteIntoProject (Project& project)
const
517 for (
auto& item : itemIDs)
518 if (auto source = project.engine.getProjectManager().getProjectItem (item.itemID))
519 if (auto newItem = project.createNewItem (source->getSourceFile(),
522 source->getDescription(),
523 source->getCategory(),
525 newItem->copyAllPropertiesFrom (*source);
527 return ! itemIDs.empty();
532Clipboard::Clips::Clips() {}
533Clipboard::Clips::~Clips() {}
535void Clipboard::Clips::addClip (
int trackOffset,
const juce::ValueTree& state)
538 ci.trackOffset = trackOffset;
539 ci.state = state.createCopy();
541 clips.push_back (ci);
544void Clipboard::Clips::addSelectedClips (
const SelectableList& selectedObjects,
546 AutomationLocked automationLocked)
549 range = Edit::getMaximumEditTimeRange();
553 Clip::Array clipsToPaste;
555 for (
auto& c : selectedClipContents.getItemsOfType<Clip>())
556 if (c->getEditTimeRange().overlaps (range))
557 clipsToPaste.add (c);
559 if (clipsToPaste.isEmpty())
562 auto& ed = clipsToPaste.getFirst()->edit;
566 auto firstSlotIndex = ed.engine.getEngineBehaviour().getEditLimits().maxClipsInTrack;
567 auto firstTrackIndex = ed.engine.getEngineBehaviour().getEditLimits().maxNumTracks;
568 auto overallStartTime = TimePosition::fromSeconds (Edit::maximumLength);
570 for (
auto clip : clipsToPaste)
572 overallStartTime =
std::min (overallStartTime,
std::max (clip->getPosition().getStart(), range.getStart()));
573 firstTrackIndex =
std::min (firstTrackIndex,
std::max (0, allTracks.indexOf (clip->getTrack())));
575 if (
auto slot = clip->getClipSlot())
576 firstSlotIndex =
std::min (firstSlotIndex, slot->getIndex());
579 for (
auto clip : clipsToPaste)
581 if (clip->getEditTimeRange().overlaps (range))
583 auto clipPos = clip->getPosition();
584 auto clippedStart =
std::max (clipPos.getStart(), range.getStart());
585 auto clippedOffset = clipPos.getOffset() + (clippedStart - clipPos.getStart());
586 auto clippedEnd =
std::min (clipPos.getEnd(), range.getEnd());
590 info.grouped = clip->isGrouped();
592 clip->flushStateToValueTree();
593 info.state = clip->state.createCopy();
595 addValueTreeProperties (info.state,
596 IDs::start, (clippedStart - overallStartTime).inSeconds(),
597 IDs::length, (clippedEnd - clippedStart).inSeconds(),
598 IDs::offset, clippedOffset.inSeconds());
600 auto acb =
dynamic_cast<AudioClipBase*
> (clip);
606 if (range == Edit::getMaximumEditTimeRange())
608 addValueTreeProperties (info.state,
609 IDs::fadeIn, acb->getFadeIn().inSeconds(),
610 IDs::fadeOut, acb->getFadeOut().inSeconds());
614 auto inOutPoints = clip->getEditTimeRange().getIntersectionWith (range);
615 TimeRange fadeIn (clipPos.getStart(), clipPos.getStart() + acb->getFadeIn());
616 TimeRange fadeOut (clipPos.getEnd() - acb->getFadeOut(), clipPos.getEnd());
618 addValueTreeProperties (info.state,
619 IDs::fadeIn, fadeIn.overlaps (inOutPoints) ? fadeIn.getIntersectionWith (inOutPoints).getLength().inSeconds() : 0.0,
620 IDs::fadeOut, fadeOut.overlaps (inOutPoints) ? fadeOut.getIntersectionWith (inOutPoints).getLength().inSeconds() : 0.0);
624 addValueTreeProperties (info.state,
625 IDs::proxyAllowed, acb->canUseProxy(),
629 info.trackOffset = allTracks.indexOf (clip->getTrack()) - firstTrackIndex;
631 if (
auto slot = clip->getClipSlot())
632 info.slotOffset = slot->getIndex() - firstSlotIndex;
634 if (acb ==
nullptr || acb->getAutoTempo())
636 info.hasBeatTimes =
true;
638 auto& ts = ed.tempoSequence;
639 info.startBeats = BeatPosition::fromBeats ((ts.toBeats (clippedStart) - ts.toBeats (overallStartTime)).inBeats());
640 info.lengthBeats = ts.toBeats (clippedEnd) - ts.toBeats (clippedStart);
641 info.offsetBeats = BeatPosition::fromBeats (ts.getBeatsPerSecondAt (clippedStart) * clippedOffset.inSeconds());
644 clips.push_back (info);
648 if (automationLocked == AutomationLocked::yes)
649 addAutomation (TrackSection::findSections (clipsToPaste), range);
654 if (range.isEmpty() || trackSections.
isEmpty())
657 auto& edit = trackSections.
getFirst().track->edit;
659 auto firstTrackIndex = edit.engine.getEngineBehaviour().getEditLimits().maxNumTracks;
660 auto overallStartTime = TimePosition::fromSeconds (Edit::maximumLength);
662 for (
const auto& trackSection : trackSections)
664 overallStartTime =
std::min (overallStartTime,
std::max (trackSection.range.getStart(), range.getStart()));
665 firstTrackIndex =
std::min (firstTrackIndex,
std::max (0, allTracks.indexOf (trackSection.track)));
668 for (
const auto& trackSection : trackSections)
670 for (
auto plugin : trackSection.track->pluginList)
672 for (
int k = 0; k < plugin->getNumAutomatableParameters(); k++)
674 auto param = plugin->getAutomatableParameter (k);
676 if (param->getCurve().getNumPoints() > 0)
678 AutomationCurveSection section;
679 section.pluginName = plugin->getName();
680 section.paramID = param->paramID;
681 section.trackOffset =
std::max (0, allTracks.indexOf (trackSection.track) - firstTrackIndex);
682 section.valueRange = param->getCurve().getValueLimits();
684 const auto endTolerence = TimeDuration::fromSeconds (0.0001);
685 auto intersection = trackSection.range.getIntersectionWith (range);
686 auto reducedIntersection = intersection.reduced (endTolerence);
687 auto clippedStart = intersection.getStart();
688 auto clippedEnd = intersection.getEnd();
690 for (
int l = 0; l < param->getCurve().getNumPoints(); ++l)
692 auto pt = param->getCurve().getPoint (l);
694 if (reducedIntersection.containsInclusive (pt.time))
695 section.points.push_back ({ pt.time, pt.value, pt.curve });
698 if (section.points.empty())
700 section.points.push_back ({ clippedStart, param->getCurve().getValueAt (clippedStart), 1.0f });
701 section.points.push_back ({ clippedEnd, param->getCurve().getValueAt (clippedEnd), 0.0f });
705 if (section.points[0].time > clippedStart + endTolerence)
706 section.points.insert (section.points.begin(), { clippedStart + endTolerence, param->getCurve().getValueAt (clippedStart + endTolerence), 0.0f });
708 if (section.points[section.points.size() - 1].time < clippedEnd - endTolerence)
709 section.points.push_back ({ clippedEnd - endTolerence, param->getCurve().getValueAt (clippedEnd - endTolerence), 0.0f });
712 for (
auto& p : section.points)
713 p.
time = p.
time - TimeDuration::fromSeconds (overallStartTime.inSeconds());
715 std::sort (section.points.begin(), section.points.end());
716 automationCurves.push_back (std::move (section));
723static void fixClipTimes (
juce::ValueTree& state,
const Clipboard::Clips::ClipInfo& clip,
725 TempoSequence& tempoSequence, TimePosition startOffset)
727 TimePosition start, offset;
730 if (clip.hasBeatTimes)
732 BeatDuration slotOffset;
734 if (clip.slotOffset.has_value())
735 for (
const auto& info : otherClips)
736 if (info.trackOffset == clip.trackOffset && info.slotOffset.has_value() && *info.slotOffset < *clip.slotOffset)
737 slotOffset = slotOffset + info.lengthBeats;
739 auto offsetInBeats = BeatDuration::fromBeats (tempoSequence.toBeats (startOffset).inBeats());
740 auto range = tempoSequence.toTime ({ clip.startBeats + offsetInBeats + slotOffset, clip.startBeats + offsetInBeats + slotOffset + clip.lengthBeats });
741 start = range.getStart();
742 length = range.getLength();
743 offset = TimePosition::fromSeconds (clip.offsetBeats.inBeats() / tempoSequence.getBeatsPerSecondAt (start));
747 start = TimePosition::fromSeconds (
static_cast<double> (state.getProperty (IDs::start)))
748 + TimeDuration::fromSeconds (startOffset.inSeconds());
749 length = TimeDuration::fromSeconds (
static_cast<double> (state.getProperty (IDs::length)));
750 offset = TimePosition::fromSeconds (
static_cast<double> (state.getProperty (IDs::offset)));
755 if (
const double srcBpm = state[IDs::bpm]; srcBpm > 0)
757 auto& destTempo = tempoSequence.getTempoAt (start);
758 length = TimeDuration::fromSeconds (length.inSeconds() * srcBpm / destTempo.getBpm());
761 state.setProperty (IDs::start, start.inSeconds(),
nullptr);
762 state.setProperty (IDs::length, length.inSeconds(),
nullptr);
763 state.setProperty (IDs::offset, offset.inSeconds(),
nullptr);
765 state.removeProperty (IDs::bpm,
nullptr);
766 state.removeProperty (IDs::key,
nullptr);
771 AutomationCurve newCurve;
772 newCurve.setOwnerParameter (targetCurve.getOwnerParameter());
773 auto dstRange = targetCurve.getValueLimits();
774 jassert (! dstRange.isEmpty());
776 for (
auto p : points)
778 if (dstRange != valueRange)
781 p.value = dstRange.getStart() + dstRange.getLength() * normalised;
784 newCurve.addPoint (p.time, p.value, p.curve);
787 if (newCurve.getLength() > 0_td)
789 if (targetRange.isEmpty())
790 targetRange = targetRange.withLength (newCurve.getLength());
792 newCurve.rescaleAllTimes (targetRange.getLength() / newCurve.getLength());
794 targetCurve.mergeOtherCurve (newCurve, targetRange, 0_tp, 0_td,
false,
false);
801bool Clipboard::Clips::pasteIntoEdit (
const EditPastingOptions& options)
const
806 auto targetTrack = options.startTrack;
807 auto targetClipOwnerID = options.targetClipOwnerID;
809 if (targetTrack ==
nullptr)
811 auto placement = options.insertPoint.chooseInsertPoint (
false, options.selectionManager,
812 [] (
auto& t) { return t.isAudioTrack() || t.isFolderTrack(); });
813 targetTrack = placement.track;
814 targetClipOwnerID = placement.clipOwner !=
nullptr ? placement.clipOwner->getClipOwnerID() : EditItemID();
815 jassert (targetTrack !=
nullptr);
819 while (targetTrack !=
nullptr && targetTrack->isFolderTrack())
820 targetTrack = targetTrack->getSiblingTrack (1,
false);
822 if (targetTrack ==
nullptr)
826 SelectableList itemsAdded;
828 for (
auto& clip : clips)
830 auto newClipState = clip.state.createCopy();
831 EditItemID::remapIDs (newClipState,
nullptr, options.edit, &remappedIDs);
832 fixClipTimes (newClipState, clip, clips, options.edit.tempoSequence, options.startTime);
834 if (newClipState.hasType (IDs::MARKERCLIP))
836 if (
auto markerTrack = options.edit.getMarkerTrack())
838 if (
auto newClip = markerTrack->insertClipWithState (newClipState))
840 itemsAdded.add (newClip);
842 if (
auto mc =
dynamic_cast<MarkerClip*
> (newClip))
843 options.edit.getMarkerManager().checkForDuplicates (*mc,
false);
851 else if (newClipState.hasType (IDs::CHORDCLIP))
853 if (
auto chordTrack = options.edit.getChordTrack())
855 if (
auto newClip = chordTrack->insertClipWithState (newClipState))
856 itemsAdded.add (newClip);
859 else if (newClipState.hasType (IDs::ARRANGERCLIP))
861 if (
auto arrangerTrack = options.edit.getArrangerTrack())
863 if (
auto newClip = arrangerTrack->insertClipWithState (newClipState))
864 itemsAdded.add (newClip);
869 if (
auto clipSlot = findClipSlotForID (options.edit, targetClipOwnerID))
873 options.edit.engine.getUIBehaviour().showWarningMessage (TRANS (
"Group clips can not be added to the clip launcher"));
877 auto calcSlotOffset = [&]
881 for (
const auto& c : clips)
882 if (c.trackOffset == clip.trackOffset && c.startBeats < clip.startBeats)
888 auto slotOffset = clip.slotOffset.has_value() ? *clip.slotOffset : calcSlotOffset();
891 auto trackIndex = tracks.indexOf (
dynamic_cast<AudioTrack*
> (&clipSlot->track));
892 auto slotIndex = clipSlot->getIndex();
894 trackIndex += clip.trackOffset;
895 slotIndex += slotOffset;
897 options.edit.getSceneList().ensureNumberOfScenes (slotIndex + 1);
898 options.edit.ensureNumberOfAudioTracks (trackIndex + 1);
900 if (
auto at = getAudioTracks (options.edit)[trackIndex])
901 clipSlot = at->getClipSlotList().getClipSlots()[slotIndex];
903 if (
auto existingClip = clipSlot->getClip())
904 existingClip->removeFromParent();
906 if (
auto newClip = insertClipWithState (*clipSlot, newClipState))
907 itemsAdded.add (newClip);
910 else if (
auto clipTrack =
dynamic_cast<ClipTrack*
> (targetTrack->getSiblingTrack (clip.trackOffset,
false)))
912 if (
auto newClip = clipTrack->insertClipWithState (newClipState))
913 itemsAdded.add (newClip);
923 for (
auto c : itemsAdded.getItemsOfType<Clip>())
927 auto originalGroup = c->getGroupID();
929 if (groupMap.
find (originalGroup) == groupMap.
end())
930 groupMap[originalGroup] = c->edit.createNewItemID();
932 c->setGroup (groupMap[originalGroup]);
936 for (
auto& curve : automationCurves)
938 if (! curve.points.empty())
940 const TimeRange destCurveTimeRange (options.startTime, TimeDuration());
942 if (
auto clipTrack =
dynamic_cast<ClipTrack*
> (targetTrack->getSiblingTrack (curve.trackOffset,
false)))
944 for (
auto plugin : clipTrack->pluginList)
946 if (plugin->getName() == curve.pluginName)
948 if (
auto targetParam = plugin->getAutomatableParameterByID (curve.paramID))
950 pastePointsToCurve (curve.points, curve.valueRange, targetParam->getCurve(), destCurveTimeRange);
963 if (itemsAdded.isEmpty())
966 if (
auto sm = options.selectionManager)
969 for (
auto i : itemsAdded)
971 if (
auto c =
dynamic_cast<Clip*
> (i); c->isGrouped())
972 sm->select (c->getGroupClip(), ! first);
974 sm->select (i, ! first);
979 if (options.setTransportToEnd && ! options.edit.getTransport().isPlaying())
980 options.edit.getTransport().setPosition (getTimeRangeForSelectedItems (itemsAdded).getEnd());
985bool Clipboard::Clips::pasteIntoEdit (Edit& edit, EditInsertPoint& insertPoint, SelectionManager* sm)
const
987 Clipboard::ContentType::EditPastingOptions options (edit, insertPoint, sm);
988 auto placement = insertPoint.chooseInsertPoint (
false, sm,
989 [] (
auto& t) {
return t.isAudioTrack() || t.isFolderTrack(); });
991 options.startTrack = placement.track;
992 options.startTime = placement.time.value_or (0_tp);
993 options.targetClipOwnerID = placement.clipOwner !=
nullptr ? placement.clipOwner->getClipOwnerID()
996 return pasteIntoEdit (options);
999bool Clipboard::Clips::pasteAfterSelected (Edit& edit, EditInsertPoint& insertPoint, SelectionManager& sm)
const
1001 EditPastingOptions options (edit, insertPoint, &sm);
1002 auto placement = insertPoint.chooseInsertPoint (
true, &sm,
1003 [] (
auto& t) {
return t.isAudioTrack() || t.isFolderTrack(); });
1005 options.startTrack = placement.track;
1006 options.startTime = placement.time.value_or (0_tp);
1007 options.targetClipOwnerID = placement.clipOwner !=
nullptr ? placement.clipOwner->getClipOwnerID()
1010 return pasteIntoEdit (options);
1013static juce::Array<ClipTrack*> findTracksToInsertInto (Edit& edit, EditInsertPoint& insertPoint, SelectionManager& sm)
1015 auto tracks = sm.getItemsOfType<ClipTrack>();
1016 bool noFolders =
true;
1018 for (
auto ft : sm.getItemsOfType<FolderTrack>())
1020 for (
auto t : ft->getAllAudioSubTracks (true))
1021 tracks.addIfNotAlreadyThere (t);
1026 for (
auto c : sm.getItemsOfType<Clip>())
1028 tracks.addIfNotAlreadyThere (c->getClipTrack());
1029 insertPoint.setNextInsertPoint (edit.getTransport().position.get(), c->getTrack());
1032 if (tracks.isEmpty() && noFolders)
1033 tracks.addArray (getClipTracks (edit));
1038static TimeDuration getNewClipsTotalLength (
const Clipboard::Clips& clips, Edit& edit)
1042 for (
auto& i : clips.clips)
1044 auto end = i.hasBeatTimes ? edit.tempoSequence.toTime (i.startBeats + i.lengthBeats)
1045 : TimePosition::fromSeconds (
static_cast<double> (i.state.getProperty (IDs::start))
1046 +
static_cast<double> (i.state.getProperty (IDs::length)));
1054bool Clipboard::Clips::pasteInsertingAtCursorPos (Edit& edit, EditInsertPoint& insertPoint, SelectionManager& sm)
const
1059 auto tracks = findTracksToInsertInto (edit, insertPoint, sm);
1060 auto insertLength = getNewClipsTotalLength (*
this, edit);
1062 if (tracks.isEmpty() || insertLength <= TimeDuration())
1065 auto cursorPos = edit.getTransport().getPosition();
1066 auto firstTrackIndex = tracks.getFirst()->getIndexInEditTrackList();
1068 for (
auto t : tracks)
1070 t->splitAt (cursorPos);
1071 t->insertSpaceIntoTrack (cursorPos, insertLength);
1072 firstTrackIndex =
std::min (firstTrackIndex, t->getIndexInEditTrackList());
1075 EditPastingOptions options (edit, insertPoint, &sm);
1076 options.startTime =
std::max (0_tp, cursorPos);
1077 return pasteIntoEdit (options);
1082Clipboard::Scenes::Scenes() {}
1083Clipboard::Scenes::~Scenes() {}
1085bool Clipboard::Scenes::pasteIntoEdit (
const EditPastingOptions& options)
const
1087 auto& sceneList = options.edit.getSceneList();
1089 auto insertIndex = sceneList.getNumScenes();
1090 if (
auto sm = options.selectionManager)
1092 auto items = sm->getSelectedObjects().getItemsOfType<Scene>();
1093 if (items.size() > 0)
1096 for (
auto s : items)
1097 insertIndex =
std::
max (insertIndex, s->getIndex() + 1);
1103 SelectableList itemsAdded;
1105 for (
auto info : scenes)
1107 if (
auto newScene = sceneList.insertScene (insertIndex))
1109 newScene->state.copyPropertiesAndChildrenFrom (info.state, &options.edit.getUndoManager());
1111 options.edit.ensureNumberOfAudioTracks (
int (info.clips.size()));
1115 auto track = tracks[
int (idx)];
1116 auto slot = track->getClipSlotList().getClipSlots()[insertIndex];
1120 auto newClipState = clip.createCopy();
1121 EditItemID::remapIDs (newClipState,
nullptr, options.edit, &remappedIDs);
1126 itemsAdded.add (newScene);
1132 if (itemsAdded.isEmpty())
1135 if (
auto sm = options.selectionManager)
1136 sm->select (itemsAdded);
1143Clipboard::Tracks::Tracks() {}
1144Clipboard::Tracks::~Tracks() {}
1146bool Clipboard::Tracks::pasteIntoEdit (
const EditPastingOptions& options)
const
1153 auto targetTrack = options.startTrack;
1159 if (options.selectionManager !=
nullptr && options.selectionManager->isSelected (targetTrack.get()))
1160 for (
auto t : options.selectionManager->getItemsOfType<Track>())
1161 if (allTracks.indexOf (t) > allTracks.indexOf (targetTrack.get()))
1164 if (options.selectionManager !=
nullptr)
1165 options.selectionManager->deselectAll();
1167 for (
auto& trackState : tracks)
1169 auto newTrackTree = trackState.createCopy();
1170 EditItemID::remapIDs (newTrackTree,
nullptr, options.edit, &remappedIDs);
1172 Track::Ptr parentTrack, precedingTrack;
1174 if (targetTrack !=
nullptr)
1175 parentTrack = targetTrack->getParentTrack();
1177 precedingTrack = targetTrack;
1179 if (
auto newTrack = options.edit.insertTrack (TrackInsertPoint (parentTrack.get(),
1180 precedingTrack.get()),
1182 options.selectionManager))
1184 newTracks.add (newTrack);
1186 targetTrack = newTrack;
1198 for (
auto track : newTracks)
1200 for (
auto param : track->getAllAutomatableParams())
1202 auto assignments = param->getAssignments();
1204 for (
int i = assignments.size(); --i >= 0;)
1206 auto ass = assignments.getUnchecked (i);
1210 if (
dynamic_cast<MacroParameter::Assignment*
> (ass.get()) !=
nullptr)
1213 const auto oldID = EditItemID::fromProperty (ass->state, IDs::source);
1214 const auto newID = remappedIDs[oldID];
1216 if (newID.isValid())
1218 ass->state.setProperty (IDs::source, newID,
nullptr);
1225 if (
auto t = getTrackContainingModifier (options.edit,
findModifierForID (options.edit, oldID)))
1226 if (t == track.get() || TrackList::isFixedTrack (t->state) || track->isAChildOf (*t))
1230 param->removeModifier (*ass);
1241Clipboard::TempoChanges::TempoChanges (
const TempoSequence& ts, TimeRange range)
1243 auto beats = ts.toBeats (range);
1245 BeatPosition startBeat = BeatPosition::fromBeats (
std::floor (beats.getStart().inBeats() + 0.5));
1246 BeatPosition endBeat = BeatPosition::fromBeats (
std::floor (beats.getEnd().inBeats() + 0.5));
1248 bool pointAtStart =
false;
1249 bool pointAtEnd =
false;
1251 for (
auto t : ts.getTempos())
1253 if (t->startBeatNumber == startBeat) pointAtStart =
true;
1254 if (t->startBeatNumber == endBeat) pointAtEnd =
true;
1256 if (range.containsInclusive (t->getStartTime()))
1257 changes.push_back ({ t->startBeatNumber -
toDuration (startBeat),
1263 changes.insert (changes.begin(),
1265 ts.getBpmAt (ts.toTime (startBeat)),
1266 ts.getTempoAt (BeatPosition::fromBeats (
juce::roundToInt (startBeat.inBeats()))).getCurve() });
1269 changes.push_back ({
toPosition (endBeat - startBeat),
1270 ts.getBpmAt (ts.toTime (endBeat)),
1271 ts.getTempoAt (BeatPosition::fromBeats (
juce::roundToInt (endBeat.inBeats()))).getCurve() });
1274Clipboard::TempoChanges::~TempoChanges() {}
1276bool Clipboard::TempoChanges::pasteIntoEdit (
const EditPastingOptions& options)
const
1278 return pasteTempoSequence (options.edit.tempoSequence, TimeRange::emptyRange (options.startTime));
1281bool Clipboard::TempoChanges::pasteTempoSequence (TempoSequence& ts, TimeRange targetRange)
const
1283 if (changes.empty())
1286 EditTimecodeRemapperSnapshot snap;
1287 snap.savePreChangeState (ts.edit);
1289 auto lengthInBeats =
toDuration (changes.back().beat);
1291 if (targetRange.isEmpty())
1292 targetRange = targetRange.withEnd (ts.toTime (ts.toBeats (targetRange.getStart()) + lengthInBeats));
1294 auto startBeat = BeatPosition::fromBeats (
std::floor (ts.toBeats (targetRange.getStart()).inBeats() + 0.5));
1295 auto endBeat = BeatPosition::fromBeats (
std::floor (ts.toBeats (targetRange.getEnd()).inBeats() + 0.5));
1297 double finalBPM = ts.getBpmAt (ts.toTime (endBeat));
1298 ts.removeTemposBetween (ts.toTime ({ startBeat, endBeat }),
false);
1299 ts.insertTempo (ts.toTime (startBeat));
1301 for (
auto& tc : changes)
1302 ts.insertTempo (BeatPosition::fromBeats (
juce::
roundToInt ((tc.beat.inBeats() / lengthInBeats.inBeats()) * (endBeat - startBeat).inBeats() + startBeat.inBeats())),
1305 ts.insertTempo (ts.toTime (endBeat));
1306 ts.insertTempo (endBeat, finalBPM, 1.0f);
1308 for (
int i = ts.getNumTempos(); --i >= 1;)
1310 auto tcurr = ts.getTempo (i);
1311 auto tprev = ts.getTempo (i - 1);
1313 if (tcurr->startBeatNumber >= startBeat && tcurr->startBeatNumber <= endBeat
1314 && tcurr->startBeatNumber == tprev->startBeatNumber
1315 && tcurr->getBpm() == tprev->getBpm())
1316 ts.removeTempo (i,
false);
1319 snap.remapEdit (ts.edit);
1323#if TRACKTION_UNIT_TESTS && ENGINE_UNIT_TESTS_CLIPBOARD
1330 ClipboardTempoTests() :
juce::UnitTest (
"ClipboardTempoTests",
"Tracktion") {}
1336 runCopyTestsUsingBeatInsertion();
1337 runTrackCopyPasteTests();
1341 template<
typename TimeType>
1357 void expectTempoSetting (TempoSetting& tempo,
double bpm,
float curve)
1365 auto edit = Edit::createSingleTrackEdit (*Engine::getEngines()[0]);
1366 auto& ts = edit->tempoSequence;
1370 ts.getTempo (0)->setBpm (120.0);
1376 expectEquals (ts.toTime ({ 8, {} }), 16.0);
1378 ts.insertTempo (ts.toBeats ({ 5, {} }), 60.0, 1.0f);
1379 ts.insertTempo (ts.toBeats ({ 9, {} }), 120.0, 1.0f);
1381 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 4, {} })), 120.0, 1.0f);
1382 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 6, {} })), 60.0, 1.0f);
1383 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 8, {} })), 60.0, 1.0f);
1384 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 10, {} })), 120.0, 1.0f);
1386 const BeatRange beatRangeToCopy (ts.toBeats ({ 6, {} }), ts.toBeats ({ 8, {} }));
1387 const auto timeRangeToCopy = ts.toTime (beatRangeToCopy);
1388 const BeatDuration numBeatsToInsert = beatRangeToCopy.getLength();
1391 Clipboard::TempoChanges tempoChanges (ts, timeRangeToCopy);
1394 const auto timeToInsertAt = ts.toTime ({ 2, {} });
1395 auto& tempoAtInsertionPoint = ts.getTempoAt (timeToInsertAt);
1397 const auto beatRangeToInsert =
BeatRange (ts.toBeats (timeToInsertAt), numBeatsToInsert);
1398 const auto lengthInTimeToInsert = ts.toTime (toPosition (beatRangeToInsert.getLength()));
1399 insertSpaceIntoEdit (*edit, TimeRange (timeToInsertAt, toDuration (lengthInTimeToInsert)));
1401 const auto numBeatsInserted = beatRangeToInsert.getLength();
1402 const int numBarsInserted =
juce::roundToInt (numBeatsInserted.inBeats() / tempoAtInsertionPoint.getMatchingTimeSig().denominator);
1403 expectWithinAbsoluteError (numBeatsInserted.inBeats(), 8.0, 0.0001);
1404 expect (numBarsInserted == 2);
1407 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 4 + numBarsInserted, {} })), 120.0, 1.0f);
1408 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 6 + numBarsInserted, {} })), 60.0, 1.0f);
1409 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 8 + numBarsInserted, {} })), 60.0, 1.0f);
1410 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 10 + numBarsInserted, {} })), 120.0, 1.0f);
1413 tempoChanges.pasteTempoSequence (ts, TimeRange (timeToInsertAt, lengthInTimeToInsert));
1416 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 0, {} })), 120.0, 1.0f);
1417 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 1, BeatDuration::fromBeats (3) })), 120.0, 1.0f);
1418 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 2, {} })), 60.0, 1.0f);
1419 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 3, BeatDuration::fromBeats (3) })), 60.0, 1.0f);
1420 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 4, {} })), 120.0, 1.0f);
1424 void runCopyTestsUsingBeatInsertion()
1426 auto edit = Edit::createSingleTrackEdit (*Engine::getEngines()[0]);
1427 auto& ts = edit->tempoSequence;
1429 beginTest (
"Simple copy/paste");
1431 ts.getTempo (0)->setBpm (120.0);
1434 expectEquals (ts.toBeats ({ 0, {} }), 0.0);
1435 expectEquals (ts.toTime ({ 0, {} }), 0.0);
1436 expectEquals (ts.toBeats ({ 8, {} }), 32.0);
1437 expectEquals (ts.toTime ({ 8, {} }), 16.0);
1439 ts.insertTempo (ts.toBeats ({ 5, {} }), 60.0, 1.0f);
1440 ts.insertTempo (ts.toBeats ({ 9, {} }), 120.0, 1.0f);
1442 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 4, {} })), 120.0, 1.0f);
1443 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 6, {} })), 60.0, 1.0f);
1444 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 8, {} })), 60.0, 1.0f);
1445 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 10, {} })), 120.0, 1.0f);
1447 const BeatRange beatRangeToCopy (ts.toBeats ({ 6, {} }), ts.toBeats ({ 8, {} }));
1448 const auto timeRangeToCopy = ts.toTime (beatRangeToCopy);
1451 Clipboard::TempoChanges tempoChanges (ts, timeRangeToCopy);
1454 const auto timeToInsertAt = ts.toTime ({ 2, {} });
1455 auto& tempoAtInsertionPoint = ts.getTempoAt (timeToInsertAt);
1456 const auto beatRangeToInsert = beatRangeToCopy.movedToStartAt (ts.toBeats (timeToInsertAt));
1459 const auto numBeatsInserted = beatRangeToInsert.getLength();
1460 const int numBarsInserted =
juce::roundToInt (numBeatsInserted.inBeats() / tempoAtInsertionPoint.getMatchingTimeSig().denominator);
1461 expectWithinAbsoluteError (numBeatsInserted.inBeats(), 8.0, 0.0001);
1462 expect (numBarsInserted == 2);
1465 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 4 + numBarsInserted, {} })), 120.0, 1.0f);
1466 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 6 + numBarsInserted, {} })), 60.0, 1.0f);
1467 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 8 + numBarsInserted, {} })), 60.0, 1.0f);
1468 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 10 + numBarsInserted, {} })), 120.0, 1.0f);
1471 tempoChanges.pasteTempoSequence (ts, TimeRange (timeToInsertAt, ts.toTime (beatRangeToInsert.getEnd())));
1474 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 0, {} })), 120.0, 1.0f);
1475 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 1, BeatDuration::fromBeats (3) })), 120.0, 1.0f);
1476 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 2, {} })), 60.0, 1.0f);
1477 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 3, BeatDuration::fromBeats (3) })), 60.0, 1.0f);
1478 expectTempoSetting (ts.getTempoAt (ts.toBeats ({ 4, {} })), 120.0, 1.0f);
1482 void runTrackCopyPasteTests()
1484 beginTest (
"Root tracks copy/paste");
1486 auto edit = Edit::createSingleTrackEdit (*Engine::getEngines()[0]);
1487 edit->ensureNumberOfAudioTracks (3);
1492 tracks[0]->setName (
"First");
1493 tracks[1]->setName (
"Second");
1494 tracks[2]->setName (
"Third");
1496 Clipboard::Tracks clipboardTracks;
1498 for (
auto at : tracks)
1499 clipboardTracks.tracks.push_back (at->state.createCopy());
1502 tracks[0]->setName (
"First Original");
1503 tracks[1]->setName (
"Second Original");
1504 tracks[2]->setName (
"Third Original");
1506 EditInsertPoint insertPoint (*edit);
1507 Clipboard::ContentType::EditPastingOptions opts (*edit, insertPoint);
1508 opts.startTrack = tracks[2];
1509 expect (clipboardTracks.pasteIntoEdit (opts));
1515 juce::UnitTest::expectEquals<juce::String> (tracks[0]->
getName(),
"First Original");
1516 juce::UnitTest::expectEquals<juce::String> (tracks[1]->
getName(),
"Second Original");
1517 juce::UnitTest::expectEquals<juce::String> (tracks[2]->
getName(),
"Third Original");
1518 juce::UnitTest::expectEquals<juce::String> (tracks[3]->
getName(),
"First");
1519 juce::UnitTest::expectEquals<juce::String> (tracks[4]->
getName(),
"Second");
1520 juce::UnitTest::expectEquals<juce::String> (tracks[5]->
getName(),
"Third");
1524 beginTest (
"Root tracks paste in to folder");
1526 auto edit = Edit::createSingleTrackEdit (*Engine::getEngines()[0]);
1529 edit->ensureNumberOfAudioTracks (3);
1532 tracks[0]->setName (
"First");
1533 tracks[1]->setName (
"Second");
1534 tracks[2]->setName (
"Third");
1537 auto ft = edit->insertNewFolderTrack (TrackInsertPoint ({ *
getAudioTracks (*edit).getLast(),
false }),
nullptr,
false);
1540 Clipboard::Tracks clipboardTracks;
1543 for (
auto at : tracks)
1544 clipboardTracks.tracks.push_back (at->state.createCopy());
1547 tracks[0]->setName (
"First Original");
1548 tracks[1]->setName (
"Second Original");
1549 tracks[2]->setName (
"Third Original");
1551 EditInsertPoint insertPoint (*edit);
1552 Clipboard::ContentType::EditPastingOptions opts (*edit, insertPoint);
1553 opts.startTrack = ft;
1554 expect (clipboardTracks.pasteIntoEdit (opts));
1560 juce::UnitTest::expectEquals<juce::String> (tracks[0]->
getName(),
"First Original");
1561 juce::UnitTest::expectEquals<juce::String> (tracks[1]->
getName(),
"Second Original");
1562 juce::UnitTest::expectEquals<juce::String> (tracks[2]->
getName(),
"Third Original");
1563 juce::UnitTest::expectEquals<juce::String> (tracks[3]->
getName(),
"First");
1564 juce::UnitTest::expectEquals<juce::String> (tracks[4]->
getName(),
"Second");
1565 juce::UnitTest::expectEquals<juce::String> (tracks[5]->
getName(),
"Third");
1569 beginTest (
"Tracks inside folder copy/paste");
1571 auto edit = Edit::createSingleTrackEdit (*Engine::getEngines()[0]);
1574 edit->ensureNumberOfAudioTracks (3);
1577 tracks[0]->setName (
"First");
1578 tracks[1]->setName (
"Second");
1579 tracks[2]->setName (
"Third");
1582 auto ft = edit->insertNewFolderTrack (TrackInsertPoint ({}),
nullptr,
false);
1586 edit->moveTrack (tracks[2], TrackInsertPoint (ft.get(),
nullptr));
1587 edit->moveTrack (tracks[1], TrackInsertPoint (ft.get(),
nullptr));
1588 edit->moveTrack (tracks[0], TrackInsertPoint (ft.get(),
nullptr));
1592 auto subTracks = ft->getInputTracks();
1594 juce::UnitTest::expectEquals<juce::String> (subTracks[0]->
getName(),
"First");
1595 juce::UnitTest::expectEquals<juce::String> (subTracks[1]->
getName(),
"Second");
1596 juce::UnitTest::expectEquals<juce::String> (subTracks[2]->
getName(),
"Third");
1600 Clipboard::Tracks clipboardTracks;
1602 for (
auto at : ft->getInputTracks())
1603 clipboardTracks.tracks.push_back (at->state.createCopy());
1606 auto subTracks = ft->getInputTracks();
1607 subTracks[0]->setName (
"First Original");
1608 subTracks[1]->setName (
"Second Original");
1609 subTracks[2]->setName (
"Third Original");
1611 EditInsertPoint insertPoint (*edit);
1612 Clipboard::ContentType::EditPastingOptions opts (*edit, insertPoint);
1613 opts.startTrack = subTracks[2];
1614 expect (clipboardTracks.pasteIntoEdit (opts));
1618 auto subTracks = ft->getInputTracks();
1620 juce::UnitTest::expectEquals<juce::String> (subTracks[0]->
getName(),
"First Original");
1621 juce::UnitTest::expectEquals<juce::String> (subTracks[1]->
getName(),
"Second Original");
1622 juce::UnitTest::expectEquals<juce::String> (subTracks[2]->
getName(),
"Third Original");
1623 juce::UnitTest::expectEquals<juce::String> (subTracks[3]->
getName(),
"First");
1624 juce::UnitTest::expectEquals<juce::String> (subTracks[4]->
getName(),
"Second");
1625 juce::UnitTest::expectEquals<juce::String> (subTracks[5]->
getName(),
"Third");
1631static ClipboardTempoTests clipboardTempoTests;
1637Clipboard::AutomationPoints::AutomationPoints (
const AutomationCurve& curve, TimeRange range)
1639 valueRange = curve.getValueLimits();
1641 bool pointAtStart =
false;
1642 bool pointAtEnd =
false;
1644 for (
int i = 0; i < curve.getNumPoints(); ++i)
1646 auto p = curve.getPoint (i);
1648 if (p.time == range.getStart()) pointAtStart =
true;
1649 if (p.time == range.getEnd()) pointAtEnd =
true;
1651 if (range.containsInclusive (p.time))
1653 p.time = p.time - TimeDuration::fromSeconds (range.getStart().inSeconds());
1659 points.
insert (points.
begin(), AutomationCurve::AutomationPoint (TimePosition(), curve.getValueAt (range.getStart()), 0));
1662 points.
push_back (AutomationCurve::AutomationPoint (TimePosition::fromSeconds (range.getLength().inSeconds()), curve.getValueAt (range.getEnd()), 0));
1665Clipboard::AutomationPoints::~AutomationPoints() {}
1667bool Clipboard::AutomationPoints::pasteIntoEdit (
const EditPastingOptions&)
const
1673bool Clipboard::AutomationPoints::pasteAutomationCurve (AutomationCurve& targetCurve, TimeRange targetRange)
const
1675 return pastePointsToCurve (points, valueRange, targetCurve, targetRange);
1680Clipboard::MIDIEvents::MIDIEvents() {}
1681Clipboard::MIDIEvents::~MIDIEvents() {}
1686 TimePosition cursorPosition,
const std::function<BeatPosition (BeatPosition)>& snapBeat,
1687 int destController)
const
1689 auto notesAdded = pasteNotesIntoClip (clip, selectedNotes, cursorPosition, snapBeat);
1690 auto controllersAdded = pasteControllersIntoClip (clip, selectedNotes, selectedEvents, cursorPosition, snapBeat, destController);
1692 return { notesAdded, controllersAdded };
1696 TimePosition cursorPosition,
const std::function<BeatPosition (BeatPosition)>& snapBeat)
const
1703 for (
auto& n : notes)
1704 midiNotes.add (MidiNote (n));
1706 auto beatRange = midiNotes.
getReference (0).getRangeBeats();
1708 for (
auto& n : midiNotes)
1709 beatRange = beatRange.getUnionWith (n.getRangeBeats());
1711 BeatPosition insertPos;
1713 if (clip.isLooping())
1714 insertPos = clip.getContentBeatAtTime (cursorPosition) +
toDuration (clip.getLoopStartBeats());
1716 insertPos = clip.getContentBeatAtTime (cursorPosition);
1718 if (! selectedNotes.
isEmpty())
1720 BeatPosition endOfSelection;
1722 for (
auto n : selectedNotes)
1723 endOfSelection =
std::
max (endOfSelection, n->getEndBeat());
1725 insertPos = endOfSelection;
1728 if (clip.isLooping())
1730 const auto offsetBeats = clip.getOffsetInBeats() +
toDuration (clip.getLoopStartBeats());
1732 if ((insertPos - offsetBeats) < BeatPosition() || insertPos - offsetBeats >= toPosition (clip.getLoopLengthBeats() - 0.001_bd))
1737 const auto offsetBeats = clip.getOffsetInBeats();
1739 if ((insertPos - offsetBeats) < BeatPosition() || insertPos - offsetBeats >= toPosition (clip.getLengthInBeats() - 0.001_bd))
1743 auto deltaBeats = insertPos - beatRange.getStart();
1745 if (snapBeat !=
nullptr)
1746 deltaBeats =
toDuration (snapBeat (toPosition (deltaBeats)));
1748 auto& sequence = clip.getSequence();
1749 auto um = &clip.edit.getUndoManager();
1752 for (
auto& n : midiNotes)
1754 n.setStartAndLength (n.getStartBeat() + deltaBeats, n.getLengthBeats(),
nullptr);
1756 if (
auto note = sequence.addNote (n, um))
1757 notesAdded.
add (note);
1766 TimePosition cursorPosition,
const std::function<BeatPosition (BeatPosition)>& snapBeat,
1767 int destController)
const
1769 if (controllers.empty())
1774 for (
auto& e : controllers)
1775 midiEvents.add (MidiControllerEvent (e));
1777 if (notes.size() > 0)
1778 destController = -1;
1781 for (
auto& e : midiEvents)
1782 controllerTypes.addIfNotAlreadyThere (e.getType());
1784 if (controllerTypes.
size() > 1)
1785 destController = -1;
1787 if (destController != -1)
1789 for (
auto& e : midiEvents)
1790 e.setType (destController, nullptr);
1792 controllerTypes.
clear();
1793 controllerTypes.
add (destController);
1798 for (
auto& e : midiEvents)
1799 beatRange = beatRange.getUnionWith (
BeatRange (e.getBeatPosition(), BeatDuration()));
1801 BeatPosition insertPos;
1803 if (clip.isLooping())
1804 insertPos = clip.getContentBeatAtTime (cursorPosition) +
toDuration (clip.getLoopStartBeats());
1806 insertPos = clip.getContentBeatAtTime (cursorPosition);
1808 if (! selectedNotes.
isEmpty())
1810 BeatPosition endOfSelection;
1812 for (
auto n : selectedNotes)
1813 endOfSelection =
std::
max (endOfSelection, n->getEndBeat());
1815 insertPos = endOfSelection;
1817 else if (! selectedEvents.
isEmpty())
1819 BeatPosition endOfSelection;
1821 for (
auto e : selectedEvents)
1822 if (controllerTypes.contains (e->getType()))
1823 endOfSelection =
std::
max (endOfSelection, e->getBeatPosition());
1825 insertPos = endOfSelection + BeatDuration::fromBeats (1.0);
1828 if (clip.isLooping())
1830 auto offsetBeats =
toDuration (clip.getLoopStartBeats() + clip.getOffsetInBeats());
1832 if (insertPos - offsetBeats < 0_bp || insertPos - offsetBeats >= toPosition (clip.getLoopLengthBeats() - 0.001_bd))
1837 auto offsetBeats = clip.getOffsetInBeats();
1839 if (insertPos - offsetBeats < 0_bp || insertPos - offsetBeats >= toPosition (clip.getLengthInBeats() - 0.001_bd))
1843 auto deltaBeats = insertPos - beatRange.getStart();
1845 if (snapBeat !=
nullptr)
1846 deltaBeats =
toDuration (snapBeat (toPosition (deltaBeats)));
1848 auto& sequence = clip.getSequence();
1849 auto um = &clip.edit.getUndoManager();
1854 for (
auto evt : sequence.getControllerEvents())
1855 if (controllerTypes.contains (evt->getType()) && evt->getBeatPosition() >= beatRange.getStart() + deltaBeats && evt->getBeatPosition() <= beatRange.getEnd() + deltaBeats)
1856 itemsToRemove.push_back (evt->state);
1858 for (
auto& v : itemsToRemove)
1859 sequence.state.removeChild (v, um);
1861 for (
auto& e : midiEvents)
1863 e.setBeatPosition (e.getBeatPosition() + deltaBeats, um);
1865 if (
auto evt = sequence.addControllerEvent (e, um))
1866 eventsAdded.
add (evt);
1872bool Clipboard::MIDIEvents::pasteIntoEdit (
const EditPastingOptions&)
const
1880 static TimePosition snapTimeToNearestBeat (Edit& e, TimePosition t)
1882 return TimecodeSnapType::get1BeatSnapType().roundTimeNearest (t, e.tempoSequence);
1888Clipboard::Pitches::Pitches() {}
1889Clipboard::Pitches::~Pitches() {}
1891bool Clipboard::Pitches::pasteIntoEdit (
const EditPastingOptions& options)
const
1893 if (pitches.empty())
1896 if (options.selectionManager !=
nullptr)
1897 options.selectionManager->deselectAll();
1899 auto startBeat = options.edit.tempoSequence.toBeats (snapTimeToNearestBeat (options.edit, options.startTime));
1900 auto firstPitchBeat = BeatPosition::fromBeats (
static_cast<double> (pitches.front().getProperty (IDs::startBeat)));
1901 auto offset = startBeat - firstPitchBeat;
1902 auto um = &options.edit.getUndoManager();
1904 for (
auto& state : pitches)
1906 auto time = options.edit.tempoSequence.toTime (BeatPosition::fromBeats (
static_cast<double> (state.getProperty (IDs::startBeat))) + offset);
1908 if (
auto pitch = options.edit.pitchSequence.insertPitch (time))
1910 jassert (pitch->state.getNumChildren() == 0);
1912 copyValueTreeProperties (pitch->state, state, um,
1915 if (options.selectionManager !=
nullptr)
1916 options.selectionManager->addToSelection (*pitch);
1925Clipboard::TimeSigs::TimeSigs() {}
1926Clipboard::TimeSigs::~TimeSigs() {}
1928bool Clipboard::TimeSigs::pasteIntoEdit (
const EditPastingOptions& options)
const
1930 if (timeSigs.empty())
1933 if (options.selectionManager !=
nullptr)
1934 options.selectionManager->deselectAll();
1936 auto startBeat = options.edit.tempoSequence.toBeats (snapTimeToNearestBeat (options.edit, options.startTime));
1937 auto firstTimeSigBeat = BeatPosition::fromBeats (
static_cast<double> (timeSigs.front().getProperty (IDs::startBeat)));
1938 auto offset = startBeat - firstTimeSigBeat;
1939 auto um = &options.edit.getUndoManager();
1941 for (
auto& state : timeSigs)
1943 auto time = options.edit.tempoSequence.toTime (BeatPosition::fromBeats (
static_cast<double> (state.getProperty (IDs::startBeat))) + offset);
1945 if (
auto timeSig = options.edit.tempoSequence.insertTimeSig (time))
1947 jassert (timeSig->state.getNumChildren() == 0);
1948 copyValueTreeProperties (timeSig->state, state, um,
1951 if (options.selectionManager !=
nullptr)
1952 options.selectionManager->addToSelection (*timeSig);
1961Clipboard::Plugins::Plugins (
const Plugin::Array& items)
1963 for (
auto& item : items)
1965 item->edit.flushPluginStateIfNeeded (*item);
1966 plugins.push_back (item->state.createCopy());
1968 if (
auto rackInstance =
dynamic_cast<RackInstance*
> (item))
1970 if (
auto type = rackInstance->type)
1972 auto newEntry =
std::make_pair (makeSafeRef (type->edit), type->state);
1974 if (
std::find (rackTypes.begin(), rackTypes.end(), newEntry) == rackTypes.end())
1975 rackTypes.push_back (newEntry);
1981Clipboard::Plugins::~Plugins() {}
1983static bool pastePluginBasedOnSelection (Edit& edit,
const Plugin::Ptr& newPlugin,
1984 SelectionManager* selectionManager)
1986 if (selectionManager ==
nullptr)
1989 if (RackType::Ptr selectedRack = selectionManager->getFirstItemOfType<RackType>())
1991 selectedRack->addPlugin (newPlugin, { 0.5f, 0.5f },
false);
1995 if (Plugin::Ptr selectedPlugin = selectionManager->getFirstItemOfType<Plugin>())
1997 if (
auto list = selectedPlugin->getOwnerList())
1999 auto index = list->indexOf (selectedPlugin.get());
2003 list->insertPlugin (newPlugin, index, selectionManager);
2008 if (
auto selectedRack = edit.getRackList().findRackContaining (*selectedPlugin))
2010 selectedRack->addPlugin (newPlugin, { 0.5f, 0.5f },
false);
2015 if (
auto selectedClip = selectionManager->getFirstItemOfType<Clip>())
2016 if (selectedClip->addClipPlugin (newPlugin, *selectionManager))
2022static bool pastePluginIntoTrack (
const Plugin::Ptr& newPlugin, EditInsertPoint& insertPoint, SelectionManager* sm)
2024 TimePosition startPos;
2026 insertPoint.chooseInsertPoint (track, startPos,
false, sm,
2027 [] (
auto& t) {
return t.isAudioTrack() || t.isFolderTrack() || t.isMasterTrack(); });
2030 if (track !=
nullptr && track->canContainPlugin (newPlugin.get()))
2032 track->pluginList.insertPlugin (newPlugin, 0, sm);
2041 EditItemID::IDMap reassignedIDs;
2043 for (
const auto& editAndTypeState : editAndTypeStates)
2045 if (editAndTypeState.first == &edit)
2048 auto typeState = editAndTypeState.second;
2049 auto reassignedRackType = typeState.createCopy();
2050 EditItemID::remapIDs (reassignedRackType,
nullptr, edit, &reassignedIDs);
2051 edit.getRackList().addRackTypeFrom (reassignedRackType);
2054 return reassignedIDs;
2057bool Clipboard::Plugins::pasteIntoEdit (
const EditPastingOptions& options)
const
2060 bool anyPasted =
false;
2062 auto rackIDMap = pasteRackTypesInToEdit (options.edit, rackTypes);
2064 auto pluginsToPaste = plugins;
2065 std::reverse (pluginsToPaste.begin(), pluginsToPaste.end());
2067 for (
auto& item : pluginsToPaste)
2069 auto stateCopy = item.createCopy();
2070 EditItemID::remapIDs (stateCopy,
nullptr, options.edit);
2073 if (stateCopy[IDs::type].toString() == IDs::rack.toString())
2075 auto oldRackID = EditItemID::fromProperty (stateCopy, IDs::rackType);
2076 auto remappedRackID = rackIDMap[oldRackID];
2078 if (remappedRackID.isValid())
2079 stateCopy.setProperty (IDs::rackType, remappedRackID,
nullptr);
2082 if (
auto newPlugin = options.edit.getPluginCache().getOrCreatePluginFor (stateCopy))
2084 if (pastePluginBasedOnSelection (options.edit, newPlugin, options.selectionManager)
2085 || pastePluginIntoTrack (newPlugin, options.insertPoint, options.selectionManager))
2090 if (
auto track = newPlugin->getOwnerTrack())
2092 for (
auto param : newPlugin->getAutomatableParameters())
2094 auto assignments = param->getAssignments();
2096 for (
int i = assignments.size(); --i >= 0;)
2098 auto ass = assignments.getUnchecked (i);
2100 if (
auto mpa =
dynamic_cast<MacroParameter::Assignment*
> (ass.get()))
2102 if (
auto mp = getMacroParameterForID (options.edit, mpa->macroParamID))
2103 if (
auto t = mp->getTrack())
2104 if (! (t == track || track->isAChildOf (*t)))
2105 param->removeModifier (*ass);
2109 if (
auto t = getTrackContainingModifier (options.edit,
2110 findModifierForID (options.edit, EditItemID::fromProperty (ass->state, IDs::source))))
2111 if (! (t == track || TrackList::isFixedTrack (t->state) || track->isAChildOf (*t)))
2112 param->removeModifier (*ass);
2126Clipboard::Takes::Takes (
const WaveCompManager& waveCompManager)
2128 items = waveCompManager.getActiveTakeTree().createCopy();
2131Clipboard::Takes::~Takes() {}
2133bool Clipboard::Takes::pasteIntoClip (WaveAudioClip& c)
const
2135 if (items.isValid())
2136 return c.getCompManager().pasteComp (items).
isValid();
2143Clipboard::Modifiers::Modifiers() {}
2144Clipboard::Modifiers::~Modifiers() {}
2146bool Clipboard::Modifiers::pasteIntoEdit (
const EditPastingOptions& options)
const
2148 if (modifiers.empty())
2151 if (options.selectionManager !=
nullptr)
2153 if (
auto firstSelectedMod = options.selectionManager->getFirstItemOfType<Modifier>())
2155 if (
auto t = getTrackContainingModifier (options.edit, firstSelectedMod))
2157 if (
auto modifierList = t->getModifierList())
2159 auto modList = getModifiersOfType<Modifier> (*modifierList);
2161 for (
int i = modList.size(); --i >= 0;)
2163 if (modList.getObjectPointer (i) == firstSelectedMod)
2165 for (
auto m : modifiers)
2167 EditItemID::remapIDs (m,
nullptr, options.edit);
2168 modifierList->insertModifier (m, i + 1, options.selectionManager);
2179 if (options.startTrack !=
nullptr)
2181 if (
auto modifierList = options.startTrack->getModifierList())
2183 for (
auto m : modifiers)
2185 EditItemID::remapIDs (m,
nullptr, options.edit);
2186 modifierList->insertModifier (m, -1, options.selectionManager);
ElementType getUnchecked(int index) const
bool isEmpty() const noexcept
int size() const noexcept
ElementType getFirst() const noexcept
void add(const ElementType &newElement)
ElementType & getReference(int index) noexcept
void addChangeListener(ChangeListener *listener)
void removeChangeListener(ChangeListener *listener)
bool isValid() const noexcept
static LookAndFeel & getDefaultLookAndFeel() noexcept
static ModifierKeys currentModifiers
constexpr ValueType getStart() const noexcept
constexpr ValueType getLength() const noexcept
void expectEquals(ValueType actual, ValueType expected, String failureMessage=String())
void beginTest(const String &testName)
void expectWithinAbsoluteError(ValueType actual, ValueType expected, ValueType maxAbsoluteError, String failureMessage=String())
The Engine is the central class for all tracktion sessions.
UIBehaviour & getUIBehaviour() const
Returns the UIBehaviour class.
EngineBehaviour & getEngineBehaviour() const
Returns the EngineBehaviour instance.
ProjectManager & getProjectManager() const
Returns the ProjectManager instance.
#define TRANS(stringLiteral)
#define JUCE_IMPLEMENT_SINGLETON(Classname)
constexpr auto enumerate(Range &&range, Index startingValue={})
int roundToInt(const FloatType value) noexcept
void insertSpaceIntoEdit(Edit &edit, TimeRange timeRange)
Inserts blank space in to an Edit, splitting clips if necessary.
juce::String getName(LaunchQType t)
Retuns the name of a LaunchQType for display purposes.
SelectableList getClipSelectionWithCollectionClipContents(const SelectableList &in)
Returns a list of clips.
void insertSpaceIntoEditFromBeatRange(Edit &edit, BeatRange beatRange)
Inserts a number of blank beats in to the Edit.
juce::Array< Track * > getAllTracks(const Edit &edit)
Returns all the tracks in an Edit.
int findClipSlotIndex(ClipSlot &slot)
Returns the index of the ClipSlot in the list it is owned by.
juce::Array< AudioTrack * > getAudioTracks(const Edit &edit)
Returns all the AudioTracks in an Edit.
Modifier::Ptr findModifierForID(ModifierList &ml, EditItemID modifierID)
Returns a Modifier if it can be found in the list.
Clip * insertClipWithState(ClipOwner &clipOwner, juce::ValueTree clipState)
Inserts a clip with the given state in to the ClipOwner's clip list.
RangeType< BeatPosition > BeatRange
A RangeType based on beats.
constexpr TimePosition toPosition(TimeDuration)
Converts a TimeDuration to a TimePosition.
RangeType< TimePosition > TimeRange
A RangeType based on real time (i.e.
constexpr TimeDuration toDuration(TimePosition)
Converts a TimePosition to a TimeDuration.
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.