You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
========================== MAJOR FEATURES + CHANGES ========================== Change 2800717 on 2015/12/11 by Max.Chen Sequencer: Sort the key times for drawing to fix path trajectory. #jira UE-24331 Change 2803299 on 2015/12/15 by Max.Chen Sequencer: Fix property names so that they're the display names. For example, "DepthOfFieldFStop" now reads as "Aperture F Stop" Change 2804586 on 2015/12/15 by Max.Chen Sequencer: Add zoom in/out with shortcuts underscore and equals. Change 2811823 on 2015/12/23 by Max.Preussner Editor: Added UI action for creating new content browser folders; code cleanup; removed dead code Based on GitHub PR #1809 by artemavrin (https://github.com/EpicGames/UnrealEngine/pull/1809) #github: 1809 Change 2811839 on 2015/12/23 by Max.Preussner StereoPanorama: Code cleanup pass Based on GitHub PR# 1756 by ETayrienHBO (https://github.com/EpicGames/UnrealEngine/pull/1756) Also: - NULL to nullptr - namespaced enums to enum classes - consistent whitespace, line breaks and parentheses #github: 1756 Change 2819172 on 2016/01/07 by Andrew.Rodham Sequencer: Marquee and move modes are now automatically activated based on sequencer hotspot Change 2819176 on 2016/01/07 by Andrew.Rodham Sequencer: Various cosmetic fixes - Added icons to tracks - Removed SAnimationOutlinerTreeNode dependency from FSequencerDisplayNode (to enable future customization of shot/event track etc) - Added spacer nodes between top level display nodes - Various hover states and highlights Change 2819445 on 2016/01/07 by Andrew.Rodham Sequencer: Rendering out a capture from the composition graph now renders at the correct size even if r.ScreenPercentage is not 100. #jira UE-24920 Change 2820747 on 2016/01/08 by Andrew.Rodham Sequencer: Added option to close the editor when capturing starts #jira UE-21932 Change 2827701 on 2016/01/13 by Max.Preussner Media: Updating audio track specs each frame to better support streaming media and variable streams. Change 2828465 on 2016/01/14 by Max.Preussner Media: Better visualization of unknown media durations Change 2828469 on 2016/01/14 by Max.Preussner Media: Checking URL scheme on URLs that didn't pass the file extension filter Change 2834888 on 2016/01/19 by Max.Preussner Core: TQueue modernization pass Change 2834934 on 2016/01/19 by Max.Preussner Core: Implemented TTripleBuffer for triple buffers. Change 2834950 on 2016/01/19 by Max.Preussner Core: Added unit tests for TTripleBuffer dirty flag Change 2835488 on 2016/01/20 by Max.Preussner Core: More descriptive method names, initialization constructor, unit tests for TTripleBuffer Change 2837515 on 2016/01/20 by Max.Chen Sequencer: Command line options for custom passes. Change 2837517 on 2016/01/20 by Max.Chen Sequencer: Fix crash in visibility track instance on PIE. Change 2837518 on 2016/01/20 by Max.Chen Sequencer: Add option to lock to frame rate while playing. #jira UETOOL-475 Change 2837523 on 2016/01/20 by Max.Chen Sequencer: Capture thumbnail on level sequence asset save. Change 2837527 on 2016/01/20 by Max.Chen Sequencer: Added preroll for subsequences. Refactor instance update to combine data in EMovieSceneUpdateData. #jira UE-25380 Change 2837537 on 2016/01/20 by Max.Chen Sequencer: Add sequencer transport controls back into viewports. #jira UE-25460 Change 2837561 on 2016/01/20 by Max.Chen Sequencer: Added ability to convert a possessable to a spawnable - This option is available for any root-level possessable object bindings - It will currently delete the existing possessable (we could make this behaviour optional in future) - There is currently no check to sett if the actor is possessed by subsequent sub-sequences. If this is the case, using a possessable, or externally owned spawnable would be a better bet. Change 2837565 on 2016/01/20 by Max.Chen [CL 2858958 by Max Chen in Main branch]
510 lines
13 KiB
C++
510 lines
13 KiB
C++
// Copyright 1998-2016 Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "SequencerPrivatePCH.h"
|
|
#include "GroupedKeyArea.h"
|
|
#include "MovieSceneSection.h"
|
|
|
|
FIndexKey::FIndexKey(FString InNodePath, UMovieSceneSection* InSection)
|
|
: NodePath(MoveTemp(InNodePath))
|
|
, Section(InSection)
|
|
, CachedHash(HashCombine(GetTypeHash(NodePath), GetTypeHash(InSection)))
|
|
{}
|
|
|
|
namespace
|
|
{
|
|
/** Structure that defines the index for a particular section */
|
|
struct FIndexEntry
|
|
{
|
|
TMap<FKeyHandle, int32> HandleToGroup;
|
|
TArray<FKeyHandle> GroupHandles;
|
|
TArray<float> RepresentativeTimes;
|
|
};
|
|
|
|
/** A persistent index is required to ensure that generated key handles are maintained for the lifetime of specific display nodes */
|
|
TMap<FIndexKey, FIndexEntry> GlobalIndex;
|
|
}
|
|
|
|
|
|
FGroupedKeyArea::FGroupedKeyArea(FSequencerDisplayNode& InNode, int32 InSectionIndex)
|
|
: Section(nullptr)
|
|
, IndexKey(FString(), nullptr)
|
|
{
|
|
TArray<TSharedRef<FSequencerSectionKeyAreaNode>> AllKeyAreaNodes;
|
|
AllKeyAreaNodes.Reserve(36);
|
|
InNode.GetChildKeyAreaNodesRecursively(AllKeyAreaNodes);
|
|
|
|
for (const auto& Node : AllKeyAreaNodes)
|
|
{
|
|
TSharedRef<IKeyArea> KeyArea = Node->GetKeyArea(InSectionIndex);
|
|
|
|
const int32 KeyAreaIndex = KeyAreas.Num();
|
|
KeyAreas.Add(KeyArea);
|
|
|
|
{
|
|
auto* OwningSection = KeyArea->GetOwningSection();
|
|
// Ensure they all belong to the same section
|
|
ensure(!Section || Section == OwningSection);
|
|
Section = OwningSection;
|
|
}
|
|
|
|
for (const FKeyHandle& KeyHandle : KeyArea->GetUnsortedKeyHandles())
|
|
{
|
|
Groups.Emplace(KeyArea->GetKeyTime(KeyHandle), KeyAreaIndex, KeyHandle);
|
|
}
|
|
}
|
|
|
|
Groups.Sort([](const FKeyGrouping& A, const FKeyGrouping& B){
|
|
return A.RepresentativeTime < B.RepresentativeTime;
|
|
});
|
|
|
|
// Remove duplicates
|
|
for (int32 Index = 0; Index < Groups.Num(); ++Index)
|
|
{
|
|
const float CurrentTime = Groups[Index].RepresentativeTime;
|
|
|
|
int32 NumToMerge = 0;
|
|
for (int32 DuplIndex = Index + 1; DuplIndex < Groups.Num(); ++DuplIndex)
|
|
{
|
|
if (!FMath::IsNearlyEqual(CurrentTime, Groups[DuplIndex].RepresentativeTime))
|
|
{
|
|
break;
|
|
}
|
|
++NumToMerge;
|
|
}
|
|
|
|
if (NumToMerge)
|
|
{
|
|
const int32 Start = Index + 1, End = Start + NumToMerge;
|
|
for (int32 MergeIndex = Start; MergeIndex < End; ++MergeIndex)
|
|
{
|
|
Groups[Index].Keys.Append(MoveTemp(Groups[MergeIndex].Keys));
|
|
}
|
|
|
|
Groups.RemoveAt(Start, NumToMerge, false);
|
|
}
|
|
}
|
|
|
|
if (Section)
|
|
{
|
|
IndexKey = FIndexKey(InNode.GetPathName(), Section);
|
|
UpdateIndex();
|
|
}
|
|
}
|
|
|
|
void FGroupedKeyArea::UpdateIndex() const
|
|
{
|
|
auto& IndexEntry = GlobalIndex.FindOrAdd(IndexKey);
|
|
|
|
TArray<FKeyHandle> NewKeyHandles;
|
|
TArray<float> NewRepresentativeTimes;
|
|
|
|
IndexEntry.HandleToGroup.Reset();
|
|
|
|
for (int32 GroupIndex = 0; GroupIndex < Groups.Num(); ++GroupIndex)
|
|
{
|
|
float RepresentativeTime = Groups[GroupIndex].RepresentativeTime;
|
|
|
|
// Find a key handle we can recycle
|
|
int32 RecycledIndex = IndexEntry.RepresentativeTimes.IndexOfByPredicate([&](float Time){
|
|
// Must be an *exact* match to recycle
|
|
return Time == RepresentativeTime;
|
|
});
|
|
|
|
if (RecycledIndex != INDEX_NONE)
|
|
{
|
|
NewKeyHandles.Add(IndexEntry.GroupHandles[RecycledIndex]);
|
|
NewRepresentativeTimes.Add(IndexEntry.RepresentativeTimes[RecycledIndex]);
|
|
|
|
IndexEntry.GroupHandles.RemoveAt(RecycledIndex, 1, false);
|
|
IndexEntry.RepresentativeTimes.RemoveAt(RecycledIndex, 1, false);
|
|
}
|
|
else
|
|
{
|
|
NewKeyHandles.Add(FKeyHandle());
|
|
NewRepresentativeTimes.Add(RepresentativeTime);
|
|
}
|
|
|
|
IndexEntry.HandleToGroup.Add(NewKeyHandles.Last(), GroupIndex);
|
|
}
|
|
|
|
IndexEntry.GroupHandles = MoveTemp(NewKeyHandles);
|
|
IndexEntry.RepresentativeTimes = MoveTemp(NewRepresentativeTimes);
|
|
}
|
|
|
|
const FKeyGrouping* FGroupedKeyArea::FindGroup(FKeyHandle InHandle) const
|
|
{
|
|
if (auto* IndexEntry = GlobalIndex.Find(IndexKey))
|
|
{
|
|
if (int32* GroupIndex = IndexEntry->HandleToGroup.Find(InHandle))
|
|
{
|
|
return Groups.IsValidIndex(*GroupIndex) ? &Groups[*GroupIndex] : nullptr;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
FKeyGrouping* FGroupedKeyArea::FindGroup(FKeyHandle InHandle)
|
|
{
|
|
if (auto* IndexEntry = GlobalIndex.Find(IndexKey))
|
|
{
|
|
if (int32* GroupIndex = IndexEntry->HandleToGroup.Find(InHandle))
|
|
{
|
|
return Groups.IsValidIndex(*GroupIndex) ? &Groups[*GroupIndex] : nullptr;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
FLinearColor FGroupedKeyArea::GetKeyTint(FKeyHandle InHandle) const
|
|
{
|
|
// Everything is untinted for now
|
|
return FLinearColor::White;
|
|
}
|
|
|
|
const FSlateBrush* FGroupedKeyArea::GetBrush(FKeyHandle InHandle) const
|
|
{
|
|
static const FSlateBrush* PartialKeyBrush = FEditorStyle::GetBrush("Sequencer.PartialKey");
|
|
const auto* Group = FindGroup(InHandle);
|
|
|
|
// Ensure that each key area is represented at least once for it to be considered a 'complete key'
|
|
for (int32 AreaIndex = 0; Group && AreaIndex < KeyAreas.Num(); ++AreaIndex)
|
|
{
|
|
if (!Group->Keys.ContainsByPredicate([&](const FKeyGrouping::FKeyIndex& Key){ return Key.AreaIndex == AreaIndex; }))
|
|
{
|
|
return PartialKeyBrush;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
TArray<FKeyHandle> FGroupedKeyArea::GetUnsortedKeyHandles() const
|
|
{
|
|
TArray<FKeyHandle> Array;
|
|
if (auto* IndexEntry = GlobalIndex.Find(IndexKey))
|
|
{
|
|
IndexEntry->HandleToGroup.GenerateKeyArray(Array);
|
|
}
|
|
return Array;
|
|
}
|
|
|
|
void FGroupedKeyArea::SetKeyTime(FKeyHandle KeyHandle, float NewKeyTime) const
|
|
{
|
|
auto* Group = FindGroup(KeyHandle);
|
|
|
|
for (auto& Key : Group->Keys)
|
|
{
|
|
KeyAreas[Key.AreaIndex]->SetKeyTime(Key.KeyHandle, NewKeyTime);
|
|
}
|
|
}
|
|
|
|
float FGroupedKeyArea::GetKeyTime(FKeyHandle KeyHandle) const
|
|
{
|
|
auto* Group = FindGroup(KeyHandle);
|
|
return Group ? Group->RepresentativeTime : 0.f;
|
|
}
|
|
|
|
FKeyHandle FGroupedKeyArea::MoveKey(FKeyHandle KeyHandle, float DeltaPosition)
|
|
{
|
|
int32* GroupIndex = nullptr;
|
|
|
|
auto* IndexEntry = GlobalIndex.Find(IndexKey);
|
|
if (IndexEntry)
|
|
{
|
|
GroupIndex = IndexEntry->HandleToGroup.Find(KeyHandle);
|
|
}
|
|
|
|
if (!GroupIndex || !Groups.IsValidIndex(*GroupIndex))
|
|
{
|
|
return KeyHandle;
|
|
}
|
|
|
|
bool bIsTimeUpdated = false;
|
|
|
|
FKeyGrouping& Group = Groups[*GroupIndex];
|
|
|
|
// Move all the keys
|
|
for (auto& Key : Group.Keys)
|
|
{
|
|
Key.KeyHandle = KeyAreas[Key.AreaIndex]->MoveKey(Key.KeyHandle, DeltaPosition);
|
|
|
|
const float KeyTime = KeyAreas[Key.AreaIndex]->GetKeyTime(Key.KeyHandle);
|
|
Group.RepresentativeTime = bIsTimeUpdated ? FMath::Min(Group.RepresentativeTime, KeyTime) : KeyTime;
|
|
bIsTimeUpdated = true;
|
|
}
|
|
|
|
// Update the representative time to the smallest of all the keys (so it will deterministically get the same key handle on regeneration)
|
|
IndexEntry->RepresentativeTimes[*GroupIndex] = Group.RepresentativeTime;
|
|
|
|
return KeyHandle;
|
|
}
|
|
|
|
void FGroupedKeyArea::DeleteKey(FKeyHandle KeyHandle)
|
|
{
|
|
if (auto* Group = FindGroup(KeyHandle))
|
|
{
|
|
for (auto& Key : Group->Keys)
|
|
{
|
|
KeyAreas[Key.AreaIndex]->DeleteKey(Key.KeyHandle);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
FGroupedKeyArea::SetKeyInterpMode(FKeyHandle KeyHandle, ERichCurveInterpMode InterpMode)
|
|
{
|
|
if (auto* Group = FindGroup(KeyHandle))
|
|
{
|
|
for (auto& Key : Group->Keys)
|
|
{
|
|
KeyAreas[Key.AreaIndex]->SetKeyInterpMode(Key.KeyHandle, InterpMode);
|
|
}
|
|
}
|
|
}
|
|
|
|
ERichCurveInterpMode
|
|
FGroupedKeyArea::GetKeyInterpMode(FKeyHandle KeyHandle) const
|
|
{
|
|
// Return RCIM_None if the keys don't all have the same interp mode
|
|
ERichCurveInterpMode InterpMode = RCIM_None;
|
|
if (auto* Group = FindGroup(KeyHandle))
|
|
{
|
|
for (auto& Key : Group->Keys)
|
|
{
|
|
if (InterpMode == RCIM_None)
|
|
{
|
|
InterpMode = KeyAreas[Key.AreaIndex]->GetKeyInterpMode(Key.KeyHandle);
|
|
}
|
|
else if (InterpMode != KeyAreas[Key.AreaIndex]->GetKeyInterpMode(Key.KeyHandle))
|
|
{
|
|
return RCIM_None;
|
|
}
|
|
}
|
|
}
|
|
return InterpMode;
|
|
}
|
|
|
|
void
|
|
FGroupedKeyArea::SetKeyTangentMode(FKeyHandle KeyHandle, ERichCurveTangentMode TangentMode)
|
|
{
|
|
if (auto* Group = FindGroup(KeyHandle))
|
|
{
|
|
for (auto& Key : Group->Keys)
|
|
{
|
|
KeyAreas[Key.AreaIndex]->SetKeyTangentMode(Key.KeyHandle, TangentMode);
|
|
}
|
|
}
|
|
}
|
|
|
|
ERichCurveTangentMode
|
|
FGroupedKeyArea::GetKeyTangentMode(FKeyHandle KeyHandle) const
|
|
{
|
|
// Return RCTM_None if the keys don't all have the same tangent mode
|
|
ERichCurveTangentMode TangentMode = RCTM_None;
|
|
if (auto* Group = FindGroup(KeyHandle))
|
|
{
|
|
for (auto& Key : Group->Keys)
|
|
{
|
|
if (TangentMode == RCTM_None)
|
|
{
|
|
TangentMode = KeyAreas[Key.AreaIndex]->GetKeyTangentMode(Key.KeyHandle);
|
|
}
|
|
else if (TangentMode != KeyAreas[Key.AreaIndex]->GetKeyTangentMode(Key.KeyHandle))
|
|
{
|
|
return RCTM_None;
|
|
}
|
|
}
|
|
}
|
|
return TangentMode;
|
|
}
|
|
|
|
void FGroupedKeyArea::SetExtrapolationMode(ERichCurveExtrapolation ExtrapMode, bool bPreInfinity)
|
|
{
|
|
for (auto& Area : KeyAreas)
|
|
{
|
|
Area->SetExtrapolationMode(ExtrapMode, bPreInfinity);
|
|
}
|
|
}
|
|
|
|
ERichCurveExtrapolation FGroupedKeyArea::GetExtrapolationMode(bool bPreInfinity) const
|
|
{
|
|
ERichCurveExtrapolation ExtrapMode = RCCE_None;
|
|
for (auto& Area : KeyAreas)
|
|
{
|
|
if (ExtrapMode == RCCE_None)
|
|
{
|
|
ExtrapMode = Area->GetExtrapolationMode(bPreInfinity);
|
|
}
|
|
else if (Area->GetExtrapolationMode(bPreInfinity) != ExtrapMode)
|
|
{
|
|
return RCCE_None;
|
|
}
|
|
}
|
|
return ExtrapMode;
|
|
}
|
|
|
|
bool FGroupedKeyArea::CanSetExtrapolationMode() const
|
|
{
|
|
for (auto& Area : KeyAreas)
|
|
{
|
|
if (Area->CanSetExtrapolationMode())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
TArray<FKeyHandle> FGroupedKeyArea::AddKeyUnique(float Time, EMovieSceneKeyInterpolation InKeyInterpolation, float TimeToCopyFrom)
|
|
{
|
|
TArray<FKeyHandle> AddedKeyHandles;
|
|
|
|
for (auto& Area : KeyAreas)
|
|
{
|
|
// If TimeToCopyFrom is valid, add a key only if there is a key to copy from
|
|
if (TimeToCopyFrom != FLT_MAX)
|
|
{
|
|
if (FRichCurve* Curve = Area->GetRichCurve())
|
|
{
|
|
if (!Curve->IsKeyHandleValid(Curve->FindKey(TimeToCopyFrom)))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
TArray<FKeyHandle> AddedGroupKeyHandles = Area->AddKeyUnique(Time, InKeyInterpolation, TimeToCopyFrom);
|
|
AddedKeyHandles.Append(AddedGroupKeyHandles);
|
|
}
|
|
|
|
return AddedKeyHandles;
|
|
}
|
|
|
|
TOptional<FKeyHandle> FGroupedKeyArea::DuplicateKey(FKeyHandle KeyToDuplicate)
|
|
{
|
|
FKeyGrouping* Group = FindGroup(KeyToDuplicate);
|
|
if (!Group)
|
|
{
|
|
return TOptional<FKeyHandle>();
|
|
}
|
|
|
|
const float Time = Group->RepresentativeTime;
|
|
|
|
const int32 NewGroupIndex = Groups.Num();
|
|
Groups.Emplace(Time);
|
|
|
|
for (const FKeyGrouping::FKeyIndex& Key : Group->Keys)
|
|
{
|
|
TOptional<FKeyHandle> NewKeyHandle = KeyAreas[Key.AreaIndex]->DuplicateKey(Key.KeyHandle);
|
|
if (NewKeyHandle.IsSet())
|
|
{
|
|
Groups[NewGroupIndex].Keys.Emplace(Key.AreaIndex, NewKeyHandle.GetValue());
|
|
}
|
|
}
|
|
|
|
// Update the global index with our new key
|
|
FIndexEntry* IndexEntry = GlobalIndex.Find(IndexKey);
|
|
if (IndexEntry)
|
|
{
|
|
FKeyHandle ThisGroupKeyHandle;
|
|
|
|
IndexEntry->GroupHandles.Add(ThisGroupKeyHandle);
|
|
IndexEntry->HandleToGroup.Add(ThisGroupKeyHandle, NewGroupIndex);
|
|
IndexEntry->RepresentativeTimes.Add(Time);
|
|
|
|
return ThisGroupKeyHandle;
|
|
}
|
|
|
|
return TOptional<FKeyHandle>();
|
|
}
|
|
|
|
FRichCurve* FGroupedKeyArea::GetRichCurve()
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
UMovieSceneSection* FGroupedKeyArea::GetOwningSection()
|
|
{
|
|
return Section;
|
|
}
|
|
|
|
bool FGroupedKeyArea::CanCreateKeyEditor()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
|
|
void FGroupedKeyArea::CopyKeys(FMovieSceneClipboardBuilder& ClipboardBuilder, const TFunctionRef<bool(FKeyHandle, const IKeyArea&)>& KeyMask) const
|
|
{
|
|
const FIndexEntry* IndexEntry = GlobalIndex.Find(IndexKey);
|
|
if (!IndexEntry)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Since we are a group of nested key areas, we test the key mask for our key handles, and forward on the results to each key area
|
|
|
|
// Using ptr as map key is fine here as we know they will not change
|
|
TMap<const IKeyArea*, TSet<FKeyHandle>> AllValidHandles;
|
|
|
|
for (auto& Pair : IndexEntry->HandleToGroup)
|
|
{
|
|
if (!KeyMask(Pair.Key, *this) || !Groups.IsValidIndex(Pair.Value))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const FKeyGrouping& Group = Groups[Pair.Value];
|
|
for (const FKeyGrouping::FKeyIndex& KeyIndex : Group.Keys)
|
|
{
|
|
const IKeyArea& KeyArea = KeyAreas[KeyIndex.AreaIndex].Get();
|
|
AllValidHandles.FindOrAdd(&KeyArea).Add(KeyIndex.KeyHandle);
|
|
}
|
|
}
|
|
|
|
for (auto& Pair : AllValidHandles)
|
|
{
|
|
Pair.Key->CopyKeys(ClipboardBuilder, [&](FKeyHandle Handle, const IKeyArea&){
|
|
return Pair.Value.Contains(Handle);
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
TSharedRef<SWidget> FGroupedKeyArea::CreateKeyEditor(ISequencer* Sequencer)
|
|
{
|
|
return SNullWidget::NullWidget;
|
|
}
|
|
|
|
|
|
FLinearColor FGroupedKeyArea::GetColor()
|
|
{
|
|
return FLinearColor(0.1f, 0.1f, 0.1f, 0.7f);
|
|
}
|
|
|
|
|
|
TSharedPtr<FStructOnScope> FGroupedKeyArea::GetKeyStruct(FKeyHandle KeyHandle)
|
|
{
|
|
FKeyGrouping* Group = FindGroup(KeyHandle);
|
|
|
|
if (Group == nullptr)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
TArray<FKeyHandle> KeyHandles;
|
|
|
|
for (auto& Key : Group->Keys)
|
|
{
|
|
KeyHandles.Add(Key.KeyHandle);
|
|
}
|
|
|
|
return Section->GetKeyStruct(KeyHandles);
|
|
}
|
|
|
|
|
|
void FGroupedKeyArea::PasteKeys(const FMovieSceneClipboardKeyTrack& KeyTrack, const FMovieSceneClipboardEnvironment& SrcEnvironment, const FSequencerPasteEnvironment& DstEnvironment)
|
|
{
|
|
checkf(false, TEXT("Pasting into grouped key areas is not supported, and should not be used. Iterate child tracks instead."));
|
|
}
|