Files
UnrealEngine/Engine/Source/Editor/Sequencer/Private/Tools/EditToolDragOperations.cpp
2025-05-18 13:04:45 +08:00

1538 lines
54 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Tools/EditToolDragOperations.h"
#include "ISequencer.h"
#include "ISequencerSection.h"
#include "MovieSceneTrack.h"
#include "MovieSceneSequence.h"
#include "Sequencer.h"
#include "SequencerSettings.h"
#include "SequencerCommonHelpers.h"
#include "MVVM/Selection/Selection.h"
#include "MVVM/ViewModels/ViewModel.h"
#include "MVVM/SectionModelStorageExtension.h"
#include "MVVM/Extensions/IDraggableTrackAreaExtension.h"
#include "MVVM/ViewModels/SectionModel.h"
#include "MVVM/ViewModels/TrackModel.h"
#include "MVVM/ViewModels/TrackRowModel.h"
#include "MVVM/ViewModels/SequencerEditorViewModel.h"
#include "MVVM/ViewModels/TrackAreaViewModel.h"
#include "MVVM/ViewModels/VirtualTrackArea.h"
#include "Algo/AllOf.h"
#include "MovieScene.h"
#include "MovieSceneTimeHelpers.h"
#include "Modules/ModuleManager.h"
#include "Channels/MovieSceneChannel.h"
#include "Channels/MovieSceneChannelProxy.h"
#include "ISequencerModule.h"
struct FInvalidKeyAndSectionSnappingCandidates : UE::Sequencer::ISnapCandidate
{
/**
* Keys and Sections added to this ISnapField will be ignored as potential candidates for snapping.
*/
FInvalidKeyAndSectionSnappingCandidates(const TSet<FSequencerSelectedKey>& InKeysToIgnore, const TSet<UMovieSceneSection*>& InSectionsToIgnore)
{
KeysToExclude = InKeysToIgnore;
SectionsToExclude = InSectionsToIgnore;
}
virtual bool IsKeyApplicable(FKeyHandle KeyHandle, const UE::Sequencer::FViewModelPtr& Owner) const override
{
using namespace UE::Sequencer;
TSharedPtr<FChannelModel> Channel = Owner.ImplicitCast();
return !Channel || (!KeysToExclude.Contains(FSequencerSelectedKey(*Channel->GetSection(), Channel, KeyHandle)) && !SectionsToExclude.Contains(Channel->GetSection()));
}
virtual bool AreSectionBoundsApplicable(UMovieSceneSection* Section) const override
{
return !SectionsToExclude.Contains(Section);
}
protected:
TSet<FSequencerSelectedKey> KeysToExclude;
TSet<UMovieSceneSection*> SectionsToExclude;
};
/** How many pixels near the mouse has to be before snapping occurs */
const float PixelSnapWidth = 20.f;
FEditToolDragOperation::FEditToolDragOperation( FSequencer& InSequencer )
: Sequencer(InSequencer)
{
Settings = Sequencer.GetSequencerSettings();
}
FCursorReply FEditToolDragOperation::GetCursor() const
{
return FCursorReply::Cursor( EMouseCursor::Default );
}
int32 FEditToolDragOperation::OnPaint(const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId) const
{
return LayerId;
}
void FEditToolDragOperation::BeginTransaction( TSet<UMovieSceneSection*>& Sections, const FText& TransactionDesc )
{
// Begin an editor transaction and mark the section as transactional so it's state will be saved
Transaction.Reset( new FScopedTransaction(TransactionDesc) );
for (auto It = Sections.CreateIterator(); It; ++It)
{
UMovieSceneSection* SectionObj = *It;
SectionObj->SetFlags( RF_Transactional );
// Save the current state of the section
if (!SectionObj->TryModify())
{
It.RemoveCurrent();
}
}
}
void FEditToolDragOperation::EndTransaction()
{
Transaction.Reset();
Sequencer.NotifyMovieSceneDataChanged( EMovieSceneDataChangeType::TrackValueChanged );
}
TRange<FFrameNumber> FEditToolDragOperation::GetSectionBoundaries(const UMovieSceneSection* Section)
{
using namespace UE::Sequencer;
// Find the borders of where you can drag to
FFrameNumber LowerBound = TNumericLimits<int32>::Lowest(), UpperBound = TNumericLimits<int32>::Max();
// Find the track node for this section
TSharedPtr<FSectionModel> SectionHandle = Sequencer.GetNodeTree()->GetSectionModel(Section);
if (SectionHandle)
{
// Get the closest borders on either side
TViewModelPtr<ITrackAreaExtension> TrackModel = SectionHandle->FindAncestorOfType<ITrackAreaExtension>();
for (const TViewModelPtr<FSectionModel>& SectionModel : TrackModel->GetTrackAreaModelListAs<FSectionModel>())
{
const UMovieSceneSection* TestSection = SectionModel->GetSection();
TArray<UMovieSceneSection*> Sections;
GetSections(Sections);
if (!TestSection || Sections.Contains(TestSection))
{
continue;
}
if (TestSection->HasEndFrame() && Section->HasStartFrame() && TestSection->GetExclusiveEndFrame() <= Section->GetInclusiveStartFrame() && TestSection->GetExclusiveEndFrame() > LowerBound)
{
LowerBound = TestSection->GetExclusiveEndFrame();
}
if (TestSection->HasStartFrame() && Section->HasEndFrame() && TestSection->GetInclusiveStartFrame() >= Section->GetExclusiveEndFrame() && TestSection->GetInclusiveStartFrame() < UpperBound)
{
UpperBound = TestSection->GetInclusiveStartFrame();
}
}
}
return TRange<FFrameNumber>(LowerBound, UpperBound);
}
FResizeSection::FResizeSection( FSequencer& InSequencer, bool bInDraggingByEnd, bool bInIsSlipping )
: FEditToolDragOperation( InSequencer )
, bDraggingByEnd(bInDraggingByEnd)
, bIsSlipping(bInIsSlipping)
, MouseDownTime(0)
{
TSet<UMovieSceneSection*> SelectedSections = Sequencer.GetViewModel()->GetSelection()->GetSelectedSections();
Sections.Reserve(SelectedSections.Num());
for (TWeakObjectPtr<UMovieSceneSection> WeakSection : SelectedSections)
{
UMovieSceneSection* Section = WeakSection.Get();
if (Section)
{
if (bDraggingByEnd && Section->HasEndFrame())
{
Sections.Add(Section);
}
else if (!bDraggingByEnd && Section->HasStartFrame())
{
Sections.Add(Section);
}
}
}
}
void FResizeSection::OnBeginDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea)
{
using namespace UE::Sequencer;
UE::MovieScene::FScopedSignedObjectModifyDefer DeferMarkAsChanged;
BeginTransaction( Sections, NSLOCTEXT("Sequencer", "DragSectionEdgeTransaction", "Resize section") );
MouseDownTime = VirtualTrackArea.PixelToFrame(LocalMousePos.X);
// Construct a snap field of unselected sections
TSet<FSequencerSelectedKey> EmptyKeySet;
FInvalidKeyAndSectionSnappingCandidates SnapCandidates(EmptyKeySet, Sections);
SnapField = FSequencerSnapField(Sequencer, SnapCandidates, ESequencerEntity::Section | ESequencerEntity::Key);
SectionInitTimes.Empty();
bool bIsDilating = MouseEvent.IsControlDown();
PreDragSectionData.Empty();
for (UMovieSceneSection* Section : Sections)
{
if (bIsDilating)
{
// Populate the resize data for this section
FPreDragSectionData ResizeData;
ResizeData.MovieSection = Section;
ResizeData.InitialRange = Section->GetRange();
TSharedPtr<FSectionModel> SectionHandle = Sequencer.GetNodeTree()->GetSectionModel(Section);
if (SectionHandle)
{
//Tell section that may not have keys it's starting to dilate (e.g. skeletal tracks will cache play rate).
SectionHandle->GetSectionInterface()->BeginDilateSection();
ResizeData.SequencerSection = SectionHandle->GetSectionInterface().Get();
}
// Add the key times for all keys of all channels on this section
FMovieSceneChannelProxy& Proxy = Section->GetChannelProxy();
for (const FMovieSceneChannelEntry& Entry : Proxy.GetAllEntries())
{
TArrayView<FMovieSceneChannel* const> ChannelPtrs = Entry.GetChannels();
for (int32 Index = 0; Index < ChannelPtrs.Num(); ++Index)
{
// Populate the cached state of this channel
FPreDragChannelData& ChannelData = ResizeData.Channels[ResizeData.Channels.Emplace()];
ChannelData.Channel = Proxy.MakeHandle(Entry.GetChannelTypeName(), Index);
ChannelPtrs[Index]->GetKeys(TRange<FFrameNumber>::All(), &ChannelData.FrameNumbers, &ChannelData.Handles);
}
}
PreDragSectionData.Emplace(ResizeData);
}
else if (TSharedPtr<FSectionModel> SectionHandle = Sequencer.GetNodeTree()->GetSectionModel(Section))
{
if (bIsSlipping)
{
SectionHandle->GetSectionInterface()->BeginSlipSection();
}
else
{
SectionHandle->GetSectionInterface()->BeginResizeSection();
}
}
SectionInitTimes.Add(Section, bDraggingByEnd ? Section->GetExclusiveEndFrame() : Section->GetInclusiveStartFrame());
}
}
void FResizeSection::OnEndDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea)
{
bool bIsDilating = MouseEvent.IsControlDown();
TOptional<EMovieSceneDataChangeType> ChangeNotification;
if (!bIsDilating)
{
TSet<UMovieSceneTrack*> Tracks;
FMovieSceneSectionMovedParams SectionMovedParams(EPropertyChangeType::ValueSet);
EMovieSceneSectionMovedResult SectionMovedResult(EMovieSceneSectionMovedResult::None);
for (UMovieSceneSection* Section : Sections)
{
if (UMovieSceneTrack* OuterTrack = Section->GetTypedOuter<UMovieSceneTrack>())
{
OuterTrack->Modify();
SectionMovedResult |= OuterTrack->OnSectionMoved(*Section, SectionMovedParams);
Tracks.Add(OuterTrack);
}
}
for (UMovieSceneTrack* Track : Tracks)
{
Track->UpdateEasing();
}
if (SectionMovedResult != EMovieSceneSectionMovedResult::None)
{
ChangeNotification = EMovieSceneDataChangeType::MovieSceneStructureItemsChanged;
}
}
EndTransaction();
if (ChangeNotification.IsSet())
{
Sequencer.NotifyMovieSceneDataChanged(ChangeNotification.GetValue());
}
}
void FResizeSection::OnDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea)
{
using namespace UE::Sequencer;
UE::MovieScene::FScopedSignedObjectModifyDefer DeferMarkAsChanged(true);
ISequencerModule& SequencerModule = FModuleManager::Get().LoadModuleChecked<ISequencerModule>("Sequencer");
bool bIsDilating = MouseEvent.IsControlDown();
ESequencerScrubberStyle ScrubStyle = Sequencer.GetScrubStyle();
FFrameRate TickResolution = Sequencer.GetFocusedTickResolution();
FFrameRate DisplayRate = Sequencer.GetFocusedDisplayRate();
// Convert the current mouse position to a time
FFrameNumber DeltaTime = (VirtualTrackArea.PixelToFrame(LocalMousePos.X) - MouseDownTime).RoundToFrame();
// Snapping
const bool bSnapToInterval = Settings->GetForceWholeFrames();
const bool bSnapToLikeTypes = ((Settings->GetIsSnapEnabled() || MouseEvent.IsShiftDown()) && Settings->GetSnapSectionTimesToElements());
SnapField.GetValue().SetSnapToInterval(bSnapToInterval);
SnapField.GetValue().SetSnapToLikeTypes(bSnapToLikeTypes);
if (bSnapToInterval || bSnapToLikeTypes)
{
TArray<FFrameTime> SectionTimes;
for (UMovieSceneSection* Section : Sections)
{
SectionTimes.Add(SectionInitTimes[Section] + DeltaTime);
}
float SnapThresholdPx = VirtualTrackArea.PixelToSeconds(PixelSnapWidth) - VirtualTrackArea.PixelToSeconds(0.f);
int32 SnapThreshold = ( SnapThresholdPx * TickResolution ).FloorToFrame().Value;
TOptional<FSequencerSnapField::FSnapResult> SnappedTime = SnapField->Snap(SectionTimes, SnapThreshold);
if (SnappedTime.IsSet())
{
// Add the snapped amount onto the delta
DeltaTime += (SnappedTime->SnappedTime - SnappedTime->OriginalTime).RoundToFrame();
}
}
auto GetMovementMaximums = [this](const FPointerEvent& MouseEvent, const UMovieSceneSection* Section, TOptional<FFrameNumber>& LeftMovementMaximum, TOptional<FFrameNumber>& RightMovementMaximum)
{
// We'll calculate this section's borders and clamp the possible delta time to be less than that
if (!Section->GetBlendType().IsValid())
{
TRange<FFrameNumber> SectionBoundaries = GetSectionBoundaries(Section);
LeftMovementMaximum = UE::MovieScene::DiscreteInclusiveLower(SectionBoundaries);
RightMovementMaximum = UE::MovieScene::DiscreteExclusiveUpper(SectionBoundaries);
}
if ((Settings->GetIsSnapEnabled() || MouseEvent.IsShiftDown()) && Settings->GetSnapKeysAndSectionsToPlayRange() && !Settings->ShouldKeepPlayRangeInSectionBounds())
{
if (!LeftMovementMaximum.IsSet() || LeftMovementMaximum.GetValue() < Sequencer.GetPlaybackRange().GetLowerBoundValue())
{
LeftMovementMaximum = Sequencer.GetPlaybackRange().GetLowerBoundValue();
}
if (!RightMovementMaximum.IsSet() || RightMovementMaximum.GetValue() > Sequencer.GetPlaybackRange().GetUpperBoundValue())
{
RightMovementMaximum = Sequencer.GetPlaybackRange().GetUpperBoundValue();
}
}
};
/********************************************************************/
EMovieSceneSectionMovedResult SectionMovedResult(EMovieSceneSectionMovedResult::None);
if (bIsDilating)
{
for(FPreDragSectionData Data: PreDragSectionData)
{
// It is only valid to dilate a fixed bound. Tracks can have mixed bounds types (ie: infinite upper, closed lower)
check(bDraggingByEnd ? Data.InitialRange.GetUpperBound().IsClosed() : Data.InitialRange.GetLowerBound().IsClosed());
TOptional<FFrameNumber> LeftMovementMaximum;
TOptional<FFrameNumber> RightMovementMaximum;
GetMovementMaximums(MouseEvent, Data.MovieSection, LeftMovementMaximum, RightMovementMaximum);
FFrameNumber StartPosition = bDraggingByEnd ? UE::MovieScene::DiscreteExclusiveUpper(Data.InitialRange) : UE::MovieScene::DiscreteInclusiveLower(Data.InitialRange);
FFrameNumber DilationOrigin;
if (bDraggingByEnd)
{
if (Data.InitialRange.GetLowerBound().IsClosed())
{
DilationOrigin = UE::MovieScene::DiscreteInclusiveLower(Data.InitialRange);
}
else
{
// We're trying to dilate a track that has an infinite lower bound as its origin.
// Sections already compute an effective range for UMG's auto-playback range, so we'll use that to have it handle finding either the
// uppermost key or the overall length of the section.
DilationOrigin = Data.MovieSection->ComputeEffectiveRange().GetLowerBoundValue();
}
}
else
{
if (Data.InitialRange.GetUpperBound().IsClosed())
{
DilationOrigin = UE::MovieScene::DiscreteExclusiveUpper(Data.InitialRange);
}
else
{
// We're trying to dilate a track that has an infinite upper bound as its origin.
DilationOrigin = Data.MovieSection->ComputeEffectiveRange().GetUpperBoundValue();
}
}
// Because we can have an one-sided infinite data range, we calculate a new range using our clamped values.
TRange<FFrameNumber> DataRange;
DataRange.SetLowerBound(TRangeBound<FFrameNumber>(DilationOrigin < StartPosition ? DilationOrigin : StartPosition));
DataRange.SetUpperBound(TRangeBound<FFrameNumber>(DilationOrigin > StartPosition ? DilationOrigin : StartPosition));
FFrameNumber NewPosition = bDraggingByEnd ? FMath::Clamp(StartPosition + DeltaTime, DilationOrigin, RightMovementMaximum.Get(TNumericLimits<int32>::Max()))
: FMath::Clamp(StartPosition + DeltaTime, LeftMovementMaximum.Get(TNumericLimits<int32>::Lowest()), DilationOrigin);
float DilationFactor = FMath::Abs(NewPosition.Value - DilationOrigin.Value) / static_cast<float>(DataRange.Size<FFrameNumber>().Value);
if (bDraggingByEnd)
{
if (Data.SequencerSection)
{
Data.SequencerSection->DilateSection(TRange<FFrameNumber>(Data.MovieSection->GetRange().GetLowerBound(), TRangeBound<FFrameNumber>::Exclusive(NewPosition)), DilationFactor);
}
else
{
Data.MovieSection->SetRange(TRange<FFrameNumber>(Data.MovieSection->GetRange().GetLowerBound(), TRangeBound<FFrameNumber>::Exclusive(NewPosition)));
}
}
else
{
if (Data.SequencerSection)
{
Data.SequencerSection->DilateSection(TRange<FFrameNumber>(TRangeBound<FFrameNumber>::Inclusive(NewPosition), Data.MovieSection->GetRange().GetUpperBound()), DilationFactor);
}
else
{
Data.MovieSection->SetRange(TRange<FFrameNumber>(TRangeBound<FFrameNumber>::Inclusive(NewPosition), Data.MovieSection->GetRange().GetUpperBound()));
}
}
TArray<FFrameNumber> NewFrameNumbers;
for (const FPreDragChannelData& ChannelData : Data.Channels)
{
// Compute new frame times for each key
NewFrameNumbers.Reset(ChannelData.FrameNumbers.Num());
for (FFrameNumber StartFrame : ChannelData.FrameNumbers)
{
FFrameNumber NewTime = DilationOrigin + FFrameNumber(FMath::FloorToInt((StartFrame - DilationOrigin).Value * DilationFactor));
NewFrameNumbers.Add(NewTime);
}
// Apply the key times to the channel
FMovieSceneChannel* Channel = ChannelData.Channel.Get();
if (Channel)
{
Channel->SetKeyTimes(ChannelData.Handles, NewFrameNumbers);
}
}
}
}
/********************************************************************/
else for (UMovieSceneSection* Section : Sections)
{
TSharedPtr<FSectionModel> SectionHandle = Sequencer.GetNodeTree()->GetSectionModel(Section);
if (!SectionHandle)
{
continue;
}
TOptional<FFrameNumber> LeftMovementMaximum;
TOptional<FFrameNumber> RightMovementMaximum;
GetMovementMaximums(MouseEvent, Section, LeftMovementMaximum, RightMovementMaximum);
TSharedPtr<ISequencerSection> SectionInterface = SectionHandle->GetSectionInterface();
FFrameNumber NewTime = SectionInitTimes[Section] + DeltaTime;
if( bDraggingByEnd )
{
FFrameNumber MinFrame = Section->HasStartFrame() ? Section->GetInclusiveStartFrame() : TNumericLimits<int32>::Lowest();
if (Settings->GetForceWholeFrames())
{
int32 IntervalSnapThreshold = FMath::RoundToInt( ( TickResolution / DisplayRate ).AsDecimal() );
MinFrame = MinFrame + IntervalSnapThreshold;
}
FFrameNumber MaxFrame = RightMovementMaximum.Get(TNumericLimits<int32>::Max());
// Dragging the end of a section
// Ensure we aren't shrinking past the start time or into another section if we can't blend
NewTime = FMath::Clamp( NewTime, MinFrame, MaxFrame );
if (bIsSlipping)
{
SectionInterface->SlipSection( NewTime );
}
else
{
SectionInterface->ResizeSection( SSRM_TrailingEdge, NewTime );
}
}
else
{
FFrameNumber MaxFrame = Section->HasEndFrame() ? Section->GetExclusiveEndFrame()-1 : TNumericLimits<int32>::Max();
if (Settings->GetForceWholeFrames())
{
int32 IntervalSnapThreshold = FMath::RoundToInt( ( TickResolution / DisplayRate ).AsDecimal() );
MaxFrame = MaxFrame - IntervalSnapThreshold;
}
FFrameNumber MinFrame = LeftMovementMaximum.Get(TNumericLimits<int32>::Lowest());
// Dragging the start of a section
// Ensure we arent expanding past the end time or into another section if we can't blend
NewTime = FMath::Clamp( NewTime, MinFrame, MaxFrame );
if (bIsSlipping)
{
SectionInterface->SlipSection( NewTime );
}
else
{
SectionInterface->ResizeSection( SSRM_LeadingEdge, NewTime );
}
}
UMovieSceneTrack* OuterTrack = Section->GetTypedOuter<UMovieSceneTrack>();
if (OuterTrack)
{
OuterTrack->Modify();
SectionMovedResult |= OuterTrack->OnSectionMoved(*Section, EPropertyChangeType::Interactive);
}
}
{
TSet<UMovieSceneTrack*> Tracks;
for (UMovieSceneSection* Section : Sections)
{
if (UMovieSceneTrack* Track = Section->GetTypedOuter<UMovieSceneTrack>())
{
Tracks.Add(Track);
}
}
for (UMovieSceneTrack* Track : Tracks)
{
Track->UpdateEasing();
}
}
if (SectionMovedResult != EMovieSceneSectionMovedResult::None)
{
Sequencer.NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::MovieSceneStructureItemsChanged);
}
else
{
Sequencer.NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::TrackValueChanged);
}
}
void FDuplicateKeysAndSections::OnBeginDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea)
{
// Begin an editor transaction and mark the section as transactional so it's state will be saved
BeginTransaction( Sections, NSLOCTEXT("Sequencer", "DuplicateKeysTransaction", "Duplicate Keys or Sections") );
// Call Modify on all of the sections that own keys we have selected so that when we duplicate keys we can restore them properly.
ModifyNonSelectedSections();
// We're going to take our current selection and make a duplicate of each item in it and leave those items behind.
// This means our existing selection will still refer to the same keys, so we're duplicating and moving the originals.
// This saves us from modifying the user's selection when duplicating. We can't move the duplicates as we can't get
// section handles for sections until the tree is rebuilt.
TArray<FKeyHandle> NewKeyHandles;
NewKeyHandles.SetNumZeroed(KeysAsArray.Num());
// Duplicate our keys into the NewKeyHandles array. Duplicating keys automatically updates their sections,
// so we don't need to actually use the new key handles.
DuplicateKeys(KeysAsArray, NewKeyHandles);
for (UMovieSceneSection* SectionToDuplicate : Sections)
{
if (!SectionToDuplicate)
{
continue;
}
UMovieSceneSection* DuplicatedSection = DuplicateObject<UMovieSceneSection>(SectionToDuplicate, SectionToDuplicate->GetOuter());
UMovieSceneTrack* OwningTrack = SectionToDuplicate->GetTypedOuter<UMovieSceneTrack>();
OwningTrack->Modify();
OwningTrack->AddSection(*DuplicatedSection);
}
// Now start the move drag
FMoveKeysAndSections::OnBeginDrag(MouseEvent, LocalMousePos, VirtualTrackArea);
}
void FDuplicateKeysAndSections::OnEndDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea)
{
FMoveKeysAndSections::OnEndDrag(MouseEvent, LocalMousePos, VirtualTrackArea);
EndTransaction();
}
FManipulateSectionEasing::FManipulateSectionEasing( FSequencer& InSequencer, TWeakObjectPtr<UMovieSceneSection> InSection, bool _bEaseIn )
: FEditToolDragOperation(InSequencer)
, WeakSection(InSection)
, bEaseIn(_bEaseIn)
, MouseDownTime(0)
{
}
void FManipulateSectionEasing::OnBeginDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea)
{
using namespace UE::Sequencer;
Transaction.Reset( new FScopedTransaction(NSLOCTEXT("Sequencer", "DragSectionEasing", "Change Section Easing")) );
UMovieSceneSection* Section = WeakSection.Get();
if (Section == nullptr)
{
return;
}
Section->SetFlags( RF_Transactional );
Section->Modify();
MouseDownTime = VirtualTrackArea.PixelToFrame(LocalMousePos.X);
// Construct a snap field of all section bounds
ISnapCandidate SnapCandidates;
SnapField = FSequencerSnapField(Sequencer, SnapCandidates, ESequencerEntity::Section);
InitValue = bEaseIn ? Section->Easing.GetEaseInDuration() : Section->Easing.GetEaseOutDuration();
}
void FManipulateSectionEasing::OnDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea)
{
UE::MovieScene::FScopedSignedObjectModifyDefer DeferMarkAsChanged(true);
ESequencerScrubberStyle ScrubStyle = Sequencer.GetScrubStyle();
FFrameRate TickResolution = Sequencer.GetFocusedTickResolution();
FFrameRate DisplayRate = Sequencer.GetFocusedDisplayRate();
// Convert the current mouse position to a time
FFrameTime DeltaTime = VirtualTrackArea.PixelToFrame(LocalMousePos.X) - MouseDownTime;
UMovieSceneSection* Section = WeakSection.Get();
if (Section == nullptr)
{
return;
}
// Snapping
const bool bSnapToInterval = Settings->GetForceWholeFrames();
const bool bSnapToLikeTypes = ((Settings->GetIsSnapEnabled() || MouseEvent.IsShiftDown()) && Settings->GetSnapSectionTimesToElements());
SnapField.GetValue().SetSnapToInterval(bSnapToInterval);
SnapField.GetValue().SetSnapToLikeTypes(bSnapToLikeTypes);
if (bSnapToInterval || bSnapToLikeTypes)
{
TArray<FFrameTime> SnapTimes;
if (bEaseIn)
{
FFrameNumber DesiredTime = (DeltaTime + Section->GetInclusiveStartFrame() + InitValue.Get(0)).RoundToFrame();
SnapTimes.Add(DesiredTime);
}
else
{
FFrameNumber DesiredTime = (Section->GetExclusiveEndFrame() - InitValue.Get(0) + DeltaTime).RoundToFrame();
SnapTimes.Add(DesiredTime);
}
float SnapThresholdPx = VirtualTrackArea.PixelToSeconds(PixelSnapWidth) - VirtualTrackArea.PixelToSeconds(0.f);
int32 SnapThreshold = ( SnapThresholdPx * TickResolution ).FloorToFrame().Value;
TOptional<FSequencerSnapField::FSnapResult> SnappedTime = SnapField->Snap(SnapTimes, SnapThreshold);
if (SnappedTime.IsSet())
{
// Add the snapped amount onto the delta
DeltaTime += SnappedTime->SnappedTime - SnappedTime->OriginalTime;
}
}
const int32 MaxEasingDuration = Section->HasStartFrame() && Section->HasEndFrame() ? UE::MovieScene::DiscreteSize(Section->GetRange()) : TNumericLimits<int32>::Max() / 2;
Section->Modify();
if (bEaseIn)
{
Section->Easing.bManualEaseIn = true;
Section->Easing.ManualEaseInDuration = FMath::Clamp(InitValue.Get(0) + DeltaTime.RoundToFrame().Value, 0, MaxEasingDuration);
}
else
{
Section->Easing.bManualEaseOut = true;
Section->Easing.ManualEaseOutDuration = FMath::Clamp(InitValue.Get(0) - DeltaTime.RoundToFrame().Value, 0, MaxEasingDuration);
}
}
void FManipulateSectionEasing::OnEndDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea)
{
EndTransaction();
}
FMoveKeysAndSections::FMoveKeysAndSections(FSequencer& InSequencer, ESequencerMoveOperationType MoveType)
: FEditToolDragOperation(InSequencer)
{
using namespace UE::Sequencer;
TSharedPtr<FSequencerEditorViewModel> SequencerViewModel = InSequencer.GetViewModel()->CastThisShared<FSequencerEditorViewModel>();
bAllowVerticalMovement = HotspotCast<FSectionHotspotBase>(SequencerViewModel->GetHotspot()) != nullptr;
if (EnumHasAnyFlags(MoveType, ESequencerMoveOperationType::MoveKeys))
{
// Filter out the keys on sections that are read only
const FKeySelection& KeySelection = Sequencer.GetViewModel()->GetSelection()->KeySelection;
Keys.Reserve(KeySelection.Num());
for (FKeyHandle Key : KeySelection)
{
TSharedPtr<FChannelModel> Channel = KeySelection.GetModelForKey(Key);
UMovieSceneSection* Section = Channel ? Channel->GetSection() : nullptr;
if (Section && !Section->IsReadOnly())
{
Keys.Emplace(FSequencerSelectedKey(*Section, Channel, Key));
}
}
KeysAsArray = Keys.Array();
}
if (EnumHasAnyFlags(MoveType, ESequencerMoveOperationType::MoveSections))
{
for (TViewModelPtr<IDraggableTrackAreaExtension> DraggableItem : Sequencer.GetViewModel()->GetSelection()->TrackArea.Filter<IDraggableTrackAreaExtension>())
{
DraggableItem->OnBeginDrag(*this);
}
}
// Always move selected marked frames along with keys and/or sections.
MarkedFrames = Sequencer.GetViewModel()->GetSelection()->MarkedFrames.GetSelected();
}
void FMoveKeysAndSections::AddSnapTime(FFrameNumber SnapTime)
{
RelativeSnapOffsets.Add(SnapTime);
}
void FMoveKeysAndSections::AddModel(TSharedPtr<UE::Sequencer::FViewModel> Model)
{
using namespace UE::Sequencer;
if (FSectionModel* SectionModel = Model->CastThis<FSectionModel>())
{
if (UMovieSceneSection* Section = SectionModel->GetSection())
{
Sections.Add(Section);
if (UMovieScene* MovieScene = Section->GetTypedOuter<UMovieScene>())
{
// If the section is in a group, we also want to add the sections it is grouped with
if (const FMovieSceneSectionGroup* SectionGroup = MovieScene->GetSectionGroup(*Section))
{
for (TWeakObjectPtr<UMovieSceneSection> WeakGroupedSection : *SectionGroup)
{
// Verify sections are still valid, and are not infinite.
if (WeakGroupedSection.IsValid())
{
Sections.Add(WeakGroupedSection.Get());
}
}
}
}
}
}
if (TSharedPtr<IDraggableTrackAreaExtension> DraggableItem = Model->CastThisShared<IDraggableTrackAreaExtension>())
{
DraggedItems.Add(DraggableItem);
}
}
void FMoveKeysAndSections::OnBeginDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea)
{
using namespace UE::Sequencer;
// Early out if we've somehow started a drag operation without any sections or keys. This prevents an empty Undo/Redo Transaction from being created.
if (!Sections.Num() && !Keys.Num())
{
return;
}
BeginTransaction(Sections, NSLOCTEXT("Sequencer", "MoveKeyAndSectionTransaction", "Move Keys or Sections"));
// Tell the Snap Field to ignore our currently selected keys and sections. We can snap to the edges of non-selected
// sections and keys. The actual snapping field will add other sequencer data (play ranges, playheads, etc.) as snap targets.
FInvalidKeyAndSectionSnappingCandidates AvoidSnapCanidates(Keys, Sections);
SnapField = FSequencerSnapField(Sequencer, AvoidSnapCanidates );
// Store the frame time of the mouse so we can see how far we've moved from the starting point.
MouseTimePrev = VirtualTrackArea.PixelToFrame(LocalMousePos.X).RoundToFrame();
// Convert initial snapoffsets so they are relative to the mouse time
for (FFrameNumber& RelativeTime : RelativeSnapOffsets)
{
RelativeTime = (RelativeTime - MouseTimePrev).FloorToFrame();
}
// Sections can be dragged vertically to adjust their row up or down, so we need to store what row each section is currently on. A section
// can be dragged above all other sections - this is accomplished by moving all other sections down. We store the row indices for all sections
// in all tracks that we're modifying so we can get them later to move them.
TSet<UMovieSceneTrack*> Tracks;
for (UMovieSceneSection* Section : Sections)
{
Tracks.Add(Section->GetTypedOuter<UMovieSceneTrack>());
}
for (UMovieSceneTrack* Track : Tracks)
{
for (UMovieSceneSection* Section : Track->GetAllSections())
{
InitialSectionRowIndicies.Add(FInitialRowIndex{ Section, Section->GetRowIndex() });
}
}
// Our Key Handles don't store their times so we need to convert the handles into an array of times
// so that we can store the relative offset to each one.
TArray<FFrameNumber> KeyTimes;
KeyTimes.SetNum(Keys.Num());
GetKeyTimes(KeysAsArray, KeyTimes);
const int32 StartNum = RelativeSnapOffsets.Num();
RelativeSnapOffsets.SetNumUninitialized(StartNum + KeyTimes.Num());
for (int32 Index = 0; Index < KeyTimes.Num(); ++Index)
{
RelativeSnapOffsets[StartNum + Index] = (KeyTimes[Index] - MouseTimePrev).RoundToFrame();
}
// Keys can be moved within sections without the section itself being moved, so we need to call Modify on any section that owns a key that isn't also being moved.
ModifyNonSelectedSections();
}
void FMoveKeysAndSections::OnDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea)
{
UE::MovieScene::FScopedSignedObjectModifyDefer DeferMarkAsChanged(true);
if (!Sections.Num() && !Keys.Num())
{
return;
}
// Convert the current mouse position to a time
FVector2D VirtualMousePos = VirtualTrackArea.PhysicalToVirtual(LocalMousePos);
FFrameTime MouseTime = VirtualTrackArea.PixelToFrame(LocalMousePos.X);
// Calculate snapping first which modifies our MouseTime to reflect where it would have to be for the closest snap to work.
FFrameTime SnapThreshold = VirtualTrackArea.PixelDeltaToFrame(PixelSnapWidth);
// The edge of each bounded section as well as each individual key is a valid marker to try and snap to intervals/sections/etc.
// We take our stored offsets and add them to our current time to figure out where on the timeline the are currently.
TArray<FFrameTime> ValidSnapMarkers;
// If they have both keys and settings selected then we snap to the interval if either one of them is enabled, otherwise respect the individual setting.
const bool bSnapToInterval = Settings->GetForceWholeFrames() && (KeysAsArray.Num() > 0 || Sections.Num());
const bool bSnapToLikeTypes = (Settings->GetIsSnapEnabled() || MouseEvent.IsShiftDown()) && ( (KeysAsArray.Num() > 0 && Settings->GetSnapKeyTimesToElements()) || (Sections.Num() > 0 && Settings->GetSnapSectionTimesToElements()) );
SnapField.GetValue().SetSnapToInterval(bSnapToInterval);
SnapField.GetValue().SetSnapToLikeTypes(bSnapToLikeTypes);
// RelativeSnapOffsets contains both our sections and our keys, and we add them all as potential things that can snap to stuff.
if (bSnapToLikeTypes || bSnapToInterval)
{
ValidSnapMarkers.SetNumUninitialized(RelativeSnapOffsets.Num());
for (int32 Index = 0; Index < RelativeSnapOffsets.Num(); ++Index)
{
ValidSnapMarkers[Index] = (RelativeSnapOffsets[Index] + MouseTime);
}
}
// Now we'll try and snap all of these points to the closest valid snap marker (which may be a section or interval)
TOptional<FSequencerSnapField::FSnapResult> SnappedTime;
if (bSnapToLikeTypes || bSnapToInterval)
{
// This may or may not set the SnappedTime depending on if there are any sections within the threshold.
SnappedTime = SnapField->Snap(ValidSnapMarkers, SnapThreshold);
}
// If they actually snapped to something (snapping may be on but settings might dictate nothing to snap to) add the difference
// to our current MouseTime so that MouseTime reflects the amount needed to move to get to the whole snap point.
if (SnappedTime.IsSet())
{
// Add the snapped amount onto the mouse time so the resulting delta brings us in alignment.
MouseTime += (SnappedTime->SnappedTime - SnappedTime->OriginalTime);
}
if ((Settings->GetIsSnapEnabled() || MouseEvent.IsShiftDown()) && Settings->GetSnapKeysAndSectionsToPlayRange() && !Settings->ShouldKeepPlayRangeInSectionBounds())
{
MouseTime = UE::MovieScene::ClampToDiscreteRange(MouseTime, Sequencer.GetPlaybackRange());
}
// We'll calculate a DeltaX based on limits on movement (snapping, section collision) and then use them on keys and sections below.
TOptional<FFrameNumber> MaxDeltaX = GetMovementDeltaX(MouseEvent, MouseTime);
FFrameNumber MouseDeltaTime = (MouseTime - MouseTimePrev).FloorToFrame();
MouseTimePrev = MouseTimePrev + MaxDeltaX.Get(MouseDeltaTime);
// Move sections horizontally (limited by our calculated delta) and vertically based on mouse cursor.
bool bSectionMovementModifiedStructure = HandleSectionMovement(MouseTime, VirtualMousePos, LocalMousePos, MaxDeltaX, MouseDeltaTime);
// Update our key times by moving them by our delta.
HandleKeyMovement(MaxDeltaX, MouseDeltaTime);
// Update our marked frames by moving them by our delta.
HandleMarkedFrameMovement(MaxDeltaX, MouseDeltaTime);
// Get a list of the unique tracks in this selection and update their easing so previews draw interactively as you drag.
TSet<UMovieSceneTrack*> Tracks;
FMovieSceneSectionMovedParams SectionMovedParams(EPropertyChangeType::Interactive);
for (UMovieSceneSection* Section : Sections)
{
if (UMovieSceneTrack* Track = Section->GetTypedOuter<UMovieSceneTrack>())
{
Track->OnSectionMoved(*Section, SectionMovedParams);
Tracks.Add(Track);
}
}
for (UMovieSceneTrack* Track : Tracks)
{
Track->UpdateEasing();
}
// If we changed the layout by rearranging sections we need to tell the Sequencer to rebuild things, otherwise just re-evaluate existing tracks.
if (bSectionMovementModifiedStructure)
{
Sequencer.NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::MovieSceneStructureItemsChanged);
}
else
{
Sequencer.NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::TrackValueChanged);
}
}
void FMoveKeysAndSections::OnEndDrag(const FPointerEvent& MouseEvent, FVector2D LocalMousePos, const UE::Sequencer::FVirtualTrackArea& VirtualTrackArea)
{
using namespace UE::Sequencer;
UE::MovieScene::FScopedSignedObjectModifyDefer DeferMarkAsChanged;
if (!Sections.Num() && !Keys.Num())
{
return;
}
InitialSectionRowIndicies.Empty();
ModifiedNonSelectedObjects.Empty();
// Tracks can tell us if the row indexes for any sections were changed during our drag/drop operation.
TSet<UMovieSceneTrack*> Tracks;
for (UMovieSceneSection* Section : Sections)
{
// Grab only unique tracks as multiple sections can reside on the same track.
Tracks.Add(Section->GetTypedOuter<UMovieSceneTrack>());
}
for (UMovieSceneTrack* Track : Tracks)
{
// Ensure all of the tracks have updated the row indices for their sections
Track->FixRowIndices();
}
FMovieSceneSectionMovedParams SectionMovedParams(EPropertyChangeType::ValueSet);
for (UMovieSceneSection* Section : Sections)
{
UMovieSceneTrack* OuterTrack = Cast<UMovieSceneTrack>(Section->GetOuter());
if (OuterTrack)
{
OuterTrack->Modify();
OuterTrack->OnSectionMoved(*Section, SectionMovedParams);
}
}
for (UMovieSceneTrack* Track : Tracks)
{
Track->UpdateEasing();
}
for (TWeakPtr<IDraggableTrackAreaExtension> WeakDraggableItem : DraggedItems)
{
if (TSharedPtr<IDraggableTrackAreaExtension> DraggableItem = WeakDraggableItem.Pin())
{
DraggableItem->OnEndDrag(*this);
}
}
EndTransaction();
}
void FMoveKeysAndSections::ModifyNonSelectedSections()
{
using namespace UE::Sequencer;
for (const FSequencerSelectedKey& Key : Keys)
{
TSharedPtr<FChannelModel> Channel = Key.WeakChannel.Pin();
UObject* OwningObject = Channel ? Channel->GetOwningObject() : nullptr;
if (!OwningObject || ModifiedNonSelectedObjects.Contains(OwningObject))
{
continue;
}
if (UMovieSceneSection* OwningSection = Cast<UMovieSceneSection>(OwningObject))
{
if (Sections.Contains(OwningSection) || OwningSection->IsReadOnly())
{
continue;
}
}
OwningObject->SetFlags(RF_Transactional);
OwningObject->Modify();
ModifiedNonSelectedObjects.Add(OwningObject);
}
}
TOptional<FFrameNumber> FMoveKeysAndSections::GetMovementDeltaX(const FPointerEvent& MouseEvent, FFrameTime MouseTime)
{
TOptional<FFrameNumber> DeltaX;
// The delta of the mouse is the difference in the current mouse time vs when we started dragging
const FFrameNumber MouseDeltaTime = (MouseTime - MouseTimePrev).FloorToFrame();
// Disallow movement if any of the sections can't move
for (UMovieSceneSection* Section : Sections)
{
// If we're moving a section that is blending with something then it's OK if it overlaps stuff, the blend amount will get updated at the end.
if (!Section)
{
continue;
}
TOptional<FFrameNumber> LeftMovementMaximum;
TOptional<FFrameNumber> RightMovementMaximum;
// We'll calculate this section's borders and clamp the possible delta time to be less than that
if (!Section->GetBlendType().IsValid())
{
TRange<FFrameNumber> SectionBoundaries = GetSectionBoundaries(Section);
LeftMovementMaximum = UE::MovieScene::DiscreteInclusiveLower(SectionBoundaries);
RightMovementMaximum = UE::MovieScene::DiscreteExclusiveUpper(SectionBoundaries);
}
if ((Settings->GetIsSnapEnabled() || MouseEvent.IsShiftDown()) && Settings->GetSnapKeysAndSectionsToPlayRange() && !Settings->ShouldKeepPlayRangeInSectionBounds())
{
if (!LeftMovementMaximum.IsSet() || LeftMovementMaximum.GetValue() < Sequencer.GetPlaybackRange().GetLowerBoundValue())
{
LeftMovementMaximum = Sequencer.GetPlaybackRange().GetLowerBoundValue();
}
if (!RightMovementMaximum.IsSet() || RightMovementMaximum.GetValue() > Sequencer.GetPlaybackRange().GetUpperBoundValue())
{
RightMovementMaximum = Sequencer.GetPlaybackRange().GetUpperBoundValue();
}
}
if (LeftMovementMaximum.IsSet())
{
if (Section->HasStartFrame())
{
FFrameNumber NewStartTime = Section->GetInclusiveStartFrame() + MouseDeltaTime;
if (NewStartTime < LeftMovementMaximum.GetValue())
{
FFrameNumber ClampedDeltaTime = LeftMovementMaximum.GetValue() - Section->GetInclusiveStartFrame();
if (!DeltaX.IsSet() || DeltaX.GetValue() > ClampedDeltaTime)
{
DeltaX = ClampedDeltaTime;
}
}
}
}
if (RightMovementMaximum.IsSet())
{
if (Section->HasEndFrame())
{
FFrameNumber NewEndTime = Section->GetExclusiveEndFrame() + MouseDeltaTime;
if (NewEndTime > RightMovementMaximum.GetValue())
{
FFrameNumber ClampedDeltaTime = RightMovementMaximum.GetValue() - Section->GetExclusiveEndFrame();
if (!DeltaX.IsSet() || DeltaX.GetValue() > ClampedDeltaTime)
{
DeltaX = ClampedDeltaTime;
}
}
}
}
}
if ((Settings->GetIsSnapEnabled() || MouseEvent.IsShiftDown()) && Settings->GetSnapKeysAndSectionsToPlayRange() && !Settings->ShouldKeepPlayRangeInSectionBounds())
{
TArray<FFrameNumber> CurrentKeyTimes;
CurrentKeyTimes.SetNum(KeysAsArray.Num());
GetKeyTimes(KeysAsArray, CurrentKeyTimes);
for (int32 Index = 0; Index < CurrentKeyTimes.Num(); ++Index)
{
FSequencerSelectedKey& SelectedKey = KeysAsArray[Index];
const bool bOwningSectionIsSelected = Sections.Contains(SelectedKey.Section);
// We don't want to apply delta if we have the key's section selected as well, otherwise they get double
// transformed (moving the section moves the keys + we add the delta to the key positions).
if (!bOwningSectionIsSelected)
{
FFrameNumber NewKeyTime = CurrentKeyTimes[Index] + MouseDeltaTime;
if (NewKeyTime < Sequencer.GetPlaybackRange().GetLowerBoundValue())
{
FFrameNumber ClampedDeltaTime = CurrentKeyTimes[Index] - Sequencer.GetPlaybackRange().GetLowerBoundValue();
if (!DeltaX.IsSet() || DeltaX.GetValue() > ClampedDeltaTime)
{
DeltaX = ClampedDeltaTime;
}
}
if (NewKeyTime > Sequencer.GetPlaybackRange().GetUpperBoundValue())
{
FFrameNumber ClampedDeltaTime = Sequencer.GetPlaybackRange().GetUpperBoundValue() - CurrentKeyTimes[Index];
if (!DeltaX.IsSet() || DeltaX.GetValue() > ClampedDeltaTime)
{
DeltaX = ClampedDeltaTime;
}
}
}
}
}
return DeltaX;
}
bool FMoveKeysAndSections::HandleSectionMovement(FFrameTime MouseTime, FVector2D VirtualMousePos, FVector2D LocalMousePos, TOptional<FFrameNumber> MaxDeltaX, FFrameNumber DesiredDeltaX)
{
using namespace UE::Sequencer;
// Don't try to process moving sections if we don't have any sections.
if (Sections.Num() == 0)
{
return false;
}
// If sections are all on different rows or from different tracks, don't set row indices for anything because it leads to odd behavior.
bool bSectionsAreOnDifferentRows = false;
TOptional<int32> LowestRowIndex;
TOptional<int32> HighestRowIndex;
UMovieSceneTrack* FirstTrack = nullptr;
for (UMovieSceneSection* Section : Sections)
{
UMovieSceneTrack* Track = Section->GetTypedOuter<UMovieSceneTrack>();
if (!LowestRowIndex.IsSet() || LowestRowIndex.GetValue() < Section->GetRowIndex())
{
LowestRowIndex = Section->GetRowIndex();
}
if (!HighestRowIndex.IsSet() || HighestRowIndex.GetValue() > Section->GetRowIndex())
{
HighestRowIndex = Section->GetRowIndex();
}
if (FirstTrack)
{
if (FirstTrack != Track)
{
bSectionsAreOnDifferentRows = true;
}
}
else
{
FirstTrack = Track;
}
}
if (LowestRowIndex.IsSet() && HighestRowIndex.IsSet() && LowestRowIndex.GetValue() != HighestRowIndex.GetValue())
{
bSectionsAreOnDifferentRows = true;
}
TArray<TSharedPtr<FViewModel>> Tracks;
// @todo_sequencer_mvvm: really this code should all be operating on models rather than looking directly at UMovieSceneSections and tracks
FSectionModelStorageExtension* SectionStorage = Sequencer.GetViewModel()->GetRootModel()->CastDynamic<FSectionModelStorageExtension>();
bool bRowIndexChanged = false;
for (UMovieSceneSection* Section : Sections)
{
TSharedPtr<FSectionModel> SectionModel = SectionStorage->FindModelForSection(Section);
TViewModelPtr<ITrackExtension> TrackExtModel = SectionModel ? SectionModel->GetParentTrackModel() : nullptr;
if (!SectionModel || !TrackExtModel)
{
continue;
}
UMovieSceneTrack* Track = TrackExtModel->GetTrack();
const TArray<UMovieSceneSection*>& AllSections = Track->GetAllSections();
TArray<UMovieSceneSection*> NonDraggedSections;
for (UMovieSceneSection* TrackSection : AllSections)
{
if (!Sections.Contains(TrackSection))
{
NonDraggedSections.Add(TrackSection);
}
}
Tracks.AddUnique(TrackExtModel.AsModel());
int32 TargetRowIndex = Section->GetRowIndex();
// @todo_sequencer_mvvm: need to go through this dragging code and figure out what's going on and what it means
// now that models and views have separated concerns
// Handle vertical dragging to re-arrange tracks. We don't support vertical rearranging if you're dragging via
// a key, as the built in offset causes it to always jump down a row even without moving the mouse.
if (Track->SupportsMultipleRows() && AllSections.Num() > 1 && bAllowVerticalMovement)
{
// Compute the max row index whilst disregarding the one we're dragging
int32 MaxRowIndex = 0;
for (UMovieSceneSection* NonDraggedSection : NonDraggedSections)
{
if (NonDraggedSection != Section)
{
MaxRowIndex = FMath::Max(NonDraggedSection->GetRowIndex() + 1, MaxRowIndex);
}
}
// Handle sub-track and non-sub-track dragging
if (TViewModelPtr<FTrackModel> TrackModel = TrackExtModel.ImplicitCast())
{
const int32 NumRows = FMath::Max(Section->GetRowIndex() + 1, MaxRowIndex);
// Find the total height of the track - this is necessary because tracks may contain key areas, but they will not use sub tracks unless there is more than one row
const FVirtualGeometry VirtualGeometry = TrackModel->GetVirtualGeometry();
// Assume same height rows
const float VirtualSectionHeight = VirtualGeometry.NestedBottom - VirtualGeometry.Top;
const float VirtualRowHeight = VirtualSectionHeight / NumRows;
const float MouseOffsetWithinRow = VirtualMousePos.Y - (VirtualGeometry.Top + (VirtualRowHeight * TargetRowIndex));
const int32 NewIndex = FMath::FloorToInt((VirtualMousePos.Y - VirtualGeometry.Top) / VirtualRowHeight);
TargetRowIndex = FMath::Clamp(NewIndex, 0, MaxRowIndex);
// If close to the top of the row, move else everything down
if (VirtualMousePos.Y <= VirtualGeometry.Top || LocalMousePos.Y <= 0)
{
TargetRowIndex = -1;
}
}
else if (TViewModelPtr<FTrackRowModel> TrackRow = TrackExtModel.ImplicitCast())
{
TSharedPtr<FTrackModel> ParentTrack = TrackExtModel.AsModel()->FindAncestorOfType<FTrackModel>();
if (ensure(ParentTrack.IsValid()))
{
int32 ChildIndex = 0;
for (TSharedPtr<FViewModel> ChildNode : ParentTrack->GetChildren())
{
FVirtualGeometry ChildVirtualGeometry;
if (IGeometryExtension* ChildGeometryExtension = ChildNode->CastThis<IGeometryExtension>())
{
ChildVirtualGeometry = ChildGeometryExtension->GetVirtualGeometry();
}
float VirtualSectionTop = ChildVirtualGeometry.Top;
float VirtualSectionBottom = ChildVirtualGeometry.NestedBottom;
if (ChildIndex == 0 && (VirtualMousePos.Y <= VirtualSectionTop || LocalMousePos.Y <= 0))
{
TargetRowIndex = 0;
for (TSharedPtr<FSectionModel> SectionNode : TrackRow->GetTrackAreaModelListAs<FSectionModel>())
{
if (!Sections.Contains(SectionNode->GetSection()))
{
TargetRowIndex = -1;
break;
}
}
break;
}
else if (VirtualMousePos.Y < VirtualSectionBottom)
{
TargetRowIndex = ChildIndex;
break;
}
else
{
TargetRowIndex = ChildIndex + 1;
}
++ChildIndex;
}
// Track if we're expanding a parent track so we can unexpand it if we stop targeting it
if (TargetRowIndex > 0)
{
if (!ParentTrack->IsExpanded() && ParentTrack != ExpandedParentTrack)
{
if (TSharedPtr<FTrackModel> ExpandedParentTrackPinned = ExpandedParentTrack.Pin())
{
ExpandedParentTrackPinned->SetExpansion(false);
ExpandedParentTrack = nullptr;
}
ExpandedParentTrack = ParentTrack;
ParentTrack->SetExpansion(true);
}
}
else if (TSharedPtr<FTrackModel> ExpandedParentTrackPinned = ExpandedParentTrack.Pin())
{
ExpandedParentTrackPinned->SetExpansion(false);
ExpandedParentTrack = nullptr;
}
}
}
}
bool bDeltaX = DesiredDeltaX != 0;
bool bDeltaY = TargetRowIndex != Section->GetRowIndex();
const int32 TargetRowDelta = TargetRowIndex - Section->GetRowIndex();
// Prevent flickering by only moving sections if the user has actually made an effort to do so
if (bDeltaY && PrevMousePosY.IsSet())
{
// Check mouse has been moved in the direction of intended move
if ((TargetRowDelta < 0 && LocalMousePos.Y - PrevMousePosY.GetValue() > 1.0f) || (TargetRowDelta > 0 && LocalMousePos.Y - PrevMousePosY.GetValue() < 1.0f))
{
// Mouse was not moved in the direction the section wants to swap
// Assume offset is due to UI relayout and block moving the section
bDeltaY = false;
}
}
// Horizontal movement
if (bDeltaX)
{
Section->MoveSection(MaxDeltaX.Get(DesiredDeltaX));
}
// Vertical movement
if (bDeltaY && !bSectionsAreOnDifferentRows &&
(
Section->GetBlendType().IsValid() ||
!Section->OverlapsWithSections(NonDraggedSections, TargetRowIndex - Section->GetRowIndex(), DesiredDeltaX.Value)
)
)
{
// Reached the top, move everything else we're not moving downwards
if (TargetRowIndex == -1)
{
if (!bSectionsAreOnDifferentRows)
{
// If the sections being moved are all at the top, and all others are below it, do nothing
bool bSectionsBeingMovedAreAtTop = true;
for (const FInitialRowIndex& InitialRowIndex : InitialSectionRowIndicies)
{
if (!Sections.Contains(InitialRowIndex.Section))
{
if (LowestRowIndex.IsSet() && InitialRowIndex.RowIndex <= LowestRowIndex.GetValue())
{
bSectionsBeingMovedAreAtTop = false;
break;
}
}
}
if (!bSectionsBeingMovedAreAtTop)
{
for (const FInitialRowIndex& InitialRowIndex : InitialSectionRowIndicies)
{
if (!Sections.Contains(InitialRowIndex.Section))
{
InitialRowIndex.Section->Modify();
InitialRowIndex.Section->SetRowIndex(InitialRowIndex.RowIndex + 1);
bRowIndexChanged = true;
}
}
}
}
}
else
{
if (!bSectionsAreOnDifferentRows)
{
// If the sections being moved are all at the bottom, and all others are aove it, do nothing
bool bSectionsBeingMovedAreAtBottom = true;
for (const FInitialRowIndex& InitialRowIndex : InitialSectionRowIndicies)
{
if (!Sections.Contains(InitialRowIndex.Section))
{
if (HighestRowIndex.IsSet() && InitialRowIndex.RowIndex >= HighestRowIndex.GetValue())
{
bSectionsBeingMovedAreAtBottom = false;
break;
}
}
}
if (!bSectionsBeingMovedAreAtBottom || TargetRowIndex < Section->GetRowIndex())
{
Section->Modify();
Section->SetRowIndex(TargetRowIndex);
bRowIndexChanged = true;
}
}
}
}
}
if (bRowIndexChanged)
{
PrevMousePosY = LocalMousePos.Y;
// @todo_sequence_mvvm: Fix this code as well - is it necessary if we no longer destructively re-populate the tree when data changes?
// Expand track node if it wasn't already expanded. This ensures that multi row tracks will show multiple rows if regenerated
//for (TSharedPtr<FViewModel> TrackModel : Tracks)
//{
// IOutlinerExtension* OutlinerExtension = TrackModel->CastThis<IOutlinerExtension>(TrackModel);
// bool bIsTrackExpanded = (!OutlinerExtension || OutlinerExtension->IsExpanded());
// if (!bIsTrackExpanded && TrackModel->GetSubTrackMode() == FSequencerTrackNode::ESubTrackMode::None)
// {
// TArray<TSharedRef<ISequencerSection> > TrackNodeSections = TrackModel->GetSections();
// if (TrackNodeSections.Num() && TrackNodeSections[0]->GetSectionObject())
// {
// int32 SectionFirstRowIndex = TrackNodeSections[0]->GetSectionObject()->GetRowIndex();
// for (TSharedRef<ISequencerSection> TrackNodeSection : TrackNodeSections)
// {
// if (TrackNodeSection->GetSectionObject() && SectionFirstRowIndex != TrackNodeSection->GetSectionObject()->GetRowIndex())
// {
// TrackNode->SetExpansion(true);
// break;
// }
// }
// }
// }
//}
}
return bRowIndexChanged;
}
void FMoveKeysAndSections::HandleKeyMovement(TOptional<FFrameNumber> MaxDeltaX, FFrameNumber DesiredDeltaX)
{
if (KeysAsArray.Num() == 0)
{
return;
}
// Apply the delta to our key times. We need to get our key time so that we can add the delta
// to each one so that we come up with a new absolute time for it.
TArray<FFrameNumber> CurrentKeyTimes;
CurrentKeyTimes.SetNum(KeysAsArray.Num());
GetKeyTimes(KeysAsArray, CurrentKeyTimes);
for (int32 Index = 0; Index < CurrentKeyTimes.Num(); ++Index)
{
FSequencerSelectedKey& SelectedKey = KeysAsArray[Index];
const bool bOwningSectionIsSelected = Sections.Contains(SelectedKey.Section);
// We don't want to apply delta if we have the key's section selected as well, otherwise they get double
// transformed (moving the section moves the keys + we add the delta to the key positions).
if (!bOwningSectionIsSelected)
{
CurrentKeyTimes[Index] += MaxDeltaX.Get(DesiredDeltaX);
}
}
// Now set the times back to the keys.
SetKeyTimes(KeysAsArray, CurrentKeyTimes);
// Expand any sections containing those keys to encompass their new location
for (int32 Index = 0; Index < CurrentKeyTimes.Num(); ++Index)
{
FSequencerSelectedKey SelectedKey = KeysAsArray[Index];
UMovieSceneSection* Section = SelectedKey.Section;
if (ModifiedNonSelectedObjects.Contains(Section))
{
// If the key moves outside of the section resize the section to fit the key
// @todo Sequencer - Doesn't account for hitting other sections
const FFrameNumber NewKeyTime = CurrentKeyTimes[Index];
TRange<FFrameNumber> SectionRange = Section->GetRange();
if (!SectionRange.Contains(NewKeyTime))
{
TRange<FFrameNumber> NewRange = TRange<FFrameNumber>::Hull(SectionRange, TRange<FFrameNumber>(NewKeyTime));
Section->SetRange(NewRange);
}
}
}
// Snap the play time to the new dragged key time if all the keyframes were dragged to the same time
if (Settings->GetSnapPlayTimeToDraggedKey() && CurrentKeyTimes.Num())
{
FFrameNumber FirstFrame = CurrentKeyTimes[0];
auto EqualsFirstFrame = [=](FFrameNumber In)
{
return In == FirstFrame;
};
if (Algo::AllOf(CurrentKeyTimes, EqualsFirstFrame))
{
Sequencer.SetLocalTime(FirstFrame);
}
}
// Explicitly mark everything as changed to ensure that the UI is responsive during a drag
for (UObject* OwningObject : ModifiedNonSelectedObjects)
{
if (UMovieSceneSignedObject* SignedObject = Cast<UMovieSceneSignedObject>(OwningObject))
{
SignedObject->MarkAsChanged();
SignedObject->BroadcastChanged();
}
}
}
void FMoveKeysAndSections::HandleMarkedFrameMovement(TOptional<FFrameNumber> MaxDeltaX, FFrameNumber DesiredDeltaX)
{
if (MarkedFrames.Num() == 0)
{
return;
}
const FFrameNumber EffectiveDelta = MaxDeltaX.Get(DesiredDeltaX);
UMovieScene* FocusedMovieScene = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene();
const TArray<FMovieSceneMarkedFrame>& AllMarkedFrames = FocusedMovieScene->GetMarkedFrames();
for (TSet<int32>::TConstIterator It = MarkedFrames.CreateConstIterator(); It; ++It)
{
const int32 MarkIndex = *It;
const FMovieSceneMarkedFrame& MarkedFrame = AllMarkedFrames[MarkIndex];
const FFrameNumber NewMarkTime = MarkedFrame.FrameNumber + EffectiveDelta;
FocusedMovieScene->SetMarkedFrame(MarkIndex, NewMarkTime);
}
FocusedMovieScene->MarkAsChanged();
FocusedMovieScene->BroadcastChanged();
}