You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
4623 lines
138 KiB
C++
4623 lines
138 KiB
C++
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "Misc/MessageDialog.h"
|
|
#include "Misc/App.h"
|
|
#include "UObject/Class.h"
|
|
#include "InputCoreTypes.h"
|
|
#include "GameFramework/Actor.h"
|
|
#include "Editor/UnrealEdEngine.h"
|
|
#include "Camera/CameraActor.h"
|
|
#include "Engine/Selection.h"
|
|
#include "LevelEditorViewport.h"
|
|
#include "UnrealEdGlobals.h"
|
|
#include "Matinee/InterpGroup.h"
|
|
#include "Matinee/InterpTrack.h"
|
|
#include "MatineeOptions.h"
|
|
#include "MatineeTransBuffer.h"
|
|
#include "MatineeViewportClient.h"
|
|
#include "MatineeViewSaveData.h"
|
|
#include "SMatineeRecorder.h"
|
|
#include "Editor.h"
|
|
#include "MatineeTrackData.h"
|
|
#include "Engine/InterpCurveEdSetup.h"
|
|
#include "Matinee.h"
|
|
#include "InterpTrackHelper.h"
|
|
#include "Matinee/MatineeActor.h"
|
|
#include "Matinee/MatineeActorCameraAnim.h"
|
|
#include "Matinee/InterpTrackInst.h"
|
|
#include "Matinee/InterpTrackMove.h"
|
|
#include "Matinee/InterpTrackFloatBase.h"
|
|
#include "Matinee/InterpTrackMoveAxis.h"
|
|
#include "Matinee/InterpTrackInstMove.h"
|
|
#include "Matinee/InterpTrackEvent.h"
|
|
#include "Matinee/InterpTrackFloatProp.h"
|
|
#include "Matinee/InterpTrackVectorBase.h"
|
|
#include "Matinee/InterpTrackLinearColorBase.h"
|
|
#include "Matinee/InterpTrackSound.h"
|
|
#include "Matinee/InterpTrackDirector.h"
|
|
#include "Matinee/InterpTrackFade.h"
|
|
#include "Matinee/InterpTrackSlomo.h"
|
|
#include "Matinee/InterpTrackColorScale.h"
|
|
#include "Matinee/InterpTrackInstDirector.h"
|
|
#include "Matinee/InterpTrackAnimControl.h"
|
|
#include "Matinee/InterpTrackParticleReplay.h"
|
|
#include "Matinee/InterpGroupInst.h"
|
|
#include "Matinee/InterpGroupDirector.h"
|
|
#include "Matinee/InterpGroupInstDirector.h"
|
|
#include "Matinee/InterpFilter.h"
|
|
|
|
#include "MatineeConstants.h"
|
|
#include "MatineeDelegates.h"
|
|
|
|
#include "EditorSupportDelegates.h"
|
|
|
|
#include "Runtime/Analytics/Analytics/Public/Interfaces/IAnalyticsProvider.h"
|
|
#include "EngineAnalytics.h"
|
|
#include "Framework/Notifications/NotificationManager.h"
|
|
#include "Widgets/Notifications/SNotificationList.h"
|
|
#include "Camera/CameraAnim.h"
|
|
#include "Misc/ConfigCacheIni.h"
|
|
#include "GameFramework/WorldSettings.h"
|
|
|
|
///// UTILS
|
|
|
|
void FMatinee::TickInterp(float DeltaTime)
|
|
{
|
|
static bool bWasPlayingLastTick = false;
|
|
|
|
if ( !bClosed )
|
|
{
|
|
UpdateViewportSettings();
|
|
}
|
|
|
|
// Don't tick if a windows close request was issued.
|
|
if( !bClosed && MatineeActor->bIsPlaying )
|
|
{
|
|
// When in 'fixed time step' playback, we may need to constrain the frame rate (by sleeping!)
|
|
ConstrainFixedTimeStepFrameRate();
|
|
|
|
// Make sure particle replay tracks have up-to-date editor-only transient state
|
|
UpdateParticleReplayTracks();
|
|
|
|
// Modify playback rate by desired speed.
|
|
float TimeDilation = MatineeActor->GetWorld()->GetWorldSettings()->GetEffectiveTimeDilation();
|
|
MatineeActor->StepInterp(DeltaTime * PlaybackSpeed * TimeDilation, true);
|
|
|
|
// If we are looping the selected section, when we pass the end, place it back to the beginning
|
|
if(bLoopingSection)
|
|
{
|
|
if(MatineeActor->InterpPosition >= IData->EdSectionEnd || MatineeActor->InterpPosition < IData->EdSectionStart)
|
|
{
|
|
MatineeActor->UpdateInterp(IData->EdSectionStart, true, true);
|
|
MatineeActor->Play();
|
|
}
|
|
}
|
|
|
|
UpdateCameraToGroup(true);
|
|
UpdateCamColours();
|
|
CurveEd->SetPositionMarker(true, MatineeActor->InterpPosition, PosMarkerColor );
|
|
}
|
|
else
|
|
{
|
|
UpdateCameraToGroup(false);
|
|
}
|
|
|
|
if( bWasPlayingLastTick && !MatineeActor->bIsPlaying )
|
|
{
|
|
// If the interp was playing last tick but is now no longer playing turn off audio.
|
|
SetAudioRealtimeOverride( false );
|
|
}
|
|
|
|
bWasPlayingLastTick = MatineeActor->bIsPlaying;
|
|
|
|
// Make sure fixed time step mode is set correctly based on whether we're currently 'playing' or not
|
|
// We need to do this here because interp sequences can stop without us ever telling them to (and
|
|
// we won't find out about it!)
|
|
UpdateFixedTimeStepPlayback();
|
|
|
|
/**Capture key frames and increment the state of recording*/
|
|
UpdateCameraRecording();
|
|
}
|
|
|
|
void FMatinee::UpdateViewportSettings()
|
|
{
|
|
if ( GCurrentLevelEditingViewportClient )
|
|
{
|
|
if ( GCurrentLevelEditingViewportClient->IsPerspective() && GCurrentLevelEditingViewportClient->AllowsCinematicPreview() )
|
|
{
|
|
bool bSafeFrames = IsSafeFrameDisplayEnabled();
|
|
bool bAspectRatioBars = AreAspectRatioBarsEnabled();
|
|
|
|
if ( GCurrentLevelEditingViewportClient->IsShowingSafeFrameBoxDisplay() != bSafeFrames )
|
|
{
|
|
GCurrentLevelEditingViewportClient->SetShowSafeFrameBoxDisplay(bSafeFrames);
|
|
}
|
|
|
|
if ( GCurrentLevelEditingViewportClient->IsShowingAspectRatioBarDisplay() != bAspectRatioBars )
|
|
{
|
|
GCurrentLevelEditingViewportClient->SetShowAspectRatioBarDisplay(bAspectRatioBars);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FMatinee::UpdateCameraRecording (void)
|
|
{
|
|
//if we're recording a real-time camera playback, capture camera frame
|
|
if (RecordingState != MatineeConstants::ERecordingState::RECORDING_COMPLETE)
|
|
{
|
|
double CurrentTime = FPlatformTime::Seconds();
|
|
double TimeSinceStateStart = (CurrentTime - RecordingStateStartTime);
|
|
|
|
switch (RecordingState)
|
|
{
|
|
case MatineeConstants::ERecordingState::RECORDING_GET_READY_PAUSE:
|
|
//if time to begin recording
|
|
if (TimeSinceStateStart >= MatineeConstants::CountdownDurationInSeconds)
|
|
{
|
|
//Set the new start time
|
|
RecordingStateStartTime = CurrentTime;
|
|
//change state
|
|
RecordingState = MatineeConstants::ERecordingState::RECORDING_ACTIVE;
|
|
|
|
//Clear all tracks that think they are recording
|
|
RecordingTracks.Empty();
|
|
|
|
//turn off looping!
|
|
bLoopingSection = false;
|
|
|
|
// Start Time moving, MUST be done done before set position, as Play rewinds time
|
|
MatineeActor->Play();
|
|
|
|
// Move to proper start time
|
|
SetInterpPosition(GetRecordingStartTime());
|
|
|
|
|
|
//if we're in camera duplication mode
|
|
if ((RecordMode == MatineeConstants::ERecordMode::RECORD_MODE_NEW_CAMERA) || (RecordMode == MatineeConstants::ERecordMode::RECORD_MODE_NEW_CAMERA_ATTACHED))
|
|
{
|
|
//add new camera
|
|
FLevelEditorViewportClient* LevelVC = GetRecordingViewport();
|
|
if (!LevelVC)
|
|
{
|
|
StopRecordingInterpValues();
|
|
return;
|
|
}
|
|
|
|
AActor* ActorToUseForBase = NULL;
|
|
if ((RecordMode == MatineeConstants::ERecordMode::RECORD_MODE_NEW_CAMERA_ATTACHED) && (GEditor->GetSelectedActorCount()==1))
|
|
{
|
|
USelection& SelectedActors = *GEditor->GetSelectedActors();
|
|
ActorToUseForBase = CastChecked< AActor >( SelectedActors.GetSelectedObject( 0 ) );
|
|
}
|
|
|
|
const FTransform Transform(LevelVC->GetViewRotation(), LevelVC->GetViewLocation());
|
|
ACameraActor* NewCam = Cast<ACameraActor>(GEditor->AddActor( LevelVC->GetWorld()->GetCurrentLevel(), ACameraActor::StaticClass(), Transform ));
|
|
NewCam->GetCameraComponent()->bConstrainAspectRatio = LevelVC->IsAspectRatioConstrained();
|
|
NewCam->GetCameraComponent()->AspectRatio = LevelVC->AspectRatio;
|
|
NewCam->GetCameraComponent()->FieldOfView = LevelVC->ViewFOV;
|
|
|
|
//make new group for the camera
|
|
UInterpGroup* NewGroup = NewObject<UInterpGroup>(IData, NAME_None, RF_Transactional);
|
|
FString NewGroupName = NSLOCTEXT( "UnrealEd", "InterpEd_RecordMode_CameraGroupName", "CameraGroupCG" ).ToString();
|
|
NewGroup->GroupName = FName(*NewGroupName);
|
|
NewGroup->EnsureUniqueName();
|
|
//add new camera group to matinee
|
|
IData->Modify();
|
|
IData->InterpGroups.Add(NewGroup);
|
|
|
|
//add group instance for camera
|
|
UInterpGroupInst* NewGroupInst = NewObject<UInterpGroupInst>(MatineeActor, NAME_None, RF_Transactional);
|
|
// Initialise group instance, saving ref to actor it works on.
|
|
NewGroupInst->InitGroupInst(NewGroup, NewCam);
|
|
const int32 NewGroupInstIndex = MatineeActor->GroupInst.Add(NewGroupInst);
|
|
|
|
//Link group with actor
|
|
MatineeActor->InitGroupActorForGroup(NewGroup, NewCam);
|
|
|
|
//unselect all, so we can select the newly added tracks
|
|
DeselectAll();
|
|
|
|
//Add new tracks to the camera group
|
|
int32 MovementTrackIndex = INDEX_NONE;
|
|
UInterpTrackMove* MoveTrack = Cast<UInterpTrackMove>(AddTrackToGroup( NewGroup, UInterpTrackMove::StaticClass(), NULL, false, MovementTrackIndex, false ));
|
|
check(MoveTrack);
|
|
|
|
//add fov track
|
|
SetTrackAddPropName( FName( TEXT( "FOVAngle" ) ) );
|
|
int32 FOVTrackIndex = INDEX_NONE;
|
|
UInterpTrack* FOVTrack = AddTrackToGroup( NewGroup, UInterpTrackFloatProp::StaticClass(), NULL, false, FOVTrackIndex, false );
|
|
|
|
//set this group as the preview group
|
|
const bool bResetViewports = false;
|
|
LockCamToGroup(NewGroup, bResetViewports);
|
|
|
|
//Select camera tracks
|
|
SelectTrack( NewGroup, MoveTrack , false);
|
|
SelectTrack( NewGroup, FOVTrack, false);
|
|
|
|
RecordingTracks.Add(MoveTrack);
|
|
RecordingTracks.Add(FOVTrack);
|
|
}
|
|
else if ((RecordMode == MatineeConstants::ERecordMode::RECORD_MODE_DUPLICATE_TRACKS) && (HasATrackSelected()))
|
|
{
|
|
//duplicate all selected tracks in their respective groups, and clear them
|
|
const bool bDeleteSelectedTracks = false;
|
|
DuplicateSelectedTracksForRecording(bDeleteSelectedTracks);
|
|
}
|
|
else if ((RecordMode == MatineeConstants::ERecordMode::RECORD_MODE_REPLACE_TRACKS) && (HasATrackSelected()))
|
|
{
|
|
const bool bDeleteSelectedTracks = true;
|
|
DuplicateSelectedTracksForRecording(bDeleteSelectedTracks);
|
|
}
|
|
else
|
|
{
|
|
//failed to be in a valid recording state (no track selected, and duplicate or replace)
|
|
StopRecordingInterpValues();
|
|
return;
|
|
}
|
|
|
|
for (int32 i = 0; i < RecordingTracks.Num(); ++i)
|
|
{
|
|
RecordingTracks[i]->bIsRecording = true;
|
|
}
|
|
|
|
//Sample state at "Start Time"
|
|
RecordKeys();
|
|
|
|
//Save the parent offsets for next frame
|
|
SaveRecordingParentOffsets();
|
|
}
|
|
break;
|
|
case MatineeConstants::ERecordingState::RECORDING_ACTIVE:
|
|
{
|
|
//apply movement of any parent object to the child object as well (since that movement is no longer processed when recording)
|
|
ApplyRecordingParentOffsets();
|
|
|
|
//Sample state at "Start Time"
|
|
RecordKeys();
|
|
|
|
//update the parent offsets for next frame
|
|
SaveRecordingParentOffsets();
|
|
|
|
//see if we're done recording (accounting for slow mo)
|
|
if (MatineeActor->InterpPosition >= GetRecordingEndTime())
|
|
{
|
|
//Set the new start time
|
|
RecordingStateStartTime = CurrentTime;
|
|
//change state
|
|
StopRecordingInterpValues();
|
|
|
|
// Stop time if it's playing.
|
|
MatineeActor->Stop();
|
|
// Move to proper start time
|
|
SetInterpPosition(GetRecordingStartTime());
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
//invalid state
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Constrains the maximum frame rate to the fixed time step rate when playing back in that mode */
|
|
void FMatinee::ConstrainFixedTimeStepFrameRate()
|
|
{
|
|
// Don't allow the fixed time step playback to run faster than real-time
|
|
if( bSnapToFrames && bFixedTimeStepPlayback )
|
|
{
|
|
// NOTE: Its important that PlaybackStartRealTime and NumContinuousFixedTimeStepFrames are reset
|
|
// when anything timing-related changes, like FApp::GetFixedDeltaTime() or playback direction.
|
|
|
|
double CurRealTime = FPlatformTime::Seconds();
|
|
|
|
// Minor hack to handle changes to world TimeDilation. We reset our frame rate gate state
|
|
// when we detect a change to time dilation.
|
|
static float LastTimeDilation = MatineeActor->GetWorld()->GetWorldSettings()->GetEffectiveTimeDilation();
|
|
if( LastTimeDilation != MatineeActor->GetWorld()->GetWorldSettings()->GetEffectiveTimeDilation() )
|
|
{
|
|
// Looks like time dilation has changed!
|
|
NumContinuousFixedTimeStepFrames = 0;
|
|
PlaybackStartRealTime = CurRealTime;
|
|
|
|
LastTimeDilation = MatineeActor->GetWorld()->GetWorldSettings()->GetEffectiveTimeDilation();
|
|
}
|
|
|
|
// How long should have it taken to get to the current frame?
|
|
const double ExpectedPlaybackTime =
|
|
NumContinuousFixedTimeStepFrames * FApp::GetFixedDeltaTime() * PlaybackSpeed;
|
|
|
|
// How long has it been (in real-time) since we started playback?
|
|
double RealTimeSincePlaybackStarted = CurRealTime - PlaybackStartRealTime;
|
|
|
|
// If we're way ahead of schedule (more than 5 ms), then we'll perform a long sleep
|
|
float WaitTime = ExpectedPlaybackTime - RealTimeSincePlaybackStarted;
|
|
if( WaitTime > 5 / 1000.0f )
|
|
{
|
|
FPlatformProcess::Sleep( WaitTime - 3 / 1000.0f );
|
|
|
|
// Update timing info after our little snooze
|
|
CurRealTime = FPlatformTime::Seconds();
|
|
RealTimeSincePlaybackStarted = CurRealTime - PlaybackStartRealTime;
|
|
WaitTime = ExpectedPlaybackTime - RealTimeSincePlaybackStarted;
|
|
}
|
|
|
|
while( RealTimeSincePlaybackStarted < ExpectedPlaybackTime )
|
|
{
|
|
// OK, we're running ahead of schedule so we need to wait a bit before the next frame
|
|
FPlatformProcess::Sleep( 0.0f );
|
|
|
|
// Check the time again
|
|
CurRealTime = FPlatformTime::Seconds();
|
|
RealTimeSincePlaybackStarted = CurRealTime - PlaybackStartRealTime;
|
|
WaitTime = ExpectedPlaybackTime - RealTimeSincePlaybackStarted;
|
|
}
|
|
|
|
// Increment number of continuous fixed time step frames
|
|
++NumContinuousFixedTimeStepFrames;
|
|
}
|
|
}
|
|
|
|
void FMatinee::SetSelectedFilter(class UInterpFilter* InFilter)
|
|
{
|
|
if ( IData->SelectedFilter != InFilter )
|
|
{
|
|
IData->SelectedFilter = InFilter;
|
|
|
|
if(InFilter != NULL)
|
|
{
|
|
// Start by hiding all groups and tracks
|
|
for(int32 GroupIdx=0; GroupIdx<IData->InterpGroups.Num(); GroupIdx++)
|
|
{
|
|
UInterpGroup* CurGroup = IData->InterpGroups[ GroupIdx ];
|
|
CurGroup->bVisible = false;
|
|
|
|
for( int32 CurTrackIndex = 0; CurTrackIndex < CurGroup->InterpTracks.Num(); ++CurTrackIndex )
|
|
{
|
|
UInterpTrack* CurTrack = CurGroup->InterpTracks[ CurTrackIndex ];
|
|
CurTrack->bVisible = false;
|
|
}
|
|
}
|
|
|
|
|
|
// Apply the filter. This will mark certain groups and tracks as visible.
|
|
InFilter->FilterData( MatineeActor );
|
|
|
|
|
|
// Make sure folders that are parents to visible groups are ALSO visible!
|
|
for(int32 GroupIdx=0; GroupIdx<IData->InterpGroups.Num(); GroupIdx++)
|
|
{
|
|
UInterpGroup* CurGroup = IData->InterpGroups[ GroupIdx ];
|
|
if( CurGroup->bVisible )
|
|
{
|
|
// Make sure my parent folder group is also visible!
|
|
if( CurGroup->bIsParented )
|
|
{
|
|
UInterpGroup* ParentFolderGroup = FindParentGroupFolder( CurGroup );
|
|
if( ParentFolderGroup != NULL )
|
|
{
|
|
ParentFolderGroup->bVisible = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No filter, so show all groups and tracks
|
|
for(int32 GroupIdx=0; GroupIdx<IData->InterpGroups.Num(); GroupIdx++)
|
|
{
|
|
UInterpGroup* CurGroup = IData->InterpGroups[ GroupIdx ];
|
|
CurGroup->bVisible = true;
|
|
|
|
// Hide tracks
|
|
for( int32 CurTrackIndex = 0; CurTrackIndex < CurGroup->InterpTracks.Num(); ++CurTrackIndex )
|
|
{
|
|
UInterpTrack* CurTrack = CurGroup->InterpTracks[ CurTrackIndex ];
|
|
CurTrack->bVisible = true;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
// The selected group filter may have changed which directly affects the vertical size of the content
|
|
// in the track window, so we'll need to update our scroll bars.
|
|
UpdateTrackWindowScrollBars();
|
|
|
|
// Update scroll position
|
|
for( FSelectedGroupIterator GroupIt(GetSelectedGroupIterator()); GroupIt; ++GroupIt )
|
|
{
|
|
if( (*GroupIt)->bVisible )
|
|
{
|
|
ScrollToGroup( *GroupIt );
|
|
|
|
// Immediately break because we want to scroll only
|
|
// to the first selected group that's visible.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return true if there is at least one selected group. false, otherwise.
|
|
*/
|
|
bool FMatinee::HasAGroupSelected() const
|
|
{
|
|
bool bHasAGroupSelected = false;
|
|
|
|
for( FSelectedGroupConstIterator GroupIt(GetSelectedGroupIterator()); GroupIt; ++GroupIt )
|
|
{
|
|
// If we reach here, then we have at least one
|
|
// group selected because the iterator is valid.
|
|
bHasAGroupSelected = true;
|
|
break;
|
|
}
|
|
|
|
return bHasAGroupSelected;
|
|
}
|
|
|
|
/**
|
|
* @param GroupClass The class type of interp group.
|
|
* @return true if there is at least one selected group. false, otherwise.
|
|
*/
|
|
bool FMatinee::HasAGroupSelected( const UClass* GroupClass ) const
|
|
{
|
|
// If the user didn't pass in a UInterpGroup derived class, then
|
|
// they probably made a typo or are calling the wrong function.
|
|
check( GroupClass->IsChildOf(UInterpGroup::StaticClass()) );
|
|
|
|
bool bHasAGroupSelected = false;
|
|
|
|
for( FSelectedGroupConstIterator GroupIt(GetSelectedGroupIterator()); GroupIt; ++GroupIt )
|
|
{
|
|
if( (*GroupIt)->IsA(GroupClass) )
|
|
{
|
|
bHasAGroupSelected = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bHasAGroupSelected;
|
|
}
|
|
|
|
/**
|
|
* @return true if there is at least one track in the Matinee; false, otherwise.
|
|
*/
|
|
bool FMatinee::HasATrack() const
|
|
{
|
|
bool bHasATrack = false;
|
|
|
|
FAllTracksConstIterator AllTracks(IData->InterpGroups);
|
|
|
|
// Upon construction, the track iterator will automatically iterate until reaching the first
|
|
// interp track. If the track iterator is valid, then we have at least one track in the Matinee.
|
|
if( AllTracks )
|
|
{
|
|
bHasATrack = true;
|
|
}
|
|
|
|
return bHasATrack;
|
|
}
|
|
|
|
/**
|
|
* @return true if there is at least one selected track. false, otherwise.
|
|
*/
|
|
bool FMatinee::HasATrackSelected() const
|
|
{
|
|
bool bHasASelectedTrack = false;
|
|
|
|
for( FSelectedTrackConstIterator TrackIt(GetSelectedTrackIterator()); TrackIt; ++TrackIt )
|
|
{
|
|
bHasASelectedTrack = true;
|
|
break;
|
|
}
|
|
|
|
return bHasASelectedTrack;
|
|
}
|
|
|
|
/**
|
|
* @param TrackClass The type of interp track.
|
|
* @return true if there is at least one selected track of the given class type. false, otherwise.
|
|
*/
|
|
bool FMatinee::HasATrackSelected( const UClass* TrackClass ) const
|
|
{
|
|
// If the user didn't pass in a UInterpTrack derived class, then
|
|
// they probably made a typo or are calling the wrong function.
|
|
check( TrackClass->IsChildOf(UInterpTrack::StaticClass()) );
|
|
|
|
bool bHasASelectedTrack = false;
|
|
|
|
for( FSelectedTrackConstIterator TrackIt(GetSelectedTrackIterator()); TrackIt; ++TrackIt )
|
|
{
|
|
if( (*TrackIt)->IsA(TrackClass) )
|
|
{
|
|
bHasASelectedTrack = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bHasASelectedTrack;
|
|
}
|
|
|
|
/**
|
|
* @param OwningGroup MatineeActor group to check for selected tracks.
|
|
* @return true if at least one interp track selected owned by the given group; false, otherwise.
|
|
*/
|
|
bool FMatinee::HasATrackSelected( const UInterpGroup* OwningGroup ) const
|
|
{
|
|
bool bHasASelectedTrack = false;
|
|
|
|
for( TArray<UInterpTrack*>::TConstIterator TrackIt(OwningGroup->InterpTracks); TrackIt; ++TrackIt )
|
|
{
|
|
if( (*TrackIt)->IsSelected() == true )
|
|
{
|
|
bHasASelectedTrack = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bHasASelectedTrack;
|
|
}
|
|
|
|
/**
|
|
* @return true if at least one folder is selected; false, otherwise.
|
|
*/
|
|
bool FMatinee::HasAFolderSelected() const
|
|
{
|
|
bool bHasAFolderSelected = false;
|
|
|
|
for( FSelectedGroupConstIterator GroupIt(GetSelectedGroupIterator()); GroupIt; ++GroupIt )
|
|
{
|
|
if( (*GroupIt)->bIsFolder )
|
|
{
|
|
bHasAFolderSelected = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bHasAFolderSelected;
|
|
}
|
|
|
|
/**
|
|
* @return true if every single selected group is a folder.
|
|
*/
|
|
bool FMatinee::AreAllSelectedGroupsFolders() const
|
|
{
|
|
// Set return value based on whether a group is selected or not because in the event
|
|
// that there are no selected groups, then the internals of the loop will never
|
|
// evaluate. If no groups are selected, then no folders are selected.
|
|
bool bAllFolders = HasAGroupSelected();
|
|
|
|
for( FSelectedGroupConstIterator GroupIt(GetSelectedGroupIterator()); GroupIt; ++GroupIt )
|
|
{
|
|
if( (*GroupIt)->bIsFolder == false )
|
|
{
|
|
bAllFolders = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bAllFolders;
|
|
}
|
|
|
|
/**
|
|
* @return true if every single selected group is parented.
|
|
*/
|
|
bool FMatinee::AreAllSelectedGroupsParented() const
|
|
{
|
|
// Assume true until we find the first group to not be parented.
|
|
bool bAllGroupsAreParented = true;
|
|
|
|
for( FSelectedGroupConstIterator GroupIt(GetSelectedGroupIterator()); GroupIt; ++GroupIt )
|
|
{
|
|
if( !(*GroupIt)->bIsParented )
|
|
{
|
|
// We found a group that is not parented.
|
|
// We can exit the loop now.
|
|
bAllGroupsAreParented = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bAllGroupsAreParented;
|
|
}
|
|
|
|
/**
|
|
* @param TrackClass The class to check against each selected track.
|
|
* @return true if every single selected track is of the given UClass; false, otherwise.
|
|
*/
|
|
bool FMatinee::AreAllSelectedTracksOfClass( const UClass* TrackClass ) const
|
|
{
|
|
bool bResult = true;
|
|
|
|
for( FSelectedTrackConstIterator TrackIt(GetSelectedTrackIterator()); TrackIt; ++TrackIt )
|
|
{
|
|
if( !(*TrackIt)->IsA(TrackClass) )
|
|
{
|
|
bResult = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bResult;
|
|
}
|
|
|
|
/**
|
|
* @param OwningGroup The group to check against each selected track.
|
|
* @return true if every single selected track is of owned by the given group; false, otherwise.
|
|
*/
|
|
bool FMatinee::AreAllSelectedTracksFromGroup( const UInterpGroup* OwningGroup ) const
|
|
{
|
|
bool bResult = true;
|
|
|
|
for( FSelectedTrackConstIterator TrackIt(GetSelectedTrackIterator()); TrackIt; ++TrackIt )
|
|
{
|
|
if( !(TrackIt.GetGroup() == OwningGroup) )
|
|
{
|
|
bResult = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bResult;
|
|
}
|
|
|
|
/**
|
|
* @return The number of the selected groups.
|
|
*/
|
|
int32 FMatinee::GetSelectedGroupCount() const
|
|
{
|
|
int32 SelectedGroupCount = 0;
|
|
|
|
for( FSelectedGroupConstIterator GroupIt(GetSelectedGroupIterator()); GroupIt; ++GroupIt )
|
|
{
|
|
SelectedGroupCount++;
|
|
}
|
|
|
|
return SelectedGroupCount;
|
|
}
|
|
|
|
/**
|
|
* @return The number of selected tracks.
|
|
*/
|
|
int32 FMatinee::GetSelectedTrackCount() const
|
|
{
|
|
int32 SelectedTracksTotal = 0;
|
|
|
|
for( FSelectedTrackConstIterator TrackIt(GetSelectedTrackIterator()); TrackIt; ++TrackIt )
|
|
{
|
|
SelectedTracksTotal++;
|
|
}
|
|
|
|
return SelectedTracksTotal;
|
|
}
|
|
|
|
/**
|
|
* Utility function for gathering all the selected tracks into a TArray.
|
|
*
|
|
* @param OutSelectedTracks [out] An array of all interp tracks that are currently-selected.
|
|
*/
|
|
void FMatinee::GetSelectedTracks( TArray<UInterpTrack*>& OutTracks )
|
|
{
|
|
// Make sure there aren't any existing items in the array in case they are non-selected tracks.
|
|
OutTracks.Empty();
|
|
|
|
for( FSelectedTrackIterator TrackIt(GetSelectedTrackIterator()); TrackIt; ++TrackIt )
|
|
{
|
|
OutTracks.Add(*TrackIt);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Utility function for gathering all the selected groups into a TArray.
|
|
*
|
|
* @param OutSelectedGroups [out] An array of all interp groups that are currently-selected.
|
|
*/
|
|
void FMatinee::GetSelectedGroups( TArray<UInterpGroup*>& OutSelectedGroups )
|
|
{
|
|
// Make sure there aren't any existing items in the array in case they are non-selected groups.
|
|
OutSelectedGroups.Empty();
|
|
|
|
for( FSelectedGroupIterator GroupIt(GetSelectedGroupIterator()); GroupIt; ++GroupIt )
|
|
{
|
|
OutSelectedGroups.Add(*GroupIt);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Selects the interp track at the given index in the given group's array of interp tracks.
|
|
* If the track is already selected, this function will do nothing.
|
|
*
|
|
* @param OwningGroup The group that stores the interp track to be selected. Cannot be NULL.
|
|
* @param TrackToSelect The interp track to select. Cannot be NULL
|
|
* @param bDeselectPreviousTracks If true, then all previously-selected tracks will be deselected. Defaults to true.
|
|
*/
|
|
void FMatinee::SelectTrack( UInterpGroup* OwningGroup, UInterpTrack* TrackToSelect, bool bDeselectPreviousTracks /*= true*/ )
|
|
{
|
|
check( OwningGroup && TrackToSelect );
|
|
|
|
const bool bTrackAlreadySelected = TrackToSelect->IsSelected();
|
|
const bool bWantsOtherTracksDeselected = ( bDeselectPreviousTracks && ( GetSelectedTrackCount() > 1 ) );
|
|
|
|
// Early out if we already have the track selected or if there are multiple
|
|
// tracks and the user does not want all but the given track selected.
|
|
if( bTrackAlreadySelected && !bWantsOtherTracksDeselected )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// By default, the previously-selected tracks should be deselected. However, the client
|
|
// code has the option of not deselecting, especially when multi-selecting tracks.
|
|
if( bDeselectPreviousTracks )
|
|
{
|
|
DeselectAllTracks();
|
|
}
|
|
|
|
// By selecting a track, we must deselect all selected groups.
|
|
// We can only have one or the other.
|
|
DeselectAllGroups(false);
|
|
|
|
// Select the track (prior to selecting the actor)
|
|
TrackToSelect->SetSelected( true );
|
|
|
|
// Update the preview camera now the track has been selected
|
|
UpdatePreviewCamera( TrackToSelect );
|
|
|
|
// Update the actor selection based on the new track selection
|
|
UpdateActorSelection();
|
|
|
|
// Update the property window to reflect the properties of the selected track.
|
|
UpdatePropertyWindow();
|
|
|
|
// Highlight the selected curve.
|
|
IData->CurveEdSetup->ChangeCurveColor(TrackToSelect, SelectedCurveColor);
|
|
CurveEd->RefreshViewport();
|
|
}
|
|
|
|
|
|
/**
|
|
* Selects the given group.
|
|
*
|
|
* @param GroupToSelect The desired group to select. Cannot be NULL.
|
|
* @param bDeselectPreviousGroups If true, then all previously-selected groups will be deselected. Defaults to true.
|
|
*/
|
|
void FMatinee::SelectGroup( UInterpGroup* GroupToSelect, bool bDeselectPreviousGroups /*= true*/, bool bSelectGroupActors /*= true*/ )
|
|
{
|
|
// Must be a valid interp group.
|
|
check( GroupToSelect );
|
|
|
|
// First, deselect the previously-selected groups by default. The client code has
|
|
// the option to prevent this, especially for case such as multi-group select.
|
|
if( bDeselectPreviousGroups )
|
|
{
|
|
DeselectAllGroups(false);
|
|
}
|
|
|
|
// By selecting a group, we must deselect any selected tracks.
|
|
DeselectAllTracks(false);
|
|
|
|
// Select the group (prior to selecting the actor)
|
|
GroupToSelect->SetSelected( true );
|
|
|
|
// Update the preview camera now the group has been selected
|
|
UpdatePreviewCamera( GroupToSelect );
|
|
|
|
if (bSelectGroupActors)
|
|
{
|
|
// Update the actor selection based on the new group selection
|
|
UpdateActorSelection();
|
|
}
|
|
|
|
// Update the property window according to the new selection.
|
|
UpdatePropertyWindow();
|
|
|
|
// Dirty the display
|
|
InvalidateTrackWindowViewports();
|
|
}
|
|
|
|
/**
|
|
* Deselects the interp track store in the given group at the given array index.
|
|
*
|
|
* @param OwningGroup The group that stores the interp track to be deselected. Cannot be NULL.
|
|
* @param TrackToDeselect The track to deslect. Cannot be NULL
|
|
* @param bUpdateVisuals If true, then all affected visual components related to track selections will be updated. Defaults to true.
|
|
*/
|
|
void FMatinee::DeselectTrack( UInterpGroup* OwningGroup, UInterpTrack* TrackToDeselect, bool bUpdateVisuals /*= true*/ )
|
|
{
|
|
check( OwningGroup && TrackToDeselect );
|
|
|
|
TrackToDeselect->SetSelected( false );
|
|
|
|
// Update the preview camera now the track has been deselected
|
|
UpdatePreviewCamera( TrackToDeselect );
|
|
|
|
// The client code has the option of opting out of updating the
|
|
// visual components that are affected by selecting tracks.
|
|
if( bUpdateVisuals )
|
|
{
|
|
// Update the curve corresponding to this track
|
|
IData->CurveEdSetup->ChangeCurveColor( TrackToDeselect, OwningGroup->GroupColor );
|
|
CurveEd->RefreshViewport();
|
|
|
|
// Update the actor selection based on the new track selection
|
|
UpdateActorSelection();
|
|
|
|
// Update the property window to reflect the properties of the selected track.
|
|
UpdatePropertyWindow();
|
|
}
|
|
|
|
// Clear any keys related to this track.
|
|
ClearKeySelectionForTrack( OwningGroup, TrackToDeselect, false );
|
|
|
|
// Always invalidate track windows
|
|
InvalidateTrackWindowViewports();
|
|
}
|
|
|
|
/**
|
|
* Deselects every selected track.
|
|
*
|
|
* @param bUpdateVisuals If true, then all affected visual components related to track selections will be updated. Defaults to true.
|
|
*/
|
|
void FMatinee::DeselectAllTracks( bool bUpdateVisuals /*= true*/ )
|
|
{
|
|
// Deselect all selected tracks and remove their matching curves.
|
|
for( FSelectedTrackIterator TrackIt(GetSelectedTrackIterator()); TrackIt; ++TrackIt )
|
|
{
|
|
UInterpTrack* CurrentTrack = *TrackIt;
|
|
IData->CurveEdSetup->ChangeCurveColor(CurrentTrack, TrackIt.GetGroup()->GroupColor);
|
|
CurrentTrack->SetSelected( false );
|
|
|
|
// Update the preview camera now the track has been deselected
|
|
UpdatePreviewCamera( CurrentTrack );
|
|
}
|
|
|
|
// The client code has the option of opting out of updating the
|
|
// visual components that are affected by selecting tracks.
|
|
if( bUpdateVisuals )
|
|
{
|
|
// Update the curve editor to reflect the curve color change
|
|
CurveEd->RefreshViewport();
|
|
|
|
// Update the actor selection based on the new track selection
|
|
UpdateActorSelection();
|
|
|
|
// Make sure there is nothing selected in the property
|
|
// window or in the level editing viewports.
|
|
UpdatePropertyWindow();
|
|
}
|
|
|
|
// Make sure all keys are cleared!
|
|
ClearKeySelection();
|
|
}
|
|
|
|
/**
|
|
* Deselects the given group.
|
|
*
|
|
* @param GroupToDeselect The desired group to deselect.
|
|
* @param bUpdateVisuals If true, then all affected visual components related to group selections will be updated. Defaults to true.
|
|
*/
|
|
void FMatinee::DeselectGroup( UInterpGroup* GroupToDeselect, bool bUpdateVisuals /*= true*/ )
|
|
{
|
|
GroupToDeselect->SetSelected( false );
|
|
|
|
// Update the preview camera now the group has been deselected
|
|
UpdatePreviewCamera( GroupToDeselect );
|
|
|
|
// The client code has the option of opting out of updating the
|
|
// visual components that are affected by selecting groups.
|
|
if( bUpdateVisuals )
|
|
{
|
|
// Update the actor selection based on the new group selection
|
|
UpdateActorSelection();
|
|
|
|
// Make sure there is nothing selected in the property window
|
|
UpdatePropertyWindow();
|
|
|
|
// Request an update of the track windows
|
|
InvalidateTrackWindowViewports();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deselects all selected groups.
|
|
*
|
|
* @param bUpdateVisuals If true, then all affected visual components related to group selections will be updated. Defaults to true.
|
|
*/
|
|
void FMatinee::DeselectAllGroups( bool bUpdateVisuals /*= true*/ )
|
|
{
|
|
for( FSelectedGroupIterator GroupIt(GetSelectedGroupIterator()); GroupIt; ++GroupIt )
|
|
{
|
|
UInterpGroup* CurrentGroup = *GroupIt;
|
|
CurrentGroup->SetSelected( false );
|
|
|
|
// Update the preview camera now the group has been deselected
|
|
UpdatePreviewCamera( CurrentGroup );
|
|
}
|
|
|
|
// The client code has the option of opting out of updating the
|
|
// visual components that are affected by selecting groups.
|
|
if( bUpdateVisuals )
|
|
{
|
|
// Update the actor selection based on the new group selection
|
|
UpdateActorSelection();
|
|
|
|
// Update the property window to reflect the group deselection
|
|
UpdatePropertyWindow();
|
|
|
|
// Request an update of the track windows
|
|
InvalidateTrackWindowViewports();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deselects all selected groups or selected tracks.
|
|
*
|
|
* @param bUpdateVisuals If true, then all affected visual components related to group selections will be updated. Defaults to true.
|
|
*/
|
|
void FMatinee::DeselectAll( bool bUpdateVisuals /*= true*/ )
|
|
{
|
|
// We either have one-to-many groups selected or one-to-many tracks selected.
|
|
// So, we need to check which one it is.
|
|
if( HasAGroupSelected() )
|
|
{
|
|
DeselectAllGroups(bUpdateVisuals);
|
|
}
|
|
else if( HasATrackSelected() )
|
|
{
|
|
DeselectAllTracks(bUpdateVisuals);
|
|
}
|
|
}
|
|
|
|
void FMatinee::UpdateActorSelection() const
|
|
{
|
|
// Ignore this selection notification if desired.
|
|
if ( AMatineeActor::IgnoreActorSelection() )
|
|
{
|
|
return;
|
|
}
|
|
|
|
AMatineeActor::PushIgnoreActorSelection();
|
|
|
|
GUnrealEd->SelectNone( true, true );
|
|
|
|
// Loop through the instances rather than the groups themselves so that we select all the actors associated with a selected group
|
|
for( auto GroupInstIt = MatineeActor->GroupInst.CreateConstIterator(); GroupInstIt; ++GroupInstIt )
|
|
{
|
|
UInterpGroupInst* const GroupInst = *GroupInstIt;
|
|
UInterpGroup* const CurrentGroup = GroupInst->Group;
|
|
if( CurrentGroup->IsSelected() || CurrentGroup->HasSelectedTracks() )
|
|
{
|
|
const bool bDeselectActors = false;
|
|
CurrentGroup->SelectGroupActor( GroupInst, bDeselectActors );
|
|
}
|
|
}
|
|
|
|
AMatineeActor::PopIgnoreActorSelection();
|
|
}
|
|
|
|
void FMatinee::ClearKeySelection()
|
|
{
|
|
Opt->SelectedKeys.Empty();
|
|
Opt->bAdjustingKeyframe = false;
|
|
Opt->bAdjustingGroupKeyframes = false;
|
|
|
|
// Dirty the track window viewports
|
|
InvalidateTrackWindowViewports();
|
|
}
|
|
|
|
/**
|
|
* Clears all selected key of a given track.
|
|
*
|
|
* @param OwningGroup The group that owns the track containing the keys.
|
|
* @param Track The track holding the keys to clear.
|
|
* @param bInvalidateDisplay Sets the Matinee track viewport to refresh (Defaults to true).
|
|
*/
|
|
void FMatinee::ClearKeySelectionForTrack( UInterpGroup* OwningGroup, UInterpTrack* Track, bool bInvalidateDisplay )
|
|
{
|
|
for( int32 SelectedKeyIndex = 0; SelectedKeyIndex < Opt->SelectedKeys.Num(); SelectedKeyIndex++ )
|
|
{
|
|
// Remove key selections only for keys matching the given group and track index.
|
|
if( (Opt->SelectedKeys[SelectedKeyIndex].Group == OwningGroup) && (Opt->SelectedKeys[SelectedKeyIndex].Track == Track) )
|
|
{
|
|
Opt->SelectedKeys.RemoveAt(SelectedKeyIndex--);
|
|
}
|
|
}
|
|
|
|
// If there are no more keys selected, then the user is not adjusting keyframes anymore.
|
|
Opt->bAdjustingKeyframe = (Opt->SelectedKeys.Num() == 1);
|
|
Opt->bAdjustingGroupKeyframes = (Opt->SelectedKeys.Num() > 1);
|
|
|
|
// Dirty the track window viewports
|
|
if( bInvalidateDisplay )
|
|
{
|
|
InvalidateTrackWindowViewports();
|
|
}
|
|
}
|
|
|
|
void FMatinee::AddKeyToSelection(UInterpGroup* InGroup, UInterpTrack* InTrack, int32 InKeyIndex, bool bAutoWind)
|
|
{
|
|
check(InGroup);
|
|
check(InTrack);
|
|
|
|
check( InKeyIndex >= 0 && InKeyIndex < InTrack->GetNumKeyframes() );
|
|
|
|
// If the sequence is currently playing, stop it before selecting the key.
|
|
// This check is necessary because calling StopPlaying if playback is stopped will zero
|
|
// the playback position, which we don't want to do.
|
|
if ( MatineeActor->bIsPlaying )
|
|
{
|
|
StopPlaying();
|
|
}
|
|
|
|
// If key is not already selected, add to selection set.
|
|
if( !KeyIsInSelection(InGroup, InTrack, InKeyIndex) )
|
|
{
|
|
// Add to array of selected keys.
|
|
Opt->SelectedKeys.Add( FInterpEdSelKey(InGroup, InTrack, InKeyIndex) );
|
|
}
|
|
|
|
// If this is the first and only keyframe selected, make track active and wind to it.
|
|
if(Opt->SelectedKeys.Num() == 1 && bAutoWind)
|
|
{
|
|
float KeyTime = InTrack->GetKeyframeTime(InKeyIndex);
|
|
SetInterpPosition(KeyTime);
|
|
|
|
// When jumping to keyframe, update the pivot so the widget is in the right place.
|
|
UInterpGroupInst* GrInst = MatineeActor->FindFirstGroupInst(InGroup);
|
|
if(GrInst)
|
|
{
|
|
AActor* GrActor = GrInst->GetGroupActor();
|
|
if (GrActor)
|
|
{
|
|
GEditor->SetPivot( GrActor->GetActorLocation(), false, true );
|
|
}
|
|
}
|
|
|
|
Opt->bAdjustingKeyframe = true;
|
|
}
|
|
|
|
if(Opt->SelectedKeys.Num() != 1)
|
|
{
|
|
Opt->bAdjustingKeyframe = false;
|
|
}
|
|
|
|
// Dirty the track window viewports
|
|
InvalidateTrackWindowViewports();
|
|
}
|
|
|
|
void FMatinee::RemoveKeyFromSelection(UInterpGroup* InGroup, UInterpTrack* InTrack, int32 InKeyIndex)
|
|
{
|
|
for(int32 i=0; i<Opt->SelectedKeys.Num(); i++)
|
|
{
|
|
if( Opt->SelectedKeys[i].Group == InGroup &&
|
|
Opt->SelectedKeys[i].Track == InTrack &&
|
|
Opt->SelectedKeys[i].KeyIndex == InKeyIndex )
|
|
{
|
|
Opt->SelectedKeys.RemoveAt(i);
|
|
|
|
// If there are no more keys selected, then the user is not adjusting keyframes anymore.
|
|
Opt->bAdjustingKeyframe = (Opt->SelectedKeys.Num() == 1);
|
|
Opt->bAdjustingGroupKeyframes = (Opt->SelectedKeys.Num() > 1);
|
|
|
|
// Dirty the track window viewports
|
|
InvalidateTrackWindowViewports();
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FMatinee::KeyIsInSelection(UInterpGroup* InGroup, UInterpTrack* InTrack, int32 InKeyIndex)
|
|
{
|
|
for(int32 i=0; i<Opt->SelectedKeys.Num(); i++)
|
|
{
|
|
if( Opt->SelectedKeys[i].Group == InGroup &&
|
|
Opt->SelectedKeys[i].Track == InTrack &&
|
|
Opt->SelectedKeys[i].KeyIndex == InKeyIndex )
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** Clear selection and then select all keys within the gree loop-section. */
|
|
void FMatinee::SelectKeysInLoopSection()
|
|
{
|
|
ClearKeySelection();
|
|
|
|
// Add keys that are within current section to selection
|
|
for(int32 i=0; i<IData->InterpGroups.Num(); i++)
|
|
{
|
|
UInterpGroup* Group = IData->InterpGroups[i];
|
|
for(int32 j=0; j<Group->InterpTracks.Num(); j++)
|
|
{
|
|
UInterpTrack* Track = Group->InterpTracks[j];
|
|
Track->Modify();
|
|
|
|
for(int32 k=0; k<Track->GetNumKeyframes(); k++)
|
|
{
|
|
// Add keys in section to selection for deletion.
|
|
float KeyTime = Track->GetKeyframeTime(k);
|
|
if(KeyTime >= IData->EdSectionStart && KeyTime <= IData->EdSectionEnd)
|
|
{
|
|
// Add to selection for deletion.
|
|
AddKeyToSelection(Group, Track, k, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Calculate the start and end of the range of the selected keys. */
|
|
void FMatinee::CalcSelectedKeyRange(float& OutStartTime, float& OutEndTime)
|
|
{
|
|
if(Opt->SelectedKeys.Num() == 0)
|
|
{
|
|
OutStartTime = 0.f;
|
|
OutEndTime = 0.f;
|
|
}
|
|
else
|
|
{
|
|
OutStartTime = BIG_NUMBER;
|
|
OutEndTime = -BIG_NUMBER;
|
|
|
|
for(int32 i=0; i<Opt->SelectedKeys.Num(); i++)
|
|
{
|
|
UInterpTrack* Track = Opt->SelectedKeys[i].Track;
|
|
float KeyTime = Track->GetKeyframeTime( Opt->SelectedKeys[i].KeyIndex );
|
|
|
|
OutStartTime = FMath::Min(KeyTime, OutStartTime);
|
|
OutEndTime = FMath::Max(KeyTime, OutEndTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
//Deletes keys if they are selected, otherwise will deleted selected tracks or groups
|
|
void FMatinee::DeleteSelection (void)
|
|
{
|
|
if (Opt->SelectedKeys.Num() > 0)
|
|
{
|
|
DeleteSelectedKeys(true);
|
|
}
|
|
else if (GetSelectedTrackCount() > 0)
|
|
{
|
|
DeleteSelectedTracks();
|
|
}
|
|
else if (GetSelectedGroupCount())
|
|
{
|
|
DeleteSelectedGroups();
|
|
}
|
|
}
|
|
|
|
void FMatinee::DeleteSelectedKeys(bool bDoTransaction)
|
|
{
|
|
if(bDoTransaction)
|
|
{
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT( "UnrealEd", "DeleteSelectedKeys", "Delete Selected Keys" ) );
|
|
MatineeActor->Modify();
|
|
Opt->Modify();
|
|
}
|
|
|
|
TArray<UInterpTrack*> ModifiedTracks;
|
|
|
|
bool bRemovedEventKeys = false;
|
|
for(int32 i=0; i<Opt->SelectedKeys.Num(); i++)
|
|
{
|
|
FInterpEdSelKey& SelKey = Opt->SelectedKeys[i];
|
|
UInterpTrack* Track = SelKey.Track;
|
|
|
|
check(Track);
|
|
check(SelKey.KeyIndex >= 0 && SelKey.KeyIndex < Track->GetNumKeyframes());
|
|
|
|
if(bDoTransaction)
|
|
{
|
|
// If not already done so, call Modify on this track now.
|
|
if( !ModifiedTracks.Contains(Track) )
|
|
{
|
|
Track->Modify();
|
|
ModifiedTracks.Add(Track);
|
|
}
|
|
}
|
|
|
|
|
|
FName OldKeyName = NAME_None;
|
|
if(const UInterpTrackEvent* TrackEvent = Cast< UInterpTrackEvent >( Track ))
|
|
{
|
|
// If this is an event key - we update the connectors later.
|
|
bRemovedEventKeys = true;
|
|
|
|
|
|
// Take a copy of the key name before it's removed
|
|
if( TrackEvent->EventTrack.IsValidIndex( SelKey.KeyIndex ))
|
|
{
|
|
OldKeyName = TrackEvent->EventTrack[SelKey.KeyIndex].EventName;
|
|
}
|
|
}
|
|
|
|
|
|
Track->RemoveKeyframe(SelKey.KeyIndex);
|
|
|
|
|
|
// If we have a valid name, check to see if it's last event key to be removed with this name?
|
|
if( !OldKeyName.IsNone() )
|
|
{
|
|
const bool bCommonName = IData->IsEventName( OldKeyName );
|
|
if( !bCommonName )
|
|
{
|
|
// Fire a delegate so other places that use the name can also update
|
|
TArray<FName> KeyNames;
|
|
KeyNames.Add( OldKeyName );
|
|
FMatineeDelegates::Get().OnEventKeyframeRemoved.Broadcast( MatineeActor, KeyNames );
|
|
}
|
|
}
|
|
|
|
|
|
// If any other keys in the selection are on the same track but after the one we just deleted, decrement the index to correct it.
|
|
for(int32 j=0; j<Opt->SelectedKeys.Num(); j++)
|
|
{
|
|
if( Opt->SelectedKeys[j].Group == SelKey.Group &&
|
|
Opt->SelectedKeys[j].Track == SelKey.Track &&
|
|
Opt->SelectedKeys[j].KeyIndex > SelKey.KeyIndex &&
|
|
j != i)
|
|
{
|
|
Opt->SelectedKeys[j].KeyIndex--;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update positions at current time, in case removal of the key changed things.
|
|
RefreshInterpPosition();
|
|
|
|
// Select no keyframe.
|
|
ClearKeySelection();
|
|
|
|
if(bDoTransaction)
|
|
{
|
|
InterpEdTrans->EndSpecial();
|
|
}
|
|
|
|
// Make sure the curve editor is in sync
|
|
CurveEd->CurveChanged();
|
|
}
|
|
|
|
void FMatinee::DuplicateSelectedKeys()
|
|
{
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT( "UnrealEd", "DuplicateSelectedKeys", "Duplicate Selected Keys" ) );
|
|
MatineeActor->Modify();
|
|
Opt->Modify();
|
|
|
|
TArray<UInterpTrack*> ModifiedTracks;
|
|
|
|
for(int32 i=0; i<Opt->SelectedKeys.Num(); i++)
|
|
{
|
|
FInterpEdSelKey& SelKey = Opt->SelectedKeys[i];
|
|
UInterpTrack* Track = SelKey.Track;
|
|
|
|
check(Track);
|
|
check(SelKey.KeyIndex >= 0 && SelKey.KeyIndex < Track->GetNumKeyframes());
|
|
|
|
// If not already done so, call Modify on this track now.
|
|
if( !ModifiedTracks.Contains(Track) )
|
|
{
|
|
Track->Modify();
|
|
ModifiedTracks.Add(Track);
|
|
}
|
|
|
|
float CurrentKeyTime = Track->GetKeyframeTime(SelKey.KeyIndex);
|
|
float NewKeyTime = CurrentKeyTime + (float)DuplicateKeyOffset/PixelsPerSec;
|
|
|
|
int32 DupKeyIndex = Track->DuplicateKeyframe(SelKey.KeyIndex, NewKeyTime);
|
|
|
|
// Change selection to select the new keyframe instead.
|
|
SelKey.KeyIndex = DupKeyIndex;
|
|
|
|
// If any other keys in the selection are on the same track but after the new key, increase the index to correct it.
|
|
for(int32 j=0; j<Opt->SelectedKeys.Num(); j++)
|
|
{
|
|
if( Opt->SelectedKeys[j].Group == SelKey.Group &&
|
|
Opt->SelectedKeys[j].Track == SelKey.Track &&
|
|
Opt->SelectedKeys[j].KeyIndex >= DupKeyIndex &&
|
|
j != i)
|
|
{
|
|
Opt->SelectedKeys[j].KeyIndex++;
|
|
}
|
|
}
|
|
}
|
|
|
|
InterpEdTrans->EndSpecial();
|
|
}
|
|
|
|
/** Adjust the view so the entire sequence fits into the viewport. */
|
|
void FMatinee::ViewFitSequence()
|
|
{
|
|
ViewStartTime = 0.f;
|
|
ViewEndTime = IData->InterpLength;
|
|
|
|
CurveEd->FitViewVertically();
|
|
SyncCurveEdView();
|
|
}
|
|
|
|
/** Adjust the view so the selected keys fit into the viewport. */
|
|
void FMatinee::ViewFitToSelected()
|
|
{
|
|
if( Opt->SelectedKeys.Num() > 0 )
|
|
{
|
|
float NewStartTime = BIG_NUMBER;
|
|
float NewEndTime = -BIG_NUMBER;
|
|
|
|
for( int32 CurKeyIndex = 0; CurKeyIndex < Opt->SelectedKeys.Num(); ++CurKeyIndex )
|
|
{
|
|
FInterpEdSelKey& CurSelKey = Opt->SelectedKeys[ CurKeyIndex ];
|
|
|
|
UInterpTrack* Track = CurSelKey.Track;
|
|
check( Track != NULL );
|
|
check( CurSelKey.KeyIndex >= 0 && CurSelKey.KeyIndex < Track->GetNumKeyframes() );
|
|
|
|
NewStartTime = FMath::Min( Track->GetKeyframeTime( CurSelKey.KeyIndex ), NewStartTime );
|
|
NewEndTime = FMath::Max( Track->GetKeyframeTime( CurSelKey.KeyIndex ), NewEndTime );
|
|
}
|
|
|
|
// Clamp the minimum size
|
|
if( NewStartTime - NewEndTime < 0.001f )
|
|
{
|
|
NewStartTime -= 0.005f;
|
|
NewEndTime += 0.005f;
|
|
}
|
|
|
|
ViewStartTime = NewStartTime;
|
|
ViewEndTime = NewEndTime;
|
|
|
|
CurveEd->FitViewVertically();
|
|
SyncCurveEdView();
|
|
}
|
|
}
|
|
|
|
/** Adjust the view so the looped section fits into the viewport. */
|
|
void FMatinee::ViewFitLoop()
|
|
{
|
|
// Do nothing if loop section is too small!
|
|
float LoopRange = IData->EdSectionEnd - IData->EdSectionStart;
|
|
if(LoopRange > 0.01f)
|
|
{
|
|
ViewStartTime = IData->EdSectionStart;
|
|
ViewEndTime = IData->EdSectionEnd;
|
|
|
|
SyncCurveEdView();
|
|
}
|
|
}
|
|
|
|
/** Adjust the view so the looped section fits into the entire sequence. */
|
|
void FMatinee::ViewFitLoopSequence()
|
|
{
|
|
// Adjust the looped section
|
|
IData->EdSectionStart = 0.0f;
|
|
IData->EdSectionEnd = IData->InterpLength;
|
|
|
|
// Adjust the view
|
|
ViewStartTime = IData->EdSectionStart;
|
|
ViewEndTime = IData->EdSectionEnd;
|
|
|
|
CurveEd->FitViewVertically();
|
|
SyncCurveEdView();
|
|
}
|
|
|
|
/** Move the view to the end of the currently selected track(s). */
|
|
void FMatinee::ViewEndOfTrack()
|
|
{
|
|
float NewEndTime = 0.0f;
|
|
|
|
if( GetSelectedTrackCount() > 0 )
|
|
{
|
|
FSelectedTrackIterator TrackIt(GetSelectedTrackIterator());
|
|
for( ; TrackIt; ++TrackIt )
|
|
{
|
|
UInterpTrack* Track = *TrackIt;
|
|
if (Track->GetTrackEndTime() > NewEndTime)
|
|
{
|
|
NewEndTime = Track->GetTrackEndTime();
|
|
}
|
|
}
|
|
}
|
|
else // If no track is selected, move to the end of the sequence
|
|
{
|
|
NewEndTime = IData->InterpLength;
|
|
}
|
|
|
|
ViewStartTime = NewEndTime - (ViewEndTime - ViewStartTime);
|
|
ViewEndTime = NewEndTime;
|
|
|
|
CurveEd->FitViewVertically();
|
|
SyncCurveEdView();
|
|
}
|
|
|
|
/** Adjust the view by the defined range. */
|
|
void FMatinee::ViewFit(float StartTime, float EndTime)
|
|
{
|
|
ViewStartTime = StartTime;
|
|
ViewEndTime = EndTime;
|
|
|
|
CurveEd->FitViewVertically();
|
|
SyncCurveEdView();
|
|
}
|
|
|
|
/** Iterate over keys changing their interpolation mode and adjusting tangents appropriately. */
|
|
void FMatinee::ChangeKeyInterpMode(EInterpCurveMode NewInterpMode/*=CIM_Unknown*/)
|
|
{
|
|
for(int32 i=0; i<Opt->SelectedKeys.Num(); i++)
|
|
{
|
|
FInterpEdSelKey& SelKey = Opt->SelectedKeys[i];
|
|
UInterpTrack* Track = SelKey.Track;
|
|
|
|
UInterpTrackMove* MoveTrack = Cast<UInterpTrackMove>(Track);
|
|
if(MoveTrack)
|
|
{
|
|
MoveTrack->PosTrack.Points[SelKey.KeyIndex].InterpMode = NewInterpMode;
|
|
MoveTrack->EulerTrack.Points[SelKey.KeyIndex].InterpMode = NewInterpMode;
|
|
|
|
MoveTrack->PosTrack.AutoSetTangents(MoveTrack->LinCurveTension);
|
|
MoveTrack->EulerTrack.AutoSetTangents(MoveTrack->AngCurveTension);
|
|
}
|
|
|
|
UInterpTrackFloatBase* FloatTrack = Cast<UInterpTrackFloatBase>(Track);
|
|
if(FloatTrack)
|
|
{
|
|
//Some FloatBase Types to not make use of FloatTrack (such as AnimControl).
|
|
//Only operate on those that do.
|
|
if (FloatTrack->FloatTrack.Points.Num())
|
|
{
|
|
FloatTrack->FloatTrack.Points[SelKey.KeyIndex].InterpMode = NewInterpMode;
|
|
|
|
FloatTrack->FloatTrack.AutoSetTangents(FloatTrack->CurveTension);
|
|
}
|
|
}
|
|
|
|
UInterpTrackVectorBase* VectorTrack = Cast<UInterpTrackVectorBase>(Track);
|
|
if(VectorTrack)
|
|
{
|
|
UInterpTrackSound* SoundTrack = Cast<UInterpTrackSound>(VectorTrack);
|
|
if (SoundTrack == NULL) // don't attempt to change interp in a vector track that is actually a sound track
|
|
{
|
|
VectorTrack->VectorTrack.Points[SelKey.KeyIndex].InterpMode = NewInterpMode;
|
|
|
|
VectorTrack->VectorTrack.AutoSetTangents(VectorTrack->CurveTension);
|
|
}
|
|
}
|
|
|
|
UInterpTrackLinearColorBase* LinearColorTrack = Cast<UInterpTrackLinearColorBase>(Track);
|
|
if(LinearColorTrack)
|
|
{
|
|
LinearColorTrack->LinearColorTrack.Points[SelKey.KeyIndex].InterpMode = NewInterpMode;
|
|
|
|
LinearColorTrack->LinearColorTrack.AutoSetTangents(LinearColorTrack->CurveTension);
|
|
}
|
|
}
|
|
|
|
CurveEd->RefreshViewport();
|
|
}
|
|
|
|
/** Increments the cursor or selected keys by 1 interval amount, as defined by the toolbar combo. */
|
|
void FMatinee::IncrementSelection()
|
|
{
|
|
bool bMoveMarker = true;
|
|
|
|
if(Opt->SelectedKeys.Num())
|
|
{
|
|
BeginMoveSelectedKeys();
|
|
{
|
|
MoveSelectedKeys(SnapAmount);
|
|
}
|
|
EndMoveSelectedKeys();
|
|
bMoveMarker = false;
|
|
}
|
|
|
|
|
|
// Move the interp marker if there are no keys selected.
|
|
if(bMoveMarker)
|
|
{
|
|
float StartTime = MatineeActor->InterpPosition;
|
|
if( bSnapToFrames && bSnapTimeToFrames )
|
|
{
|
|
StartTime = SnapTimeToNearestFrame( MatineeActor->InterpPosition );
|
|
}
|
|
|
|
SetInterpPosition( StartTime + SnapAmount );
|
|
}
|
|
}
|
|
|
|
/** Decrements the cursor or selected keys by 1 interval amount, as defined by the toolbar combo. */
|
|
void FMatinee::DecrementSelection()
|
|
{
|
|
bool bMoveMarker = true;
|
|
|
|
if(Opt->SelectedKeys.Num())
|
|
{
|
|
BeginMoveSelectedKeys();
|
|
{
|
|
MoveSelectedKeys(-SnapAmount);
|
|
}
|
|
EndMoveSelectedKeys();
|
|
bMoveMarker = false;
|
|
}
|
|
|
|
// Move the interp marker if there are no keys selected.
|
|
if(bMoveMarker)
|
|
{
|
|
float StartTime = MatineeActor->InterpPosition;
|
|
if( bSnapToFrames && bSnapTimeToFrames )
|
|
{
|
|
StartTime = SnapTimeToNearestFrame( MatineeActor->InterpPosition );
|
|
}
|
|
|
|
SetInterpPosition( StartTime - SnapAmount );
|
|
}
|
|
}
|
|
|
|
void FMatinee::SelectNextKey()
|
|
{
|
|
// Keyframe operations can only happen when only one track is selected
|
|
if( GetSelectedTrackCount() == 1 )
|
|
{
|
|
FSelectedTrackIterator TrackIt(GetSelectedTrackIterator());
|
|
UInterpTrack* Track = *TrackIt;
|
|
|
|
int32 NumKeys = Track->GetNumKeyframes();
|
|
|
|
if(NumKeys)
|
|
{
|
|
int32 i;
|
|
for(i=0; i < NumKeys-1 && Track->GetKeyframeTime(i) < (MatineeActor->InterpPosition + KINDA_SMALL_NUMBER); i++);
|
|
|
|
ClearKeySelection();
|
|
AddKeyToSelection(TrackIt.GetGroup(), Track, i, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FMatinee::SelectPreviousKey()
|
|
{
|
|
// Keyframe operations can only happen when only one track is selected
|
|
if( GetSelectedTrackCount() == 1 )
|
|
{
|
|
FSelectedTrackIterator TrackIt(GetSelectedTrackIterator());
|
|
UInterpTrack* Track = *TrackIt;
|
|
|
|
int32 NumKeys = Track->GetNumKeyframes();
|
|
|
|
if(NumKeys)
|
|
{
|
|
int32 i;
|
|
for(i=NumKeys-1; i > 0 && Track->GetKeyframeTime(i) > (MatineeActor->InterpPosition - KINDA_SMALL_NUMBER); i--);
|
|
|
|
ClearKeySelection();
|
|
AddKeyToSelection(TrackIt.GetGroup(), Track, i, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Turns snap on and off in Matinee. Updates state of snap button as well. */
|
|
void FMatinee::SetSnapEnabled(bool bInSnapEnabled)
|
|
{
|
|
bSnapEnabled = bInSnapEnabled;
|
|
|
|
if(bSnapToKeys)
|
|
{
|
|
CurveEd->SetInSnap(false, SnapAmount, bSnapToFrames);
|
|
}
|
|
else
|
|
{
|
|
CurveEd->SetInSnap(bSnapEnabled, SnapAmount, bSnapToFrames);
|
|
}
|
|
|
|
// Save to ini when it changes.
|
|
GConfig->SetBool( TEXT("Matinee"), TEXT("SnapEnabled"), bSnapEnabled, GEditorPerProjectIni );
|
|
}
|
|
|
|
|
|
/** Toggles snapping the current timeline position to 'frames' in Matinee. */
|
|
void FMatinee::SetSnapTimeToFrames( bool bInValue )
|
|
{
|
|
|
|
bSnapTimeToFrames = bInValue;
|
|
|
|
// Save to ini when it changes.
|
|
GConfig->SetBool( TEXT("Matinee"), TEXT("SnapTimeToFrames"), bSnapTimeToFrames, GEditorPerProjectIni );
|
|
|
|
// Go ahead and apply the change right now if we need to
|
|
if( IsInitialized() && bSnapToFrames && bSnapTimeToFrames )
|
|
{
|
|
SetInterpPosition( SnapTimeToNearestFrame( MatineeActor->InterpPosition ) );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Toggles fixed time step mode */
|
|
void FMatinee::SetFixedTimeStepPlayback( bool bInValue )
|
|
{
|
|
bFixedTimeStepPlayback = bInValue;
|
|
|
|
// Save to ini when it changes.
|
|
GConfig->SetBool( TEXT("Matinee"), TEXT("FixedTimeStepPlayback"), bFixedTimeStepPlayback, GEditorPerProjectIni );
|
|
|
|
// Update fixed time step state
|
|
UpdateFixedTimeStepPlayback();
|
|
}
|
|
|
|
|
|
|
|
/** Updates 'fixed time step' mode based on current playback state and user preferences */
|
|
void FMatinee::UpdateFixedTimeStepPlayback()
|
|
{
|
|
// Turn on 'benchmarking' mode if we're using a fixed time step
|
|
bool bIsBenchmarking = MatineeActor->bIsPlaying && bSnapToFrames && bFixedTimeStepPlayback;
|
|
FApp::SetBenchmarking(bIsBenchmarking);
|
|
|
|
// Set the time interval between fixed ticks
|
|
FApp::SetFixedDeltaTime(SnapAmount);
|
|
}
|
|
|
|
|
|
|
|
/** Toggles 'prefer frame numbers' setting */
|
|
void FMatinee::SetPreferFrameNumbers( bool bInValue )
|
|
{
|
|
bPreferFrameNumbers = bInValue;
|
|
|
|
// Save to ini when it changes.
|
|
GConfig->SetBool( TEXT("Matinee"), TEXT("PreferFrameNumbers"), bPreferFrameNumbers, GEditorPerProjectIni );
|
|
}
|
|
|
|
|
|
|
|
/** Toggles 'show time cursor pos for all keys' setting */
|
|
void FMatinee::SetShowTimeCursorPosForAllKeys( bool bInValue )
|
|
{
|
|
bShowTimeCursorPosForAllKeys = bInValue;
|
|
|
|
// Save to ini when it changes.
|
|
GConfig->SetBool( TEXT("Matinee"), TEXT("ShowTimeCursorPosForAllKeys"), bShowTimeCursorPosForAllKeys, GEditorPerProjectIni );
|
|
}
|
|
|
|
|
|
|
|
/** Snaps the specified time value to the closest frame */
|
|
float FMatinee::SnapTimeToNearestFrame( float InTime ) const
|
|
{
|
|
// Compute the new time value by rounding
|
|
const int32 InterpPositionInFrames = FMath::RoundToInt( InTime / SnapAmount );
|
|
const float NewTime = InterpPositionInFrames * SnapAmount;
|
|
|
|
return NewTime;
|
|
}
|
|
|
|
|
|
|
|
/** Take the InTime and snap it to the current SnapAmount. Does nothing if bSnapEnabled is false */
|
|
float FMatinee::SnapTime(float InTime, bool bIgnoreSelectedKeys)
|
|
{
|
|
if(bSnapEnabled)
|
|
{
|
|
if(bSnapToKeys)
|
|
{
|
|
// Iterate over all tracks finding the closest snap position to the supplied time.
|
|
|
|
bool bFoundSnap = false;
|
|
float BestSnapPos = 0.f;
|
|
float BestSnapDist = BIG_NUMBER;
|
|
for(int32 i=0; i<IData->InterpGroups.Num(); i++)
|
|
{
|
|
UInterpGroup* Group = IData->InterpGroups[i];
|
|
for(int32 j=0; j<Group->InterpTracks.Num(); j++)
|
|
{
|
|
UInterpTrack* Track = Group->InterpTracks[j];
|
|
|
|
// If we are ignoring selected keys - build an array of the indices of selected keys on this track.
|
|
TArray<int32> IgnoreKeys;
|
|
if(bIgnoreSelectedKeys)
|
|
{
|
|
for(int32 SelIndex=0; SelIndex<Opt->SelectedKeys.Num(); SelIndex++)
|
|
{
|
|
if( Opt->SelectedKeys[SelIndex].Group == Group &&
|
|
Opt->SelectedKeys[SelIndex].Track == Track )
|
|
{
|
|
IgnoreKeys.AddUnique( Opt->SelectedKeys[SelIndex].KeyIndex );
|
|
}
|
|
}
|
|
}
|
|
|
|
float OutPos = 0.f;
|
|
bool bTrackSnap = Track->GetClosestSnapPosition(InTime, IgnoreKeys, OutPos);
|
|
if(bTrackSnap) // If we found a snap location
|
|
{
|
|
// See if its closer than the closest location so far.
|
|
float SnapDist = FMath::Abs(InTime - OutPos);
|
|
if(SnapDist < BestSnapDist)
|
|
{
|
|
BestSnapPos = OutPos;
|
|
BestSnapDist = SnapDist;
|
|
bFoundSnap = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find how close we have to get to snap, in 'time' instead of pixels.
|
|
float SnapTolerance = (float)KeySnapPixels/(float)PixelsPerSec;
|
|
|
|
// If we are close enough to snap position - do it.
|
|
if(bFoundSnap && (BestSnapDist < SnapTolerance))
|
|
{
|
|
bDrawSnappingLine = true;
|
|
SnappingLinePosition = BestSnapPos;
|
|
|
|
return BestSnapPos;
|
|
}
|
|
else
|
|
{
|
|
bDrawSnappingLine = false;
|
|
|
|
return InTime;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Don't draw snapping line when just snapping to grid.
|
|
bDrawSnappingLine = false;
|
|
|
|
return SnapTimeToNearestFrame( InTime );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bDrawSnappingLine = false;
|
|
|
|
return InTime;
|
|
}
|
|
}
|
|
|
|
void FMatinee::BeginMoveMarker()
|
|
{
|
|
if(GrabbedMarkerType == EMatineeMarkerType::ISM_SeqEnd)
|
|
{
|
|
UnsnappedMarkerPos = IData->InterpLength;
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT("UnrealEd", "MoveEndMarker", "Move End Marker") );
|
|
IData->Modify();
|
|
}
|
|
else if(GrabbedMarkerType == EMatineeMarkerType::ISM_LoopStart)
|
|
{
|
|
UnsnappedMarkerPos = IData->EdSectionStart;
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT("UnrealEd", "MoveLoopStartMarker", "Move Loop Start Marker") );
|
|
IData->Modify();
|
|
}
|
|
else if(GrabbedMarkerType == EMatineeMarkerType::ISM_LoopEnd)
|
|
{
|
|
UnsnappedMarkerPos = IData->EdSectionEnd;
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT("UnrealEd", "MoveLoopEndMarker", "Move Loop End Marker") );
|
|
IData->Modify();
|
|
}
|
|
}
|
|
|
|
void FMatinee::EndMoveMarker()
|
|
{
|
|
if( GrabbedMarkerType == EMatineeMarkerType::ISM_SeqEnd ||
|
|
GrabbedMarkerType == EMatineeMarkerType::ISM_LoopStart ||
|
|
GrabbedMarkerType == EMatineeMarkerType::ISM_LoopEnd)
|
|
{
|
|
InterpEdTrans->EndSpecial();
|
|
}
|
|
}
|
|
|
|
void FMatinee::SetInterpEnd(float NewInterpLength)
|
|
{
|
|
// Ensure non-negative end time.
|
|
IData->InterpLength = FMath::Max(NewInterpLength, 0.f);
|
|
|
|
CurveEd->SetEndMarker(true, IData->InterpLength);
|
|
|
|
// Ensure the current position is always inside the valid sequence area.
|
|
if(MatineeActor->InterpPosition > IData->InterpLength)
|
|
{
|
|
SetInterpPosition(IData->InterpLength);
|
|
}
|
|
|
|
// Ensure loop points are inside sequence.
|
|
IData->EdSectionStart = FMath::Clamp(IData->EdSectionStart, 0.f, IData->InterpLength);
|
|
IData->EdSectionEnd = FMath::Clamp(IData->EdSectionEnd, 0.f, IData->InterpLength);
|
|
CurveEd->SetRegionMarker(true, IData->EdSectionStart, IData->EdSectionEnd, RegionFillColor);
|
|
|
|
// Update the CameraAnim if necessary
|
|
AMatineeActorCameraAnim* const CamAnimMatineeActor = Cast<AMatineeActorCameraAnim>(MatineeActor);
|
|
if ( (CamAnimMatineeActor != nullptr) && (CamAnimMatineeActor->CameraAnim != nullptr) )
|
|
{
|
|
CamAnimMatineeActor->CameraAnim->AnimLength = IData->InterpLength;
|
|
}
|
|
}
|
|
|
|
void FMatinee::MoveLoopMarker(float NewMarkerPos, bool bIsStart)
|
|
{
|
|
if(bIsStart)
|
|
{
|
|
IData->EdSectionStart = NewMarkerPos;
|
|
IData->EdSectionEnd = FMath::Max(IData->EdSectionStart, IData->EdSectionEnd);
|
|
}
|
|
else
|
|
{
|
|
IData->EdSectionEnd = NewMarkerPos;
|
|
IData->EdSectionStart = FMath::Min(IData->EdSectionStart, IData->EdSectionEnd);
|
|
}
|
|
|
|
// Ensure loop points are inside sequence.
|
|
IData->EdSectionStart = FMath::Clamp(IData->EdSectionStart, 0.f, IData->InterpLength);
|
|
IData->EdSectionEnd = FMath::Clamp(IData->EdSectionEnd, 0.f, IData->InterpLength);
|
|
|
|
CurveEd->SetRegionMarker(true, IData->EdSectionStart, IData->EdSectionEnd, RegionFillColor);
|
|
}
|
|
|
|
void FMatinee::BeginMoveSelectedKeys()
|
|
{
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT("UnrealEd", "MoveSelectedKeys", "Move Selected Keys") );
|
|
Opt->Modify();
|
|
|
|
TArray<UInterpTrack*> ModifiedTracks;
|
|
for(int32 i=0; i<Opt->SelectedKeys.Num(); i++)
|
|
{
|
|
FInterpEdSelKey& SelKey = Opt->SelectedKeys[i];
|
|
|
|
UInterpTrack* Track = SelKey.Track;
|
|
check(Track);
|
|
|
|
// If not already done so, call Modify on this track now.
|
|
if( !ModifiedTracks.Contains(Track) )
|
|
{
|
|
Track->Modify();
|
|
ModifiedTracks.Add(Track);
|
|
}
|
|
|
|
SelKey.UnsnappedPosition = Track->GetKeyframeTime(SelKey.KeyIndex);
|
|
}
|
|
|
|
// When moving a key in time, turn off 'recording', so we dont end up assigning an objects location at one time to a key at another time.
|
|
Opt->bAdjustingKeyframe = false;
|
|
Opt->bAdjustingGroupKeyframes = false;
|
|
}
|
|
|
|
void FMatinee::EndMoveSelectedKeys()
|
|
{
|
|
InterpEdTrans->EndSpecial();
|
|
}
|
|
|
|
void FMatinee::MoveSelectedKeys(float DeltaTime)
|
|
{
|
|
for(int32 i=0; i<Opt->SelectedKeys.Num(); i++)
|
|
{
|
|
FInterpEdSelKey& SelKey = Opt->SelectedKeys[i];
|
|
|
|
UInterpTrack* Track = SelKey.Track;
|
|
check(Track);
|
|
|
|
SelKey.UnsnappedPosition += DeltaTime;
|
|
float NewTime = SnapTime(SelKey.UnsnappedPosition, true);
|
|
|
|
// Do nothing if already at target time.
|
|
if( Track->GetKeyframeTime(SelKey.KeyIndex) != NewTime )
|
|
{
|
|
int32 OldKeyIndex = SelKey.KeyIndex;
|
|
int32 NewKeyIndex = Track->SetKeyframeTime(SelKey.KeyIndex, NewTime);
|
|
SelKey.KeyIndex = NewKeyIndex;
|
|
|
|
// If the key changed index we need to search for any other selected keys on this track that may need their index adjusted because of this change.
|
|
int32 KeyMove = NewKeyIndex - OldKeyIndex;
|
|
if(KeyMove > 0)
|
|
{
|
|
for(int32 j=0; j<Opt->SelectedKeys.Num(); j++)
|
|
{
|
|
if( j == i ) // Don't look at one we just changed.
|
|
continue;
|
|
|
|
FInterpEdSelKey& TestKey = Opt->SelectedKeys[j];
|
|
if( TestKey.Track == SelKey.Track &&
|
|
TestKey.Group == SelKey.Group &&
|
|
TestKey.KeyIndex > OldKeyIndex &&
|
|
TestKey.KeyIndex <= NewKeyIndex)
|
|
{
|
|
TestKey.KeyIndex--;
|
|
}
|
|
}
|
|
}
|
|
else if(KeyMove < 0)
|
|
{
|
|
for(int32 j=0; j<Opt->SelectedKeys.Num(); j++)
|
|
{
|
|
if( j == i )
|
|
continue;
|
|
|
|
FInterpEdSelKey& TestKey = Opt->SelectedKeys[j];
|
|
if( TestKey.Track == SelKey.Track &&
|
|
TestKey.Group == SelKey.Group &&
|
|
TestKey.KeyIndex < OldKeyIndex &&
|
|
TestKey.KeyIndex >= NewKeyIndex)
|
|
{
|
|
TestKey.KeyIndex++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} // FOR each selected key
|
|
|
|
// Update positions at current time but with new keyframe times.
|
|
RefreshInterpPosition();
|
|
|
|
CurveEd->RefreshViewport();
|
|
}
|
|
|
|
|
|
void FMatinee::BeginDrag3DHandle(UInterpGroup* Group, int32 TrackIndex)
|
|
{
|
|
if(TrackIndex < 0 || TrackIndex >= Group->InterpTracks.Num())
|
|
{
|
|
return;
|
|
}
|
|
|
|
UInterpTrackMove* MoveTrack = Cast<UInterpTrackMove>( Group->InterpTracks[TrackIndex] );
|
|
if(MoveTrack)
|
|
{
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT("UnrealEd", "Drag3DTrajectoryHandle", "Drag 3D Trajectory Handle") );
|
|
MoveTrack->Modify();
|
|
bDragging3DHandle = true;
|
|
}
|
|
}
|
|
|
|
void FMatinee::Move3DHandle(UInterpGroup* Group, int32 TrackIndex, int32 KeyIndex, bool bArriving, const FVector& Delta)
|
|
{
|
|
if(!bDragging3DHandle)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if(TrackIndex < 0 || TrackIndex >= Group->InterpTracks.Num())
|
|
{
|
|
return;
|
|
}
|
|
|
|
UInterpTrackMove* MoveTrack = Cast<UInterpTrackMove>( Group->InterpTracks[TrackIndex] );
|
|
if(MoveTrack)
|
|
{
|
|
if(KeyIndex < 0 || KeyIndex >= MoveTrack->PosTrack.Points.Num())
|
|
{
|
|
return;
|
|
}
|
|
|
|
UInterpGroupInst* GrInst = MatineeActor->FindFirstGroupInst(Group);
|
|
check(GrInst);
|
|
check(GrInst->TrackInst.Num() == Group->InterpTracks.Num());
|
|
UInterpTrackInstMove* MoveInst = CastChecked<UInterpTrackInstMove>( GrInst->TrackInst[TrackIndex] );
|
|
|
|
FTransform RefTM = MoveTrack->GetMoveRefFrame( MoveInst );
|
|
FVector LocalDelta = RefTM.InverseTransformVector(Delta);
|
|
|
|
uint8 InterpMode = MoveTrack->PosTrack.Points[KeyIndex].InterpMode;
|
|
|
|
if(bArriving)
|
|
{
|
|
MoveTrack->PosTrack.Points[KeyIndex].ArriveTangent -= LocalDelta;
|
|
|
|
// If keeping tangents smooth, update the LeaveTangent
|
|
if(InterpMode != CIM_CurveBreak)
|
|
{
|
|
MoveTrack->PosTrack.Points[KeyIndex].LeaveTangent = MoveTrack->PosTrack.Points[KeyIndex].ArriveTangent;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MoveTrack->PosTrack.Points[KeyIndex].LeaveTangent += LocalDelta;
|
|
|
|
// If keeping tangents smooth, update the ArriveTangent
|
|
if(InterpMode != CIM_CurveBreak)
|
|
{
|
|
MoveTrack->PosTrack.Points[KeyIndex].ArriveTangent = MoveTrack->PosTrack.Points[KeyIndex].LeaveTangent;
|
|
}
|
|
}
|
|
|
|
// If adjusting an 'Auto' keypoint, switch it to 'User'
|
|
if(InterpMode == CIM_CurveAuto || InterpMode == CIM_CurveAutoClamped)
|
|
{
|
|
MoveTrack->PosTrack.Points[KeyIndex].InterpMode = CIM_CurveUser;
|
|
MoveTrack->EulerTrack.Points[KeyIndex].InterpMode = CIM_CurveUser;
|
|
}
|
|
|
|
// Update the curve editor to see curves change.
|
|
CurveEd->RefreshViewport();
|
|
}
|
|
}
|
|
|
|
void FMatinee::EndDrag3DHandle()
|
|
{
|
|
if(bDragging3DHandle)
|
|
{
|
|
InterpEdTrans->EndSpecial();
|
|
}
|
|
}
|
|
|
|
void FMatinee::MoveInitialPosition(const FVector& Delta, const FRotator& DeltaRot)
|
|
{
|
|
// If no movement track selected, do nothing.
|
|
if( !HasATrackSelected( UInterpTrackMove::StaticClass() ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
FRotationTranslationMatrix RotMatrix(DeltaRot, FVector(0));
|
|
FTranslationMatrix TransMatrix(Delta);
|
|
|
|
// Iterate only through selected movement tracks because those are the only relevant tracks.
|
|
for( TTrackClassTypeIterator<UInterpTrackMove> MoveTrackIter(GetSelectedTrackIterator<UInterpTrackMove>()); MoveTrackIter; ++MoveTrackIter )
|
|
{
|
|
// To move the initial position, we have to track down the interp
|
|
// track instance corresponding to the selected movement track.
|
|
|
|
UInterpGroupInst* GroupInst = MatineeActor->FindFirstGroupInst( MoveTrackIter.GetGroup() );
|
|
|
|
// Look for an instance of a movement track
|
|
for( int32 TrackInstIndex = 0; TrackInstIndex < GroupInst->TrackInst.Num(); TrackInstIndex++ )
|
|
{
|
|
UInterpTrackInstMove* MoveInst = Cast<UInterpTrackInstMove>(GroupInst->TrackInst[TrackInstIndex]);
|
|
|
|
if(MoveInst)
|
|
{
|
|
// Apply to reference frame of movement track.
|
|
|
|
FMatrix ResetTM = FRotationTranslationMatrix(MoveInst->ResetRotation, MoveInst->ResetLocation);
|
|
|
|
// Apply to reset information as well.
|
|
|
|
FVector ResetOrigin = ResetTM.GetOrigin();
|
|
ResetTM.SetOrigin(FVector(0));
|
|
ResetTM = ResetTM * RotMatrix;
|
|
ResetTM.SetOrigin(ResetOrigin);
|
|
ResetTM = ResetTM * TransMatrix;
|
|
|
|
MoveInst->ResetLocation = ResetTM.GetOrigin();
|
|
MoveInst->ResetRotation = ResetTM.Rotator();
|
|
}
|
|
}
|
|
}
|
|
|
|
RefreshInterpPosition();
|
|
|
|
// Dirty the track window viewports
|
|
InvalidateTrackWindowViewports();
|
|
}
|
|
|
|
// Small struct to help keep track of selected tracks
|
|
struct FSelectedTrackData
|
|
{
|
|
UInterpTrack* Track;
|
|
int32 SelectedIndex;
|
|
};
|
|
|
|
/**
|
|
* Adds a keyframe to the selected track.
|
|
*
|
|
* There must be one and only one track selected for a keyframe to be added.
|
|
*/
|
|
void FMatinee::AddKey()
|
|
{
|
|
// To add keyframes easier, if a group is selected with only one
|
|
// track, select the track so the keyframe can be placed.
|
|
if( GetSelectedGroupCount() == 1 )
|
|
{
|
|
UInterpGroup* SelectedGroup = *GetSelectedGroupIterator();
|
|
|
|
if( SelectedGroup->InterpTracks.Num() == 1 )
|
|
{
|
|
// Note: We shouldn't have to deselect currently
|
|
// selected tracks because a group is selected.
|
|
const bool bDeselectPreviousTracks = false;
|
|
const int32 FirstTrackIndex = 0;
|
|
|
|
SelectTrack( SelectedGroup, SelectedGroup->InterpTracks[FirstTrackIndex], bDeselectPreviousTracks );
|
|
}
|
|
}
|
|
|
|
if( !HasATrackSelected() )
|
|
{
|
|
FNotificationInfo NotificationInfo(NSLOCTEXT("UnrealEd", "NoTrackSelected", "No track selected. Select a track from the track view before trying again."));
|
|
NotificationInfo.ExpireDuration = 3.0f;
|
|
FSlateNotificationManager::Get().AddNotification(NotificationInfo);
|
|
return;
|
|
}
|
|
|
|
// Array of tracks that were selected
|
|
TArray<FSelectedTrackData> TracksToAddKeys;
|
|
|
|
if( GetSelectedTrackCount() > 1 )
|
|
{
|
|
// Populate the list of tracks that we need to add keys to.
|
|
FSelectedTrackIterator TrackIt(GetSelectedTrackIterator());
|
|
for( ; TrackIt; ++TrackIt )
|
|
{
|
|
// Only allow keys to be added to multiple tracks at once if they are subtracks of a movement track.
|
|
if( (*TrackIt)->IsA( UInterpTrackMoveAxis::StaticClass() ) )
|
|
{
|
|
FSelectedTrackData Data;
|
|
Data.Track = *TrackIt;
|
|
Data.SelectedIndex = TrackIt.GetTrackIndex();
|
|
TracksToAddKeys.Add( Data );
|
|
}
|
|
else
|
|
{
|
|
TracksToAddKeys.Empty();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( TracksToAddKeys.Num() == 0 )
|
|
{
|
|
FMessageDialog::Open( EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "InterpEd_Track_TooManySelected", "Only 1 track can be selected for this operation.") );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FSelectedTrackIterator TrackIt(GetSelectedTrackIterator());
|
|
|
|
// There is only one track selected.
|
|
FSelectedTrackData Data;
|
|
Data.Track = *TrackIt;
|
|
Data.SelectedIndex = TrackIt.GetTrackIndex();
|
|
TracksToAddKeys.Add( Data );
|
|
}
|
|
|
|
// A mapping of tracks to indices where keys were added
|
|
TrackToNewKeyIndexMap.Empty();
|
|
AddKeyInfoMap.Empty();
|
|
|
|
if( TracksToAddKeys.Num() > 0 )
|
|
{
|
|
// Add keys to all tracks in the array
|
|
for( int32 TrackIndex = 0; TrackIndex < TracksToAddKeys.Num(); ++TrackIndex )
|
|
{
|
|
UInterpTrack* Track = TracksToAddKeys[ TrackIndex ].Track;
|
|
int32 SelectedTrackIndex = TracksToAddKeys[ TrackIndex ].SelectedIndex;
|
|
|
|
UInterpTrackInst* TrInst = NULL;
|
|
UInterpGroup* Group = Cast<UInterpGroup>(Track->GetOuter());
|
|
if( Group )
|
|
{
|
|
UInterpGroupInst* GrInst = MatineeActor->FindFirstGroupInst(Group);
|
|
check(GrInst);
|
|
|
|
TrInst = GrInst->TrackInst[ SelectedTrackIndex ];
|
|
check(TrInst);
|
|
}
|
|
else
|
|
{
|
|
// The track is a subtrack, get the tracks group from its parent track.
|
|
UInterpTrack* ParentTrack = CastChecked<UInterpTrack>( Track->GetOuter() );
|
|
|
|
Group = CastChecked<UInterpGroup>( ParentTrack->GetOuter() );
|
|
int32 ParentTrackIndex = Group->InterpTracks.Find( ParentTrack );
|
|
|
|
UInterpGroupInst* GrInst = MatineeActor->FindFirstGroupInst( Group );
|
|
check(GrInst);
|
|
|
|
TrInst = GrInst->TrackInst[ ParentTrackIndex ];
|
|
check(TrInst);
|
|
}
|
|
|
|
UInterpTrackHelper* TrackHelper = NULL;
|
|
UClass* Class = LoadObject<UClass>( NULL, *Track->GetSlateHelperClassName(), NULL, LOAD_None, NULL );
|
|
if ( Class != NULL )
|
|
{
|
|
TrackHelper = Class->GetDefaultObject<UInterpTrackHelper>();
|
|
}
|
|
|
|
float fKeyTime = SnapTime( MatineeActor->InterpPosition, false );
|
|
|
|
//Save off important info
|
|
AddKeyInfo Info;
|
|
Info.TrInst = TrInst;
|
|
Info.TrackHelper = TrackHelper;
|
|
Info.fKeyTime = fKeyTime;
|
|
AddKeyInfoMap.Add(Track,Info);
|
|
|
|
if ( (TrackHelper == NULL) || !TrackHelper->PreCreateKeyframe(Track, fKeyTime) )
|
|
{
|
|
//Slate window options should wind up here and return...
|
|
return;
|
|
}
|
|
|
|
FinishAddKey(Track, false);
|
|
}
|
|
|
|
CommitAddedKeys();
|
|
}
|
|
}
|
|
|
|
void FMatinee::FinishAddKey(UInterpTrack* Track, bool bCommitKeys)
|
|
{
|
|
AddKeyInfo *Info = AddKeyInfoMap.Find(Track);
|
|
if (!Info)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UInterpTrackInst* TrInst = Info->TrInst;
|
|
UInterpTrackHelper* TrackHelper = Info->TrackHelper;
|
|
float fKeyTime = Info->fKeyTime;
|
|
AddKeyInfoMap.Remove(Track);
|
|
|
|
// Check if it's possible to add a keyframe to the track
|
|
bool bAddKeyFrame = true;
|
|
|
|
if( Track->SubTracks.Num() > 0 )
|
|
{
|
|
for( int32 SubTrackIndex = 0; SubTrackIndex < Track->SubTracks.Num(); ++SubTrackIndex )
|
|
{
|
|
bAddKeyFrame &= Track->CanAddChildKeyframe( TrInst );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bAddKeyFrame = Track->CanAddKeyframe( TrInst );
|
|
}
|
|
|
|
if( bAddKeyFrame )
|
|
{
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT("UnrealEd", "AddKey", "Add Key") );
|
|
Track->Modify();
|
|
Opt->Modify();
|
|
|
|
if( Track->SubTracks.Num() > 0 )
|
|
{
|
|
// Add a keyframe to each subtrack. We have to do this manually here because we need to know the indices where keyframes were added.
|
|
for( int32 SubTrackIndex = 0; SubTrackIndex < Track->SubTracks.Num(); ++SubTrackIndex )
|
|
{
|
|
UInterpTrack* SubTrack = Track->SubTracks[ SubTrackIndex ];
|
|
SubTrack->Modify();
|
|
// Add key at current time, snapped to the grid if its on.
|
|
int32 NewKeyIndex = Track->AddChildKeyframe( SubTrack, fKeyTime, TrInst, InitialInterpMode );
|
|
check( NewKeyIndex != INDEX_NONE );
|
|
TrackToNewKeyIndexMap.Add( SubTrack, NewKeyIndex );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Add key at current time, snapped to the grid if its on.
|
|
int32 NewKeyIndex = Track->AddKeyframe( fKeyTime, TrInst, InitialInterpMode );
|
|
checkf( NewKeyIndex != INDEX_NONE, TEXT("Could not add a key at %f to Track %s "), fKeyTime, *Track->GetName());
|
|
|
|
|
|
// Check to see if this is going to be the event first key to have this name
|
|
bool bCommonName = true;
|
|
FName NewKeyName = TrackHelper->GetKeyframeAddDataName();
|
|
if( !NewKeyName.IsNone() )
|
|
{
|
|
bCommonName = IData->IsEventName( NewKeyName );
|
|
}
|
|
|
|
|
|
TrackHelper->PostCreateKeyframe( Track, NewKeyIndex );
|
|
TrackToNewKeyIndexMap.Add( Track, NewKeyIndex );
|
|
|
|
|
|
// Is this the first event key to be added with this name?
|
|
if( !bCommonName )
|
|
{
|
|
// Fire a delegate so other places that use the name can also update
|
|
FMatineeDelegates::Get().OnEventKeyframeAdded.Broadcast( MatineeActor, NewKeyName, NewKeyIndex );
|
|
}
|
|
}
|
|
|
|
InterpEdTrans->EndSpecial();
|
|
}
|
|
else
|
|
{
|
|
FMessageDialog::Open( EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "NothingToKeyframe", "Nothing to keyframe, or selected object can't be keyframed on this type of track.\n") );
|
|
}
|
|
|
|
if (bCommitKeys)
|
|
{
|
|
CommitAddedKeys();
|
|
}
|
|
}
|
|
|
|
void FMatinee::CommitAddedKeys()
|
|
{
|
|
if( TrackToNewKeyIndexMap.Num() > 0 )
|
|
{
|
|
// Select the newly added keyframes.
|
|
ClearKeySelection();
|
|
|
|
for( TMap<UInterpTrack*, int32>::TIterator It(TrackToNewKeyIndexMap); It; ++It)
|
|
{
|
|
UInterpTrack* Track = It.Key();
|
|
int32 NewKeyIndex = It.Value();
|
|
|
|
AddKeyToSelection(Track->GetOwningGroup(), Track, NewKeyIndex, true); // Probably don't need to auto-wind - should already be there!
|
|
}
|
|
|
|
// Update to current time, in case new key affects state of scene.
|
|
RefreshInterpPosition();
|
|
|
|
}
|
|
|
|
// Dirty the track window viewports
|
|
InvalidateTrackWindowViewports();
|
|
|
|
//empty out our temporily stored data
|
|
TrackToNewKeyIndexMap.Empty();
|
|
}
|
|
|
|
|
|
/**
|
|
* Call utility to split an animation in the selected AnimControl track.
|
|
*
|
|
* Only one interp track can be selected and it must be a anim control track for the function.
|
|
*/
|
|
void FMatinee::SplitAnimKey()
|
|
{
|
|
// Only one track can be selected at a time when dealing with keyframes.
|
|
// Also, there must be an anim control track selected.
|
|
if( GetSelectedTrackCount() != 1 || !HasATrackSelected(UInterpTrackAnimControl::StaticClass()) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Split keys only for anim tracks
|
|
TTrackClassTypeIterator<UInterpTrackAnimControl> AnimTrackIt(GetSelectedTrackIterator<UInterpTrackAnimControl>());
|
|
UInterpTrackAnimControl* AnimTrack = *AnimTrackIt;
|
|
|
|
// Call split utility.
|
|
int32 NewKeyIndex = AnimTrack->SplitKeyAtPosition(MatineeActor->InterpPosition);
|
|
|
|
// If we created a new key - select it by default.
|
|
if(NewKeyIndex != INDEX_NONE)
|
|
{
|
|
ClearKeySelection();
|
|
AddKeyToSelection(AnimTrackIt.GetGroup(), AnimTrack, NewKeyIndex, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copies the currently selected track.
|
|
*
|
|
* @param bCut Whether or not we should cut instead of simply copying the track.
|
|
*/
|
|
void FMatinee::CopySelectedGroupOrTrack(bool bCut)
|
|
{
|
|
const bool bHasAGroupSelected = HasAGroupSelected();
|
|
const bool bHasATrackSelected = HasATrackSelected();
|
|
|
|
if( !bHasAGroupSelected && !bHasATrackSelected )
|
|
{
|
|
FMessageDialog::Open( EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "InterpEd_Copy_NeedToSelectGroup", "No selected tracks or groups to copy. Please highlight a track or group to copy by clicking on the track or group's name to the left.") );
|
|
return;
|
|
}
|
|
|
|
// Sanity check. There should only be only tracks
|
|
// selected or only groups selected. Not both!
|
|
check( bHasAGroupSelected ^ bHasATrackSelected );
|
|
|
|
// Make sure to clear the buffer before adding to it again.
|
|
GUnrealEd->MatineeCopyPasteBuffer.Empty();
|
|
|
|
// If no tracks are selected, copy the group.
|
|
if( bHasAGroupSelected )
|
|
{
|
|
// Add all the selected groups to the copy-paste buffer
|
|
for( FSelectedGroupIterator GroupIt(GetSelectedGroupIterator()); GroupIt; ++GroupIt )
|
|
{
|
|
UObject* CopiedObject = (UObject*)StaticDuplicateObject( *GroupIt, GetTransientPackage() );
|
|
GUnrealEd->MatineeCopyPasteBuffer.Add(CopiedObject);
|
|
}
|
|
|
|
// Delete the active group if we are doing a cut operation.
|
|
if(bCut)
|
|
{
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT("UnrealEd", "InterpEd_Cut_SelectedTrackOrGroup", "Cut Selected Track or Group") );
|
|
{
|
|
DeleteSelectedGroups();
|
|
}
|
|
InterpEdTrans->EndSpecial();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Keep a list of all the tracks that should be deleted if the user is cutting, this doesn't include those who have selected keys
|
|
TMultiMap<UInterpTrack*, int32> CutKeyframes;
|
|
TArray<UInterpTrack*> DeleteTracks;
|
|
|
|
for( FSelectedTrackIterator TrackIt(GetSelectedTrackIterator()); TrackIt; ++TrackIt )
|
|
{
|
|
UInterpTrack* Track = *TrackIt;
|
|
|
|
// Only allow base tracks to be copied. Subtracks should never be copied because this could result in subtracks being pasted where they dont belong (like directly in groups).
|
|
if( Track->GetOuter()->IsA( UInterpGroup::StaticClass() ) )
|
|
{
|
|
UInterpTrack* CopiedTrack = (UInterpTrack*)StaticDuplicateObject(Track, GetTransientPackage());
|
|
|
|
// If we have keyframes selected in this track, make sure only those are included in the copy
|
|
if ( Opt->SelectedKeys.Num() > 0 )
|
|
{
|
|
// Make a list of all the keys we want to keep
|
|
TArray< int32 > ValidKeys;
|
|
for( int32 iSelectedKey = 0; iSelectedKey < Opt->SelectedKeys.Num(); iSelectedKey++ )
|
|
{
|
|
const FInterpEdSelKey& rSelKey = Opt->SelectedKeys[iSelectedKey];
|
|
if ( rSelKey.Track == Track )
|
|
{
|
|
ValidKeys.Add( rSelKey.KeyIndex );
|
|
}
|
|
}
|
|
|
|
// Only remove superfluous keys if we have any for this track
|
|
if ( ValidKeys.Num() > 0 )
|
|
{
|
|
check( CopiedTrack->GetNumKeyframes() == Track->GetNumKeyframes() );
|
|
for( int32 iKeyIndex = CopiedTrack->GetNumKeyframes(); iKeyIndex >= 0; iKeyIndex-- )
|
|
{
|
|
if ( !ValidKeys.Contains( iKeyIndex ) )
|
|
{
|
|
CopiedTrack->RemoveKeyframe( iKeyIndex );
|
|
}
|
|
else if( bCut )
|
|
{
|
|
CutKeyframes.Add( Track, iKeyIndex );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DeleteTracks.Add( Track );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DeleteTracks.Add( Track );
|
|
}
|
|
|
|
GUnrealEd->MatineeCopyPasteBuffer.Add( CopiedTrack );
|
|
}
|
|
}
|
|
|
|
// Delete the originating track if we are cutting and it hasn't had keys copied from it
|
|
if( bCut && ( DeleteTracks.Num() > 0 || CutKeyframes.Num() > 0 ) )
|
|
{
|
|
if ( DeleteTracks.Num() > 0 )
|
|
{
|
|
// Deselect all tracks
|
|
DeselectAllTracks();
|
|
|
|
// Only select the tracks that were valid to cut
|
|
for( int32 TrackIndex = 0; TrackIndex < DeleteTracks.Num(); ++TrackIndex )
|
|
{
|
|
UInterpTrack* Track = DeleteTracks[ TrackIndex ];
|
|
SelectTrack( Track->GetOwningGroup(), Track, false );
|
|
}
|
|
}
|
|
|
|
if ( CutKeyframes.Num() > 0 )
|
|
{
|
|
// Deselect all keys
|
|
ClearKeySelection();
|
|
|
|
// Only select the keys that were valid to cut
|
|
for(auto It(CutKeyframes.CreateIterator()); It; ++It)
|
|
{
|
|
UInterpTrack* Track = It.Key();
|
|
const int32 KeyIndex = It.Value();
|
|
AddKeyToSelection(Track->GetOwningGroup(), Track, KeyIndex, false);
|
|
}
|
|
}
|
|
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT("UnrealEd", "InterpEd_Cut_SelectedTrackOrGroup", "Cut Selected Track or Group") );
|
|
{
|
|
// Transact all the cut key frames
|
|
if ( CutKeyframes.Num() > 0 )
|
|
{
|
|
DeleteSelectedKeys(true);
|
|
}
|
|
|
|
// Followed by the deleted tracks
|
|
if ( DeleteTracks.Num() > 0 )
|
|
{
|
|
DeleteSelectedTracks();
|
|
}
|
|
}
|
|
InterpEdTrans->EndSpecial();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pastes the previously copied track.
|
|
*/
|
|
void FMatinee::PasteSelectedGroupOrTrack()
|
|
{
|
|
// See if we are pasting a track or group.
|
|
if(GUnrealEd->MatineeCopyPasteBuffer.Num())
|
|
{
|
|
// Variables only used when pasting tracks.
|
|
UInterpGroup* GroupToPasteTracks = NULL;
|
|
TArray<UInterpTrack*> TracksToSelect;
|
|
FText ErrorMsg = FText::GetEmpty();
|
|
|
|
for( TArray<UObject*>::TIterator BufferIt(GUnrealEd->MatineeCopyPasteBuffer); BufferIt; ++BufferIt )
|
|
{
|
|
UObject* CurrentObject = *BufferIt;
|
|
|
|
if( CurrentObject->IsA(UInterpGroup::StaticClass()) )
|
|
{
|
|
DuplicateGroup(CastChecked<UInterpGroup>(CurrentObject));
|
|
}
|
|
else if( CurrentObject->IsA(UInterpTrack::StaticClass()) )
|
|
{
|
|
UInterpTrack* CurrentTrack = CastChecked< UInterpTrack >( CurrentObject );
|
|
|
|
int32 GroupsSelectedCount = GetSelectedGroupCount();
|
|
|
|
if( GroupsSelectedCount == 1 )
|
|
{
|
|
UInterpTrack* TrackToPaste = CastChecked<UInterpTrack>(CurrentObject);
|
|
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT("UnrealEd", "InterpEd_Paste_SelectedTrackOrGroup", "Paste Selected Track or Group.") );
|
|
{
|
|
if( NULL == GroupToPasteTracks )
|
|
{
|
|
GroupToPasteTracks = *GetSelectedGroupIterator();
|
|
}
|
|
|
|
GroupToPasteTracks->Modify();
|
|
|
|
// Defer selection of the pasted track so the group is not deselected,
|
|
// which would cause all other tracks to fail when pasting.
|
|
const bool bSelectTrack = false;
|
|
UInterpTrack* PastedTrack = AddTrackToSelectedGroup(TrackToPaste->GetClass(), TrackToPaste, bSelectTrack);
|
|
|
|
// Save off the created track so we can select it later.
|
|
if( NULL != PastedTrack )
|
|
{
|
|
TracksToSelect.Add(PastedTrack);
|
|
}
|
|
}
|
|
InterpEdTrans->EndSpecial();
|
|
}
|
|
else if ( CurrentTrack->GetNumKeyframes() == 1 )
|
|
{
|
|
// Special case pasting, if the track only has one keyframe, assume the user is just interested in pasting that
|
|
TArray<UInterpTrack*> ValidTracks;
|
|
for( FSelectedTrackIterator TrackIt(GetSelectedTrackIterator()); TrackIt; ++TrackIt )
|
|
{
|
|
UInterpTrack* Track = *TrackIt;
|
|
|
|
// Only allow this if the class the same type, due to uniqueness of the keys
|
|
if ( Track->GetClass() == CurrentTrack->GetClass() )
|
|
{
|
|
ValidTracks.Add( Track );
|
|
}
|
|
}
|
|
|
|
if ( ValidTracks.Num() > 0 )
|
|
{
|
|
// Make a list of any tracks which can be pasted into so we can setup a transaction
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT("UnrealEd", "InterpEd_Paste_SelectedKeyframe", "Paste Selected Keyframe") );
|
|
{
|
|
// For each track, duplicate the keyframe into it
|
|
for( int32 iTrackIndex = 0; iTrackIndex < ValidTracks.Num(); iTrackIndex++ )
|
|
{
|
|
UInterpTrack* Track = ValidTracks[iTrackIndex];
|
|
|
|
// Check to see if there is already a key at the interp position in the destination track, and adjust it accordingly
|
|
float KeyTime = MatineeActor->InterpPosition;
|
|
while ( Track->GetKeyframeIndex( KeyTime ) != INDEX_NONE )
|
|
{
|
|
KeyTime += (float)DuplicateKeyOffset/PixelsPerSec;
|
|
}
|
|
|
|
// Add the keyframe to this track
|
|
Track->Modify();
|
|
CurrentTrack->DuplicateKeyframe( 0, KeyTime, Track );
|
|
}
|
|
}
|
|
InterpEdTrans->EndSpecial();
|
|
}
|
|
else
|
|
{
|
|
ErrorMsg = NSLOCTEXT("UnrealEd", "InterpEd_Paste_NeedToSameTrack", "No track of similar type selected. Please select a track of the same type as the keyframe was copied from.");
|
|
}
|
|
}
|
|
else if( GroupsSelectedCount < 1 )
|
|
{
|
|
ErrorMsg = NSLOCTEXT("UnrealEd", "InterpEd_Paste_NeedToSelectGroup", "No selected groups to paste into. Please highlight a group to copy by clicking on the group's name to the left.");
|
|
}
|
|
else if( GroupsSelectedCount > 1 )
|
|
{
|
|
ErrorMsg = NSLOCTEXT("UnrealEd", "InterpEd_Paste_OneGroup", "Can only have one group selected when pasting.");
|
|
}
|
|
}
|
|
}
|
|
|
|
// If an error occurred, display it now
|
|
if ( !ErrorMsg.IsEmpty() )
|
|
{
|
|
FMessageDialog::Open( EAppMsgType::Ok, ErrorMsg );
|
|
}
|
|
|
|
// If we pasted tracks to a group, then we still need to select them.
|
|
if( NULL != GroupToPasteTracks )
|
|
{
|
|
// Don't deselect previous tracks because (1) if a group was selected, then no other tracks
|
|
// were selected and (2) we don't want to deselect tracks we just selected in the loop.
|
|
const bool bDeselectPreviousTracks = false;
|
|
for( int32 TrackIndex = 0; TrackIndex < TracksToSelect.Num(); ++TrackIndex )
|
|
{
|
|
SelectTrack(GroupToPasteTracks, TracksToSelect[TrackIndex], bDeselectPreviousTracks);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return Whether or not we can paste a track/group.
|
|
*/
|
|
bool FMatinee::CanPasteGroupOrTrack()
|
|
{
|
|
bool bResult = false;
|
|
|
|
// Make sure we at least have something in the buffer.
|
|
// Camera anims cant paste items
|
|
if( GUnrealEd->MatineeCopyPasteBuffer.Num() && !IsCameraAnim() )
|
|
{
|
|
// We don't currently support pasting on multiple groups or tracks.
|
|
// So, we have have to make sure we have one group or one track selected.
|
|
const bool bCanPasteOnGroup = (GetSelectedGroupCount() < 2);
|
|
const bool bCanPasteOnTrack = (GetSelectedTrackCount() == 1);
|
|
|
|
// Copy-paste can only happen if only one group OR only one track is selected.
|
|
// We cannot paste if there is one track and one group selected.
|
|
if( bCanPasteOnGroup ^ bCanPasteOnTrack )
|
|
{
|
|
bResult = true;
|
|
|
|
// Can we paste on top of a group?
|
|
if( bCanPasteOnGroup )
|
|
{
|
|
for( TArray<UObject*>::TIterator BufferIt(GUnrealEd->MatineeCopyPasteBuffer); BufferIt; ++BufferIt )
|
|
{
|
|
const bool bIsAGroup = (*BufferIt)->IsA(UInterpGroup::StaticClass());
|
|
const bool bIsATrack = (*BufferIt)->IsA(UInterpTrack::StaticClass());
|
|
|
|
// We can paste groups or tracks on top of selected groups. If there
|
|
// is one object in the buffer that isn't either, then we can't paste.
|
|
if( !bIsAGroup && !bIsATrack )
|
|
{
|
|
bResult = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Can we paste on top of a track?
|
|
else
|
|
{
|
|
for( TArray<UObject*>::TIterator BufferIt(GUnrealEd->MatineeCopyPasteBuffer); BufferIt; ++BufferIt )
|
|
{
|
|
const bool bIsATrack = (*BufferIt)->IsA(UInterpTrack::StaticClass());
|
|
|
|
// We can only paste tracks on top of tracks. If there exists any other
|
|
// objects in the buffer that aren't tracks, then we can't paste.
|
|
if( !bIsATrack )
|
|
{
|
|
bResult = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if( bCanPasteOnTrack )
|
|
{
|
|
bResult = true;
|
|
|
|
// Special case, allow pasting into tracks if the track we have copied only has one keyframe in it
|
|
for( TArray<UObject*>::TIterator BufferIt(GUnrealEd->MatineeCopyPasteBuffer); BufferIt; ++BufferIt )
|
|
{
|
|
UInterpTrack* Track = Cast<UInterpTrack>(*BufferIt);
|
|
|
|
// We can only paste keyframes into tracks. If there exists any other
|
|
// objects in the buffer that aren't tracks, or we have too many keyframes then we can't paste.
|
|
if( !Track || Track->GetNumKeyframes() != 1 )
|
|
{
|
|
bResult = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return bResult;
|
|
}
|
|
|
|
|
|
/**
|
|
* Adds a new track to the specified group.
|
|
*
|
|
* @param Group The group to add a track to
|
|
* @param TrackClass The class of track object we are going to add.
|
|
* @param TrackToCopy A optional track to copy instead of instantiating a new one.
|
|
* @param bAllowPrompts true if we should allow a dialog to be summoned to ask for initial information
|
|
* @param OutNewTrackIndex [Out] The index of the newly created track in its parent group
|
|
* @param bSelectTrack true if we should select the track after adding it
|
|
*
|
|
* @return Returns newly created track (or NULL if failed)
|
|
*/
|
|
UInterpTrack* FMatinee::AddTrackToGroup( UInterpGroup* Group, UClass* TrackClass, UInterpTrack* TrackToCopy, bool bAllowPrompts, int32& OutNewTrackIndex, bool bSelectTrack /*= true*/ )
|
|
{
|
|
OutNewTrackIndex = INDEX_NONE;
|
|
|
|
if( Group == NULL )
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
UInterpGroupInst* GrInst = MatineeActor->FindFirstGroupInst( Group );
|
|
check(GrInst);
|
|
|
|
UInterpTrack* TrackDef = TrackClass->GetDefaultObject<UInterpTrack>();
|
|
|
|
UInterpTrackHelper* TrackHelper = NULL;
|
|
|
|
bool bCopyingTrack = (TrackToCopy!=NULL);
|
|
|
|
UClass *Class = LoadObject<UClass>( NULL, *TrackDef->GetSlateHelperClassName(), NULL, LOAD_None, NULL );
|
|
if ( Class != NULL )
|
|
{
|
|
TrackHelper = Class->GetDefaultObject<UInterpTrackHelper>();
|
|
}
|
|
|
|
if ( (TrackHelper == NULL) || !TrackHelper->PreCreateTrack( Group, TrackDef, bCopyingTrack, bAllowPrompts ) )
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
Group->Modify();
|
|
|
|
// Construct track and track instance objects.
|
|
UInterpTrack* NewTrack = NULL;
|
|
if(TrackToCopy)
|
|
{
|
|
NewTrack = Cast<UInterpTrack>(StaticDuplicateObject( TrackToCopy, Group ));
|
|
}
|
|
else
|
|
{
|
|
NewTrack = NewObject<UInterpTrack>(Group, TrackClass, NAME_None, RF_Transactional);
|
|
}
|
|
|
|
check(NewTrack);
|
|
|
|
if (FEngineAnalytics::IsAvailable())
|
|
{
|
|
FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.Matinee.NewTrack"), TEXT("Class"), TrackClass->GetName());
|
|
}
|
|
|
|
OutNewTrackIndex = Group->InterpTracks.Add(NewTrack);
|
|
|
|
check( NewTrack->TrackInstClass );
|
|
check( NewTrack->TrackInstClass->IsChildOf(UInterpTrackInst::StaticClass()) );
|
|
|
|
TrackHelper->PostCreateTrack( NewTrack, bCopyingTrack, OutNewTrackIndex );
|
|
|
|
if(bCopyingTrack == false)
|
|
{
|
|
NewTrack->SetTrackToSensibleDefault();
|
|
}
|
|
|
|
NewTrack->Modify();
|
|
|
|
// We need to create a InterpTrackInst in each instance of the active group (the one we are adding the track to).
|
|
for(int32 i=0; i<MatineeActor->GroupInst.Num(); i++)
|
|
{
|
|
UInterpGroupInst* GroupInst = MatineeActor->GroupInst[i];
|
|
if(GroupInst->Group == Group)
|
|
{
|
|
GroupInst->Modify();
|
|
|
|
UInterpTrackInst* NewTrackInst = NewObject<UInterpTrackInst>(GroupInst, NewTrack->TrackInstClass, NAME_None, RF_Transactional);
|
|
|
|
const int32 NewInstIndex = GroupInst->TrackInst.Add(NewTrackInst);
|
|
check(NewInstIndex == OutNewTrackIndex);
|
|
|
|
// Initialize track, giving selected object.
|
|
NewTrackInst->InitTrackInst(NewTrack);
|
|
|
|
// Save state into new track before doing anything else (because we didn't do it on ed mode change).
|
|
NewTrackInst->SaveActorState(NewTrack);
|
|
NewTrackInst->Modify();
|
|
}
|
|
}
|
|
|
|
if(bCopyingTrack == false)
|
|
{
|
|
// Bit of a hack here, but useful. Whenever you put down a movement track, add a key straight away at the start.
|
|
// Should be ok to add at the start, because it should not be having its location (or relative location) changed in any way already as we scrub.
|
|
UInterpTrackMove* MoveTrack = Cast<UInterpTrackMove>(NewTrack);
|
|
if(MoveTrack)
|
|
{
|
|
UInterpGroupInst* GroupInst = MatineeActor->FindFirstGroupInst(Group);
|
|
UInterpTrackInst* TrInst = GroupInst->TrackInst[OutNewTrackIndex];
|
|
MoveTrack->AddKeyframe( 0.0f, TrInst, InitialInterpMode );
|
|
}
|
|
}
|
|
|
|
if ( bSelectTrack )
|
|
{
|
|
SelectTrack( Group, NewTrack );
|
|
}
|
|
return NewTrack;
|
|
}
|
|
|
|
/**
|
|
* Adds a new track to the selected group.
|
|
*
|
|
* @param TrackClass The class of the track we are adding.
|
|
* @param TrackToCopy A optional track to copy instead of instantiating a new one. If NULL, a new track will be instantiated.
|
|
* @param bSelectTrack If true, select the track after adding it
|
|
*
|
|
* @return The newly-created track if created; NULL, otherwise.
|
|
*/
|
|
UInterpTrack* FMatinee::AddTrackToSelectedGroup(UClass* TrackClass, UInterpTrack* TrackToCopy, bool bSelectTrack /* = true */)
|
|
{
|
|
// In order to add a track to a group, there can only be one group selected.
|
|
check( GetSelectedGroupCount() == 1 );
|
|
UInterpGroup* Group = *GetSelectedGroupIterator();
|
|
|
|
return AddTrackToGroupAndRefresh(Group, NSLOCTEXT( "UnrealEd", "NewTrack", "NewTrack").ToString(), TrackClass, TrackToCopy, bSelectTrack);
|
|
}
|
|
|
|
/**
|
|
* Adds a new track to a group and appropriately updates/refreshes the editor
|
|
*
|
|
* @param Group The group to add this track to
|
|
* @param NewTrackName The default name of the new track to add
|
|
* @param TrackClass The class of the track we are adding.
|
|
* @param TrackToCopy A optional track to copy instead of instantiating a new one. If NULL, a new track will be instantiated.
|
|
* @param bSelectTrack If true, select the track after adding it
|
|
* return New interp track that was created
|
|
*/
|
|
UInterpTrack* FMatinee::AddTrackToGroupAndRefresh(UInterpGroup* Group, const FString& NewTrackName, UClass* TrackClass, UInterpTrack* TrackToCopy, bool bSelectTrack /* = true */)
|
|
{
|
|
UInterpTrack* TrackDef = TrackClass->GetDefaultObject<UInterpTrack>();
|
|
|
|
// If bOnePerGrouop - check we don't already have a track of this type in the group.
|
|
if(TrackDef->bOnePerGroup)
|
|
{
|
|
DisableTracksOfClass(Group, TrackClass);
|
|
}
|
|
|
|
// Warn when creating dynamic track on a static actor, warn and offer to bail out.
|
|
if(TrackDef->AllowStaticActors()==false)
|
|
{
|
|
UInterpGroupInst* GrInst = MatineeActor->FindFirstGroupInst(Group);
|
|
check(GrInst);
|
|
|
|
AActor* GrActor = GrInst->GetGroupActor();
|
|
if(GrActor && GrActor->IsRootComponentStatic())
|
|
{
|
|
const bool bConfirm = EAppReturnType::Yes == FMessageDialog::Open( EAppMsgType::YesNo, NSLOCTEXT("UnrealEd", "WarnNewMoveTrackOnStatic", "WARNING: The track you are creating requires a Dynamic Actor, but the currently active group is using a Static Actor.\nAre you sure you want to create the track?") );
|
|
if(!bConfirm)
|
|
{
|
|
return NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
InterpEdTrans->BeginSpecial( FText::FromString( NewTrackName ) );
|
|
|
|
// Add the track!
|
|
int32 NewTrackIndex = INDEX_NONE;
|
|
UInterpTrack* ReturnTrack = AddTrackToGroup( Group, TrackClass, TrackToCopy, true, NewTrackIndex, bSelectTrack );
|
|
if (ReturnTrack)
|
|
{
|
|
ReturnTrack->EnableTrack( true );
|
|
}
|
|
|
|
InterpEdTrans->EndSpecial();
|
|
|
|
|
|
if( NewTrackIndex != INDEX_NONE )
|
|
{
|
|
// Make sure particle replay tracks have up-to-date editor-only transient state
|
|
UpdateParticleReplayTracks();
|
|
|
|
// A new track may have been added, so we'll need to update the scroll bar
|
|
UpdateTrackWindowScrollBars();
|
|
|
|
// Update graphics to show new track!
|
|
InvalidateTrackWindowViewports();
|
|
|
|
// If we added a movement track to this group, we'll need to make sure that the actor's transformations are captured
|
|
// so that we can restore them later.
|
|
MatineeActor->RecaptureActorState();
|
|
}
|
|
|
|
return ReturnTrack;
|
|
}
|
|
|
|
/**
|
|
* Deletes the currently active track.
|
|
*/
|
|
void FMatinee::DeleteSelectedTracks()
|
|
{
|
|
// This function should only be called if there is at least one selected track.
|
|
check( HasATrackSelected() );
|
|
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT("UnrealEd", "TrackDelete", "Track Delete") );
|
|
MatineeActor->Modify();
|
|
IData->Modify();
|
|
|
|
// Deselect everything.
|
|
ClearKeySelection();
|
|
|
|
// Take a copy of all the valid event names
|
|
TArray<FName> OldEventNames;
|
|
MatineeActor->MatineeData->GetAllEventNames(OldEventNames);
|
|
|
|
for( FSelectedTrackIterator TrackIt(GetSelectedTrackIterator()); TrackIt; ++TrackIt )
|
|
{
|
|
UInterpTrack* ActiveTrack = *TrackIt;
|
|
|
|
// Only allow base tracks to be deleted, Subtracks will be deleted by their parent
|
|
if( ActiveTrack->GetOuter()->IsA( UInterpGroup::StaticClass() ) )
|
|
{
|
|
if (FEngineAnalytics::IsAvailable())
|
|
{
|
|
FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.Matinee.DelTrack"), TEXT("Class"), ActiveTrack->GetClass()->GetName());
|
|
}
|
|
|
|
ActiveTrack->Modify();
|
|
|
|
UInterpGroup* Group = TrackIt.GetGroup();
|
|
Group->Modify();
|
|
|
|
const int32 TrackIndex = TrackIt.GetTrackIndex();
|
|
|
|
for(int32 i=0; i<MatineeActor->GroupInst.Num(); i++)
|
|
{
|
|
UInterpGroupInst* GrInst = MatineeActor->GroupInst[i];
|
|
if( GrInst->Group == Group )
|
|
{
|
|
UInterpTrackInst* TrInst = GrInst->TrackInst[TrackIndex];
|
|
|
|
GrInst->Modify();
|
|
TrInst->Modify();
|
|
|
|
// Before deleting this track - find each instance of it and restore state.
|
|
TrInst->RestoreActorState( GrInst->Group->InterpTracks[TrackIndex] );
|
|
|
|
// Clean up the track instance
|
|
TrInst->TermTrackInst( GrInst->Group->InterpTracks[TrackIndex] );
|
|
|
|
GrInst->TrackInst.RemoveAt(TrackIndex);
|
|
}
|
|
}
|
|
|
|
AActor* GroupActor = MatineeActor->FindFirstGroupInst(Group)->GetGroupActor();
|
|
|
|
// Remove from the Curve editor, if its there.
|
|
IData->CurveEdSetup->RemoveCurve(ActiveTrack);
|
|
// Remove any subtrack curves if the parent is being removed
|
|
for( int32 SubTrackIndex = 0; SubTrackIndex < ActiveTrack->SubTracks.Num(); ++SubTrackIndex )
|
|
{
|
|
IData->CurveEdSetup->RemoveCurve( ActiveTrack->SubTracks[ SubTrackIndex ] );
|
|
}
|
|
|
|
// Finally, remove the track completely.
|
|
// WARNING: Do not deference or use this iterator after the call to RemoveCurrent()!
|
|
TrackIt.RemoveCurrent();
|
|
}
|
|
}
|
|
|
|
IData->UpdateEventNames();
|
|
|
|
// Take another copy of all the valid event names
|
|
TArray<FName> RemainingEventNames;
|
|
MatineeActor->MatineeData->GetAllEventNames(RemainingEventNames);
|
|
|
|
// Check to see which event name no longer exist
|
|
TArray<FName> RemovedEventNames;
|
|
for( int32 EventNameIndex = 0; EventNameIndex < OldEventNames.Num(); ++EventNameIndex )
|
|
{
|
|
const FName& OldEventName = OldEventNames[EventNameIndex];
|
|
if ( !RemainingEventNames.Contains(OldEventName) )
|
|
{
|
|
RemovedEventNames.Add(OldEventName);
|
|
}
|
|
}
|
|
if ( RemovedEventNames.Num() > 0 )
|
|
{
|
|
// Fire a delegate so other places that use the name can also update
|
|
FMatineeDelegates::Get().OnEventKeyframeRemoved.Broadcast( MatineeActor, RemovedEventNames );
|
|
}
|
|
|
|
InterpEdTrans->EndSpecial();
|
|
|
|
// Update the curve editor
|
|
CurveEd->CurveChanged();
|
|
|
|
// A track may have been deleted, so we'll need to update our track window scroll bar
|
|
UpdateTrackWindowScrollBars();
|
|
|
|
// Update the property window to reflect the change in selection.
|
|
UpdatePropertyWindow();
|
|
|
|
MatineeActor->RecaptureActorState();
|
|
}
|
|
|
|
/**
|
|
* Deletes all selected groups.
|
|
*/
|
|
void FMatinee::DeleteSelectedGroups()
|
|
{
|
|
// There must be one group selected to use this funciton.
|
|
check( HasAGroupSelected() );
|
|
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT("UnrealEd", "GroupDelete", "Group Delete") );
|
|
MatineeActor->Modify();
|
|
IData->Modify();
|
|
|
|
// Deselect everything.
|
|
ClearKeySelection();
|
|
|
|
// Take a copy of all the valid event names
|
|
TArray<FName> OldEventNames;
|
|
MatineeActor->MatineeData->GetAllEventNames(OldEventNames);
|
|
|
|
for( FSelectedGroupIterator GroupIt(GetSelectedGroupIterator()); GroupIt; ++GroupIt )
|
|
{
|
|
UInterpGroup* GroupToDelete = *GroupIt;
|
|
if (FEngineAnalytics::IsAvailable())
|
|
{
|
|
FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.Matinee.DelGroup"), TEXT("Name"), GroupToDelete->GroupName.ToString());
|
|
}
|
|
|
|
// Mark InterpGroup and all InterpTracks as Modified.
|
|
GroupToDelete->Modify();
|
|
for(int32 j=0; j<GroupToDelete->InterpTracks.Num(); j++)
|
|
{
|
|
UInterpTrack* Track = GroupToDelete->InterpTracks[j];
|
|
if (FEngineAnalytics::IsAvailable())
|
|
{
|
|
FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.Matinee.DelTrack"), TEXT("Class"), Track->GetClass()->GetName());
|
|
}
|
|
|
|
Track->Modify();
|
|
|
|
// Remove from the Curve editor, if its there.
|
|
IData->CurveEdSetup->RemoveCurve(Track);
|
|
}
|
|
|
|
// First, destroy any instances of this group.
|
|
int32 i=0;
|
|
while( i<MatineeActor->GroupInst.Num() )
|
|
{
|
|
UInterpGroupInst* GrInst = MatineeActor->GroupInst[i];
|
|
if(GrInst->Group == GroupToDelete)
|
|
{
|
|
// Mark InterpGroupInst and all InterpTrackInsts as Modified.
|
|
GrInst->Modify();
|
|
for(int32 j=0; j<GrInst->TrackInst.Num(); j++)
|
|
{
|
|
GrInst->TrackInst[j]->Modify();
|
|
}
|
|
|
|
// Restore all state in this group before exiting
|
|
GrInst->RestoreGroupActorState();
|
|
|
|
// Clean up GroupInst
|
|
GrInst->TermGroupInst(false);
|
|
// Don't actually delete the TrackInsts - but we do want to call TermTrackInst on them.
|
|
|
|
// Remove from the MatineeActor's list of GroupInsts
|
|
MatineeActor->GroupInst.RemoveAt(i);
|
|
}
|
|
else
|
|
{
|
|
i++;
|
|
}
|
|
}
|
|
|
|
MatineeActor->DeleteGroupinfo(GroupToDelete);
|
|
|
|
// We're being deleted, so we need to unparent any child groups
|
|
// @todo: Should we support optionally deleting all sub-groups when deleting the parent?
|
|
for( int32 CurGroupIndex = IData->InterpGroups.Find( GroupToDelete ) + 1; CurGroupIndex < IData->InterpGroups.Num(); ++CurGroupIndex )
|
|
{
|
|
UInterpGroup* CurGroup = IData->InterpGroups[ CurGroupIndex ];
|
|
if( CurGroup->bIsParented )
|
|
{
|
|
CurGroup->Modify();
|
|
|
|
// Unparent this child
|
|
CurGroup->bIsParented = false;
|
|
}
|
|
else
|
|
{
|
|
// We've reached a root object, so we're done processing children. Bail!
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Prevent group from being selected as well as any tracks associated to the group.
|
|
// WARNING: Do not deference or use this iterator after the call to RemoveCurrent()!
|
|
GroupIt.RemoveCurrent();
|
|
}
|
|
|
|
IData->UpdateEventNames();
|
|
|
|
// Take another copy of all the valid event names
|
|
TArray<FName> RemainingEventNames;
|
|
MatineeActor->MatineeData->GetAllEventNames(RemainingEventNames);
|
|
|
|
// Check to see which event name no longer exist
|
|
TArray<FName> RemovedEventNames;
|
|
for( int32 EventNameIndex = 0; EventNameIndex < OldEventNames.Num(); ++EventNameIndex )
|
|
{
|
|
const FName& OldEventName = OldEventNames[EventNameIndex];
|
|
if ( !RemainingEventNames.Contains(OldEventName) )
|
|
{
|
|
RemovedEventNames.Add(OldEventName);
|
|
}
|
|
}
|
|
if ( RemovedEventNames.Num() > 0 )
|
|
{
|
|
// Fire a delegate so other places that use the name can also update
|
|
FMatineeDelegates::Get().OnEventKeyframeRemoved.Broadcast( MatineeActor, RemovedEventNames );
|
|
}
|
|
|
|
// Tell curve editor stuff might have changed.
|
|
CurveEd->CurveChanged();
|
|
|
|
// A group may have been deleted, so we'll need to update our track window scroll bar
|
|
UpdateTrackWindowScrollBars();
|
|
|
|
// Deselect everything.
|
|
ClearKeySelection();
|
|
|
|
InterpEdTrans->EndSpecial();
|
|
|
|
// Stop having the camera locked to this group if it currently is.
|
|
if( CamViewGroup && IsGroupSelected(CamViewGroup) )
|
|
{
|
|
LockCamToGroup(NULL);
|
|
}
|
|
|
|
// Update the property window to reflect the change in selection.
|
|
UpdatePropertyWindow();
|
|
|
|
// Reimage actor world locations. This must happen after the group was removed.
|
|
MatineeActor->RecaptureActorState();
|
|
}
|
|
|
|
/**
|
|
* Disables all tracks of a class type in this group
|
|
* @param Group - group in which to disable tracks of TrackClass type
|
|
* @param TrackClass - Type of track to disable
|
|
*/
|
|
void FMatinee::DisableTracksOfClass(UInterpGroup* Group, UClass* TrackClass)
|
|
{
|
|
for( int32 TrackIndex = 0; TrackIndex < Group->InterpTracks.Num(); TrackIndex++ )
|
|
{
|
|
if( Group->InterpTracks[TrackIndex]->GetClass() == TrackClass )
|
|
{
|
|
Group->InterpTracks[TrackIndex]->EnableTrack( false );
|
|
}
|
|
}
|
|
}
|
|
|
|
void FMatinee::UpdatePreviewCamera( UInterpGroup* AssociatedGroup ) const
|
|
{
|
|
UInterpGroupDirector* DirGroup = Cast<UInterpGroupDirector>(AssociatedGroup);
|
|
if ( DirGroup )
|
|
{
|
|
UInterpTrackDirector* DirTrack = DirGroup->GetDirectorTrack();
|
|
if ( DirTrack )
|
|
{
|
|
UpdatePreviewCamera( DirTrack );
|
|
}
|
|
}
|
|
}
|
|
|
|
void FMatinee::UpdatePreviewCamera( UInterpTrack* AssociatedTrack ) const
|
|
{
|
|
UInterpTrackDirector* DirTrack = Cast<UInterpTrackDirector>(AssociatedTrack);
|
|
if ( DirTrack )
|
|
{
|
|
// If the track selection state has changed, update our camera actor
|
|
UInterpGroupDirector* DirGroup = CastChecked<UInterpGroupDirector>( DirTrack->GetOuter() );
|
|
const bool TrackOrGroupSelected = ( DirTrack->IsSelected() | DirGroup->IsSelected() );
|
|
DirTrack->UpdatePreviewCamera( MatineeActor, TrackOrGroupSelected );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Duplicates the specified group
|
|
*
|
|
* @param GroupToDuplicate Group we are going to duplicate.
|
|
*/
|
|
void FMatinee::DuplicateGroup(UInterpGroup* GroupToDuplicate)
|
|
{
|
|
if(GroupToDuplicate==NULL)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FName NewGroupName;
|
|
|
|
// See if we are duplicating a director group.
|
|
bool bDirGroup = GroupToDuplicate->IsA(UInterpGroupDirector::StaticClass());
|
|
|
|
// If we are a director group, make sure we don't have a director group yet in our interp data.
|
|
if(bDirGroup)
|
|
{
|
|
UInterpGroupDirector* DirGroup = IData->FindDirectorGroup();
|
|
|
|
if(DirGroup)
|
|
{
|
|
FMessageDialog::Open( EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "UnableToPasteOnlyOneDirectorGroup", "Unable to complete paste operation. You can only have 1 director group per UnrealMatinee.") );
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const FText GroupName = FText::FromName( GroupToDuplicate->GroupName );
|
|
|
|
const FText DialogTitle = GroupToDuplicate->bIsFolder ? NSLOCTEXT("UnrealEd", "NewFolderNameWindowTitle", "New Folder Name") : NSLOCTEXT("UnrealEd", "NewGroupNameWindowTitle", "New Group Name");
|
|
|
|
FText ResultText = GenericTextEntryModal(DialogTitle, DialogTitle, GroupName);
|
|
|
|
//@todo We shouldn't be changing the name out from under them. Instead let them know that spaces aren't valid when entering the new name [9/30/2013 justin.sargent]
|
|
FString TempString = ResultText.ToString();
|
|
TempString = TempString.Replace(TEXT(" "),TEXT("_"));
|
|
NewGroupName = FName( *TempString );
|
|
}
|
|
|
|
|
|
|
|
// Begin undo transaction
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT("UnrealEd", "NewGroup", "New Group") );
|
|
{
|
|
MatineeActor->Modify();
|
|
IData->Modify();
|
|
|
|
// Create new InterpGroup.
|
|
UInterpGroup* NewGroup = (UInterpGroup*)StaticDuplicateObject( GroupToDuplicate, IData, NAME_None, RF_Transactional );
|
|
|
|
if(!bDirGroup)
|
|
{
|
|
NewGroup->GroupName = NewGroupName;
|
|
}
|
|
IData->InterpGroups.Add(NewGroup);
|
|
|
|
|
|
// All groups must have a unique name.
|
|
NewGroup->EnsureUniqueName();
|
|
|
|
// Randomly generate a group colour for the new group.
|
|
NewGroup->GroupColor = FColor::MakeRandomColor();
|
|
NewGroup->Modify();
|
|
|
|
// Pasted groups are always unparented. If we wanted to support pasting a group into a folder, we'd
|
|
// need to be sure to insert the new group in the appropriate place in the group list.
|
|
NewGroup->bIsParented = false;
|
|
|
|
// Create new InterpGroupInst
|
|
UInterpGroupInst* NewGroupInst = NULL;
|
|
|
|
if(bDirGroup)
|
|
{
|
|
NewGroupInst = NewObject<UInterpGroupInstDirector>(MatineeActor, NAME_None, RF_Transactional);
|
|
}
|
|
else
|
|
{
|
|
NewGroupInst = NewObject<UInterpGroupInst>(MatineeActor, NAME_None, RF_Transactional);
|
|
}
|
|
|
|
// Initialise group instance, saving ref to actor it works on.
|
|
NewGroupInst->InitGroupInst(NewGroup, NULL);
|
|
|
|
const int32 NewGroupInstIndex = MatineeActor->GroupInst.Add(NewGroupInst);
|
|
MatineeActor->InitGroupActorForGroup(NewGroup, NULL);
|
|
|
|
NewGroupInst->Modify();
|
|
|
|
|
|
// If a director group, create a director track for it now.
|
|
if(bDirGroup)
|
|
{
|
|
UInterpGroupDirector* DirGroup = Cast<UInterpGroupDirector>(NewGroup);
|
|
check(DirGroup);
|
|
|
|
// See if the director group has a director track yet, if not make one and make the corresponding track inst as well.
|
|
UInterpTrackDirector* NewDirTrack = DirGroup->GetDirectorTrack();
|
|
|
|
if(NewDirTrack==NULL)
|
|
{
|
|
NewDirTrack = NewObject<UInterpTrackDirector>(NewGroup, NAME_None, RF_Transactional);
|
|
NewGroup->InterpTracks.Add(NewDirTrack);
|
|
|
|
UInterpTrackInst* NewDirTrackInst = NewObject<UInterpTrackInstDirector>(NewGroupInst, NAME_None, RF_Transactional);
|
|
NewGroupInst->TrackInst.Add(NewDirTrackInst);
|
|
|
|
NewDirTrackInst->InitTrackInst(NewDirTrack);
|
|
NewDirTrackInst->SaveActorState(NewDirTrack);
|
|
|
|
// Save for undo then redo.
|
|
NewDirTrackInst->Modify();
|
|
NewDirTrack->Modify();
|
|
}
|
|
}
|
|
|
|
// Select the group we just duplicated
|
|
SelectGroup(NewGroup);
|
|
}
|
|
InterpEdTrans->EndSpecial();
|
|
|
|
// A new group may have been added (via duplication), so we'll need to update our scroll bar
|
|
UpdateTrackWindowScrollBars();
|
|
|
|
// Update graphics to show new group.
|
|
InvalidateTrackWindowViewports();
|
|
|
|
// If adding a camera- make sure its frustum colour is updated.
|
|
UpdateCamColours();
|
|
|
|
// Reimage actor world locations. This must happen after the group was created.
|
|
MatineeActor->RecaptureActorState();
|
|
}
|
|
|
|
/**
|
|
* Duplicates selected tracks in their respective groups and clears them to begin real time recording, and selects them
|
|
* @param bInDeleteSelectedTracks - true if the currently selected tracks should be destroyed when recording begins
|
|
*/
|
|
|
|
void FMatinee::DuplicateSelectedTracksForRecording (const bool bInDeleteSelectedTracks)
|
|
{
|
|
TArray <UInterpGroup*> OwnerGroups;
|
|
TArray <int32> RecordTrackIndex;
|
|
TArray <UInterpTrack*> OldSelectedTracks;
|
|
for( FSelectedTrackIterator TrackIt(GetSelectedTrackIterator()); TrackIt; ++TrackIt )
|
|
{
|
|
UInterpTrack* TrackToCopy = *TrackIt;
|
|
check(TrackToCopy);
|
|
//make sure we support this type of track for duplication
|
|
if ((Cast<UInterpTrackMove>(TrackToCopy)==NULL) && (Cast<UInterpTrackFloatProp>(TrackToCopy)==NULL))
|
|
{
|
|
//not supporting this track type for now
|
|
continue;
|
|
}
|
|
|
|
OldSelectedTracks.Add(TrackToCopy);
|
|
|
|
UInterpGroup* OwnerGroup = TrackIt.GetGroup();
|
|
|
|
FString NewTrackName = FText::Format( NSLOCTEXT( "UnrealEd", "CaptureTrack", "{0}CT"), FText::FromString(TrackToCopy->GetSlateHelperClassName()) ).ToString();
|
|
UInterpTrack* NewTrack = AddTrackToGroupAndRefresh(OwnerGroup, NewTrackName, TrackToCopy->GetClass(), TrackToCopy, false);
|
|
if (NewTrack)
|
|
{
|
|
RecordingTracks.Add(NewTrack);
|
|
OwnerGroups.Add(OwnerGroup);
|
|
RecordTrackIndex.Add(OwnerGroup->InterpTracks.Find(NewTrack));
|
|
|
|
//guard around movement tracks being relative
|
|
int32 FinalIndex = 0;
|
|
UInterpTrackMove* MovementTrack = Cast<UInterpTrackMove>(NewTrack);
|
|
|
|
//remove all keys
|
|
for (int32 KeyFrameIndex = NewTrack->GetNumKeyframes()-1; KeyFrameIndex >= FinalIndex; --KeyFrameIndex)
|
|
{
|
|
NewTrack->RemoveKeyframe(KeyFrameIndex);
|
|
}
|
|
|
|
// remove all subtrack keys. We cant do this inside the parent tracks remove keyframe as the keyframe index does not
|
|
// necessarily represent a valid index in a subtrack.
|
|
for( int32 SubTrackIndex = 0; SubTrackIndex < NewTrack->SubTracks.Num(); ++SubTrackIndex )
|
|
{
|
|
UInterpTrack* SubTrack = NewTrack->SubTracks[ SubTrackIndex ];
|
|
for (int32 KeyFrameIndex = SubTrack->GetNumKeyframes()-1; KeyFrameIndex >= FinalIndex; --KeyFrameIndex)
|
|
{
|
|
SubTrack->RemoveKeyframe(KeyFrameIndex);
|
|
}
|
|
}
|
|
NewTrack->TrackTitle = NewTrackName;
|
|
// Make sure the curve editor is in sync
|
|
CurveEd->CurveChanged();
|
|
}
|
|
}
|
|
|
|
if (bInDeleteSelectedTracks)
|
|
{
|
|
DeleteSelectedTracks();
|
|
}
|
|
|
|
//empty selection
|
|
DeselectAllTracks(false);
|
|
DeselectAllGroups(false);
|
|
|
|
//add all copied tracks to selection
|
|
const bool bDeselectOtherTracks = false;
|
|
bool bNewGroupSelected = false;
|
|
for (int32 i = 0; i < RecordingTracks.Num(); ++i)
|
|
{
|
|
UInterpTrack* TrackToSelect = RecordingTracks[i];
|
|
UInterpGroup* OwnerGroup = OwnerGroups[i];
|
|
|
|
UInterpTrackMove* TrackToSelectAsMoveTrack = Cast<UInterpTrackMove>(TrackToSelect);
|
|
|
|
if (!bNewGroupSelected && OwnerGroup && (TrackToSelectAsMoveTrack!= NULL))
|
|
{
|
|
//set this group as the preview group
|
|
LockCamToGroup(OwnerGroup);
|
|
bNewGroupSelected = true;
|
|
}
|
|
|
|
SelectTrack( OwnerGroup, TrackToSelect, bDeselectOtherTracks);
|
|
}
|
|
|
|
// Update the property window to reflect the group deselection
|
|
UpdatePropertyWindow();
|
|
// Request an update of the track windows
|
|
InvalidateTrackWindowViewports();
|
|
}
|
|
|
|
/**Used during recording to capture a key frame at the current position of the timeline*/
|
|
void FMatinee::RecordKeys(void)
|
|
{
|
|
for( FSelectedTrackIterator TrackIt(GetSelectedTrackIterator()); TrackIt; ++TrackIt )
|
|
{
|
|
UInterpTrack* TrackToSample = *TrackIt;
|
|
UInterpGroup* ParentGroup = TrackIt.GetGroup();
|
|
|
|
UInterpGroupInst* GrInst = MatineeActor->FindFirstGroupInst(ParentGroup);
|
|
check(GrInst);
|
|
UInterpTrackInst* TrInst = GrInst->TrackInst[TrackIt.GetTrackIndex()];
|
|
check(TrInst);
|
|
|
|
UInterpTrackHelper* TrackHelper = NULL;
|
|
UClass* Class = LoadObject<UClass>( NULL, *TrackToSample->GetSlateHelperClassName(), NULL, LOAD_None, NULL );
|
|
if ( Class != NULL )
|
|
{
|
|
TrackHelper = Class->GetDefaultObject<UInterpTrackHelper>();
|
|
}
|
|
|
|
float fKeyTime = SnapTime( MatineeActor->InterpPosition, false );
|
|
if ( (TrackHelper == NULL) || !TrackHelper->PreCreateKeyframe(TrackToSample, fKeyTime) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
TrackToSample->Modify();
|
|
|
|
// Add key at current time, snapped to the grid if its on.
|
|
int32 NewKeyIndex = TrackToSample->AddKeyframe( MatineeActor->InterpPosition, TrInst, InitialInterpMode );
|
|
TrackToSample->UpdateKeyframe( NewKeyIndex, TrInst);
|
|
}
|
|
|
|
// Dirty the track window viewports
|
|
InvalidateTrackWindowViewports();
|
|
}
|
|
|
|
|
|
/**Store off parent positions so we can apply the parents delta of movement to the child*/
|
|
void FMatinee::SaveRecordingParentOffsets(void)
|
|
{
|
|
RecordingParentOffsets.Empty();
|
|
if (RecordMode == MatineeConstants::ERecordMode::RECORD_MODE_NEW_CAMERA_ATTACHED)
|
|
{
|
|
for( FSelectedTrackIterator TrackIt(GetSelectedTrackIterator()); TrackIt; ++TrackIt )
|
|
{
|
|
UInterpTrack* TrackToSample = *TrackIt;
|
|
UInterpGroup* ParentGroup = TrackIt.GetGroup();
|
|
|
|
UInterpGroupInst* GrInst = MatineeActor->FindFirstGroupInst(ParentGroup);
|
|
check(GrInst);
|
|
UInterpTrackInst* TrInst = GrInst->TrackInst[TrackIt.GetTrackIndex()];
|
|
check(TrInst);
|
|
|
|
//get the actor that is currently recording
|
|
AActor* Actor = TrInst->GetGroupActor();
|
|
if(!Actor)
|
|
{
|
|
return;
|
|
}
|
|
|
|
//get the parent actor
|
|
/** @todo UE4 no longer using general base attachment system
|
|
AActor* ParentActor = Actor->Base;
|
|
if (ParentActor)
|
|
{
|
|
//save the offsets
|
|
RecordingParentOffsets.Set(ParentActor, ParentActor->GetActorLocation());
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
}
|
|
|
|
/**Apply the movement of the parent to child during recording*/
|
|
void FMatinee::ApplyRecordingParentOffsets(void)
|
|
{
|
|
if (RecordMode == MatineeConstants::ERecordMode::RECORD_MODE_NEW_CAMERA_ATTACHED)
|
|
{
|
|
//list of unique actors to apply parent transforms to
|
|
TArray<AActor*> RecordingActorsWithParents;
|
|
for( FSelectedTrackIterator TrackIt(GetSelectedTrackIterator()); TrackIt; ++TrackIt )
|
|
{
|
|
UInterpTrack* TrackToSample = *TrackIt;
|
|
UInterpGroup* ParentGroup = TrackIt.GetGroup();
|
|
|
|
UInterpGroupInst* GrInst = MatineeActor->FindFirstGroupInst(ParentGroup);
|
|
check(GrInst);
|
|
|
|
//get the actor that is currently recording
|
|
AActor* Actor = GrInst->GetGroupActor();
|
|
if(!Actor)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/** @todo UE4 no longer using general base attachment system
|
|
//get the parent actor
|
|
AActor* ParentActor = Actor->Base;
|
|
if (ParentActor)
|
|
{
|
|
//keep a list of actors to apply offsets to.
|
|
RecordingActorsWithParents.AddUniqueItem(Actor);
|
|
}
|
|
*/
|
|
}
|
|
|
|
/** @todo UE4 no longer using general base attachment system
|
|
//now apply parent offsets to list
|
|
for (int32 i = 0; i < RecordingActorsWithParents.Num(); ++i)
|
|
{
|
|
AActor* Actor = RecordingActorsWithParents(i);
|
|
check(Actor);
|
|
|
|
//get parent actor
|
|
AActor* ParentActor = Actor->Base;
|
|
check(ParentActor);
|
|
|
|
//get the old position out of the map
|
|
FVector OldOffset = RecordingParentOffsets.FindRef(ParentActor);
|
|
|
|
//find the delta of the parent actor
|
|
FVector Delta = ParentActor->GetActorLocation() - OldOffset;
|
|
|
|
//apply the delta to the actor we're recoding
|
|
Actor->DirectSetLocation(Actor->GetActorLocation() + Delta);
|
|
|
|
//we have to move the level viewport as well.
|
|
ACameraActor* CameraActor = Cast<ACameraActor>(Actor);
|
|
if (CameraActor)
|
|
{
|
|
//add new camera
|
|
FLevelEditorViewportClient* LevelVC = GetRecordingViewport();
|
|
if (LevelVC)
|
|
{
|
|
LevelVC->ViewLocation += Delta;
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the custom recording viewport if it has been created yet
|
|
* @return - NULL, if no record viewport exists yet, or the current recording viewport
|
|
*/
|
|
FLevelEditorViewportClient* FMatinee::GetRecordingViewport(void)
|
|
{
|
|
if( MatineeRecorderWindow.IsValid() )
|
|
{
|
|
return MatineeRecorderWindow.Pin()->GetViewport();
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/** Call utility to crop the current key in the selected track. */
|
|
void FMatinee::CropAnimKey(bool bCropBeginning)
|
|
{
|
|
// Check we have a group and track selected
|
|
if( HasATrackSelected() )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Check an AnimControlTrack is selected to avoid messing with the transaction system preemptively.
|
|
if( HasATrackSelected(UInterpTrackAnimControl::StaticClass()) )
|
|
{
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT("UnrealEd", "CropAnimationKey", "Crop Animation Key") );
|
|
{
|
|
for( TTrackClassTypeIterator<UInterpTrackAnimControl> AnimTrackIt(GetSelectedTrackIterator<UInterpTrackAnimControl>()); AnimTrackIt; ++AnimTrackIt )
|
|
{
|
|
UInterpTrackAnimControl* AnimTrack = *AnimTrackIt;
|
|
|
|
// Call crop utility.
|
|
AnimTrack->Modify();
|
|
AnimTrack->CropKeyAtPosition(MatineeActor->InterpPosition, bCropBeginning);
|
|
}
|
|
}
|
|
InterpEdTrans->EndSpecial();
|
|
}
|
|
}
|
|
|
|
/** Jump the position of the interpolation to the current time, updating Actors. */
|
|
void FMatinee::SetInterpPosition( float NewPosition, bool Scrubbing )
|
|
{
|
|
#if WITH_EDITORONLY_DATA
|
|
MatineeActor->bIsScrubbing = Scrubbing;
|
|
#endif
|
|
|
|
bool bTimeChanged = (NewPosition != MatineeActor->InterpPosition);
|
|
|
|
// Make sure particle replay tracks have up-to-date editor-only transient state
|
|
UpdateParticleReplayTracks();
|
|
|
|
// Move preview position in interpolation to where we want it, and update any properties
|
|
MatineeActor->UpdateInterp( NewPosition, true, bTimeChanged );
|
|
|
|
// When playing/scrubbing, we release the current keyframe from editing
|
|
if(bTimeChanged)
|
|
{
|
|
Opt->bAdjustingKeyframe = false;
|
|
Opt->bAdjustingGroupKeyframes = false;
|
|
}
|
|
|
|
// If we are locking the camera to a group, update it here
|
|
UpdateCameraToGroup(true);
|
|
|
|
// Set the camera frustum colours to show which is being viewed.
|
|
UpdateCamColours();
|
|
|
|
// Redraw viewport.
|
|
InvalidateTrackWindowViewports();
|
|
|
|
// Update the position of the marker in the curve view.
|
|
CurveEd->SetPositionMarker( true, MatineeActor->InterpPosition, PosMarkerColor );
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
MatineeActor->bIsScrubbing = false;
|
|
#endif
|
|
}
|
|
|
|
/** Make sure particle replay tracks have up-to-date editor-only transient state */
|
|
void FMatinee::UpdateParticleReplayTracks()
|
|
{
|
|
// Check to see if InterpData exists.
|
|
if( MatineeActor->MatineeData )
|
|
{
|
|
for( int32 CurGroupIndex = 0; CurGroupIndex < MatineeActor->MatineeData->InterpGroups.Num(); ++CurGroupIndex )
|
|
{
|
|
UInterpGroup* CurGroup = MatineeActor->MatineeData->InterpGroups[ CurGroupIndex ];
|
|
if( CurGroup != NULL )
|
|
{
|
|
for( int32 CurTrackIndex = 0; CurTrackIndex < CurGroup->InterpTracks.Num(); ++CurTrackIndex )
|
|
{
|
|
UInterpTrack* CurTrack = CurGroup->InterpTracks[ CurTrackIndex ];
|
|
if( CurTrack != NULL )
|
|
{
|
|
UInterpTrackParticleReplay* ParticleReplayTrack = Cast< UInterpTrackParticleReplay >( CurTrack );
|
|
if( ParticleReplayTrack != NULL )
|
|
{
|
|
// Copy time step
|
|
ParticleReplayTrack->FixedTimeStep = SnapAmount;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Refresh the Matinee position marker and viewport state */
|
|
void FMatinee::RefreshInterpPosition()
|
|
{
|
|
SetInterpPosition( MatineeActor->InterpPosition );
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Get the actor that the camera should currently be viewed through.
|
|
* We look here to see if the viewed group has a Director Track, and if so, return that Group.
|
|
*/
|
|
AActor* FMatinee::GetViewedActor()
|
|
{
|
|
if( CamViewGroup != NULL )
|
|
{
|
|
UInterpGroupDirector* DirGroup = Cast<UInterpGroupDirector>(CamViewGroup);
|
|
if(DirGroup)
|
|
{
|
|
return MatineeActor->FindViewedActor();
|
|
}
|
|
else
|
|
{
|
|
UInterpGroupInst* GroupInst = MatineeActor->FindFirstGroupInst(CamViewGroup);
|
|
if( GroupInst != NULL )
|
|
{
|
|
return GroupInst->GetGroupActor();
|
|
}
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/** Can input NULL to unlock camera from all group. */
|
|
void FMatinee::LockCamToGroup(class UInterpGroup* InGroup, const bool bResetViewports)
|
|
{
|
|
// If different from current locked group - release current.
|
|
if(CamViewGroup && (CamViewGroup != InGroup))
|
|
{
|
|
// Reset viewports (clear roll etc). But not when recording
|
|
if (bResetViewports)
|
|
{
|
|
for(int32 i=0; i<GEditor->LevelViewportClients.Num(); i++)
|
|
{
|
|
FLevelEditorViewportClient* LevelVC =GEditor->LevelViewportClients[i];
|
|
if(LevelVC && LevelVC->IsPerspective() && LevelVC->AllowsCinematicPreview() )
|
|
{
|
|
LevelVC->RemoveCameraRoll();
|
|
LevelVC->ViewFOV = LevelVC->FOVAngle;
|
|
LevelVC->bEnableFading = false;
|
|
LevelVC->bEnableColorScaling = false;
|
|
LevelVC->SetMatineeActorLock(nullptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
CamViewGroup = NULL;
|
|
}
|
|
|
|
// If non-null new group - switch to it now.
|
|
if(InGroup)
|
|
{
|
|
CamViewGroup = InGroup;
|
|
|
|
// Move camera to track now.
|
|
UpdateCameraToGroup(true);
|
|
}
|
|
}
|
|
|
|
/** Update the colours of any CameraActors we are manipulating to match their group colours, and indicate which is 'active'. */
|
|
void FMatinee::UpdateCamColours()
|
|
{
|
|
AActor* ViewedActor = MatineeActor->FindViewedActor();
|
|
|
|
for(int32 i=0; i<MatineeActor->GroupInst.Num(); i++)
|
|
{
|
|
UInterpGroupInst* Inst = MatineeActor->GroupInst[i];
|
|
if(ACameraActor* Cam = Cast<ACameraActor>(Inst->GetGroupActor()))
|
|
{
|
|
const FColor OverrideColor = (Inst->GetGroupActor() == ViewedActor) ? ActiveCamColor : Inst->Group->GroupColor;
|
|
Cam->GetCameraComponent()->OverrideFrustumColor(OverrideColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If we are viewing through a particular group - move the camera to correspond.
|
|
*/
|
|
void FMatinee::UpdateCameraToGroup(const bool bInUpdateStandardViewports, bool bUpdateViewportTransform )
|
|
{
|
|
bool bEnableColorScaling = false;
|
|
FVector ColorScale(1.f,1.f,1.f);
|
|
|
|
// If viewing through the director group, see if we have a fade track, and if so see how much fading we should do.
|
|
float FadeAmount = 0.f;
|
|
if(CamViewGroup)
|
|
{
|
|
UInterpGroupDirector* DirGroup = Cast<UInterpGroupDirector>(CamViewGroup);
|
|
if(DirGroup)
|
|
{
|
|
UInterpTrackFade* FadeTrack = DirGroup->GetFadeTrack();
|
|
if(FadeTrack && !FadeTrack->IsDisabled())
|
|
{
|
|
FadeAmount = FadeTrack->GetFadeAmountAtTime(MatineeActor->InterpPosition);
|
|
}
|
|
|
|
// Set TimeDilation in the LevelInfo based on what the Slomo track says (if there is one).
|
|
UInterpTrackSlomo* SlomoTrack = DirGroup->GetSlomoTrack();
|
|
if(SlomoTrack && !SlomoTrack->IsDisabled())
|
|
{
|
|
MatineeActor->GetWorld()->GetWorldSettings()->MatineeTimeDilation = SlomoTrack->GetSlomoFactorAtTime(MatineeActor->InterpPosition);
|
|
}
|
|
|
|
UInterpTrackColorScale* ColorTrack = DirGroup->GetColorScaleTrack();
|
|
if(ColorTrack && !ColorTrack->IsDisabled())
|
|
{
|
|
bEnableColorScaling = true;
|
|
ColorScale = ColorTrack->GetColorScaleAtTime(MatineeActor->InterpPosition);
|
|
}
|
|
}
|
|
}
|
|
|
|
AActor* DefaultViewedActor = GetViewedActor();
|
|
|
|
if (bInUpdateStandardViewports)
|
|
{
|
|
// Move any perspective viewports to coincide with moved actor.
|
|
for(int32 i=0; i<GEditor->LevelViewportClients.Num(); i++)
|
|
{
|
|
FLevelEditorViewportClient* LevelVC =GEditor->LevelViewportClients[i];
|
|
if(LevelVC && LevelVC->IsPerspective() && LevelVC->AllowsCinematicPreview() )
|
|
{
|
|
UpdateLevelViewport(DefaultViewedActor, LevelVC, FadeAmount, ColorScale, bEnableColorScaling, bUpdateViewportTransform );
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Updates a viewport from a given actor
|
|
* @param InActor - The actor to track the viewport to
|
|
* @param InViewportClient - The viewport to update
|
|
* @param InFadeAmount - Fade amount for camera
|
|
* @param InColorScale - Color scale for render
|
|
* @param bInEnableColorScaling - whether to use color scaling or not
|
|
*/
|
|
void FMatinee::UpdateLevelViewport(AActor* InActor, FLevelEditorViewportClient* InViewportClient, const float InFadeAmount, const FVector& InColorScale, const bool bInEnableColorScaling, bool bUpdateViewportTransform )
|
|
{
|
|
//if we're recording matinee and this is the proper recording window. Do NOT update the viewport (it's being controlled by input)
|
|
if ((RecordingState!=MatineeConstants::ERecordingState::RECORDING_COMPLETE) && InViewportClient->IsMatineeRecordingWindow())
|
|
{
|
|
//if this actor happens to be a camera, let's copy the appropriate viewport settings back to the camera
|
|
ACameraActor* CameraActor = Cast<ACameraActor>(InActor);
|
|
if (CameraActor)
|
|
{
|
|
CameraActor->GetCameraComponent()->FieldOfView = InViewportClient->ViewFOV;
|
|
CameraActor->GetCameraComponent()->AspectRatio = InViewportClient->AspectRatio;
|
|
CameraActor->SetActorLocation(InViewportClient->GetViewLocation(), false);
|
|
CameraActor->SetActorRotation(InViewportClient->GetViewRotation() );
|
|
}
|
|
return;
|
|
}
|
|
|
|
ACameraActor* Cam = Cast<ACameraActor>(InActor);
|
|
if (InActor)
|
|
{
|
|
if( bUpdateViewportTransform )
|
|
{
|
|
InViewportClient->SetViewLocation( InActor->GetActorLocation() );
|
|
InViewportClient->SetViewRotation( InActor->GetActorRotation() );
|
|
}
|
|
|
|
InViewportClient->FadeAmount = InFadeAmount;
|
|
InViewportClient->bEnableFading = true;
|
|
|
|
InViewportClient->bEnableColorScaling = bInEnableColorScaling;
|
|
InViewportClient->ColorScale = InColorScale;
|
|
|
|
if (PreviousCamera != Cam)
|
|
{
|
|
PreviousCamera = Cam;
|
|
InViewportClient->SetIsCameraCut();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
InViewportClient->ViewFOV = InViewportClient->FOVAngle;
|
|
|
|
InViewportClient->FadeAmount = InFadeAmount;
|
|
InViewportClient->bEnableFading = true;
|
|
}
|
|
|
|
// Set the actor lock.
|
|
InViewportClient->SetMatineeActorLock(InActor);
|
|
|
|
// If viewing through a camera - enforce aspect ratio.
|
|
if(Cam)
|
|
{
|
|
// If the Camera's aspect ratio is zero, put a more reasonable default here - this at least stops it from crashing
|
|
// nb. the AspectRatio will be reported as a Map Check Warning
|
|
if( Cam->GetCameraComponent()->AspectRatio == 0 )
|
|
{
|
|
InViewportClient->AspectRatio = 1.7f;
|
|
}
|
|
else
|
|
{
|
|
InViewportClient->AspectRatio = Cam->GetCameraComponent()->AspectRatio;
|
|
}
|
|
|
|
//if this isn't the recording viewport OR (it is the recording viewport and it's playing or we're scrubbing the timeline
|
|
if ((InViewportClient != GetRecordingViewport()) || (MatineeActor && ( MatineeActor->bIsPlaying || (TrackWindow.IsValid() && TrackWindow->InterpEdVC->bGrabbingHandle ) ) ))
|
|
{
|
|
//don't stop the camera from zooming when not playing back
|
|
InViewportClient->ViewFOV = Cam->GetCameraComponent()->FieldOfView;
|
|
|
|
// If there are selected actors, invalidate the viewports hit proxies, otherwise they won't be selectable afterwards
|
|
if ( InViewportClient->Viewport && GEditor->GetSelectedActorCount() > 0 )
|
|
{
|
|
InViewportClient->Viewport->InvalidateHitProxy();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update ControllingActorViewInfo, so it is in sync with the updated viewport
|
|
bUpdatingCameraGuard = true;
|
|
InViewportClient->UpdateViewForLockedActor();
|
|
bUpdatingCameraGuard = false;
|
|
}
|
|
|
|
/** Restores a viewports' settings that were overridden by UpdateLevelViewport, where necessary. */
|
|
void FMatinee::SaveLevelViewports()
|
|
{
|
|
for( int32 ViewIndex = 0; ViewIndex < GEditor->LevelViewportClients.Num(); ++ViewIndex )
|
|
{
|
|
FLevelEditorViewportClient* LevelVC = GEditor->LevelViewportClients[ ViewIndex ];
|
|
if( LevelVC && LevelVC->IsPerspective() && LevelVC->AllowsCinematicPreview() )
|
|
{
|
|
FMatineeViewSaveData SaveData;
|
|
SaveData.ViewIndex = ViewIndex;
|
|
SaveData.ViewLocation = LevelVC->GetViewLocation();
|
|
SaveData.ViewRotation = LevelVC->GetViewRotation();
|
|
|
|
SavedViewportData.Add( SaveData );
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Restores viewports' settings that were overridden by UpdateLevelViewport, where necessary. */
|
|
void FMatinee::RestoreLevelViewports()
|
|
{
|
|
for( int32 SaveIndex = 0; SaveIndex < SavedViewportData.Num(); ++SaveIndex )
|
|
{
|
|
const FMatineeViewSaveData& SavedData = SavedViewportData[ SaveIndex ];
|
|
if( SavedData.ViewIndex < GEditor->LevelViewportClients.Num() )
|
|
{
|
|
FLevelEditorViewportClient* LevelVC = GEditor->LevelViewportClients[ SavedData.ViewIndex ];
|
|
if ( LevelVC && LevelVC->IsPerspective() && LevelVC->AllowsCinematicPreview() )
|
|
{
|
|
LevelVC->SetMatineeActorLock( nullptr );
|
|
LevelVC->SetViewRotation( SavedData.ViewRotation );
|
|
LevelVC->SetViewLocation( SavedData.ViewLocation );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Redraw
|
|
FEditorSupportDelegates::RedrawAllViewports.Broadcast();
|
|
}
|
|
|
|
// Notification from the EdMode that a perspective camera has moves.
|
|
// If we are locking the camera to a particular actor - we update its location to match.
|
|
void FMatinee::CamMoved(const FVector& NewCamLocation, const FRotator& NewCamRotation)
|
|
{
|
|
// Don't update if we were in the middle of synchronizing the camera location.
|
|
if ( bUpdatingCameraGuard )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If cam not locked to something, do nothing.
|
|
AActor* ViewedActor = GetViewedActor();
|
|
if(ViewedActor)
|
|
{
|
|
// Update actors location/rotation from camera
|
|
ViewedActor->SetActorLocation(NewCamLocation, false);
|
|
ViewedActor->SetActorRotation(NewCamRotation);
|
|
|
|
// The camera was moved already we dont need to set it again
|
|
bool bUpdateViewportTransform = false;
|
|
|
|
// In case we were modifying a keyframe for this actor.
|
|
ActorModified( bUpdateViewportTransform );
|
|
}
|
|
}
|
|
|
|
|
|
void FMatinee::ActorModified( bool bUpdateViewportTransform )
|
|
{
|
|
// We only see if we need to update a track if we have a keyframe selected.
|
|
if(Opt->bAdjustingKeyframe || Opt->bAdjustingGroupKeyframes)
|
|
{
|
|
check(Opt->SelectedKeys.Num() > 0);
|
|
|
|
// For sanitys sake, make sure all these keys are part of the same group
|
|
FInterpEdSelKey& SelKey = Opt->SelectedKeys[0];
|
|
for( int32 iSelectedKey = 1; iSelectedKey < Opt->SelectedKeys.Num(); iSelectedKey++ )
|
|
{
|
|
FInterpEdSelKey& rSelKey = Opt->SelectedKeys[iSelectedKey];
|
|
if ( rSelKey.Group != SelKey.Group)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Find the actor controlled by the selected group.
|
|
UInterpGroupInst* GrInst = MatineeActor->FindFirstGroupInst(SelKey.Group);
|
|
if( GrInst == NULL || GrInst->GetGroupActor() == NULL )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// See if this is one of the actors that was just moved.
|
|
bool bTrackActorModified = false;
|
|
|
|
TArray<UObject*> NewObjects;
|
|
for ( FSelectionIterator It( GEditor->GetSelectedActorIterator() ) ; It ; ++It )
|
|
{
|
|
AActor* Actor = static_cast<AActor*>( *It );
|
|
checkSlow( Actor->IsA(AActor::StaticClass()) );
|
|
|
|
if ( Actor == GrInst->GetGroupActor() )
|
|
{
|
|
bTrackActorModified = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If so, update the selected keyframe on the selected track to reflect its new position.
|
|
if(bTrackActorModified)
|
|
{
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT("UnrealEd", "UpdateKeyframe", "Update Key Frame") );
|
|
|
|
for( int32 iSelectedKey = 0; iSelectedKey < Opt->SelectedKeys.Num(); iSelectedKey++ )
|
|
{
|
|
FInterpEdSelKey& rSelKey = Opt->SelectedKeys[iSelectedKey];
|
|
rSelKey.Track->Modify();
|
|
|
|
UInterpTrack* Parent = Cast<UInterpTrack>( rSelKey.Track->GetOuter() );
|
|
if( Parent )
|
|
{
|
|
// This track is a subtrack of some other track.
|
|
// Get the parent track index and let the parent update the childs keyframe
|
|
UInterpTrackInst* TrInst = GrInst->TrackInst[ rSelKey.Group->InterpTracks.Find( Parent ) ];
|
|
Parent->UpdateChildKeyframe( rSelKey.Track, rSelKey.KeyIndex, TrInst );
|
|
}
|
|
else
|
|
{
|
|
// This track is a normal track parented to a group
|
|
UInterpTrackInst* TrInst = GrInst->TrackInst[ rSelKey.Group->InterpTracks.Find( rSelKey.Track ) ];
|
|
rSelKey.Track->UpdateKeyframe( rSelKey.KeyIndex, TrInst );
|
|
}
|
|
}
|
|
|
|
InterpEdTrans->EndSpecial();
|
|
}
|
|
}
|
|
|
|
// This might have been a camera propety - update cameras.
|
|
UpdateCameraToGroup( true, bUpdateViewportTransform );
|
|
}
|
|
|
|
void FMatinee::ActorSelectionChange( const bool bClearSelectionIfInvalid /*= true*/ )
|
|
{
|
|
// Ignore this selection notification if desired.
|
|
if(AMatineeActor::IgnoreActorSelection())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// When an actor selection changed and the interp groups associated to the selected actors does NOT match
|
|
// the selected interp groups (or tracks if only interp tracks are selected), that means the user selected
|
|
// an actor in the level editing viewport and we need to synchronize the selection in Matinee.
|
|
|
|
TArray<UInterpGroup*> ActorGroups;
|
|
|
|
// First, gather all the interp groups associated to the selected actors
|
|
// so that we can compare them to selected groups or tracks in Matinee.
|
|
for( FSelectionIterator It( GEditor->GetSelectedActorIterator() ) ; It ; ++It )
|
|
{
|
|
AActor* Actor = static_cast<AActor*>( *It );
|
|
checkSlow( Actor->IsA(AActor::StaticClass()) );
|
|
|
|
UInterpGroupInst* GroupInstance = MatineeActor->FindGroupInst(Actor);
|
|
|
|
if(GroupInstance)
|
|
{
|
|
check(GroupInstance->Group);
|
|
ActorGroups.AddUnique(GroupInstance->Group);
|
|
}
|
|
}
|
|
|
|
if( ActorGroups.Num() > 0 )
|
|
{
|
|
// There are actors referenced in the opened Matinee. Now, figure
|
|
// out if selected actors matches the selection in Matinee.
|
|
|
|
bool bSelectionIsOutOfSync = false;
|
|
|
|
// If all selected groups or tracks match up to the selected actors, then the user
|
|
// selected a group or track in Matinee. In this case, we are in sync with Matinee.
|
|
|
|
if( HasATrackSelected() )
|
|
{
|
|
for( TArray<UInterpGroup*>::TIterator GroupIter(ActorGroups); GroupIter; ++GroupIter )
|
|
{
|
|
if( !HasATrackSelected(*GroupIter) )
|
|
{
|
|
// NOTE: Since one selected actor did not have a selected track, we will clear the track selection in favor
|
|
// of selecting the groups instead. We do this because we don't know if the actor without the selected
|
|
// track even has an interp track. It is guaranteed, however, to have an interp group associated to it.
|
|
bSelectionIsOutOfSync = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for( TArray<UInterpGroup*>::TIterator GroupIter(ActorGroups); GroupIter; ++GroupIter )
|
|
{
|
|
if( !IsGroupSelected(*GroupIter) )
|
|
{
|
|
bSelectionIsOutOfSync = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// The selected actors don't match up to the selection state in Matinee!
|
|
if( bSelectionIsOutOfSync )
|
|
{
|
|
// Clear out all selections because the user might have deselected something, which means
|
|
// it wouldn't be in the array of interp groups that was gathered in this function.
|
|
DeselectAll(false);
|
|
|
|
for( TArray<UInterpGroup*>::TIterator GroupIter(ActorGroups); GroupIter; ++GroupIter )
|
|
{
|
|
// We're updating the selection to match the selected actors; don't select the actors in this group
|
|
SelectGroup(*GroupIter, false, false);
|
|
|
|
ScrollToGroup( *GroupIter );
|
|
}
|
|
}
|
|
}
|
|
// If there are no interp groups associated to the selected
|
|
// actors, then clear out any existing Matinee selections.
|
|
else if( bClearSelectionIfInvalid )
|
|
{
|
|
AMatineeActor::PushIgnoreActorSelection();
|
|
DeselectAll();
|
|
AMatineeActor::PopIgnoreActorSelection();
|
|
}
|
|
}
|
|
|
|
bool FMatinee::ProcessKeyPress(FKey Key, bool bCtrlDown, bool bAltDown)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Zooms the curve editor and track editor in or out by the specified amount
|
|
*
|
|
* @param ZoomAmount Amount to zoom in or out
|
|
* @param bZoomToTimeCursorPos True if we should zoom to the time cursor position, otherwise mouse cursor position
|
|
*/
|
|
void FMatinee::ZoomView( float ZoomAmount, bool bZoomToTimeCursorPos )
|
|
{
|
|
// Proportion of interp we are currently viewing
|
|
const float OldTimeRange = ViewEndTime - ViewStartTime;
|
|
float CurrentZoomFactor = OldTimeRange / TrackViewSizeX;
|
|
|
|
float NewZoomFactor = FMath::Clamp<float>(CurrentZoomFactor * ZoomAmount, 0.0003f, 1.0f);
|
|
float NewTimeRange = NewZoomFactor * TrackViewSizeX;
|
|
|
|
// zoom into scrub position
|
|
if(bZoomToScrubPos)
|
|
{
|
|
float ViewMidTime = MatineeActor->InterpPosition;
|
|
ViewStartTime = ViewMidTime - 0.5*NewTimeRange;
|
|
ViewEndTime = ViewMidTime + 0.5*NewTimeRange;
|
|
}
|
|
else
|
|
{
|
|
bool bZoomedToCursorPos = false;
|
|
|
|
if (TrackWindow.IsValid() && TrackWindow->IsHovered())
|
|
{
|
|
// Figure out where the mouse cursor is over the Matinee track editor timeline
|
|
FIntPoint ClientMousePos = TrackWindow->GetMousePos();
|
|
int32 ViewportClientAreaX = ClientMousePos.X;
|
|
int32 MouseXOverTimeline = ClientMousePos.X - LabelWidth;
|
|
|
|
if( MouseXOverTimeline >= 0 && MouseXOverTimeline < TrackViewSizeX )
|
|
{
|
|
// zoom into the mouse cursor's position over the view
|
|
const float CursorPosInTime = ViewStartTime + ( MouseXOverTimeline / PixelsPerSec );
|
|
const float CursorPosScalar = ( CursorPosInTime - ViewStartTime ) / OldTimeRange;
|
|
|
|
ViewStartTime = CursorPosInTime - CursorPosScalar * NewTimeRange;
|
|
ViewEndTime = CursorPosInTime + ( 1.0f - CursorPosScalar ) * NewTimeRange;
|
|
|
|
bZoomedToCursorPos = true;
|
|
}
|
|
}
|
|
|
|
// We'll only zoom to the middle if we weren't already able to zoom to the cursor position. Useful
|
|
// if the mouse is outside of the window but the window still has focus for the zoom event
|
|
if( !bZoomedToCursorPos )
|
|
{
|
|
// zoom into middle of view
|
|
float ViewMidTime = ViewStartTime + 0.5f*(ViewEndTime - ViewStartTime);
|
|
ViewStartTime = ViewMidTime - 0.5*NewTimeRange;
|
|
ViewEndTime = ViewMidTime + 0.5*NewTimeRange;
|
|
}
|
|
}
|
|
|
|
SyncCurveEdView();
|
|
}
|
|
|
|
struct TopLevelGroupInfo
|
|
{
|
|
/** Index in original list */
|
|
int32 GroupIndex;
|
|
|
|
/** Number of children */
|
|
int32 ChildCount;
|
|
};
|
|
|
|
void FMatinee::MoveActiveBy(int32 MoveBy)
|
|
{
|
|
const bool bOnlyOneGroupSelected = (GetSelectedGroupCount() == 1);
|
|
const bool bOnlyOneTrackSelected = (GetSelectedTrackCount() == 1);
|
|
|
|
// Only one group or one track can be selected for this operation
|
|
if( !(bOnlyOneGroupSelected ^ bOnlyOneTrackSelected) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// We only support moving 1 unit in either direction
|
|
check( FMath::Abs( MoveBy ) == 1 );
|
|
|
|
InterpEdTrans->BeginSpecial( NSLOCTEXT("UnrealEd", "InterpEd_Move_SelectedTrackOrGroup", "Move Selected Track or Group") );
|
|
|
|
// If no track selected, move group
|
|
if( bOnlyOneGroupSelected )
|
|
{
|
|
UInterpGroup* SelectedGroup = *(GetSelectedGroupIterator());
|
|
int32 SelectedGroupIndex = IData->InterpGroups.Find(SelectedGroup);
|
|
|
|
// Is this a root group or a child group? We'll only allow navigation through groups within the current scope.
|
|
const bool bIsChildGroup = SelectedGroup->bIsParented;
|
|
|
|
// If we're moving a child group, then don't allow it to move outside of it's current folder's sub-group list
|
|
if( bIsChildGroup )
|
|
{
|
|
int32 TargetGroupIndex = SelectedGroupIndex + MoveBy;
|
|
|
|
if( TargetGroupIndex >= 0 && TargetGroupIndex < IData->InterpGroups.Num() )
|
|
{
|
|
UInterpGroup* GroupToCheck = IData->InterpGroups[ TargetGroupIndex ];
|
|
if( !GroupToCheck->bIsParented )
|
|
{
|
|
// Uh oh, we've reached the end of our parent group's list. We'll deny movement.
|
|
TargetGroupIndex = SelectedGroupIndex;
|
|
}
|
|
}
|
|
|
|
if(TargetGroupIndex != SelectedGroupIndex && TargetGroupIndex >= 0 && TargetGroupIndex < IData->InterpGroups.Num())
|
|
{
|
|
IData->Modify();
|
|
|
|
UInterpGroup* TempGroup = IData->InterpGroups[TargetGroupIndex];
|
|
IData->InterpGroups[TargetGroupIndex] = IData->InterpGroups[SelectedGroupIndex];
|
|
IData->InterpGroups[SelectedGroupIndex] = TempGroup;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We're moving a root group. This is a bit tricky. Our (single level) 'hierarchy' of groups is really just
|
|
// a flat list of elements with a bool that indicates whether the element is a child of the previous non-child
|
|
// element, so we need to be careful to skip over all child groups when reordering things.
|
|
|
|
// Also, we'll also skip over the director group if we find one, since those will always appear immutable to the
|
|
// user in the GUI. The director group draws at the top of the group list and never appears underneath
|
|
// another group or track, so we don't want to consider it when rearranging groups through the UI.
|
|
|
|
// Digest information about the group list
|
|
TArray< TopLevelGroupInfo > TopLevelGroups;
|
|
int32 SelectedGroupTLIndex = INDEX_NONE;
|
|
{
|
|
int32 LastParentListIndex = INDEX_NONE;
|
|
for( int32 CurGroupIndex = 0; CurGroupIndex < IData->InterpGroups.Num(); ++CurGroupIndex )
|
|
{
|
|
UInterpGroup* CurGroup = IData->InterpGroups[ CurGroupIndex ];
|
|
|
|
if( CurGroup->bIsParented )
|
|
{
|
|
// Add a new child to the last top level group
|
|
check( LastParentListIndex != INDEX_NONE );
|
|
++TopLevelGroups[ LastParentListIndex ].ChildCount;
|
|
}
|
|
else
|
|
{
|
|
// A new top level group!
|
|
TopLevelGroupInfo NewTopLevelGroup;
|
|
NewTopLevelGroup.GroupIndex = CurGroupIndex;
|
|
|
|
// Start at zero; we'll count these as we go along
|
|
NewTopLevelGroup.ChildCount = 0;
|
|
|
|
LastParentListIndex = TopLevelGroups.Add( NewTopLevelGroup );
|
|
|
|
// If this is the active group, then keep track of that
|
|
if( CurGroup == SelectedGroup )
|
|
{
|
|
SelectedGroupTLIndex = LastParentListIndex;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Make sure we found ourselves in the list
|
|
check( SelectedGroupTLIndex != INDEX_NONE );
|
|
|
|
|
|
|
|
// Determine our top-level list target
|
|
int32 TargetTLIndex = SelectedGroupTLIndex + MoveBy;
|
|
if( TargetTLIndex >= 0 && TargetTLIndex < TopLevelGroups.Num() )
|
|
{
|
|
// Skip over director groups if we need to
|
|
if( IData->InterpGroups[ TopLevelGroups[ TargetTLIndex ].GroupIndex ]->IsA( UInterpGroupDirector::StaticClass() ) )
|
|
{
|
|
TargetTLIndex += MoveBy;
|
|
}
|
|
}
|
|
|
|
// Make sure we're still in range
|
|
if( TargetTLIndex >= 0 && TargetTLIndex < TopLevelGroups.Num() )
|
|
{
|
|
// Compute the list index that we'll be 'inserting before'
|
|
int32 InsertBeforeTLIndex = TargetTLIndex;
|
|
if( MoveBy > 0 )
|
|
{
|
|
++InsertBeforeTLIndex;
|
|
}
|
|
|
|
// Compute our list destination
|
|
int32 TargetGroupIndex;
|
|
if( InsertBeforeTLIndex < TopLevelGroups.Num() )
|
|
{
|
|
// Grab the top-level target group
|
|
UInterpGroup* TLTargetGroup = IData->InterpGroups[ TopLevelGroups[ InsertBeforeTLIndex ].GroupIndex ];
|
|
|
|
// Setup 'insert' target group index
|
|
TargetGroupIndex = TopLevelGroups[ InsertBeforeTLIndex ].GroupIndex;
|
|
}
|
|
else
|
|
{
|
|
// We need to be at the very end of the list!
|
|
TargetGroupIndex = IData->InterpGroups.Num();
|
|
}
|
|
|
|
|
|
// OK, time to move!
|
|
const int32 NumChildGroups = CountGroupFolderChildren( SelectedGroup );
|
|
const int32 NumGroupsToMove = NumChildGroups + 1;
|
|
|
|
|
|
// We're about to modify stuff
|
|
IData->Modify();
|
|
|
|
|
|
// Remove source groups from master list
|
|
TArray< UInterpGroup* > GroupsToMove;
|
|
for( int32 GroupToMoveIndex = 0; GroupToMoveIndex < NumGroupsToMove; ++GroupToMoveIndex )
|
|
{
|
|
GroupsToMove.Add( IData->InterpGroups[ SelectedGroupIndex ] );
|
|
IData->InterpGroups.RemoveAt( SelectedGroupIndex );
|
|
|
|
// Adjust our target index for removed groups
|
|
if( TargetGroupIndex >= SelectedGroupIndex )
|
|
{
|
|
--TargetGroupIndex;
|
|
}
|
|
};
|
|
|
|
|
|
// Reinsert source groups at destination index
|
|
for( int32 GroupToMoveIndex = 0; GroupToMoveIndex < NumGroupsToMove; ++GroupToMoveIndex )
|
|
{
|
|
int32 DestGroupIndex = TargetGroupIndex + GroupToMoveIndex;
|
|
IData->InterpGroups.Insert( GroupsToMove[ GroupToMoveIndex ], DestGroupIndex );
|
|
};
|
|
|
|
// Make sure the curve editor is in sync
|
|
CurveEd->CurveChanged();
|
|
}
|
|
else
|
|
{
|
|
// Out of range, we can't move any further
|
|
}
|
|
}
|
|
}
|
|
// If a track is selected, move it instead.
|
|
else
|
|
{
|
|
FSelectedTrackIterator TrackIt(GetSelectedTrackIterator());
|
|
UInterpGroup* Group = TrackIt.GetGroup();
|
|
int32 TrackIndex = TrackIt.GetTrackIndex();
|
|
|
|
// Move the track itself.
|
|
int32 TargetTrackIndex = TrackIndex + MoveBy;
|
|
|
|
Group->Modify();
|
|
|
|
if(TargetTrackIndex >= 0 && TargetTrackIndex < Group->InterpTracks.Num())
|
|
{
|
|
UInterpTrack* TempTrack = Group->InterpTracks[TargetTrackIndex];
|
|
Group->InterpTracks[TargetTrackIndex] = Group->InterpTracks[TrackIndex];
|
|
Group->InterpTracks[TrackIndex] = TempTrack;
|
|
|
|
// Now move any track instances inside their group instance.
|
|
for(int32 i=0; i<MatineeActor->GroupInst.Num(); i++)
|
|
{
|
|
UInterpGroupInst* GrInst = MatineeActor->GroupInst[i];
|
|
if(GrInst->Group == Group)
|
|
{
|
|
check(GrInst->TrackInst.Num() == Group->InterpTracks.Num());
|
|
|
|
GrInst->Modify();
|
|
|
|
UInterpTrackInst* TempTrInst = GrInst->TrackInst[TargetTrackIndex];
|
|
GrInst->TrackInst[TargetTrackIndex] = GrInst->TrackInst[TrackIndex];
|
|
GrInst->TrackInst[TrackIndex] = TempTrInst;
|
|
}
|
|
}
|
|
|
|
// Update selection to keep same track selected.
|
|
TrackIt.MoveIteratorBy(MoveBy);
|
|
|
|
// Selection stores keys by track index - safest to invalidate here.
|
|
ClearKeySelection();
|
|
}
|
|
}
|
|
|
|
InterpEdTrans->EndSpecial();
|
|
|
|
UInterpGroup* Group = NULL;
|
|
int32 LabelTop = 0;
|
|
int32 LabelBottom = 0;
|
|
|
|
if( HasATrackSelected() )
|
|
{
|
|
FSelectedTrackIterator TrackIter = GetSelectedTrackIterator();
|
|
Group = TrackIter.GetGroup();
|
|
GetTrackLabelPositions( Group, TrackIter.GetTrackIndex(), LabelTop, LabelBottom );
|
|
}
|
|
else
|
|
{
|
|
FSelectedGroupIterator GroupIter = GetSelectedGroupIterator();
|
|
Group = *GroupIter;
|
|
GetGroupLabelPosition( Group, LabelTop, LabelBottom );
|
|
|
|
}
|
|
|
|
// Attempt to autoscroll when the user moves a track or group label out of view.
|
|
if( Group != NULL )
|
|
{
|
|
// Figure out which window we are panning
|
|
TSharedPtr<SMatineeViewport> CurrentWindow = Group->IsA(UInterpGroupDirector::StaticClass()) ? DirectorTrackWindow : TrackWindow;
|
|
if (CurrentWindow.IsValid() && CurrentWindow->InterpEdVC.IsValid())
|
|
{
|
|
const int32 ThumbTop = CurrentWindow->GetThumbPosition();
|
|
const uint32 ViewportHeight = CurrentWindow->InterpEdVC->Viewport->GetSizeXY().Y;
|
|
const uint32 ContentHeight = CurrentWindow->InterpEdVC->ComputeGroupListContentHeight();
|
|
const uint32 ContentBoxHeight = CurrentWindow->InterpEdVC->ComputeGroupListBoxHeight( ViewportHeight );
|
|
const int32 ThumbBottom = ThumbTop + ContentBoxHeight;
|
|
|
|
|
|
// Start the scrollbar at the current location. If the
|
|
// selected track title is visible, nothing will be scrolled.
|
|
int32 NewScrollPosition = ThumbTop;
|
|
|
|
// If the user moved the track title up and it's not viewable anymore,
|
|
// move the scrollbar up so that the selected track is visible.
|
|
if( MoveBy < 0 && (LabelTop - ThumbTop) < 0 )
|
|
{
|
|
NewScrollPosition += (LabelTop - ThumbTop);
|
|
}
|
|
// If the user moved the track title down and it's not viewable anymore,
|
|
// move the scrollbar down so that the selected track is visible.
|
|
else if( MoveBy > 0 && ThumbBottom < LabelBottom )
|
|
{
|
|
NewScrollPosition += (LabelBottom - ThumbBottom);
|
|
}
|
|
|
|
CurrentWindow->SetThumbPosition(NewScrollPosition);
|
|
CurrentWindow->AdjustScrollBar();
|
|
}
|
|
}
|
|
|
|
// Dirty the track window viewports
|
|
InvalidateTrackWindowViewports();
|
|
}
|
|
|
|
void FMatinee::MoveActiveUp()
|
|
{
|
|
MoveActiveBy(-1);
|
|
}
|
|
|
|
void FMatinee::MoveActiveDown()
|
|
{
|
|
MoveActiveBy(+1);
|
|
}
|
|
|
|
void FMatinee::InterpEdUndo()
|
|
{
|
|
GEditor->Trans->Undo();
|
|
|
|
if (IData != NULL)
|
|
{
|
|
CurveEd->SetRegionMarker(true, IData->EdSectionStart, IData->EdSectionEnd, RegionFillColor);
|
|
CurveEd->SetEndMarker(true, IData->InterpLength);
|
|
}
|
|
|
|
Opt->bAdjustingKeyframe = false;
|
|
Opt->bAdjustingGroupKeyframes = false;
|
|
|
|
// A new group may have been added (via duplication), so we'll need to update our scroll bar
|
|
UpdateTrackWindowScrollBars();
|
|
|
|
// Make sure that the viewports get updated after the Undo operation
|
|
InvalidateTrackWindowViewports();
|
|
|
|
if(IData != NULL)
|
|
{
|
|
IData->UpdateEventNames();
|
|
}
|
|
|
|
if(MatineeActor != NULL)
|
|
{
|
|
MatineeActor->EnsureActorGroupConsistency();
|
|
MatineeActor->RecaptureActorState();
|
|
}
|
|
}
|
|
|
|
void FMatinee::InterpEdRedo()
|
|
{
|
|
GEditor->Trans->Redo();
|
|
|
|
if (IData != NULL)
|
|
{
|
|
CurveEd->SetRegionMarker(true, IData->EdSectionStart, IData->EdSectionEnd, RegionFillColor);
|
|
CurveEd->SetEndMarker(true, IData->InterpLength);
|
|
}
|
|
|
|
Opt->bAdjustingKeyframe = false;
|
|
Opt->bAdjustingGroupKeyframes = false;
|
|
|
|
// A new group may have been added (via duplication), so we'll need to update our scroll bar
|
|
UpdateTrackWindowScrollBars();
|
|
|
|
// Make sure that the viewports get updated after the Undo operation
|
|
InvalidateTrackWindowViewports();
|
|
|
|
if(IData != NULL)
|
|
{
|
|
IData->UpdateEventNames();
|
|
}
|
|
|
|
if(MatineeActor != NULL)
|
|
{
|
|
MatineeActor->EnsureActorGroupConsistency();
|
|
MatineeActor->RecaptureActorState();
|
|
}
|
|
}
|
|
|