// Copyright Epic Games, Inc. All Rights Reserved. #include "CompElementCollectionViewModel.h" #include "Engine/Level.h" #include "Kismet2/SClassPickerDialog.h" #include "CompositingElement.h" #include "ClassViewerFilter.h" #include "EngineAnalytics.h" #include "IAnalyticsProviderET.h" #include "LevelEditor.h" #include "ComposureEditorSettings.h" #include "Misc/FilterCollection.h" #include "ScopedTransaction.h" #include "CompElementViewModel.h" #include "CompElementEditorCommands.h" #include "Framework/Commands/GenericCommands.h" #include "ScopedWorldLevelContext.h" #define LOCTEXT_NAMESPACE "CompElementsView" /* CompElementCollectionViewModel_Impl *****************************************************************************/ namespace CompElementCollectionViewModel_Impl { static const FName LvlEditorModuleName("LevelEditor"); /** Opens a modal class picker dialog for selecting the type of element the user wishes to add. */ static UClass* PromptForElementClass(const FText& PromptTitle, const TArray< TSubclassOf >& ChoiceClasses); /** */ static TArray GetChildElementNamesRecursive(TWeakObjectPtr RootElement); /** Sends analytics of added compositing element */ static void SendAnalyticsElementAdded(const FString& AddedElementClassName) { if (!FEngineAnalytics::IsAvailable()) { return; } TArray EventAttributes; EventAttributes.Add(FAnalyticsEventAttribute(TEXT("ClassName"), AddedElementClassName)); FEngineAnalytics::GetProvider().RecordEvent(TEXT("Usage.Composure.AddedElement"), EventAttributes); } } class FCompElementClassFilter : public IClassViewerFilter { public: /** All children of these classes will be included unless filtered out by another setting. */ TSet AllowedChildrenOfClasses; TSet ExcludedChildrenOfClasses; /** Disallowed class flags. */ EClassFlags DisallowedClassFlags; virtual bool IsClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const UClass* InClass, TSharedRef< FClassViewerFilterFuncs > InFilterFuncs) override { return !InClass->HasAnyClassFlags(DisallowedClassFlags) && InFilterFuncs->IfInChildOfClassesSet(AllowedChildrenOfClasses, InClass) != EFilterReturn::Failed; } virtual bool IsUnloadedClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const TSharedRef< const IUnloadedBlueprintData > InUnloadedClassData, TSharedRef< FClassViewerFilterFuncs > InFilterFuncs) override { return !InUnloadedClassData->HasAnyClassFlags(DisallowedClassFlags) && InFilterFuncs->IfInChildOfClassesSet(AllowedChildrenOfClasses, InUnloadedClassData) != EFilterReturn::Failed; } }; static UClass* CompElementCollectionViewModel_Impl::PromptForElementClass(const FText& PromptTitle, const TArray< TSubclassOf >& ChoiceClasses) { FClassViewerInitializationOptions Options; Options.Mode = EClassViewerMode::ClassPicker; TSharedPtr Filter = MakeShareable(new FCompElementClassFilter); Options.ClassFilters.Add(Filter.ToSharedRef()); Options.ExtraPickerCommonClasses.Reserve(ChoiceClasses.Num()); for (TSubclassOf Class : ChoiceClasses) { Options.ExtraPickerCommonClasses.Add(Class); } Filter->DisallowedClassFlags = CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists | CLASS_HideDropDown; Filter->AllowedChildrenOfClasses.Add(ACompositingElement::StaticClass()); UClass* ChosenClass = nullptr; const bool bPressedOk = SClassPickerDialog::PickClass(PromptTitle, Options, ChosenClass, ACompositingElement::StaticClass()); return bPressedOk ? ChosenClass : nullptr; } static TArray CompElementCollectionViewModel_Impl::GetChildElementNamesRecursive(TWeakObjectPtr RootElement) { TArray ElementsToSelect; if (RootElement.IsValid()) { for (ACompositingElement* Child : RootElement->GetChildElements()) { if (Child) { ElementsToSelect.Append(GetChildElementNamesRecursive(Child)); ElementsToSelect.Add(Child->GetCompElementName()); } } } return ElementsToSelect; } /* FCompElementCollectionViewModel *****************************************************************************/ FCompElementCollectionViewModel::FCompElementCollectionViewModel(const TSharedRef& InElementsManager, const TWeakObjectPtr& InEditor) : CompElementManager(InElementsManager) , Editor(InEditor) , CommandList(MakeShareable(new FUICommandList)) , Filters(MakeShareable(new FCompElementFilterCollection)) {} FCompElementCollectionViewModel::~FCompElementCollectionViewModel() { if (Editor.IsValid()) { Editor->UnregisterForUndo(this); } FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked(CompElementCollectionViewModel_Impl::LvlEditorModuleName); LevelEditor.OnActorSelectionChanged().RemoveAll(this); Filters->OnChanged().RemoveAll(this); CompElementManager->OnElementsChanged().RemoveAll(this); } void FCompElementCollectionViewModel::Initialize() { BindCommands(); CompElementManager->OnElementsChanged().AddSP(this, &FCompElementCollectionViewModel::OnElementsChanged); Filters->OnChanged().AddSP(this, &FCompElementCollectionViewModel::OnFilterChanged); FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked(CompElementCollectionViewModel_Impl::LvlEditorModuleName); // Tell the level editor we want to be notified when selection changes LevelEditor.OnActorSelectionChanged().AddRaw(this, &FCompElementCollectionViewModel::OnActorSelectionChanged); if (Editor.IsValid()) { Editor->RegisterForUndo(this); } Refresh(); } void FCompElementCollectionViewModel::AddFilter(const TSharedRef& InFilter) { Filters->Add(InFilter); OnFilterChanged(); } void FCompElementCollectionViewModel::RemoveFilter(const TSharedRef& InFilter) { Filters->Remove(InFilter); OnFilterChanged(); } TArray< TSharedPtr >& FCompElementCollectionViewModel::GetRootCompElements() { return FilteredRootItems; } void FCompElementCollectionViewModel::GetChildElements(TSharedPtr ParentPtr, TArray< TSharedPtr >& OutChildElements) { FilteredChildren.MultiFind(ParentPtr, OutChildElements, /*bMaintainOrder =*/true); } TSharedPtr FCompElementCollectionViewModel::GetSelectionProxy(const TSharedPtr& SelectedItem) const { if (SelectedItem->IsEditable()) { return SelectedItem; } struct GetSelectionProxy_Impl { static TSharedPtr FindEditableParent(const TSharedPtr& Target, const TArray< TSharedPtr >& SearchList) { for (const TSharedPtr& Element : SearchList) { if (Element == Target) { return Element; } else { TSharedPtr SearchResult = FindEditableParent(Target, Element->Children); if (SearchResult.IsValid()) { if (!SearchResult->IsEditable()) { return Element; } return SearchResult; } } } return nullptr; } }; return GetSelectionProxy_Impl::FindEditableParent(SelectedItem, RootViewModels); } const TArray< TSharedPtr >& FCompElementCollectionViewModel::GetSelectedElements() const { return SelectedElements; } void FCompElementCollectionViewModel::GetSelectedElementNames(OUT TArray& OutSelectedElementNames) const { AppendSelectedElementNames(OutSelectedElementNames); } void FCompElementCollectionViewModel::SetSelectedElements(const TArray< TSharedPtr >& InSelectedElements) { SelectedElements.Empty(); SelectedElements.Append(InSelectedElements); RefreshActorSelections(); SelectionChanged.Broadcast(); } void FCompElementCollectionViewModel::SetSelectedElement(const FName& ElementName) { SelectedElements.Empty(); for (TSharedPtr ViewModel : FilteredRootItems) { if (ElementName == ViewModel->GetFName()) { SelectedElements.Add(ViewModel); break; } TArray< TSharedPtr > ChildElements; FilteredChildren.MultiFind(ViewModel, ChildElements); for (TSharedPtr Element : ChildElements) { if (ElementName == Element->GetFName()) { SelectedElements.Add(Element); } } } RefreshActorSelections(); SelectionChanged.Broadcast(); } const TSharedRef FCompElementCollectionViewModel::GetCommandList() const { return CommandList; } void FCompElementCollectionViewModel::BindCommands() { const FCompElementEditorCommands& Commands = FCompElementEditorCommands::Get(); FUICommandList& ActionList = *CommandList; ActionList.MapAction(Commands.CreateEmptyComp, FExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::CreateTopLevelElement_Executed), FCanExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::CreateTopLevelElement_CanExecute)); ActionList.MapAction(Commands.CreateNewElement, FExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::CreateChildElement_Executed), FCanExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::CreateChildElement_CanExecute)); ActionList.MapAction(Commands.RefreshCompList, FExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::RefreshList_Executed), FCanExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::RefreshList_CanExecute)); ActionList.MapAction(Commands.OpenElementPreview, FExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::OpenPreview_Executed), FCanExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::OpenPreview_CanExecute)); const FGenericCommands& GenericCommands = FGenericCommands::Get(); ActionList.MapAction(GenericCommands.Cut, FExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::CutElements_Executed), FCanExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::CutElements_CanExecute)); ActionList.MapAction(GenericCommands.Copy, FExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::CopyElements_Executed), FCanExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::CopyElements_CanExecute)); ActionList.MapAction(GenericCommands.Paste, FExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::PasteElements_Executed), FCanExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::PasteElements_CanExecute)); ActionList.MapAction(GenericCommands.Duplicate, FExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::DuplicateElements_Executed), FCanExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::DuplicateElements_CanExecute)); ActionList.MapAction(GenericCommands.Delete, FExecuteAction::CreateSP( this, &FCompElementCollectionViewModel::DeleteElements_Executed ), FCanExecuteAction::CreateSP( this, &FCompElementCollectionViewModel::DeleteElements_CanExecute ) ); ActionList.MapAction(GenericCommands.Rename, FExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::RequestRenameElement_Executed), FCanExecuteAction::CreateSP(this, &FCompElementCollectionViewModel::RequestRenameElement_CanExecute)); } void FCompElementCollectionViewModel::Refresh() { CompElementManager->RefreshElementsList(); } void FCompElementCollectionViewModel::OnFilterChanged() { RefreshFilteredElements(); ElementsChanged.Broadcast(ECompElementEdActions::Reset, nullptr, NAME_None); } void FCompElementCollectionViewModel::OnElementsChanged(const ECompElementEdActions Action, const TWeakObjectPtr& ChangedComp, const FName& ChangedProperty) { switch (Action) { case ECompElementEdActions::Add: OnElementAdded(ChangedComp); break; case ECompElementEdActions::Rename: // We purposely ignore re-filtering in this case SortFilteredElements(); break; case ECompElementEdActions::Modify: RefreshFilteredElements(); break; case ECompElementEdActions::Delete: OnElementDelete(); break; case ECompElementEdActions::Reset: default: //OnResetElements(); break; } OnResetElements(); ElementsChanged.Broadcast(Action, ChangedComp, ChangedProperty); } void FCompElementCollectionViewModel::OnElementAdded(const TWeakObjectPtr& AddedElementPtr) { if (!AddedElementPtr.IsValid()) { OnResetElements(); return; } ACompositingElement* AddedElementObj = AddedElementPtr.Get(); TSharedPtr ParentPtr; if (AddedElementObj->IsSubElement() && !TryGetViewModel(AddedElementObj->GetElementParent(), ParentPtr)) { OnResetElements(); return; } CompElementCollectionViewModel_Impl::SendAnalyticsElementAdded(AddedElementObj->GetClass()->GetName()); const TSharedRef NewElementModel = FCompElementViewModel::Create(AddedElementObj, CompElementManager); if (ParentPtr) { ParentPtr->Children.Add(NewElementModel); ensureMsgf(FilteredRootItems.Remove(NewElementModel) == 0, TEXT("Catching an issue when the comp elements list already contains an entry for this item - please notify the dev team with a repro.")); TArray< TSharedPtr > ExistingChildren; FilteredChildren.GenerateValueArray(ExistingChildren); // We specifically ignore filters when dealing with single additions if (ensureMsgf(!ExistingChildren.Contains(NewElementModel), TEXT("Catching an issue when the comp elements list already contains an entry for this item - please notify the dev team with a repro."))) { FilteredChildren.Add(ParentPtr, NewElementModel); } } else { RootViewModels.Add(NewElementModel); // We specifically ignore filters when dealing with single additions if (ensureMsgf(!FilteredRootItems.Contains(NewElementModel), TEXT("Catching an issue when the comp elements list already contains an entry for this item - please notify the dev team with a repro."))) { FilteredRootItems.Add(NewElementModel); } } SortFilteredElements(); } void FCompElementCollectionViewModel::OnElementDelete() { TArray< TWeakObjectPtr > AuthoratativeElementList; CompElementManager->AddAllCompElementsTo(AuthoratativeElementList); DestructivelyPurgeInvalidViewModels(AuthoratativeElementList); } void FCompElementCollectionViewModel::OnElementAttached(const TWeakObjectPtr& AttachedElement) { if (!AttachedElement.IsValid() || !AttachedElement->IsSubElement()) { OnResetElements(); return; } TSharedPtr ElementModel; TSharedPtr ParentModel; if (!TryGetViewModel(AttachedElement, ElementModel) || !TryGetViewModel(AttachedElement->GetElementParent(), ParentModel)) { OnResetElements(); return; } RootViewModels.Remove(ElementModel); ParentModel->Children.Add(ElementModel); RefreshFilteredElements(); } void FCompElementCollectionViewModel::OnResetElements() { TArray< TWeakObjectPtr > AuthoratativeElementList; // Expected: AuthoratativeElementList doesn't contain invalid entries CompElementManager->AddAllCompElementsTo(AuthoratativeElementList); FilteredRootItems.Empty(); FilteredChildren.Empty(); // Purge any invalid view-models, // This function also removes any elements already with view-model representations from AuthoratativeElementList DestructivelyPurgeInvalidViewModels(AuthoratativeElementList); RebuildViewModelHierarchy(); // Create any missing view-models CreateViewModels(AuthoratativeElementList); // Rebuild the filtered elements list RefreshFilteredElements(); } void FCompElementCollectionViewModel::DestructivelyPurgeInvalidViewModels(TArray< TWeakObjectPtr >& InElements) { struct PurgeInvalidViewModels_Impl { typedef TFunction&, const TSharedPtr&)> FOnRemovalCallback; void RemoveInvalidViewModels(TArray< TSharedPtr >& ViewModelList, FOnRemovalCallback& OnRemoval, TSharedPtr Parent = nullptr) { for (int32 ElementIndex = ViewModelList.Num() - 1; ElementIndex >= 0; --ElementIndex) { const TSharedPtr ElementViewModel = ViewModelList[ElementIndex]; TWeakObjectPtr ElementObj = ElementViewModel->GetDataSource(); if (!ElementObj.IsValid() || ElementsSrcList.Remove(ElementObj) == 0) { ViewModelList.RemoveAtSwap(ElementIndex); OnRemoval(ElementViewModel, Parent); } else { RemoveInvalidViewModels(ElementViewModel->Children, OnRemoval, ElementViewModel); } } } PurgeInvalidViewModels_Impl(TArray< TWeakObjectPtr >& InElementsSrcList) : ElementsSrcList(InElementsSrcList) {} TArray< TWeakObjectPtr >& ElementsSrcList; }; PurgeInvalidViewModels_Impl::FOnRemovalCallback OnInvalidViewModelFound([this](const TSharedPtr& InvalidViewModel, const TSharedPtr& Parent) { SelectedElements.Remove(InvalidViewModel); if (!Parent.IsValid()) { FilteredRootItems.Remove(InvalidViewModel); FilteredChildren.Remove(InvalidViewModel); } else { FilteredChildren.RemoveSingle(Parent, InvalidViewModel); } }); PurgeInvalidViewModels_Impl(InElements).RemoveInvalidViewModels(RootViewModels, OnInvalidViewModelFound); } void FCompElementCollectionViewModel::CreateViewModels(TArray< TWeakObjectPtr >& InElements) { struct CreateViewModels_Impl { const TSharedRef CreateViewModel(const TWeakObjectPtr& ElementPtr) { const TSharedRef NewViewModel = FCompElementViewModel::Create(ElementPtr, This->CompElementManager); //ViewModelRefTable.Add(ElementPtr, NewViewModel); if (!ElementPtr.IsValid()) { UE_LOG(LogTemp, Warning, TEXT("FCompElementCollectionViewModel::CreateViewModels - Invalid element")); return NewViewModel; } if (ElementPtr->IsSubElement()) { TSharedPtr ParentViewModel; TWeakObjectPtr ParentObj = ElementPtr->GetElementParent(); const int32 ParentIndex = Elements.Find(ParentObj); if (ParentIndex != INDEX_NONE && Visited[ParentIndex] == false) { Visited[ParentIndex] = true; ParentViewModel = CreateViewModel(Elements[ParentIndex]); } else { This->TryGetViewModel(ParentObj, ParentViewModel); } if (ParentViewModel.IsValid()) { ParentViewModel->Children.Add(NewViewModel); } else { UE_LOG(LogTemp, Warning, TEXT("FCompElementCollectionViewModel::CreateViewModels - Invalid parent view model %d"), ParentIndex); // fallback to adding it to RootViews This->RootViewModels.Add(NewViewModel); } } else { This->RootViewModels.Add(NewViewModel); } return NewViewModel; } CreateViewModels_Impl(FCompElementCollectionViewModel* InThis, TArray< TWeakObjectPtr >& ElementList) : This(InThis) , Elements(ElementList) { Visited.SetNumZeroed(Elements.Num()); } FCompElementCollectionViewModel* This; TArray< TWeakObjectPtr >& Elements; TArray Visited; }; CreateViewModels_Impl CreateHelper(this, InElements); for (int32 i = 0; i < InElements.Num(); ++i) { if (CreateHelper.Visited[i] == false) { CreateHelper.Visited[i] = true; CreateHelper.CreateViewModel(InElements[i]); } } } void FCompElementCollectionViewModel::RebuildViewModelHierarchy() { TArray< TSharedPtr > AllViewModels; GetAllViewModels(AllViewModels); // can't rely on TryGetViewModel() since it relies on walking the hierarchy which we are // in the midst of reforming auto FindViewModel = [&AllViewModels](ACompositingElement* ElementObj)->TSharedPtr { for (const TSharedPtr& ViewModel : AllViewModels) { if (ElementObj == ViewModel->GetDataSource()) { return ViewModel; } } return nullptr; }; RootViewModels.Empty(RootViewModels.Num()); for (const TSharedPtr& ViewModel : AllViewModels) { TWeakObjectPtr ElementPtr = ViewModel->GetDataSource(); if (ElementPtr.IsValid()) { ACompositingElement* ElementObj = ElementPtr.Get(); if (!ElementObj->IsSubElement()) { RootViewModels.Add(ViewModel); } const TArray Children = ElementObj->GetChildElements(); ViewModel->Children.Empty(Children.Num()); for (ACompositingElement* Child : Children) { TSharedPtr ChildViewModel = FindViewModel(Child); if (ChildViewModel.IsValid()) { ViewModel->Children.Add(ChildViewModel); } } } else { ViewModel->Children.Empty(); } } } void FCompElementCollectionViewModel::RefreshFilteredElements() { FilteredRootItems.Empty(); FilteredChildren.Empty(); struct RefreshFilteredElements_Impl { bool FilterChildren(const TSharedPtr& ViewModel, FFilteredChildList& OutFilteredChildren) { bool bChildIncluded = false; for (const TSharedPtr& Child : ViewModel->Children) { if (FilterChildren(Child, OutFilteredChildren) || FilterRef->PassesAllFilters(Child)) { TArray< TSharedPtr > ExistingChildren; OutFilteredChildren.GenerateValueArray(ExistingChildren); if (ensureMsgf(!ExistingChildren.Contains(Child), TEXT("Catching an issue when the comp elements list already contains an entry for this item - please notify the dev team with a repro."))) { OutFilteredChildren.Add(ViewModel, Child); } bChildIncluded = true; } } return bChildIncluded; } RefreshFilteredElements_Impl(const TSharedRef& InFilterRef) : FilterRef(InFilterRef) {} const TSharedRef FilterRef; }; RefreshFilteredElements_Impl FilterHelper(Filters); for (const TSharedPtr& ViewModel : RootViewModels) { if (FilterHelper.FilterChildren(ViewModel, FilteredChildren) || Filters->PassesAllFilters(ViewModel)) { if (ensureMsgf(!FilteredRootItems.Contains(ViewModel), TEXT("Catching an issue when the comp elements list already contains an entry for this item - please notify the dev team with a repro."))) { FilteredRootItems.Add(ViewModel); } } } SortFilteredElements(); } void FCompElementCollectionViewModel::SortFilteredElements() { struct FCompareRootElements { FORCEINLINE bool operator()(const TSharedPtr& Lhs, const TSharedPtr& Rhs) const { return Lhs->GetFName().Compare(Rhs->GetFName()) < 0; } }; FilteredRootItems.Sort(FCompareRootElements()); struct FCompareChildElements { FORCEINLINE bool operator()(const TSharedPtr& Lhs, const TSharedPtr& Rhs) const { TWeakObjectPtr LhsDataSrc = Lhs->GetDataSource(); TWeakObjectPtr RhsDataSrc = Rhs->GetDataSource(); if (!LhsDataSrc.IsValid() || !RhsDataSrc.IsValid()) { return LhsDataSrc.IsValid(); } else { ACompositingElement* LhsParent = LhsDataSrc->GetElementParent(); ACompositingElement* RhsParent = RhsDataSrc->GetElementParent(); if (LhsParent && LhsParent == RhsParent) { const TArray Children = LhsParent->GetChildElements(); const int32 LhsIndex = Children.Find(LhsDataSrc.Get()); const int32 RhsIndex = Children.Find(RhsDataSrc.Get()); return LhsIndex < RhsIndex; } else if (LhsParent && RhsParent) { return LhsParent->GetCompElementName().Compare(RhsParent->GetCompElementName()) < 0; } else if (LhsParent != RhsParent) { return LhsParent != nullptr; } } return Lhs->GetFName().Compare(Rhs->GetFName()) < 0; } }; FilteredChildren.ValueSort(FCompareChildElements()); } TSharedPtr FCompElementCollectionViewModel::GetViewModel(TWeakObjectPtr CompObjPtr) { struct GetViewModel_Impl { TSharedPtr RecursiveSearch(const TArray< TSharedPtr >& RootModels) { TSharedPtr FoundViewModel; for (const TSharedPtr& ViewModel : RootModels) { if (ViewModel.IsValid()) { if (ViewModel->GetDataSource() == DataSrcToMatch) { FoundViewModel = ViewModel; } else { FoundViewModel = RecursiveSearch(ViewModel->Children); } } if (FoundViewModel.IsValid()) { break; } } return FoundViewModel; } GetViewModel_Impl(TWeakObjectPtr InCompObjPtr) : DataSrcToMatch(InCompObjPtr) {} TWeakObjectPtr DataSrcToMatch; }; return GetViewModel_Impl(CompObjPtr).RecursiveSearch(RootViewModels); } bool FCompElementCollectionViewModel::TryGetViewModel(TWeakObjectPtr CompObjPtr, TSharedPtr& OutViewModel) { OutViewModel = GetViewModel(CompObjPtr); return OutViewModel.IsValid(); } void FCompElementCollectionViewModel::GetAllViewModels(TArray< TSharedPtr >& OutAllViewModels) { struct GetAllViewModels_Impl { void RecursiveAppend(const TArray< TSharedPtr >& RootModels) { ViewModelsOut.Append(RootModels); for (const TSharedPtr& ViewModel : RootModels) { RecursiveAppend(ViewModel->Children); } } GetAllViewModels_Impl(TArray< TSharedPtr >& InViewModelsOut) : ViewModelsOut(InViewModelsOut) {} TArray< TSharedPtr >& ViewModelsOut; }; GetAllViewModels_Impl(OutAllViewModels).RecursiveAppend(RootViewModels); } void FCompElementCollectionViewModel::AppendSelectedElementNames(TArray& OutElementNames) const { for (TSharedPtr SelectedItem : SelectedElements) { OutElementNames.Add(SelectedItem->GetFName()); } } void FCompElementCollectionViewModel::OnActorSelectionChanged(const TArray& NewSelection, bool bForceRefresh) { SelectedElements.Empty(); for (UObject* SelectedObj : NewSelection) { ACompositingElement* CompActor = Cast(SelectedObj); TSharedPtr FoundViewModel; if (TryGetViewModel(CompActor, FoundViewModel)) { SelectedElements.Add(FoundViewModel); } else { SelectedElements.Empty(); break; } } SelectionChanged.Broadcast(); } void FCompElementCollectionViewModel::RefreshActorSelections() const { TArray SelectedElementNames; AppendSelectedElementNames(SelectedElementNames); Editor->SelectNone(/*bNotifySelectNone =*/SelectedElementNames.Num() == 0, /*bDeselectBSPSurfs =*/true); CompElementManager->SelectElementActors(SelectedElementNames, /*bSelectActors =*/true, /*bNotifySelectActors =*/true, /*bSelectEvenIfHidden =*/true); } void FCompElementCollectionViewModel::CreateTopLevelElement_Executed() { TArray< TSubclassOf > HighlightedClasses; const UComposureEditorSettings* CompEditorSettings = GetDefault(); for (FSoftObjectPath FeaturedClass : CompEditorSettings->GetFeaturedCompShotClasses()) { TSubclassOf ClassObj = Cast(FeaturedClass.TryLoad()); if (ClassObj != nullptr) { HighlightedClasses.Add(ClassObj); } } if (HighlightedClasses.Num() == 0) { HighlightedClasses.Add(ACompositingElement::StaticClass()); } UClass* ChosenClass = CompElementCollectionViewModel_Impl::PromptForElementClass(LOCTEXT("PickCompClass", "Pick an Comp Class"), HighlightedClasses); if (ChosenClass) { const FScopedTransaction Transaction(LOCTEXT("CreateEmptyComp", "Create Comp")); const FName NewCompName = GenerateUniqueCompName(); CompElementManager->CreateElement(NewCompName, ChosenClass); SetSelectedElement(NewCompName); if (RequestRenameElement_CanExecute()) { RequestRenameElement_Executed(); } } } FName FCompElementCollectionViewModel::GenerateUniqueCompName() const { FName ShotName; int32 CompIndex = 0; TWeakObjectPtr ExistingComp; do { ++CompIndex; ShotName = FName(*FString::Printf(TEXT("%03d0_comp"), CompIndex)); } while (CompElementManager->TryGetElement(ShotName, ExistingComp)); return ShotName; } bool FCompElementCollectionViewModel::CreateTopLevelElement_CanExecute() const { return true; } void FCompElementCollectionViewModel::CreateChildElement_Executed() { const UComposureEditorSettings* CompEditorSettings = GetDefault(); TArray< TSubclassOf > HighlightedClasses; for (FSoftObjectPath FeaturedClass : CompEditorSettings->GetFeaturedElementClasses()) { TSubclassOf ClassObj = Cast(FeaturedClass.TryLoad()); if (ClassObj != nullptr) { HighlightedClasses.Add(ClassObj); } } UClass* ChosenClass = CompElementCollectionViewModel_Impl::PromptForElementClass(LOCTEXT("PickElementClass", "Pick an Element Type"), HighlightedClasses); if (ChosenClass) { TSharedPtr SelectedParent; AActor* LevelContext = nullptr; if (SelectedElements.Num() > 0) { SelectedParent = SelectedElements[0]; TWeakObjectPtr ParentObj = SelectedParent->GetDataSource(); if (ParentObj.IsValid()) { LevelContext = ParentObj.Get(); } } const FScopedTransaction Transaction(LOCTEXT("CreateNewElement", "New Comp Element")); const FName NewCompName = GenerateUniqueElementName(ChosenClass); CompElementManager->CreateElement(NewCompName, ChosenClass, LevelContext); if (SelectedParent.IsValid()) { CompElementManager->AttachCompElement(SelectedParent->GetFName(), NewCompName); } SetSelectedElement(NewCompName); if (RequestRenameElement_CanExecute()) { RequestRenameElement_Executed(); } } } FName FCompElementCollectionViewModel::GenerateUniqueElementName(TSubclassOf ElementClass) const { FName ElementName; FString BaseName = LOCTEXT("DefaultElementName", "layer_element").ToString(); const UDefaultComposureEditorSettings* CompEdSettings = GetDefault(); if (ElementClass) { const FString* DefaultNamePtr = CompEdSettings->DefaultElementNames.Find(ElementClass->GetFName()); if (DefaultNamePtr) { BaseName = *DefaultNamePtr; } } int32 ElementIndex = 0; TWeakObjectPtr ExistingElement; do { ++ElementIndex; ElementName = FName(*FString::Printf(TEXT("%s%d"), *BaseName, ElementIndex)); } while (CompElementManager->TryGetElement(ElementName, ExistingElement)); return ElementName; } bool FCompElementCollectionViewModel::CreateChildElement_CanExecute() const { return SelectedElements.Num() == 1 && SelectedElements[0]->GetDataSource().IsValid(); } void FCompElementCollectionViewModel::CutElements_Executed() { CopyElements_Executed(); DeleteElements_Executed(); } bool FCompElementCollectionViewModel::CutElements_CanExecute() const { return CopyElements_CanExecute(); } void FCompElementCollectionViewModel::CopyElements_Executed() { TArray CachedElementNames; TArray ChildElementsToCopy; for (TSharedPtr Element : SelectedElements) { CachedElementNames.Add(Element->GetFName()); ChildElementsToCopy.Append(CompElementCollectionViewModel_Impl::GetChildElementNamesRecursive(Element->GetDataSource())); } CompElementManager->SelectElementActors(ChildElementsToCopy, /*bSelectActors =*/true, /*bNotifySelectActors =*/true, /*bSelectEvenIfHidden =*/true); Editor->CopySelectedActorsToClipboard(GEditor->GetEditorWorldContext().World(), false); Editor->SelectNone(/*bNotifySelectNone =*/false, /*bDeselectBSPSurfs =*/true); CompElementManager->SelectElementActors(CachedElementNames, /*bSelectActors =*/true, /*bNotifySelectActors =*/true, /*bSelectEvenIfHidden =*/true); } bool FCompElementCollectionViewModel::CopyElements_CanExecute() const { return SelectedElements.Num() > 0; } void FCompElementCollectionViewModel::PasteElements_Executed() { TSharedPtr PrevSelection; ULevel* LevelContext = nullptr; TWeakObjectPtr PrevSelectionObj; if (SelectedElements.Num() > 0) { PrevSelection = SelectedElements[0]; PrevSelectionObj = PrevSelection->GetDataSource(); if (PrevSelectionObj.IsValid()) { LevelContext = PrevSelectionObj->GetLevel(); } } if (LevelContext == nullptr) { for (TSharedPtr Model : RootViewModels) { TWeakObjectPtr ModelObj = Model->GetDataSource(); if (ModelObj.IsValid()) { LevelContext = ModelObj->GetLevel(); if (LevelContext) { break; } } } } UWorld* TargetWorld = GEditor->GetEditorWorldContext().World(); if (LevelContext) { TargetWorld = LevelContext->GetWorld(); } else { LevelContext = TargetWorld->GetCurrentLevel(); } { FScopedWorldLevelContext ScopedLevelContext(TargetWorld, LevelContext); Editor->PasteSelectedActorsFromClipboard(TargetWorld, FText::FromString("Comp Element Paste"), EPasteTo::PT_Here); } if (SelectedElements.Num() > 0) { TSharedPtr NewPastedElement = SelectedElements[0]; if (PrevSelectionObj.IsValid()) { if (ACompositingElement* PrevSelectionParent = PrevSelectionObj->GetElementParent()) { // Selected element has a parent, make it a sibling CompElementManager->AttachCompElement(PrevSelectionParent->GetFName(), NewPastedElement->GetFName()); } else { // Selected element is root, make pasted element a child CompElementManager->AttachCompElement(PrevSelection->GetFName(), NewPastedElement->GetFName()); } } else if (ACompositingElement* Parent = NewPastedElement->GetDataSource()->GetElementParent()) { Parent->DetatchAsChildLayer(NewPastedElement->GetDataSource().Get()); } } Refresh(); } bool FCompElementCollectionViewModel::PasteElements_CanExecute() const { // Currently allowing anything to be pasted, but it may be worth revisiting this to filter out non-related actors return true; } void FCompElementCollectionViewModel::DuplicateElements_Executed() { CopyElements_Executed(); PasteElements_Executed(); } bool FCompElementCollectionViewModel::DuplicateElements_CanExecute() const { return CopyElements_CanExecute(); } void FCompElementCollectionViewModel::DeleteElements_Executed() { if( SelectedElements.Num() == 0 ) { return; } TArray SelectedElementNames; for (TSharedPtr Element : SelectedElements) { SelectedElementNames.Append(CompElementCollectionViewModel_Impl::GetChildElementNamesRecursive(Element->GetDataSource())); } const FScopedTransaction Transaction(LOCTEXT("DeleteComp", "Delete Comp Elements")); CompElementManager->SelectElementActors(SelectedElementNames, /*bSelectActors =*/true, /*bNotifySelectActors =*/true, /*bSelectEvenIfHidden =*/true); TArray ElementsToDelete; AppendSelectedElementNames(ElementsToDelete); CompElementManager->DeleteElements(ElementsToDelete); } bool FCompElementCollectionViewModel::DeleteElements_CanExecute() const { return SelectedElements.Num() > 0; } void FCompElementCollectionViewModel::RequestRenameElement_Executed() { if (SelectedElements.Num() == 1) { OnRenameRequested().Broadcast(); } } bool FCompElementCollectionViewModel::RequestRenameElement_CanExecute() const { return SelectedElements.Num() == 1 && SelectedElements[0]->IsEditable(); } void FCompElementCollectionViewModel::OpenPreview_Executed() { if (SelectedElements.Num() > 0) { SelectedElements[0]->BroadcastPreviewRequest(); } } bool FCompElementCollectionViewModel::OpenPreview_CanExecute() const { return SelectedElements.Num() == 1 && SelectedElements[0]->GetDataSource().IsValid(); } void FCompElementCollectionViewModel::RefreshList_Executed() { Refresh(); } bool FCompElementCollectionViewModel::RefreshList_CanExecute() const { return true; } #undef LOCTEXT_NAMESPACE