// Copyright Epic Games, Inc. All Rights Reserved. #include "MVVM/ViewModels/ObjectBindingModel.h" #include "MVVM/ViewModels/FolderModel.h" #include "MVVM/ViewModels/SequenceModel.h" #include "MVVM/ViewModels/TrackModel.h" #include "MVVM/ViewModels/LayerBarModel.h" #include "MVVM/ViewModels/BindingLifetimeTrackModel.h" #include "MVVM/ViewModels/ViewModelIterators.h" #include "MVVM/TrackModelStorageExtension.h" #include "MVVM/ObjectBindingModelStorageExtension.h" #include "MVVM/ViewModels/SequencerEditorViewModel.h" #include "MVVM/ViewModels/OutlinerViewModelDragDropOp.h" #include "MVVM/ViewModels/OutlinerColumns/OutlinerColumnTypes.h" #include "MVVM/Views/SOutlinerObjectBindingView.h" #include "MVVM/Views/STrackLane.h" #include "MVVM/Selection/Selection.h" #include "MVVM/Extensions/IRecyclableExtension.h" #include "MVVM/Extensions/IBindingLifetimeExtension.h" #include "ISequencerObjectSchema.h" #include "Algo/Sort.h" #include "AnimatedRange.h" #include "ClassViewerModule.h" #include "Containers/ArrayBuilder.h" #include "Engine/LevelStreaming.h" #include "Framework/Application/SlateApplication.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "IDetailCustomization.h" #include "IDetailsView.h" #include "ISequencerModule.h" #include "ISequencerTrackEditor.h" #include "IStructureDetailsView.h" #include "Modules/ModuleManager.h" #include "MovieScene.h" #include "MovieSceneBinding.h" #include "MovieSceneDynamicBindingCustomization.h" #include "UniversalObjectLocator.h" #include "MovieSceneFolder.h" #include "ObjectBindingTagCache.h" #include "ObjectEditorUtils.h" #include "PropertyEditorModule.h" #include "PropertyPath.h" #include "SObjectBindingTag.h" #include "ScopedTransaction.h" #include "Sequencer.h" #include "SequencerCommands.h" #include "SequencerNodeTree.h" #include "SequencerSettings.h" #include "StructUtils/PropertyBag.h" #include "MVVM/Views/ViewUtilities.h" #include "Misc/SequencerObjectBindingHelper.h" #include "Styling/AppStyle.h" #include "Styling/SlateIconFinder.h" #include "Widgets/SSequencerBindingLifetimeOverlay.h" #include "Decorations/MovieSceneMuteSoloDecoration.h" #define LOCTEXT_NAMESPACE "ObjectBindingModel" namespace UE::Sequencer { bool GSequencerObjectBindingShowNestedProperties = false; FAutoConsoleVariableRef CVarSequencerObjectBindingShowNestedProperties( TEXT("Sequencer.ObjectBinding.ShowNestedProperties"), GSequencerObjectBindingShowNestedProperties, TEXT("(Default: false) When enabled, always show bound object properties as sub-menus reflecting the hierarchy of nested structures. When disabled, only do that for Level Sequences, make others use flat menus."), ECVF_Default ); FObjectBindingModel::FObjectBindingModel(FSequenceModel* InOwnerModel, const FMovieSceneBinding& InBinding) : ObjectBindingID(InBinding.GetObjectGuid()) , TrackAreaList(EViewModelListType::TrackArea) , TopLevelChildTrackAreaList(GetTopLevelChildTrackAreaGroupType()) , OwnerModel(InOwnerModel) { RegisterChildList(&TrackAreaList); RegisterChildList(&TopLevelChildTrackAreaList); SetIdentifier(*ObjectBindingID.ToString()); } FObjectBindingModel::~FObjectBindingModel() { } EViewModelListType FObjectBindingModel::GetTopLevelChildTrackAreaGroupType() { static EViewModelListType TopLevelChildTrackAreaGroup = RegisterCustomModelListType(); return TopLevelChildTrackAreaGroup; } void FObjectBindingModel::OnConstruct() { if (!LayerBar) { TSharedPtr EditorViewModel = GetEditor(); TSharedPtr Sequencer = EditorViewModel->GetSequencerImpl(); if (Sequencer->GetSequencerSettings()->GetShowLayerBars()) { LayerBar = MakeShared(AsShared()); LayerBar->SetLinkedOutlinerItem(SharedThis(this)); GetChildrenForList(&TopLevelChildTrackAreaList).AddChild(LayerBar); } } UMovieScene* MovieScene = OwnerModel->GetMovieScene(); check(MovieScene); FMovieSceneBinding* Binding = MovieScene->FindBinding(ObjectBindingID); check(Binding); FScopedViewModelListHead RecycledHead(AsShared(), EViewModelListType::Recycled); GetChildrenForList(&OutlinerChildList).MoveChildrenTo(RecycledHead.GetChildren(), IRecyclableExtension::CallOnRecycle); for (UMovieSceneTrack* Track : Binding->GetTracks()) { AddTrack(Track); } } void FObjectBindingModel::SetParentBindingID(const FGuid& InObjectBindingID) { ParentObjectBindingID = InObjectBindingID; } FGuid FObjectBindingModel::GetDesiredParentBinding() const { return ParentObjectBindingID; } EObjectBindingType FObjectBindingModel::GetType() const { return EObjectBindingType::Unknown; } const UClass* FObjectBindingModel::FindObjectClass() const { return UObject::StaticClass(); } bool FObjectBindingModel::SupportsRebinding() const { return true; } FTrackAreaParameters FObjectBindingModel::GetTrackAreaParameters() const { FTrackAreaParameters Params; Params.LaneType = ETrackAreaLaneType::Nested; return Params; } FViewModelVariantIterator FObjectBindingModel::GetTrackAreaModelList() const { return &TrackAreaList; } FViewModelVariantIterator FObjectBindingModel::GetTopLevelChildTrackAreaModels() const { return &TopLevelChildTrackAreaList; } void FObjectBindingModel::AddTrack(UMovieSceneTrack* Track) { FTrackModelStorageExtension* TrackStorage = OwnerModel->CastDynamic(); TViewModelPtr TrackModel = TrackStorage->CreateModelForTrack(Track, AsShared()); GetChildrenForList(&OutlinerChildList).AddChild(TrackModel); if (TrackModel->IsA()) { if (!BindingLifetimeOverlayModel) { BindingLifetimeOverlayModel = MakeShared(AsShared(), GetEditor(), TrackModel.ImplicitCast()); BindingLifetimeOverlayModel->SetLinkedOutlinerItem(SharedThis(this)); GetChildrenForList(&TrackAreaList).AddChild(BindingLifetimeOverlayModel); } } } void FObjectBindingModel::RemoveTrack(UMovieSceneTrack* Track) { FTrackModelStorageExtension* TrackStorage = OwnerModel->CastDynamic(); TSharedPtr TrackModel = GetChildrenOfType().FindBy(Track, &FTrackModel::GetTrack); if (TrackModel) { TrackModel->RemoveFromParent(); if (TrackModel->IsA()) { if (BindingLifetimeOverlayModel) { BindingLifetimeOverlayModel->RemoveFromParent(); BindingLifetimeOverlayModel.Reset(); } } } } FGuid FObjectBindingModel::GetObjectGuid() const { return ObjectBindingID; } FOutlinerSizing FObjectBindingModel::GetOutlinerSizing() const { const float CompactHeight = 28.f; FViewDensityInfo Density = GetEditor()->GetViewDensity(); return FOutlinerSizing(Density.UniformHeight.Get(CompactHeight)); } void FObjectBindingModel::GetIdentifierForGrouping(TStringBuilder<128>& OutString) const { FOutlinerItemModel::GetIdentifier().ToString(OutString); } TSharedPtr FObjectBindingModel::CreateOutlinerViewForColumn(const FCreateOutlinerViewParams& InParams, const FName& InColumnName) { TSharedPtr EditorViewModel = GetEditor(); TSharedPtr Sequencer = EditorViewModel->GetSequencerImpl(); if (InColumnName == FCommonOutlinerNames::Label) { const FMovieSceneSequenceID SequenceID = OwnerModel->GetSequenceID(); const MovieScene::FFixedObjectBindingID FixedObjectBindingID(ObjectBindingID, SequenceID); return SNew(SOutlinerItemViewBase, SharedThis(this), EditorViewModel, InParams.TreeViewRow) .AdditionalLabelContent() [ SNew(SObjectBindingTags, FixedObjectBindingID, Sequencer->GetObjectBindingTagCache()) ]; } if (InColumnName == FCommonOutlinerNames::Add) { return UE::Sequencer::MakeAddButton( LOCTEXT("TrackText", "Track"), FOnGetContent::CreateSP(this, &FObjectBindingModel::GetAddTrackMenuContent), SharedThis(this)); } // Ask track editors to populate the column. // @todo: this is potentially very slow and will not scale as the number of track editors increases. const bool bIsEditColumn = InColumnName == FCommonOutlinerNames::Edit; TSharedPtr Box; auto GetEditBox = [&Box] { if (!Box) { Box = SNew(SHorizontalBox); auto CollapsedIfAllSlotsCollapsed = [Box]() -> EVisibility { for (int32 Index = 0; Index < Box->NumSlots(); ++Index) { EVisibility SlotVisibility = Box->GetSlot(Index).GetWidget()->GetVisibility(); if (SlotVisibility != EVisibility::Collapsed) { return EVisibility::SelfHitTestInvisible; } } return EVisibility::Collapsed; }; // Make the edit box collapsed if all of its slots are collapsed (or it has none) Box->SetVisibility(MakeAttributeLambda(CollapsedIfAllSlotsCollapsed)); } return Box.ToSharedRef(); }; for (const TSharedPtr& TrackEditor : Sequencer->GetTrackEditors()) { TrackEditor->BuildObjectBindingColumnWidgets(GetEditBox, SharedThis(this), InParams, InColumnName); if (bIsEditColumn) { // Backwards compat GetEditBox(); TrackEditor->BuildObjectBindingEditButtons(Box, ObjectBindingID, FindObjectClass()); } } return Box && Box->NumSlots() != 0 ? Box : nullptr; } bool FObjectBindingModel::GetDefaultExpansionState() const { // Object binding nodes are always expanded by default return true; } bool FObjectBindingModel::CanRename() const { return true; } void FObjectBindingModel::Rename(const FText& NewName) { TSharedPtr Sequencer = OwnerModel->GetSequencer(); UMovieSceneSequence* MovieSceneSequence = OwnerModel->GetSequence(); if (MovieSceneSequence && Sequencer) { UMovieScene* MovieScene = MovieSceneSequence->GetMovieScene(); FScopedTransaction Transaction(LOCTEXT("SetTrackName", "Set Track Name")); // Modify the movie scene so that it gets marked dirty and renames are saved consistently. MovieScene->Modify(); FMovieSceneSpawnable* Spawnable = MovieScene->FindSpawnable(ObjectBindingID); FMovieScenePossessable* Possessable = MovieScene->FindPossessable(ObjectBindingID); // If there is only one binding, set the name of the bound actor TArrayView> Objects = Sequencer->FindObjectsInCurrentSequence(ObjectBindingID); if (Objects.Num() == 1) { if (AActor* Actor = Cast(Objects[0].Get())) { Actor->SetActorLabel(NewName.ToString()); } } if (Spawnable) { // Otherwise set our display name Spawnable->SetName(NewName.ToString()); } else if (Possessable) { Possessable->SetName(NewName.ToString()); } else { MovieScene->SetObjectDisplayName(ObjectBindingID, NewName); } } } FText FObjectBindingModel::GetLabel() const { UMovieSceneSequence* MovieSceneSequence = OwnerModel->GetSequence(); if (MovieSceneSequence != nullptr) { return MovieSceneSequence->GetMovieScene()->GetObjectDisplayName(ObjectBindingID); } return FText(); } FSlateColor FObjectBindingModel::GetLabelColor() const { TSharedPtr Sequencer = OwnerModel->GetSequencer(); if (!Sequencer) { return FLinearColor::Red; } TArrayView > BoundObjects = Sequencer->FindBoundObjects(ObjectBindingID, OwnerModel->GetSequenceID()); if (BoundObjects.Num() > 0) { int32 NumValidObjects = 0; for (const TWeakObjectPtr<>& BoundObject : BoundObjects) { if (BoundObject.IsValid()) { ++NumValidObjects; } } if (NumValidObjects == BoundObjects.Num()) { return FOutlinerItemModel::GetLabelColor(); } if (NumValidObjects > 0) { return FLinearColor::Yellow; } } // Find the last objecting binding ancestor and ask it for the invalid color to use. // e.g. Spawnables don't have valid object bindings when their track hasn't spawned them yet, // so we override the default behavior of red with a gray so that users don't think there is something wrong. FMovieSceneEvaluationState* EvaluationState = Sequencer->GetEvaluationState(); TFunction GetObjectBindingAncestorInvalidLabelColor = [&](const FObjectBindingModel& InObjectBindingModel) -> FSlateColor { if (!EvaluationState->GetBindingActivation(InObjectBindingModel.GetObjectGuid(), OwnerModel->GetSequenceID())) { return FSlateColor::UseSubduedForeground(); } if (TSharedPtr ParentBindingModel = InObjectBindingModel.FindAncestorOfType()) { return GetObjectBindingAncestorInvalidLabelColor(*ParentBindingModel.Get()); } return InObjectBindingModel.GetInvalidBindingLabelColor(); }; return GetObjectBindingAncestorInvalidLabelColor(*this); } FText FObjectBindingModel::GetTooltipForSingleObjectBinding() const { return FText::Format(LOCTEXT("PossessableBoundObjectToolTip", "(BindingID: {0}"), FText::FromString(LexToString(ObjectBindingID))); } FText FObjectBindingModel::GetLabelToolTipText() const { TSharedPtr Sequencer = OwnerModel->GetSequencer(); if (!Sequencer) { return FText(); } TArrayView> BoundObjects = Sequencer->FindBoundObjects(ObjectBindingID, OwnerModel->GetSequenceID()); if ( BoundObjects.Num() == 0 ) { return FText::Format(LOCTEXT("InvalidBoundObjectToolTip", "The object bound to this track is missing (BindingID: {0})."), FText::FromString(LexToString(ObjectBindingID))); } else { TArray ValidBoundObjectLabels; FName BoundObjectClass; bool bAddEllipsis = false; int32 NumMissing = 0; for (const TWeakObjectPtr<>& Ptr : BoundObjects) { UObject* Obj = Ptr.Get(); if (Obj == nullptr) { ++NumMissing; continue; } if (Obj->GetClass()) { BoundObjectClass = Obj->GetClass()->GetFName(); } if (AActor* Actor = Cast(Obj)) { ValidBoundObjectLabels.Add(Actor->GetActorLabel()); } else { ValidBoundObjectLabels.Add(Obj->GetName()); } if (ValidBoundObjectLabels.Num() > 3) { bAddEllipsis = true; break; } } // If only 1 bound object, display a simpler tooltip. if (ValidBoundObjectLabels.Num() == 1 && NumMissing == 0) { return GetTooltipForSingleObjectBinding(); } else if (ValidBoundObjectLabels.Num() == 0 && NumMissing == 1) { return FText::Format(LOCTEXT("InvalidBoundObjectToolTip", "The object bound to this track is missing (BindingID: {0})."), FText::FromString(LexToString(ObjectBindingID))); } FString MultipleBoundObjectLabel = FString::Join(ValidBoundObjectLabels, TEXT(", ")); if (bAddEllipsis) { MultipleBoundObjectLabel += FString::Printf(TEXT("... %d more"), BoundObjects.Num()-3); } if (NumMissing != 0) { MultipleBoundObjectLabel += FString::Printf(TEXT(" (%d missing)"), NumMissing); } return FText::FromString(MultipleBoundObjectLabel + FString::Printf(TEXT(" Class: %s (BindingID: %s)"), *LexToString(BoundObjectClass), *LexToString(ObjectBindingID))); } } const FSlateBrush* FObjectBindingModel::GetIconBrush() const { const UClass* ClassForObjectBinding = FindObjectClass(); if (ClassForObjectBinding) { return FSlateIconFinder::FindIconBrushForClass(ClassForObjectBinding); } return FAppStyle::GetBrush("Sequencer.InvalidSpawnableIcon"); } TSharedRef FObjectBindingModel::GetAddTrackMenuContent() { TSharedPtr Sequencer = OwnerModel->GetSequencerImpl(); check(Sequencer); UObject* BoundObject = Sequencer->FindSpawnedObjectOrTemplate(ObjectBindingID); const UClass* MainSelectionObjectClass = FindObjectClass(); TArray ObjectBindings; ObjectBindings.Add(ObjectBindingID); TArray ObjectClasses; ObjectClasses.Add(const_cast(MainSelectionObjectClass)); // Only include other selected object bindings if this binding is selected. Otherwise, this will lead to // confusion with multiple tracks being added to possibly unrelated objects if (OwnerModel->GetEditor()->GetSelection()->Outliner.IsSelected(SharedThis(this))) { for (TViewModelPtr ObjectBindingNode : OwnerModel->GetEditor()->GetSelection()->Outliner.Filter()) { const FGuid Guid = ObjectBindingNode->GetObjectGuid(); for (auto RuntimeObject : Sequencer->FindBoundObjects(Guid, OwnerModel->GetSequenceID())) { if (RuntimeObject.Get() != nullptr) { ObjectBindings.AddUnique(Guid); ObjectClasses.Add(RuntimeObject->GetClass()); continue; } } } } ISequencerModule& SequencerModule = FModuleManager::GetModuleChecked( "Sequencer" ); TSharedRef CommandList = MakeShared(); TSharedRef Extender = SequencerModule.GetAddTrackMenuExtensibilityManager()->GetAllExtenders(CommandList, TArrayBuilder().Add(BoundObject)).ToSharedRef(); TArray> AllExtenders; AllExtenders.Add(Extender); TArrayView ContextObjects = BoundObject ? MakeArrayView(&BoundObject, 1) : TArrayView(); TMap> Map = IObjectSchema::ComputeRelevancy(ContextObjects); for (const TPair>& Pair : Map) { TSharedPtr NewExtension = Pair.Key->ExtendObjectBindingMenu(CommandList, Sequencer, Pair.Value); if (NewExtension) { AllExtenders.Add(NewExtension); } } if (AllExtenders.Num()) { Extender = FExtender::Combine(AllExtenders); } const UClass* ObjectClass = UClass::FindCommonBase(ObjectClasses); for (const TSharedPtr& CurTrackEditor : Sequencer->GetTrackEditors()) { CurTrackEditor->ExtendObjectBindingTrackMenu(Extender, ObjectBindings, ObjectClass); } // The menu are generated through reflection and sometime the API exposes some recursivity (think about a Widget returning it parent which is also a Widget). Just by reflection // it is not possible to determine when the root object is reached. It needs a kind of simulation which is not implemented. Also, even if the recursivity was correctly handled, the possible // permutations tend to grow exponentially. Until a clever solution is found, the simple approach is to disable recursively searching those menus. User can still search the current one though. // See UE-131257 const bool bInRecursivelySearchable = false; FMenuBuilder AddTrackMenuBuilder(true, nullptr, Extender, false, &FCoreStyle::Get(), true, NAME_None, bInRecursivelySearchable); const int32 NumStartingBlocks = AddTrackMenuBuilder.GetMultiBox()->GetBlocks().Num(); AddTrackMenuBuilder.BeginSection("Tracks", LOCTEXT("TracksMenuHeader" , "Tracks")); Sequencer->BuildObjectBindingTrackMenu(AddTrackMenuBuilder, ObjectBindings, ObjectClass); AddTrackMenuBuilder.EndSection(); TArray KeyablePropertyPaths; if (BoundObject != nullptr) { TSharedRef SequencerInterface = Sequencer.ToSharedRef(); FSequencerObjectBindingHelper::GetKeyablePropertyPaths(BoundObject, SequencerInterface, KeyablePropertyPaths); } AddPropertyMenuItems(AddTrackMenuBuilder, NumStartingBlocks, KeyablePropertyPaths, 0); return AddTrackMenuBuilder.MakeWidget(); } void FObjectBindingModel::AddPropertyMenuItems(FMenuBuilder& AddTrackMenuBuilder, int32 NumStartingBlocks, TArray KeyablePropertyPaths, int32 PropertyNameIndexStart) { // KeyablePropertyPaths contain a property path for each property, nested or not, that we can key. For instance: // // [MyFloat] (float, via the float property track) // [SomeStruct] [MyColor] (SomeStruct isn't keyable so key its color, via the color property track) // [SomeStruct] [OtherStruct] [MyInt] (SomeStruct and OtherStruct aren't keyable so key the integer, via the int property track) // [SomeStruct] [KnownStruct] (KnownStruct has a custom track to key its properties) // If PropertyNameIndexStart is greater that zero, we are showing the sub-menu of a property path. // That is, if we have property paths like this: // // [SomeStruct] [OtherStruct] [MyInt] // [SomeStruct] [OtherStruct] [MyColor] // // ...and if we are showing the sub-menu for [OtherStruct] // // ...then PropertyNameIndexStart is 2, and we only need to show [MyInt] and [MyColor]. static const FString DefaultPropertyCategory = TEXT("Default"); // Properties with the category "Default" have no category and should be sorted to the top struct FCategorySortPredicate { bool operator()(const FString& A, const FString& B) const { if (A == DefaultPropertyCategory) { return true; } else if (B == DefaultPropertyCategory) { return false; } else { return A.Compare(B) < 0; } } }; bool bDefaultCategoryFound = false; const bool bIsRootMenu = (PropertyNameIndexStart == 0); // Create property menu data based on keyable property paths TMap> KeyablePropertyMenuData; for (const FPropertyPath& KeyablePropertyPath : KeyablePropertyPaths) { if (!ensure(KeyablePropertyPath.GetNumProperties() > PropertyNameIndexStart)) { continue; } const FPropertyInfo& PropertyInfo = KeyablePropertyPath.GetPropertyInfo(PropertyNameIndexStart); if (const FProperty* Property = PropertyInfo.Property.Get()) { FPropertyMenuData KeyableMenuData; KeyableMenuData.PropertyPath = KeyablePropertyPath; if (PropertyInfo.ArrayIndex != INDEX_NONE) { KeyableMenuData.MenuName = FText::Format(LOCTEXT("PropertyMenuTextFormat", "{0} [{1}]"), Property->GetDisplayNameText(), FText::AsNumber(PropertyInfo.ArrayIndex)).ToString(); } else { KeyableMenuData.MenuName = Property->GetDisplayNameText().ToString(); } FString CategoryText = FObjectEditorUtils::GetCategory(Property); if (CategoryText == DefaultPropertyCategory) { bDefaultCategoryFound = true; } KeyablePropertyMenuData.FindOrAdd(CategoryText).Add(KeyableMenuData); } } KeyablePropertyMenuData.KeySort(FCategorySortPredicate()); // Always add an extension point for Properties section even if none are found (Components rely on this) if (!bDefaultCategoryFound && bIsRootMenu) { AddTrackMenuBuilder.BeginSection(SequencerMenuExtensionPoints::AddTrackMenu_PropertiesSection, LOCTEXT("PropertiesMenuHeader", "Properties")); AddTrackMenuBuilder.EndSection(); } // Add menu items TSharedPtr Sequencer = OwnerModel->GetSequencerImpl(); check(Sequencer); const bool bUseSubMenus = GSequencerObjectBindingShowNestedProperties || Sequencer->IsLevelEditorSequencer(); for (TPair>& Pair : KeyablePropertyMenuData) { const FString CategoryText = Pair.Key; TArray& KeyablePropertySubMenuData = Pair.Value; // Sort on the property name KeyablePropertySubMenuData.Sort(); if (CategoryText == DefaultPropertyCategory) { AddTrackMenuBuilder.BeginSection(SequencerMenuExtensionPoints::AddTrackMenu_PropertiesSection, LOCTEXT("PropertiesMenuHeader", "Properties")); } else { AddTrackMenuBuilder.BeginSection(NAME_None, FText::FromString(CategoryText)); } for (int32 MenuDataIndex = 0; MenuDataIndex < KeyablePropertySubMenuData.Num(); ) { // If this menu data only has one property name left in it, add the menu item if (KeyablePropertySubMenuData[MenuDataIndex].PropertyPath.GetNumProperties() == PropertyNameIndexStart + 1) { AddPropertyMenuItem(AddTrackMenuBuilder, KeyablePropertySubMenuData[MenuDataIndex]); ++MenuDataIndex; } // If we don't want sub-menus, concatenate the property names left to handle, and add the menu item. else if (!bUseSubMenus) { TArray PropertyNames; const FPropertyPath& CurPropertyPath(KeyablePropertySubMenuData[MenuDataIndex].PropertyPath); for (int32 PropertyNameIndex = PropertyNameIndexStart; PropertyNameIndex < CurPropertyPath.GetNumProperties(); ++PropertyNameIndex) { const FPropertyInfo& CurPropertyInfo(CurPropertyPath.GetPropertyInfo(PropertyNameIndex)); if (CurPropertyInfo.ArrayIndex != INDEX_NONE) { PropertyNames.Add(FText::Format( LOCTEXT("PropertyMenuTextFormat", "{0} [{1}]"), CurPropertyInfo.Property.Get()->GetDisplayNameText(), FText::AsNumber(CurPropertyInfo.ArrayIndex)).ToString()); } else { PropertyNames.Add(CurPropertyInfo.Property.Get()->GetDisplayNameText().ToString()); } } KeyablePropertySubMenuData[MenuDataIndex].MenuName = FString::Join(PropertyNames, TEXT(".")); AddPropertyMenuItem(AddTrackMenuBuilder, KeyablePropertySubMenuData[MenuDataIndex]); ++MenuDataIndex; } // Otherwise, look to the next menu data to gather up new data else { TArray KeyableSubMenuPropertyPaths; KeyableSubMenuPropertyPaths.Add(KeyablePropertySubMenuData[MenuDataIndex].PropertyPath); for (; MenuDataIndex < KeyablePropertySubMenuData.Num() - 1; ) { if (KeyablePropertySubMenuData[MenuDataIndex].MenuName == KeyablePropertySubMenuData[MenuDataIndex + 1].MenuName) { ++MenuDataIndex; KeyableSubMenuPropertyPaths.Add(KeyablePropertySubMenuData[MenuDataIndex].PropertyPath); } else { break; } } AddTrackMenuBuilder.AddSubMenu( FText::FromString(KeyablePropertySubMenuData[MenuDataIndex].MenuName), FText::GetEmpty(), FNewMenuDelegate::CreateSP(this, &FObjectBindingModel::HandleAddTrackSubMenuNew, KeyableSubMenuPropertyPaths, PropertyNameIndexStart + 1)); ++MenuDataIndex; } } AddTrackMenuBuilder.EndSection(); } if (AddTrackMenuBuilder.GetMultiBox()->GetBlocks().Num() == NumStartingBlocks) { TSharedRef EmptyTip = SNew(SBox) .Padding(FMargin(15.f, 7.5f)) [ SNew(STextBlock) .Text(LOCTEXT("NoKeyablePropertiesFound", "No keyable properties or tracks")) .ColorAndOpacity(FSlateColor::UseSubduedForeground()) ]; AddTrackMenuBuilder.AddWidget(EmptyTip, FText(), true, false); } } void FObjectBindingModel::HandleAddTrackSubMenuNew(FMenuBuilder& AddTrackMenuBuilder, TArray KeyablePropertyPaths, int32 PropertyNameIndexStart) { AddPropertyMenuItems(AddTrackMenuBuilder, 0, KeyablePropertyPaths, PropertyNameIndexStart); } void FObjectBindingModel::HandlePropertyMenuItemExecute(FPropertyPath PropertyPath) { TSharedPtr Sequencer = OwnerModel->GetSequencerImpl(); UObject* BoundObject = Sequencer->FindSpawnedObjectOrTemplate(ObjectBindingID); TArray KeyableBoundObjects; if (BoundObject != nullptr) { if (Sequencer->CanKeyProperty(FCanKeyPropertyParams(BoundObject->GetClass(), PropertyPath))) { KeyableBoundObjects.Add(BoundObject); } } // Only include other selected object bindings if this binding is selected. Otherwise, this will lead to // confusion with multiple tracks being added to possibly unrelated objects if (OwnerModel->GetEditor()->GetSelection()->Outliner.IsSelected(SharedThis(this))) { for (TViewModelPtr ObjectBindingNode : OwnerModel->GetEditor()->GetSelection()->Outliner.Filter()) { FGuid Guid = ObjectBindingNode->GetObjectGuid(); for (auto RuntimeObject : Sequencer->FindBoundObjects(Guid, OwnerModel->GetSequenceID())) { if (Sequencer->CanKeyProperty(FCanKeyPropertyParams(RuntimeObject->GetClass(), PropertyPath))) { KeyableBoundObjects.AddUnique(RuntimeObject.Get()); } } } } // When auto setting track defaults are disabled, force add a key so that the changed // value is saved and is propagated to the property. FKeyPropertyParams KeyPropertyParams(KeyableBoundObjects, PropertyPath, Sequencer->GetAutoSetTrackDefaults() == false ? ESequencerKeyMode::ManualKeyForced : ESequencerKeyMode::ManualKey); Sequencer->KeyProperty(KeyPropertyParams); } void FObjectBindingModel::AddPropertyMenuItem(FMenuBuilder& AddTrackMenuBuilder, const FPropertyMenuData& KeyablePropertyMenuData) { FUIAction AddTrackMenuAction(FExecuteAction::CreateSP(this, &FObjectBindingModel::HandlePropertyMenuItemExecute, KeyablePropertyMenuData.PropertyPath)); AddTrackMenuBuilder.AddMenuEntry(FText::FromString(KeyablePropertyMenuData.MenuName), FText(), FSlateIcon(), AddTrackMenuAction); } void FObjectBindingModel::BuildContextMenu(FMenuBuilder& MenuBuilder) { TSharedPtr EditorViewModel = GetEditor(); FSequencer* Sequencer = EditorViewModel->GetSequencerImpl().Get(); ISequencerModule& SequencerModule = FModuleManager::GetModuleChecked("Sequencer"); UObject* BoundObject = Sequencer->FindSpawnedObjectOrTemplate(ObjectBindingID); const UClass* ObjectClass = FindObjectClass(); TSharedPtr Extender = EditorViewModel->GetSequencerMenuExtender( SequencerModule.GetObjectBindingContextMenuExtensibilityManager(), TArrayBuilder().Add(BoundObject), &FSequencerCustomizationInfo::OnBuildObjectBindingContextMenu, SharedThis(this)); if (Extender.IsValid()) { MenuBuilder.PushExtender(Extender.ToSharedRef()); } // Extenders can go in there. MenuBuilder.BeginSection("ObjectBindingActions"); MenuBuilder.EndSection(); // External extension. Sequencer->BuildCustomContextMenuForGuid(MenuBuilder, ObjectBindingID); // Track editor extension. TArray ObjectBindings; ObjectBindings.Add(ObjectBindingID); for (const TSharedPtr& TrackEditor : Sequencer->GetTrackEditors()) { TrackEditor->BuildObjectBindingContextMenu(MenuBuilder, ObjectBindings, ObjectClass); } // Up-call. FOutlinerItemModel::BuildContextMenu(MenuBuilder); } void FObjectBindingModel::BuildOrganizeContextMenu(FMenuBuilder& MenuBuilder) { MenuBuilder.AddSubMenu( LOCTEXT("TagsLabel", "Tags"), LOCTEXT("TagsTooltip", "Show this object binding's tags"), FNewMenuDelegate::CreateSP(this, &FObjectBindingModel::AddTagMenu) ); FOutlinerItemModel::BuildOrganizeContextMenu(MenuBuilder); } void FObjectBindingModel::BuildSidebarMenu(FMenuBuilder& MenuBuilder) { const TSharedPtr EditorViewModel = GetEditor(); if (!EditorViewModel.IsValid()) { return; } FSequencer* const Sequencer = EditorViewModel->GetSequencerImpl().Get(); if (!Sequencer) { return; } ISequencerModule& SequencerModule = FModuleManager::GetModuleChecked(TEXT("Sequencer")); UObject* const BoundObject = Sequencer->FindSpawnedObjectOrTemplate(ObjectBindingID); const TSharedPtr Extender = EditorViewModel->GetSequencerMenuExtender(SequencerModule.GetSidebarExtensibilityManager() , TArrayBuilder().Add(BoundObject), &FSequencerCustomizationInfo::OnBuildSidebarMenu, SharedThis(this)); if (Extender.IsValid()) { MenuBuilder.PushExtender(Extender.ToSharedRef()); } MenuBuilder.BeginSection(TEXT("ObjectBindingActions"), LOCTEXT("ObjectBindingsMenuSection", "Object Bindings")); MenuBuilder.EndSection(); // External extension. Sequencer->BuildCustomContextMenuForGuid(MenuBuilder, ObjectBindingID); // Track editor extension. TArray ObjectBindings; ObjectBindings.Add(ObjectBindingID); const UClass* const ObjectClass = FindObjectClass(); for (const TSharedPtr& TrackEditor : Sequencer->GetTrackEditors()) { TrackEditor->BuildObjectBindingContextMenu(MenuBuilder, ObjectBindings, ObjectClass); } FOutlinerItemModel::BuildSidebarMenu(MenuBuilder); } void FObjectBindingModel::AddTagMenu(FMenuBuilder& MenuBuilder) { MenuBuilder.AddMenuEntry(FSequencerCommands::Get().OpenTaggedBindingManager); TSharedPtr Sequencer = OwnerModel->GetSequencerImpl(); UMovieSceneSequence* Sequence = Sequencer->GetRootMovieSceneSequence(); UMovieScene* MovieScene = Sequence->GetMovieScene(); MenuBuilder.BeginSection(NAME_None, LOCTEXT("ObjectTagsHeader", "Object Tags")); { TSet AllTags; // Gather all the tags on all currently selected object binding IDs FMovieSceneSequenceID SequenceID = OwnerModel->GetSequenceID(); for (TViewModelPtr ObjectBindingNode : OwnerModel->GetEditor()->GetSelection()->Outliner.Filter()) { const FGuid& ObjectID = ObjectBindingNode->GetObjectGuid(); UE::MovieScene::FFixedObjectBindingID BindingID(ObjectID, SequenceID); for (auto It = Sequencer->GetObjectBindingTagCache()->IterateTags(BindingID); It; ++It) { AllTags.Add(It.Value()); } } bool bIsReadOnly = MovieScene->IsReadOnly(); for (const FName& TagName : AllTags) { MenuBuilder.AddMenuEntry( FText::FromName(TagName), FText(), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &FObjectBindingModel::ToggleTag, TagName), FCanExecuteAction::CreateLambda([bIsReadOnly] { return bIsReadOnly == false; }), FGetActionCheckState::CreateSP(this, &FObjectBindingModel::GetTagCheckState, TagName) ), NAME_None, EUserInterfaceActionType::ToggleButton ); } } MenuBuilder.EndSection(); MenuBuilder.BeginSection(NAME_None, LOCTEXT("AddNewHeader", "Add Tag")); { if (!MovieScene->IsReadOnly()) { TSharedRef Widget = SNew(SObjectBindingTag) .OnCreateNew(this, &FObjectBindingModel::HandleAddTag); MenuBuilder.AddWidget(Widget, FText()); } } MenuBuilder.EndSection(); } ECheckBoxState FObjectBindingModel::GetTagCheckState(FName TagName) { ECheckBoxState CheckBoxState = ECheckBoxState::Undetermined; TSharedPtr Sequencer = OwnerModel->GetSequencerImpl(); FMovieSceneSequenceID SequenceID = OwnerModel->GetSequenceID(); for (TViewModelPtr ObjectBindingNode : OwnerModel->GetEditor()->GetSelection()->Outliner.Filter()) { const FGuid& ObjectID = ObjectBindingNode->GetObjectGuid(); UE::MovieScene::FFixedObjectBindingID BindingID(ObjectID, SequenceID); ECheckBoxState ThisCheckState = Sequencer->GetObjectBindingTagCache()->HasTag(BindingID, TagName) ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; if (CheckBoxState == ECheckBoxState::Undetermined) { CheckBoxState = ThisCheckState; } else if (CheckBoxState != ThisCheckState) { return ECheckBoxState::Undetermined; } } return CheckBoxState; } void FObjectBindingModel::ToggleTag(FName TagName) { TSharedPtr Sequencer = OwnerModel->GetSequencerImpl(); FMovieSceneSequenceID SequenceID = OwnerModel->GetSequenceID(); for (TViewModelPtr ObjectBindingNode : OwnerModel->GetEditor()->GetSelection()->Outliner.Filter()) { const FGuid& ObjectID = ObjectBindingNode->GetObjectGuid(); UE::MovieScene::FFixedObjectBindingID BindingID(ObjectID, SequenceID); if (!Sequencer->GetObjectBindingTagCache()->HasTag(BindingID, TagName)) { HandleAddTag(TagName); return; } } HandleDeleteTag(TagName); } void FObjectBindingModel::HandleDeleteTag(FName TagName) { FScopedTransaction Transaction(FText::Format(LOCTEXT("RemoveBindingTag", "Remove tag '{0}' from binding(s)"), FText::FromName(TagName))); TSharedPtr Sequencer = OwnerModel->GetSequencer(); UMovieScene* MovieScene = Sequencer->GetRootMovieSceneSequence()->GetMovieScene(); MovieScene->Modify(); FMovieSceneSequenceID SequenceID = OwnerModel->GetSequenceID(); for (TViewModelPtr ObjectBindingNode : OwnerModel->GetEditor()->GetSelection()->Outliner.Filter()) { const FGuid& ObjectID = ObjectBindingNode->GetObjectGuid(); MovieScene->UntagBinding(TagName, UE::MovieScene::FFixedObjectBindingID(ObjectID, SequenceID)); } } void FObjectBindingModel::HandleAddTag(FName TagName) { FScopedTransaction Transaction(FText::Format(LOCTEXT("CreateBindingTag", "Add new tag {0} to binding(s)"), FText::FromName(TagName))); TSharedPtr Sequencer = OwnerModel->GetSequencer(); UMovieScene* MovieScene = Sequencer->GetRootMovieSceneSequence()->GetMovieScene(); MovieScene->Modify(); FMovieSceneSequenceID SequenceID = OwnerModel->GetSequenceID(); for (TViewModelPtr ObjectBindingNode : OwnerModel->GetEditor()->GetSelection()->Outliner.Filter()) { const FGuid& ObjectID = ObjectBindingNode->GetObjectGuid(); MovieScene->TagBinding(TagName, UE::MovieScene::FFixedObjectBindingID(ObjectID, SequenceID)); } } void FObjectBindingModel::SortChildren() { ISortableExtension::SortChildren(SharedThis(this), ESortingMode::PriorityFirst); } FSortingKey FObjectBindingModel::GetSortingKey() const { FSortingKey SortingKey; if (OwnerModel) { UMovieScene* MovieScene = OwnerModel->GetMovieScene(); const FMovieSceneBinding* MovieSceneBinding = MovieScene->FindBinding(ObjectBindingID); if (MovieSceneBinding) { SortingKey.CustomOrder = MovieSceneBinding->GetSortingOrder(); } SortingKey.DisplayName = MovieScene->GetObjectDisplayName(ObjectBindingID); } // When inside object bindings, we come before tracks. Elsewhere, we come after tracks. const bool bHasParentObjectBinding = (CastParent() != nullptr); SortingKey.PrioritizeBy(bHasParentObjectBinding ? 2 : 1); return SortingKey; } void FObjectBindingModel::SetCustomOrder(int32 InCustomOrder) { if (OwnerModel) { UMovieScene* MovieScene = OwnerModel->GetMovieScene(); FMovieSceneBinding* MovieSceneBinding = MovieScene->FindBinding(ObjectBindingID); if (MovieSceneBinding) { MovieSceneBinding->SetSortingOrder(InCustomOrder); } } } bool FObjectBindingModel::CanDrag() const { // Can only drag top level object bindings TSharedPtr ObjectBindingExtension = FindAncestorOfType(); return ObjectBindingExtension == nullptr; } bool FObjectBindingModel::CanDelete(FText* OutErrorMessage) const { return true; } void FObjectBindingModel::Delete() { if (OwnerModel) { TSharedPtr Sequencer = OwnerModel->GetSequencer(); UMovieScene* MovieScene = Sequencer->GetRootMovieSceneSequence()->GetMovieScene(); MovieScene->Modify(); // Untag this binding UE::MovieScene::FFixedObjectBindingID BindingID(ObjectBindingID, OwnerModel->GetSequenceID()); for (auto It = OwnerModel->GetSequencerImpl()->GetObjectBindingTagCache()->IterateTags(BindingID); It; ++It) { MovieScene->UntagBinding(It.Value(), BindingID); } // Delete any child object bindings - this will remove their tracks implicitly // so no need to delete those manually for (const TViewModelPtr& ChildObject : GetChildrenOfType(EViewModelListType::Outliner).ToArray()) { ChildObject->Delete(); } // Remove from a parent folder if necessary. if (TViewModelPtr ParentFolder = CastParent()) { ParentFolder->GetFolder()->RemoveChildObjectBinding(ObjectBindingID); } BindingLifetimeOverlayModel.Reset(); } } bool FObjectBindingModel::IsMuted() const { if (OwnerModel) { UMovieScene* MovieScene = OwnerModel->GetMovieScene(); FMovieSceneBinding* MovieSceneBinding = MovieScene->FindBinding(ObjectBindingID); if (MovieSceneBinding) { if (UMovieSceneMuteSoloDecoration* MuteSoloDecoration = MovieSceneBinding->FindDecoration()) { return MuteSoloDecoration->IsMuted(); } } } return false; } void FObjectBindingModel::SetIsMuted(bool bIsMuted) { if (OwnerModel) { UMovieScene* MovieScene = OwnerModel->GetMovieScene(); FMovieSceneBinding* MovieSceneBinding = MovieScene->FindBinding(ObjectBindingID); if (MovieSceneBinding) { const bool bAlwaysMarkDirty = false; MovieScene->Modify(bAlwaysMarkDirty); if (UMovieSceneMuteSoloDecoration* MuteSoloDecoration = Cast(MovieSceneBinding->GetOrCreateDecoration(UMovieSceneMuteSoloDecoration::StaticClass(), MovieScene, [this](UObject* Decoration) {}))) { MuteSoloDecoration->SetMuted(bIsMuted); } } } } bool FObjectBindingModel::IsSolo() const { if (OwnerModel) { UMovieScene* MovieScene = OwnerModel->GetMovieScene(); FMovieSceneBinding* MovieSceneBinding = MovieScene->FindBinding(ObjectBindingID); if (MovieSceneBinding) { if (UMovieSceneMuteSoloDecoration* MuteSoloDecoration = MovieSceneBinding->FindDecoration()) { return MuteSoloDecoration->IsSoloed(); } } } return false; } void FObjectBindingModel::SetIsSoloed(bool bIsSoloed) { if (OwnerModel) { UMovieScene* MovieScene = OwnerModel->GetMovieScene(); FMovieSceneBinding* MovieSceneBinding = MovieScene->FindBinding(ObjectBindingID); if (MovieSceneBinding) { const bool bAlwaysMarkDirty = false; MovieScene->Modify(bAlwaysMarkDirty); if (UMovieSceneMuteSoloDecoration* MuteSoloDecoration = Cast(MovieSceneBinding->GetOrCreateDecoration(UMovieSceneMuteSoloDecoration::StaticClass(), MovieScene, [this](UObject* Decoration) {}))) { MuteSoloDecoration->SetSoloed(bIsSoloed); } } } } } // namespace UE::Sequencer #undef LOCTEXT_NAMESPACE