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

1968 lines
62 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "DataHierarchyViewModelBase.h"
#include "SDropTarget.h"
#include "Widgets/Views/STableRow.h"
#include "Widgets/SDataHierarchyEditor.h"
#include "Widgets/Text/SInlineEditableTextBlock.h"
#include "Widgets/SToolTip.h"
#include "Widgets/Input/SCheckBox.h"
#include "Editor.h"
#include "DataHierarchyEditorCommands.h"
#include "DataHierarchyEditorMisc.h"
#include "DataHierarchyEditorModule.h"
#include "IPropertyRowGenerator.h"
#include "ScopedTransaction.h"
#include "Framework/Commands/GenericCommands.h"
#include "ScopedTransaction.h"
#include "Framework/Notifications/NotificationManager.h"
#include "ToolMenus.h"
#include "Logging/StructuredLog.h"
#include "Widgets/Notifications/SNotificationList.h"
#define LOCTEXT_NAMESPACE "DataHierarchyEditor"
const TArray<UHierarchyElement*>& UHierarchyElement::GetChildren() const
{
return Children;
}
UHierarchyElement* UHierarchyElement::FindChildWithIdentity(FHierarchyElementIdentity ChildIdentity, bool bSearchRecursively)
{
TObjectPtr<UHierarchyElement>* FoundItem = Children.FindByPredicate([ChildIdentity](UHierarchyElement* Child)
{
return Child->GetPersistentIdentity() == ChildIdentity;
});
if(FoundItem)
{
return *FoundItem;
}
if(bSearchRecursively)
{
for(UHierarchyElement* Child : Children)
{
UHierarchyElement* FoundChild = Child->FindChildWithIdentity(ChildIdentity, bSearchRecursively);
if(FoundChild)
{
return FoundChild;
}
}
}
return nullptr;
}
UHierarchyElement* UHierarchyElement::CopyAndAddItemAsChild(const UHierarchyElement& ItemToCopy)
{
UHierarchyElement* NewChild = Cast<UHierarchyElement>(StaticDuplicateObject(&ItemToCopy, this));
if(NewChild->GetPersistentIdentity() != ItemToCopy.GetPersistentIdentity())
{
check(false);
}
GetChildrenMutable().Add(NewChild);
return NewChild;
}
UHierarchyElement* UHierarchyElement::CopyAndAddItemUnderParentIdentity(const UHierarchyElement& ItemToCopy, FHierarchyElementIdentity ParentIdentity)
{
UHierarchyElement* ParentItem = FindChildWithIdentity(ParentIdentity, true);
if(ParentItem)
{
UHierarchyElement* NewChild = Cast<UHierarchyElement>(StaticDuplicateObject(&ItemToCopy, ParentItem));
if(NewChild->GetPersistentIdentity() != ItemToCopy.GetPersistentIdentity())
{
check(false);
}
ParentItem->GetChildrenMutable().Add(NewChild);
return NewChild;
}
return nullptr;
}
bool UHierarchyElement::RemoveChildWithIdentity(FHierarchyElementIdentity ChildIdentity, bool bSearchRecursively)
{
int32 RemovedChildrenCount = Children.RemoveAll([ChildIdentity](UHierarchyElement* Child)
{
return Child->GetPersistentIdentity() == ChildIdentity;
});
if(RemovedChildrenCount > 1)
{
UE_LOG(LogDataHierarchyEditor, Warning, TEXT("More than one child with the same identity has been found in parent %s"), *ToString());
}
bool bChildrenRemoved = RemovedChildrenCount > 0;
if(bSearchRecursively && bChildrenRemoved == false)
{
for(UHierarchyElement* Child : Children)
{
bChildrenRemoved |= Child->RemoveChildWithIdentity(ChildIdentity, bSearchRecursively);
}
}
return bChildrenRemoved;
}
TArray<FHierarchyElementIdentity> UHierarchyElement::GetParentIdentities() const
{
TArray<FHierarchyElementIdentity> ParentIdentities;
for(UHierarchyElement* Parent = Cast<UHierarchyElement>(GetOuter()); Parent != nullptr; Parent = Cast<UHierarchyElement>(Parent->GetOuter()))
{
ParentIdentities.Add(Parent->GetPersistentIdentity());
}
return ParentIdentities;
}
bool UHierarchyElement::Modify(bool bAlwaysMarkDirty)
{
bool bSavedToTransactionBuffer = true;
for(UHierarchyElement* Child : Children)
{
bSavedToTransactionBuffer &= Child->Modify(bAlwaysMarkDirty);
}
bSavedToTransactionBuffer &= UObject::Modify(bAlwaysMarkDirty);
return bSavedToTransactionBuffer;
}
void UHierarchyElement::PostLoad()
{
if(Guid_DEPRECATED.IsValid())
{
SetIdentity(FHierarchyElementIdentity({Guid_DEPRECATED}, {}));
}
bool bAnyChildNullptr = false;
for(auto It = Children.CreateIterator(); It; ++It)
{
if(*It == nullptr)
{
bAnyChildNullptr = true;
It.RemoveCurrent();
}
}
if(bAnyChildNullptr)
{
UPackage* Package = GetPackage();
UE_LOG(LogDataHierarchyEditor, Warning, TEXT("HierarchyElement %s found nullptr child in asset %s. Removed all nullptr children. This is indicative of something wrong. Check if the hierarchy is still correct and fix it, if necessary."), *ToString(), *GetNameSafe(Package))
}
Super::PostLoad();
}
UHierarchySection* UHierarchyRoot::AddSection(FText InNewSectionName, int32 InsertIndex, TSubclassOf<UHierarchySection> SectionClass)
{
TSet<FName> ExistingSectionNames;
for(FName& SectionName : GetSections())
{
ExistingSectionNames.Add(SectionName);
}
FName NewName = UE::DataHierarchyEditor::GetUniqueName(FName(InNewSectionName.ToString()), ExistingSectionNames);
UHierarchySection* NewSectionItem = NewObject<UHierarchySection>(this, SectionClass);
NewSectionItem->SetSectionName(NewName);
NewSectionItem->SetFlags(RF_Transactional);
if(InsertIndex == INDEX_NONE)
{
Sections.Add(NewSectionItem);
}
else
{
Sections.Insert(NewSectionItem, InsertIndex);
}
return NewSectionItem;
}
UHierarchySection* UHierarchyRoot::FindSectionByIdentity(FHierarchyElementIdentity SectionIdentity)
{
for(UHierarchySection* Section : Sections)
{
if(Section->GetPersistentIdentity() == SectionIdentity)
{
return Section;
}
}
return nullptr;
}
void UHierarchyRoot::DuplicateSectionFromOtherRoot(const UHierarchySection& SectionToCopy)
{
if(FindSectionByIdentity(SectionToCopy.GetPersistentIdentity()) != nullptr || SectionToCopy.GetOuter() == this)
{
return;
}
Sections.Add(Cast<UHierarchySection>(StaticDuplicateObject(&SectionToCopy, this)));
}
void UHierarchyRoot::RemoveSection(FText SectionName)
{
if(Sections.ContainsByPredicate([SectionName](UHierarchySection* Section)
{
return Section->GetSectionNameAsText().EqualTo(SectionName);
}))
{
Sections.RemoveAll([SectionName](UHierarchySection* Section)
{
return Section->GetSectionNameAsText().EqualTo(SectionName);
});
}
}
void UHierarchyRoot::RemoveSectionByIdentity(FHierarchyElementIdentity SectionIdentity)
{
Sections.RemoveAll([SectionIdentity](UHierarchySection* Section)
{
return Section->GetPersistentIdentity() == SectionIdentity;
});
}
TSet<FName> UHierarchyRoot::GetSections() const
{
TSet<FName> OutSections;
for(UHierarchySection* Section : Sections)
{
OutSections.Add(Section->GetSectionName());
}
return OutSections;
}
int32 UHierarchyRoot::GetSectionIndex(UHierarchySection* Section) const
{
return Sections.Find(Section);
}
bool UHierarchyRoot::Modify(bool bAlwaysMarkDirty)
{
bool bSavedToTransactionBuffer = true;
for(UHierarchySection* Section : Sections)
{
bSavedToTransactionBuffer &= Section->Modify();
}
bSavedToTransactionBuffer &= Super::Modify(bAlwaysMarkDirty);
return bSavedToTransactionBuffer;
}
void UHierarchyRoot::EmptyAllData()
{
Children.Empty();
Sections.Empty();
}
void UHierarchyRoot::Serialize(FStructuredArchive::FRecord Record)
{
// If the root isn't transient, neither should any of its hierarchy elements be.
// This is expected to happen as the source elements are transient by default.
// When source hierarchy elements are put into the hierarchy we have to make sure to remove the flag after
if(Record.GetArchiveState().IsSaving() && this->HasAnyFlags(RF_Transient) == false)
{
TArray<UHierarchyElement*> AllElements;
GetChildrenOfType(AllElements, true);
for(UHierarchyElement* Element : AllElements)
{
Element->ClearFlags(RF_Transient);
}
}
Super::Serialize(Record);
}
bool FHierarchyCategoryViewModel::IsTopCategoryActive() const
{
if(UHierarchyCategory* Category = GetDataMutable<UHierarchyCategory>())
{
const UHierarchyCategory* Result = Category;
const UHierarchyCategory* TopLevelCategory = Result;
for (; TopLevelCategory != nullptr; TopLevelCategory = TopLevelCategory->GetTypedOuter<UHierarchyCategory>() )
{
if(TopLevelCategory != nullptr)
{
Result = TopLevelCategory;
}
}
return HierarchyViewModel->IsHierarchySectionActive(Result->GetSection());
}
return false;
}
FHierarchyElementViewModel::FCanPerformActionResults FHierarchyCategoryViewModel::CanDropOnInternal(TSharedPtr<FHierarchyElementViewModel> DraggedElement, EItemDropZone ItemDropZone)
{
FCanPerformActionResults Results(false);
TArray<TSharedPtr<FHierarchyCategoryViewModel>> TargetChildrenCategories;
GetChildrenViewModelsForType<UHierarchyCategory, FHierarchyCategoryViewModel>(TargetChildrenCategories);
TArray<TSharedPtr<FHierarchyCategoryViewModel>> SiblingCategories;
Parent.Pin()->GetChildrenViewModelsForType<UHierarchyCategory, FHierarchyCategoryViewModel>(SiblingCategories);
// we only allow drops if some general conditions are fulfilled
if(DraggedElement->GetData() != GetData() &&
(!DraggedElement->HasParent(AsShared(), false) || ItemDropZone != EItemDropZone::OntoItem) &&
!HasParent(DraggedElement, true))
{
// categories can be dropped on categories, but only if the resulting sibling categories or children categories have different names
if(DraggedElement->GetData()->IsA<UHierarchyCategory>())
{
if(ItemDropZone != EItemDropZone::OntoItem)
{
bool bContainsSiblingWithSameName = SiblingCategories.ContainsByPredicate([DraggedElement](TSharedPtr<FHierarchyCategoryViewModel> HierarchyCategoryViewModel)
{
return DraggedElement->ToString() == HierarchyCategoryViewModel->ToString() && DraggedElement != HierarchyCategoryViewModel;
});
if(bContainsSiblingWithSameName)
{
Results.bCanPerform = false;
Results.CanPerformMessage = LOCTEXT("CantDropCategorNextToCategorySameSiblingNames", "A category of the same name already exists here, potentially in a different section. Please rename your category first.");
return Results;
}
Results.CanPerformMessage = LOCTEXT("MoveCategoryText", "Move category here");
// if we are making a category a sibling of another at the root level, the section will be set to the currently active section. Let that be known.
if(Parent.Pin()->GetData()->IsA<UHierarchyRoot>())
{
UHierarchyCategory* DraggedCategory = Cast<UHierarchyCategory>(DraggedElement->GetDataMutable());
if(DraggedCategory->GetSection() != HierarchyViewModel->GetActiveHierarchySectionData())
{
FText SectionChangeBaseText = LOCTEXT("CategorySectionWillUpdateDueToDrop", "The section of the category will change to {0} after the drop");
FText ActualSectionChangeText = FText::FormatOrdered(SectionChangeBaseText, HierarchyViewModel->GetActiveHierarchySectionData() == nullptr ? FText::FromString("All") : HierarchyViewModel->GetActiveHierarchySectionData()->GetSectionNameAsText());
Results.CanPerformMessage = FText::FormatOrdered(FText::AsCultureInvariant("{0}\n{1}"), Results.CanPerformMessage, ActualSectionChangeText);
}
}
}
else
{
bool bContainsChildrenCategoriesWithSameName = TargetChildrenCategories.ContainsByPredicate([DraggedElement](TSharedPtr<FHierarchyCategoryViewModel> HierarchyCategoryViewModel)
{
return DraggedElement->ToString() == HierarchyCategoryViewModel->ToString();
});
if(bContainsChildrenCategoriesWithSameName)
{
Results.bCanPerform = false;
Results.CanPerformMessage = LOCTEXT("CantDropCategoryOnCategorySameChildCategoryName", "A sub-category of the same name already exists! Please rename your category first.");
return Results;
}
Results.CanPerformMessage = LOCTEXT("CreateSubcategory", "Drop category here to create a sub-category");
}
Results.bCanPerform = true;
return Results;
}
else if(DraggedElement->GetData()->IsA<UHierarchyItem>())
{
// items can generally be dropped onto categories
Results.bCanPerform = EItemDropZone::OntoItem == ItemDropZone;
if(Results.bCanPerform)
{
if(DraggedElement->IsForHierarchy() == false)
{
FText Message = LOCTEXT("AddItemToCategoryDragMessage", "Add {0} to {1}");
Results.CanPerformMessage = FText::FormatOrdered(Message, FText::FromString(DraggedElement->ToString()), FText::FromString(ToString()));
}
else
{
FText Message = LOCTEXT("MoveItemToCategoryDragMessage", "Move {0} to {1}");
Results.CanPerformMessage = FText::FormatOrdered(Message, FText::FromString(DraggedElement->ToString()), FText::FromString(ToString()));
}
}
}
}
return Results;
}
void UHierarchyCategory::FixupSectionLinkage()
{
UHierarchyRoot* OwningRoot = GetTypedOuter<UHierarchyRoot>();
if(Section != nullptr && Section->GetTypedOuter<UHierarchyRoot>() != OwningRoot)
{
UHierarchySection* CorrectSection = OwningRoot->FindSectionByIdentity(Section->GetPersistentIdentity());
ensure(CorrectSection != nullptr);
Section = CorrectSection;
}
}
FHierarchyElementIdentity UHierarchyCategory::ConstructIdentity()
{
FHierarchyElementIdentity Identity;
Identity.Names.Add("Category");
Identity.Guids.Add(FGuid::NewGuid());
return Identity;
}
void UHierarchyCategory::PostLoad()
{
Super::PostLoad();
// Some categories were never initialized with a proper identity. We fix this up here.
if(Identity.IsValid() == false)
{
SetIdentity(UHierarchyCategory::ConstructIdentity());
}
}
void UHierarchySection::SetSectionNameAsText(const FText& Text)
{
Section = FName(Text.ToString());
}
UDataHierarchyViewModelBase::UDataHierarchyViewModelBase()
{
Commands = MakeShared<FUICommandList>();
}
UDataHierarchyViewModelBase::~UDataHierarchyViewModelBase()
{
RefreshSourceViewDelegate.Unbind();
RefreshHierarchyWidgetDelegate.Unbind();
RefreshSectionsViewDelegate.Unbind();
}
void UDataHierarchyViewModelBase::Initialize()
{
HierarchyRoot = GetHierarchyRoot();
HierarchyRoot->SetFlags(RF_Transactional);
TArray<UHierarchyElement*> AllItems;
HierarchyRoot->GetChildrenOfType<UHierarchyElement>(AllItems, true);
for(UHierarchyElement* Item : AllItems)
{
Item->SetFlags(RF_Transactional);
}
for(UHierarchySection* Section : HierarchyRoot->GetSectionDataMutable())
{
Section->SetFlags(RF_Transactional);
}
UToolMenus* ToolMenus = UToolMenus::Get();
FName MenuName = GetContextMenuName();
if(!ToolMenus->IsMenuRegistered(MenuName))
{
UToolMenu* HierarchyMenu = ToolMenus->RegisterMenu(MenuName, NAME_None, EMultiBoxType::Menu);
HierarchyMenu->AddDynamicSection(NAME_None, FNewToolMenuDelegate::CreateStatic(&UDataHierarchyViewModelBase::GenerateDynamicContextMenu));
}
SetupCommands();
TSharedPtr<FHierarchyElementViewModel> ViewModel = CreateViewModelForElement(HierarchyRoot, nullptr);
HierarchyRootViewModel = StaticCastSharedPtr<FHierarchyRootViewModel>(ViewModel);
if(!ensureMsgf(HierarchyRootViewModel.IsValid(), TEXT("Make sure that CreateViewModelForData creates a FHierarchyRootViewModel (or derived) for UHierarchyRoot elements")))
{
return;
}
HierarchyRootViewModel->Initialize();
HierarchyRootViewModel->AddChildFilter(FHierarchyElementViewModel::FOnFilterChild::CreateUObject(this, &UDataHierarchyViewModelBase::FilterForHierarchySection));
HierarchyRootViewModel->AddChildFilter(FHierarchyElementViewModel::FOnFilterChild::CreateUObject(this, &UDataHierarchyViewModelBase::FilterForUncategorizedRootItemsInAllSection));
HierarchyRootViewModel->SyncViewModelsToData();
DefaultHierarchySectionViewModel = MakeShared<FHierarchySectionViewModel>(nullptr, GetHierarchyRootViewModel().ToSharedRef(), this);
SetActiveHierarchySection(DefaultHierarchySectionViewModel);
InitializeInternal();
bIsInitialized = true;
OnInitializedDelegate.ExecuteIfBound();
}
void UDataHierarchyViewModelBase::Finalize()
{
HierarchyRootViewModel.Reset();
HierarchyRoot = nullptr;
FinalizeInternal();
bIsFinalized = true;
}
TSharedPtr<FHierarchyElementViewModel> UDataHierarchyViewModelBase::CreateViewModelForElement(UHierarchyElement* Element, TSharedPtr<FHierarchyElementViewModel> Parent)
{
// We first give the internal implementation a chance to create view models
if(TSharedPtr<FHierarchyElementViewModel> CustomViewModel = CreateCustomViewModelForElement(Element, Parent))
{
return CustomViewModel;
}
// If it wasn't implemented or wasn't covered, we make sure to have default view models
if(UHierarchyItem* Item = Cast<UHierarchyItem>(Element))
{
return MakeShared<FHierarchyItemViewModel>(Item, Parent.ToSharedRef(), this);
}
else if(UHierarchyCategory* Category = Cast<UHierarchyCategory>(Element))
{
return MakeShared<FHierarchyCategoryViewModel>(Category, Parent.ToSharedRef(), this);
}
else if(UHierarchySection* Section = Cast<UHierarchySection>(Element))
{
// For sections, we require the parent to be a root view model
TSharedPtr<FHierarchyRootViewModel> RootViewModel = StaticCastSharedPtr<FHierarchyRootViewModel>(Parent);
ensure(RootViewModel.IsValid());
return MakeShared<FHierarchySectionViewModel>(Section, RootViewModel.ToSharedRef(), this);
}
else if(UHierarchyRoot* Root = Cast<UHierarchyRoot>(Element))
{
// If the root is the hierarchy root, we know it's for the hierarchy. If not, it's the transient source root
bool bIsForHierarchy = GetHierarchyRoot() == Element;
return MakeShared<FHierarchyRootViewModel>(Root, this, bIsForHierarchy);
}
ensureMsgf(false, TEXT("This should never be reached. Either a custom or a default view model must exist for each Hierarchy Element"));
return nullptr;
}
TSubclassOf<UHierarchyCategory> UDataHierarchyViewModelBase::GetCategoryDataClass() const
{
return UHierarchyCategory::StaticClass();
}
TSubclassOf<UHierarchySection> UDataHierarchyViewModelBase::GetSectionDataClass() const
{
return UHierarchySection::StaticClass();
}
void UDataHierarchyViewModelBase::ForceFullRefresh()
{
RefreshSourceItemsRequestedDelegate.ExecuteIfBound();
// todo (me) during merge at startup this can be nullptr for some reason
if(HierarchyRootViewModel.IsValid())
{
HierarchyRootViewModel->SyncViewModelsToData();
}
RefreshAllViewsRequestedDelegate.ExecuteIfBound(true);
}
void UDataHierarchyViewModelBase::ForceFullRefreshOnTimer()
{
ensure(FullRefreshNextFrameHandle.IsValid());
ForceFullRefresh();
FullRefreshNextFrameHandle.Invalidate();
}
void UDataHierarchyViewModelBase::RequestFullRefreshNextFrame()
{
if(!FullRefreshNextFrameHandle.IsValid() && GEditor != nullptr)
{
FullRefreshNextFrameHandle = GEditor->GetTimerManager()->SetTimerForNextTick(FTimerDelegate::CreateUObject(this, &UDataHierarchyViewModelBase::ForceFullRefreshOnTimer));
}
}
void UDataHierarchyViewModelBase::RefreshAllViews(bool bFullRefresh) const
{
RefreshAllViewsRequestedDelegate.ExecuteIfBound(bFullRefresh);
}
void UDataHierarchyViewModelBase::RefreshSourceView(bool bFullRefresh) const
{
RefreshSourceViewDelegate.ExecuteIfBound(bFullRefresh);
}
void UDataHierarchyViewModelBase::RefreshHierarchyView(bool bFullRefresh) const
{
RefreshHierarchyWidgetDelegate.ExecuteIfBound(bFullRefresh);
}
void UDataHierarchyViewModelBase::RefreshSectionsView() const
{
RefreshSectionsViewDelegate.ExecuteIfBound();
}
void UDataHierarchyViewModelBase::PostUndo(bool bSuccess)
{
ForceFullRefresh();
}
void UDataHierarchyViewModelBase::PostRedo(bool bSuccess)
{
PostUndo(bSuccess);
}
bool UDataHierarchyViewModelBase::MatchesContext(const FTransactionContext& InContext, const TArray<TPair<UObject*, FTransactionObjectEvent>>& TransactionObjectContexts) const
{
for(const TPair<UObject*, FTransactionObjectEvent>& TransactionObjectContext : TransactionObjectContexts)
{
if(TransactionObjectContext.Key->IsA<UHierarchyElement>())
{
return true;
}
}
return false;
}
bool UDataHierarchyViewModelBase::FilterForHierarchySection(TSharedPtr<const FHierarchyElementViewModel> ViewModel) const
{
if(ActiveHierarchySection.IsValid())
{
// If the currently selected section data is nullptr, it's the All section, and we let everything pass
if(ActiveHierarchySection.Pin()->GetData() == nullptr)
{
return true;
}
// if not, we check against identical section data
return ActiveHierarchySection.Pin()->GetData() == ViewModel->GetSection();
}
return true;
}
bool UDataHierarchyViewModelBase::FilterForUncategorizedRootItemsInAllSection(TSharedPtr<const FHierarchyElementViewModel> ViewModel) const
{
if(ActiveHierarchySection.IsValid())
{
// we want to filter out all items that are directly added to the root if we aren't in the 'All' section
if(ActiveHierarchySection.Pin()->GetData() == nullptr)
{
return true;
}
return ViewModel->GetData<UHierarchyCategory>() != nullptr;
}
return true;
}
void UDataHierarchyViewModelBase::ToolMenuRequestRename(const FToolMenuContext& Context) const
{
UHierarchyMenuContext* HierarchyMenuContext = Context.FindContext<UHierarchyMenuContext>();
if(HierarchyMenuContext->MenuHierarchyElements.Num() == 1)
{
HierarchyMenuContext->MenuHierarchyElements[0]->RequestRename();
}
}
bool UDataHierarchyViewModelBase::ToolMenuCanRequestRename(const FToolMenuContext& Context) const
{
UHierarchyMenuContext* HierarchyMenuContext = Context.FindContext<UHierarchyMenuContext>();
if(HierarchyMenuContext->MenuHierarchyElements.Num() == 1)
{
return HierarchyMenuContext->MenuHierarchyElements[0]->CanRename();
}
return false;
}
void UDataHierarchyViewModelBase::ToolMenuDelete(const FToolMenuContext& Context) const
{
UHierarchyMenuContext* HierarchyMenuContext = Context.FindContext<UHierarchyMenuContext>();
DeleteElements(HierarchyMenuContext->MenuHierarchyElements);
}
bool UDataHierarchyViewModelBase::ToolMenuCanDelete(const FToolMenuContext& Context) const
{
UHierarchyMenuContext* HierarchyMenuContext = Context.FindContext<UHierarchyMenuContext>();
for(TSharedPtr<FHierarchyElementViewModel> MenuHierarchyElement : HierarchyMenuContext->MenuHierarchyElements)
{
if(MenuHierarchyElement->CanDelete() == false)
{
return false;
}
}
return HierarchyMenuContext->MenuHierarchyElements.Num() > 0;
}
void UDataHierarchyViewModelBase::ToolMenuNavigateTo(const FToolMenuContext& Context) const
{
UHierarchyMenuContext* HierarchyMenuContext = Context.FindContext<UHierarchyMenuContext>();
if(HierarchyMenuContext->MenuHierarchyElements.Num() == 1)
{
if(TSharedPtr<FHierarchyElementViewModel> MatchingViewModelInHierarchy = GetHierarchyRootViewModel()->FindViewModelForChild(HierarchyMenuContext->MenuHierarchyElements[0]->GetData()->GetPersistentIdentity(), true))
{
NavigateToElementInHierarchy(MatchingViewModelInHierarchy.ToSharedRef());
}
}
}
bool UDataHierarchyViewModelBase::ToolMenuCanNavigateTo(const FToolMenuContext& Context) const
{
UHierarchyMenuContext* HierarchyMenuContext = Context.FindContext<UHierarchyMenuContext>();
if(HierarchyMenuContext->MenuHierarchyElements.Num() != 1)
{
return false;
}
TSharedPtr<FHierarchyElementViewModel> ViewModel = HierarchyMenuContext->MenuHierarchyElements[0];
if(ViewModel->IsForHierarchy())
{
return false;
}
if(TSharedPtr<FHierarchyElementViewModel> MatchingViewModelInHierarchy = GetHierarchyRootViewModel()->FindViewModelForChild(ViewModel->GetData()->GetPersistentIdentity(), true))
{
return MatchingViewModelInHierarchy.IsValid();
}
return false;
}
const TArray<TSharedPtr<FHierarchyElementViewModel>>& UDataHierarchyViewModelBase::GetHierarchyItems() const
{
return HierarchyRootViewModel->GetFilteredChildren();
}
FName UDataHierarchyViewModelBase::GetContextMenuName() const
{
return FName(FString(TEXT("HierarchyEditor.") + GetClass()->GetName()));
}
TSharedRef<FHierarchyDragDropOp> UDataHierarchyViewModelBase::CreateDragDropOp(TSharedRef<FHierarchyElementViewModel> Item)
{
TSharedRef<FHierarchyDragDropOp> DragDropOp = MakeShared<FHierarchyDragDropOp>(Item);
DragDropOp->Construct();
return DragDropOp;
}
void UDataHierarchyViewModelBase::OnGetChildren(TSharedPtr<FHierarchyElementViewModel> Element, TArray<TSharedPtr<FHierarchyElementViewModel>>& OutChildren) const
{
OutChildren.Append(Element->GetFilteredChildren());
}
void UDataHierarchyViewModelBase::SetActiveHierarchySection(TSharedPtr<FHierarchySectionViewModel> Section)
{
ActiveHierarchySection = Section;
RefreshHierarchyView(true);
OnHierarchySectionActivatedDelegate.ExecuteIfBound(Section);
}
TSharedPtr<FHierarchySectionViewModel> UDataHierarchyViewModelBase::GetActiveHierarchySection() const
{
return ActiveHierarchySection.Pin();
}
UHierarchySection* UDataHierarchyViewModelBase::GetActiveHierarchySectionData() const
{
return ActiveHierarchySection.Pin()->GetDataMutable<UHierarchySection>();
}
bool UDataHierarchyViewModelBase::IsHierarchySectionActive(const UHierarchySection* Section) const
{
return ActiveHierarchySection.Pin()->GetData() == Section;
}
FString UDataHierarchyViewModelBase::OnElementToStringDebug(TSharedPtr<FHierarchyElementViewModel> ElementViewModel) const
{
return ElementViewModel->ToString();
}
FHierarchyElementViewModel::~FHierarchyElementViewModel()
{
Children.Empty();
FilteredChildren.Empty();
}
UHierarchyElement* FHierarchyElementViewModel::AddChild(TSubclassOf<UHierarchyElement> NewChildClass, FHierarchyElementIdentity ChildIdentity)
{
UHierarchyElement* NewChild = NewObject<UHierarchyElement>(GetDataMutable(), NewChildClass);
NewChild->SetFlags(RF_Transactional);
NewChild->Modify();
NewChild->SetIdentity(ChildIdentity);
GetDataMutable()->GetChildrenMutable().Add(NewChild);
SyncViewModelsToData();
HierarchyViewModel->OnHierarchyChanged().Broadcast();
return NewChild;
}
void FHierarchyElementViewModel::Tick(float DeltaTime)
{
if(bRenamePending)
{
RequestRename();
}
}
TStatId FHierarchyElementViewModel::GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FHierarchyElementViewModel, STATGROUP_Tickables);
}
void FHierarchyElementViewModel::RefreshChildrenData()
{
TArray<TSharedPtr<FHierarchyElementViewModel>> TmpChildren = Children;
for(TSharedPtr<FHierarchyElementViewModel> Child : TmpChildren)
{
if(Child->RepresentsExternalData() && Child->DoesExternalDataStillExist(HierarchyViewModel->GetRefreshContext()) == false)
{
UE_LOGFMT(LogDataHierarchyEditor, Verbose, "Hierarchy Element {ElementName} no longer has valid external data. Deleting.", Child->ToString());
Child->Delete();
}
}
/** Every item view model can define its own sort order for its children. */
SortChildrenData();
RefreshChildrenDataInternal();
/** All remaining children are supposed to exist at this point, as internal data won't be removed by refreshing & external data was cleaned up already.
* This will not call RefreshChildrenData on data that has just been added as no view models exist for these yet.
*/
for(TSharedPtr<FHierarchyElementViewModel> Child : Children)
{
Child->RefreshChildrenData();
}
}
void FHierarchyElementViewModel::SyncViewModelsToData()
{
// this will recursively remove all outdated external data as well as give individual view models the chance to add new data
RefreshChildrenData();
// now that the data is refreshed, we can sync to the data by recycling view models & creating new ones
// old view models will get deleted automatically
TArray<TSharedPtr<FHierarchyElementViewModel>> NewChildren;
for(UHierarchyElement* Child : Element->GetChildren())
{
int32 ViewModelIndex = FindIndexOfChild(Child);
// if we couldn't find a view model for a data child, we create it here
if(ViewModelIndex == INDEX_NONE)
{
TSharedPtr<FHierarchyElementViewModel> ChildViewModel = HierarchyViewModel->CreateViewModelForElement(Child, AsShared());
if(ensure(ChildViewModel.IsValid()))
{
ChildViewModel->Initialize();
ChildViewModel->SyncViewModelsToData();
NewChildren.Add(ChildViewModel);
}
}
// if we could find the view model, we refresh its contained view models and readd it
else
{
Children[ViewModelIndex]->SyncViewModelsToData();
NewChildren.Add(Children[ViewModelIndex]);
}
}
Children.Empty();
Children.Append(NewChildren);
for(TSharedPtr<FHierarchyElementViewModel> Child : Children)
{
Child->OnChildRequestedDeletion().BindSP(this, &FHierarchyElementViewModel::DeleteChild);
Child->GetOnSynced().BindSP(this, &FHierarchyElementViewModel::PropagateOnChildSynced);
}
/** Give the view models a chance to further customize the children sync process. */
SyncViewModelsToDataInternal();
// then we sort the view models according to the data order as this is what will determine widget order created from the view models
Children.Sort([this](const TSharedPtr<FHierarchyElementViewModel>& ItemA, const TSharedPtr<FHierarchyElementViewModel>& ItemB)
{
return FindIndexOfDataChild(ItemA) < FindIndexOfDataChild(ItemB);
});
// we refresh the filtered children here as well
GetFilteredChildren();
OnSyncedDelegate.ExecuteIfBound();
}
const TArray<TSharedPtr<FHierarchyElementViewModel>>& FHierarchyElementViewModel::GetFilteredChildren() const
{
FilteredChildren.Empty();
if(CanHaveChildren())
{
for(TSharedPtr<FHierarchyElementViewModel> Child : Children)
{
bool bPassesFilter = true;
for(const FOnFilterChild& OnFilterChild : ChildFilters)
{
bPassesFilter &= OnFilterChild.Execute(Child);
if(!bPassesFilter)
{
break;
}
}
if(bPassesFilter)
{
FilteredChildren.Add(Child);
}
}
}
return FilteredChildren;
}
void FHierarchyElementViewModel::SortChildrenData() const
{
GetDataMutable()->GetChildrenMutable().StableSort([](const UHierarchyElement& ItemA, const UHierarchyElement& ItemB)
{
return ItemA.IsA<UHierarchyCategory>() && ItemB.IsA<UHierarchyItem>();
});
}
int32 FHierarchyElementViewModel::GetHierarchyDepth() const
{
if(Parent.IsValid())
{
return 1 + Parent.Pin()->GetHierarchyDepth();
}
return 0;
}
void FHierarchyElementViewModel::AddChildFilter(FOnFilterChild InFilterChild)
{
if(ensure(InFilterChild.IsBound()))
{
ChildFilters.Add(InFilterChild);
}
}
bool FHierarchyElementViewModel::HasParent(TSharedPtr<FHierarchyElementViewModel> ParentCandidate, bool bRecursive) const
{
if(Parent.IsValid())
{
if(Parent == ParentCandidate)
{
return true;
}
else if(bRecursive)
{
return Parent.Pin()->HasParent(ParentCandidate, bRecursive);
}
}
return false;
}
TSharedRef<FHierarchyElementViewModel> FHierarchyElementViewModel::DuplicateToThis(TSharedPtr<FHierarchyElementViewModel> ItemToDuplicate, int32 InsertIndex)
{
UHierarchyElement* NewItem = Cast<UHierarchyElement>(StaticDuplicateObject(ItemToDuplicate->GetData(), GetDataMutable()));
if(InsertIndex == INDEX_NONE)
{
GetDataMutable()->GetChildrenMutable().Add(NewItem);
}
else
{
GetDataMutable()->GetChildrenMutable().Insert(NewItem, InsertIndex);
}
SyncViewModelsToData();
HierarchyViewModel->OnHierarchyChanged().Broadcast();
TSharedPtr<FHierarchyElementViewModel> ViewModel = FindViewModelForChild(NewItem);
return ViewModel.ToSharedRef();
}
TSharedRef<FHierarchyElementViewModel> FHierarchyElementViewModel::ReparentToThis(TSharedPtr<FHierarchyElementViewModel> ItemToMove, int32 InsertIndex)
{
UHierarchyElement* NewItem = Cast<UHierarchyElement>(StaticDuplicateObject(ItemToMove->GetData(), GetDataMutable()));
if(InsertIndex == INDEX_NONE)
{
GetDataMutable()->GetChildrenMutable().Add(NewItem);
}
else
{
GetDataMutable()->GetChildrenMutable().Insert(NewItem, InsertIndex);
}
ItemToMove->Delete();
SyncViewModelsToData();
HierarchyViewModel->OnHierarchyChanged().Broadcast();
TSharedPtr<FHierarchyElementViewModel> ViewModel = FindViewModelForChild(NewItem);
return ViewModel.ToSharedRef();
}
TSharedPtr<FHierarchyElementViewModel> FHierarchyElementViewModel::FindViewModelForChild(UHierarchyElement* Child, bool bSearchRecursively) const
{
int32 Index = FindIndexOfChild(Child);
if(Index != INDEX_NONE)
{
return Children[Index];
}
if(bSearchRecursively)
{
for(TSharedPtr<FHierarchyElementViewModel> ChildViewModel : Children)
{
TSharedPtr<FHierarchyElementViewModel> FoundViewModel = ChildViewModel->FindViewModelForChild(Child, bSearchRecursively);
if(FoundViewModel.IsValid())
{
return FoundViewModel;
}
}
}
return nullptr;
}
TSharedPtr<FHierarchyElementViewModel> FHierarchyElementViewModel::FindViewModelForChild(FHierarchyElementIdentity ChildIdentity, bool bSearchRecursively) const
{
for(TSharedPtr<FHierarchyElementViewModel> Child : Children)
{
if(Child->GetData()->GetPersistentIdentity() == ChildIdentity)
{
return Child;
}
}
if(bSearchRecursively)
{
for(TSharedPtr<FHierarchyElementViewModel> ChildViewModel : Children)
{
TSharedPtr<FHierarchyElementViewModel> FoundViewModel = ChildViewModel->FindViewModelForChild(ChildIdentity, bSearchRecursively);
if(FoundViewModel.IsValid())
{
return FoundViewModel;
}
}
}
return nullptr;
}
int32 FHierarchyElementViewModel::FindIndexOfChild(UHierarchyElement* Child) const
{
return Children.FindLastByPredicate([Child](TSharedPtr<FHierarchyElementViewModel> Item)
{
return Item->GetData() == Child;
});
}
int32 FHierarchyElementViewModel::FindIndexOfDataChild(TSharedPtr<FHierarchyElementViewModel> Child) const
{
return GetData()->GetChildren().Find(Child->GetDataMutable());
}
int32 FHierarchyElementViewModel::FindIndexOfDataChild(UHierarchyElement* Child) const
{
return GetData()->GetChildren().Find(Child);
}
void FHierarchyElementViewModel::Delete()
{
OnChildRequestedDeletionDelegate.Execute(AsShared());
}
void FHierarchyElementViewModel::DeleteChild(TSharedPtr<FHierarchyElementViewModel> Child)
{
ensure(Child->GetParent().Pin() == AsShared());
GetDataMutable()->Modify();
GetDataMutable()->GetChildrenMutable().Remove(Child->GetDataMutable());
Children.Remove(Child);
}
TOptional<EItemDropZone> FHierarchyElementViewModel::OnCanRowAcceptDrop(const FDragDropEvent& DragDropEvent, EItemDropZone ItemDropZone, TSharedPtr<FHierarchyElementViewModel> Item)
{
if(TSharedPtr<FHierarchyDragDropOp> DragDropOp = DragDropEvent.GetOperationAs<FHierarchyDragDropOp>())
{
FCanPerformActionResults Results = CanDropOn(DragDropOp->GetDraggedElement().Pin(), ItemDropZone);
DragDropOp->SetDescription(Results.CanPerformMessage);
return Results.bCanPerform ? ItemDropZone : TOptional<EItemDropZone>();
}
return TOptional<EItemDropZone>();
}
FReply FHierarchyElementViewModel::OnDroppedOnRow(const FDragDropEvent& DragDropEvent, EItemDropZone ItemDropZone, TSharedPtr<FHierarchyElementViewModel> Item)
{
if(TSharedPtr<FHierarchyDragDropOp> HierarchyDragDropOp = DragDropEvent.GetOperationAs<FHierarchyDragDropOp>())
{
OnDroppedOn(HierarchyDragDropOp->GetDraggedElement().Pin(), ItemDropZone);
return FReply::Handled();
}
return FReply::Unhandled();
}
void FHierarchyElementViewModel::OnRowDragLeave(const FDragDropEvent& DragDropEvent)
{
if(TSharedPtr<FHierarchyDragDropOp> HierarchyDragDropOp = DragDropEvent.GetOperationAs<FHierarchyDragDropOp>())
{
HierarchyDragDropOp->SetDescription(FText::GetEmpty());
}
}
FHierarchyElementViewModel::FCanPerformActionResults FHierarchyElementViewModel::CanDrag()
{
FCanPerformActionResults Results = IsEditableByUser();
if(Results.bCanPerform == false)
{
return Results;
}
return CanDragInternal();
}
FHierarchyElementViewModel::FCanPerformActionResults FHierarchyElementViewModel::CanDropOnInternal(TSharedPtr<FHierarchyElementViewModel>, EItemDropZone ItemDropZone)
{
return false;
}
void FHierarchyElementViewModel::PropagateOnChildSynced()
{
OnSyncedDelegate.ExecuteIfBound();
}
FReply FHierarchyElementViewModel::OnDragDetected(const FGeometry& Geometry, const FPointerEvent& PointerEvent, bool bIsSource)
{
FCanPerformActionResults CanDragResults = CanDrag();
if(CanDragResults == true)
{
// if the drag is coming from source, we check if any of the hierarchy data already contains that element and we don't start a drag drop in that case
if(bIsSource)
{
TArray<TSharedPtr<FHierarchyElementViewModel>> AllChildren;
GetChildrenViewModelsForType<UHierarchyElement, FHierarchyElementViewModel>(AllChildren, true);
bool bCanDrag = GetHierarchyViewModel()->GetHierarchyRootViewModel()->FindViewModelForChild(GetData()->GetPersistentIdentity(), true) == nullptr;
if(bCanDrag)
{
for(TSharedPtr<FHierarchyElementViewModel>& ChildViewModel : AllChildren)
{
if(GetHierarchyViewModel()->GetHierarchyRootViewModel()->FindViewModelForChild(ChildViewModel->GetData()->GetPersistentIdentity(), true) != nullptr)
{
bCanDrag = false;
break;
}
}
}
if(bCanDrag == false)
{
return FReply::Unhandled();
}
}
TSharedRef<FHierarchyDragDropOp> HierarchyDragDropOp = HierarchyViewModel->CreateDragDropOp(AsShared());
HierarchyDragDropOp->SetFromSourceList(bIsSource);
return FReply::Handled().BeginDragDrop(HierarchyDragDropOp);
}
else
{
// if we can't drag and have a message, we show it as a slate notification
if(CanDragResults.CanPerformMessage.IsEmpty() == false)
{
FNotificationInfo CantDragInfo(CanDragResults.CanPerformMessage);
FSlateNotificationManager::Get().AddNotification(CantDragInfo);
}
}
return FReply::Unhandled();
}
FHierarchyRootViewModel::~FHierarchyRootViewModel()
{
}
void FHierarchyRootViewModel::Initialize()
{
GetOnSynced().BindSP(this, &FHierarchyRootViewModel::PropagateOnSynced);
}
FHierarchyElementViewModel::FCanPerformActionResults FHierarchyRootViewModel::CanDropOnInternal(TSharedPtr<FHierarchyElementViewModel> DraggedElement, EItemDropZone ItemDropZone)
{
FCanPerformActionResults Results(false);
// we only allow drops if some general conditions are fulfilled
if(DraggedElement->GetData() != GetData() &&
(!DraggedElement->HasParent(AsShared(), false) || ItemDropZone != EItemDropZone::OntoItem) &&
!HasParent(DraggedElement, true))
{
Results.bCanPerform =
// items can be dropped onto the root directly if the section is set to "All"
(DraggedElement->GetData()->IsA<UHierarchyItem>() && HierarchyViewModel->GetActiveHierarchySectionData() == nullptr)
||
// categories can be dropped onto the root always
(DraggedElement->GetData()->IsA<UHierarchyCategory>());
if(Results.bCanPerform)
{
if(DraggedElement->IsForHierarchy() == false)
{
FText Message = LOCTEXT("CanDropSourceItemOnRootDragMessage", "Add {0} to the hierarchy root.");
Message = FText::FormatOrdered(Message, FText::FromString(DraggedElement->ToString()));
Results.CanPerformMessage = Message;
}
else
{
FText Message = LOCTEXT("CanDropHierarchyItemOnRootDragMessage", "Move {0} to the hierarchy root.");
Message = FText::FormatOrdered(Message, FText::FromString(DraggedElement->ToString()));
Results.CanPerformMessage = Message;
}
}
else
{
FText Message = LOCTEXT("CantDropHierarchyItemOnRootDragMessage", "Can not add {0} here. Please add it to a category!");
Message = FText::FormatOrdered(Message, FText::FromString(DraggedElement->ToString()));
Results.CanPerformMessage = Message;
}
}
return Results;
}
void FHierarchyRootViewModel::OnDroppedOnInternal(TSharedPtr<FHierarchyElementViewModel> DroppedItem, EItemDropZone ItemDropZone)
{
FScopedTransaction Transaction(LOCTEXT("Transaction_OnDropOnRoot", "Dropped item on root"));
HierarchyViewModel->GetHierarchyRoot()->Modify();
if(DroppedItem->GetDataMutable()->IsA<UHierarchyItem>() || DroppedItem->GetDataMutable()->IsA<UHierarchyCategory>())
{
TSharedPtr<FHierarchyElementViewModel> NewViewModel;
// we duplicate the item if the dragged item is from source
if(DroppedItem->IsForHierarchy() == false)
{
NewViewModel = DuplicateToThis(DroppedItem);
}
else
{
NewViewModel = ReparentToThis(DroppedItem);
}
if(UHierarchyCategory* AsCategory = Cast<UHierarchyCategory>(NewViewModel->GetDataMutable()))
{
AsCategory->SetSection(HierarchyViewModel->GetActiveHierarchySectionData());
}
HierarchyViewModel->RefreshHierarchyView();
}
}
TSharedPtr<FHierarchySectionViewModel> FHierarchyRootViewModel::AddSection()
{
FScopedTransaction ScopedTransaction(LOCTEXT("NewSectionAdded","Added Section"));
HierarchyViewModel->GetHierarchyRoot()->Modify();
UHierarchySection* SectionData = GetDataMutable<UHierarchyRoot>()->AddSection(LOCTEXT("HierarchyEditorDefaultNewSectionName", "Section"), 0, HierarchyViewModel->GetSectionDataClass());
SectionData->Modify();
TSharedPtr<FHierarchyElementViewModel> ViewModel = HierarchyViewModel->CreateViewModelForElement(SectionData, StaticCastSharedRef<FHierarchyRootViewModel>(AsShared()));
TSharedPtr<FHierarchySectionViewModel> SectionViewModel = StaticCastSharedPtr<FHierarchySectionViewModel>(ViewModel);
if(!ensureMsgf(SectionViewModel.IsValid(), TEXT("Make sure that CreateViewModelForData creates a FHierarchySectionViewModel (or derived) for UHierarchySection elements")))
{
return nullptr;
}
SectionViewModels.Add(SectionViewModel);
SyncViewModelsToData();
HierarchyViewModel->SetActiveHierarchySection(SectionViewModel);
OnSectionAddedDelegate.ExecuteIfBound(SectionViewModel);
OnSectionsChangedDelegate.ExecuteIfBound();
return SectionViewModel;
}
void FHierarchyRootViewModel::DeleteSection(TSharedPtr<FHierarchyElementViewModel> InSectionViewModel)
{
TSharedPtr<FHierarchySectionViewModel> SectionViewModel = StaticCastSharedPtr<FHierarchySectionViewModel>(InSectionViewModel);
GetDataMutable<UHierarchyRoot>()->GetSectionDataMutable().Remove(SectionViewModel->GetDataMutable<UHierarchySection>());
SectionViewModels.Remove(SectionViewModel);
OnSectionDeletedDelegate.ExecuteIfBound(SectionViewModel);
OnSectionsChangedDelegate.ExecuteIfBound();
}
void FHierarchyRootViewModel::PropagateOnSynced()
{
OnSyncPropagatedDelegate.ExecuteIfBound();
}
void FHierarchyRootViewModel::SyncViewModelsToDataInternal()
{
const UHierarchyRoot* RootData = GetData<UHierarchyRoot>();
TArray<TSharedPtr<FHierarchySectionViewModel>> NewSectionViewModels;
TArray<TSharedPtr<FHierarchySectionViewModel>> SectionViewModelsToDelete;
for(TSharedPtr<FHierarchySectionViewModel> SectionViewModel : SectionViewModels)
{
if(!RootData->GetSectionData().Contains(SectionViewModel->GetData()))
{
SectionViewModelsToDelete.Add(SectionViewModel);
}
}
for (TSharedPtr<FHierarchySectionViewModel> SectionViewModel : SectionViewModelsToDelete)
{
SectionViewModel->Delete();
}
for(UHierarchySection* Section : RootData->GetSectionData())
{
TSharedPtr<FHierarchySectionViewModel>* SectionViewModelPtr = SectionViewModels.FindByPredicate([Section](TSharedPtr<FHierarchySectionViewModel> SectionViewModel)
{
return SectionViewModel->GetData() == Section;
});
TSharedPtr<FHierarchySectionViewModel> SectionViewModel = nullptr;
if(SectionViewModelPtr)
{
SectionViewModel = *SectionViewModelPtr;
}
if(SectionViewModel == nullptr)
{
SectionViewModel = MakeShared<FHierarchySectionViewModel>(Section, StaticCastSharedRef<FHierarchyRootViewModel>(AsShared()), HierarchyViewModel);
SectionViewModel->SyncViewModelsToData();;
}
NewSectionViewModels.Add(SectionViewModel);
}
SectionViewModels.Empty();
SectionViewModels.Append(NewSectionViewModels);
for(TSharedPtr<FHierarchySectionViewModel> SectionViewModel : SectionViewModels)
{
SectionViewModel->OnChildRequestedDeletion().BindSP(this, &FHierarchyRootViewModel::DeleteSection);
}
SectionViewModels.Sort([this](const TSharedPtr<FHierarchySectionViewModel>& ItemA, const TSharedPtr<FHierarchySectionViewModel>& ItemB)
{
return
GetDataMutable<UHierarchyRoot>()->GetSectionData().Find(Cast<UHierarchySection>(ItemA->GetDataMutable()))
<
GetDataMutable<UHierarchyRoot>()->GetSectionData().Find(Cast<UHierarchySection>(ItemB->GetDataMutable()));
});
}
FString FHierarchySectionViewModel::ToString() const
{
return GetSectionNameAsText().ToString();
}
void FHierarchySectionViewModel::SetSectionName(FName InSectionName)
{
Cast<UHierarchySection>(Element)->SetSectionName(InSectionName);
}
FName FHierarchySectionViewModel::GetSectionName() const
{
if(UHierarchySection* Section = Cast<UHierarchySection>(Element))
{
return Section->GetSectionName();
}
return NAME_None;
}
void FHierarchySectionViewModel::SetSectionNameAsText(const FText& Text)
{
Cast<UHierarchySection>(Element)->SetSectionNameAsText(Text);
}
FText FHierarchySectionViewModel::GetSectionNameAsText() const
{
if(UHierarchySection* Section = Cast<UHierarchySection>(Element))
{
return Section->GetSectionNameAsText();
}
return LOCTEXT("DefaultSectionName", "All");
}
FText FHierarchySectionViewModel::GetSectionTooltip() const
{
if(UHierarchySection* Section = Cast<UHierarchySection>(Element))
{
return Section->GetTooltip();
}
return FText::GetEmpty();
}
FHierarchyElementViewModel::FCanPerformActionResults FHierarchySectionViewModel::CanDragInternal()
{
// We only allow hierarchy sections to be dragged, excluding the All section that has no valid data
return IsForHierarchy() && GetData() != nullptr;
}
bool FHierarchySectionViewModel::CanRenameInternal()
{
return IsForHierarchy() && GetData() != nullptr;
}
bool FHierarchySectionViewModel::CanDeleteInternal()
{
return IsForHierarchy() && GetData() != nullptr;
}
FHierarchyElementViewModel::FCanPerformActionResults FHierarchySectionViewModel::CanDropOnInternal(TSharedPtr<FHierarchyElementViewModel> DraggedElement, EItemDropZone ItemDropZone)
{
if(bDropDisallowed)
{
return false;
}
FCanPerformActionResults Results(false);
// we don't allow dropping onto source sections and we don't specify a message as the sections aren't going to light up as valid drop targets
if(IsForHierarchy() == false)
{
return false;
}
if(const UHierarchyCategory* Category = Cast<UHierarchyCategory>(DraggedElement->GetData()))
{
if(ItemDropZone == EItemDropZone::OntoItem)
{
FText Message = LOCTEXT("DropCategoryOnSectionDragMessage", "Add {0} to section {1}");
Message = FText::FormatOrdered(Message, FText::FromString(DraggedElement->ToString()), FText::FromString(ToString()));
Results.bCanPerform = GetData() != Category->GetSection();
Results.CanPerformMessage = Results.bCanPerform ? Message : FText::GetEmpty();
}
}
else if(UHierarchySection* DraggedSection = Cast<UHierarchySection>(DraggedElement->GetDataMutable()))
{
const bool bSameSection = GetData() == DraggedSection;
// If we drag a section onto a section, nothing happens
if(ItemDropZone == EItemDropZone::OntoItem)
{
Results.bCanPerform = false;
return Results;
}
// The 'All' section does not accept any drop actions.
if(GetData() == nullptr)
{
Results.bCanPerform = false;
return Results;
}
int32 DraggedSectionIndex = GetHierarchyViewModel()->GetHierarchyRoot()->GetSectionIndex(DraggedSection);
int32 InsertionIndex = GetHierarchyViewModel()->GetHierarchyRoot()->GetSectionIndex(GetDataMutable<UHierarchySection>());
// we add 1 to the insertion index if it's below an item because we either want to insert at the current index to place the item above, or at current+1 for below
InsertionIndex += ItemDropZone == EItemDropZone::AboveItem ? -1 : 1;
Results.bCanPerform = !bSameSection && DraggedSectionIndex != InsertionIndex;
if(Results.bCanPerform)
{
if(ItemDropZone != EItemDropZone::OntoItem)
{
FText Message = LOCTEXT("MoveSectionLeftDragMessage", "Move section here");
Message = FText::FormatOrdered(Message, FText::FromString(DraggedElement->ToString()));
Results.CanPerformMessage = Message;
}
}
}
else if(UHierarchyItem* Item = Cast<UHierarchyItem>(DraggedElement->GetDataMutable()))
{
FText Message = LOCTEXT("CantDropItemOnSectionDragMessage", "Can't drop items onto sections. Please drag a category onto section {0}");
Message = FText::FormatOrdered(Message, FText::FromString(ToString()));
Results.bCanPerform = false;
Results.CanPerformMessage = Message;
}
return Results;
}
void FHierarchySectionViewModel::OnDroppedOnInternal(TSharedPtr<FHierarchyElementViewModel> DroppedItem, EItemDropZone ItemDropZone)
{
if(DroppedItem->GetData()->IsA<UHierarchySection>())
{
FScopedTransaction Transaction(LOCTEXT("Transaction_OnSectionMoved", "Moved section"));
HierarchyViewModel->GetHierarchyRoot()->Modify();
UHierarchySection* DraggedSectionData = DroppedItem->GetDataMutable<UHierarchySection>();
int32 IndexOfThis = HierarchyViewModel->GetHierarchyRoot()->GetSectionData().Find(GetDataMutable<UHierarchySection>());
int32 DraggedSectionIndex = HierarchyViewModel->GetHierarchyRoot()->GetSectionData().Find(DraggedSectionData);
TArray<TObjectPtr<UHierarchySection>>& SectionData = HierarchyViewModel->GetHierarchyRoot()->GetSectionDataMutable();
int32 Count = SectionData.Num();
bool bDropSucceeded = false;
// above constitutes to the left here
if(ItemDropZone == EItemDropZone::AboveItem)
{
SectionData.RemoveAt(DraggedSectionIndex);
SectionData.Insert(DraggedSectionData, FMath::Max(IndexOfThis, 0));
bDropSucceeded = true;
}
else if(ItemDropZone == EItemDropZone::BelowItem)
{
SectionData.RemoveAt(DraggedSectionIndex);
if(IndexOfThis + 1 > SectionData.Num())
{
SectionData.Add(DraggedSectionData);
}
else
{
SectionData.Insert(DraggedSectionData, FMath::Min(IndexOfThis+1, Count));
}
bDropSucceeded = true;
}
if(bDropSucceeded)
{
HierarchyViewModel->ForceFullRefresh();
HierarchyViewModel->OnHierarchyChanged().Broadcast();
}
}
else if(UHierarchyCategory* HierarchyCategory = DroppedItem->GetDataMutable<UHierarchyCategory>())
{
FScopedTransaction Transaction(LOCTEXT("Transaction_OnSectionDrop", "Moved category to section"));
HierarchyViewModel->GetHierarchyRoot()->Modify();
HierarchyCategory->SetSection(GetDataMutable<UHierarchySection>());
// we null out any sections for all contained categories
TArray<UHierarchyCategory*> AllChildCategories;
HierarchyCategory->GetChildrenOfType<UHierarchyCategory>(AllChildCategories, true);
for(UHierarchyCategory* ChildCategory : AllChildCategories)
{
ChildCategory->SetSection(nullptr);
}
// we only need to reparent if the parent isn't already the root. This stops unnecessary reordering
if(DroppedItem->GetParent() != HierarchyViewModel->GetHierarchyRootViewModel())
{
HierarchyViewModel->GetHierarchyRootViewModel()->ReparentToThis(DroppedItem);
}
HierarchyViewModel->RefreshHierarchyView();
HierarchyViewModel->OnHierarchyChanged().Broadcast();
}
}
void FHierarchySectionViewModel::FinalizeInternal()
{
if(HierarchyViewModel->GetActiveHierarchySection() == AsShared())
{
HierarchyViewModel->SetActiveHierarchySection(HierarchyViewModel->GetDefaultHierarchySectionViewModel());
}
// we make sure to reset all categories' section entry that were referencing this section
TArray<UHierarchyCategory*> AllCategories;
HierarchyViewModel->GetHierarchyRoot()->GetChildrenOfType<UHierarchyCategory>(AllCategories, true);
for(UHierarchyCategory* Category : AllCategories)
{
if(Category->GetSection() == GetData())
{
Category->SetSection(nullptr);
}
}
}
::FHierarchyElementViewModel::FCanPerformActionResults FHierarchyItemViewModel::CanDropOnInternal(TSharedPtr<FHierarchyElementViewModel> DraggedElement, EItemDropZone ItemDropZone)
{
bool bAllowDrop = false;
TSharedPtr<FHierarchyElementViewModel> SourceDropItem = DraggedElement;
TSharedPtr<FHierarchyElementViewModel> TargetDropItem = AsShared();
// we only allow drops if some general conditions are fulfilled
if(SourceDropItem->GetData() != TargetDropItem->GetData() &&
(!SourceDropItem->HasParent(TargetDropItem, false) || ItemDropZone != EItemDropZone::OntoItem) &&
!TargetDropItem->HasParent(SourceDropItem, true))
{
// items can be generally be dropped above/below other items
bAllowDrop = (SourceDropItem->GetData()->IsA<UHierarchyItem>() && ItemDropZone != EItemDropZone::OntoItem);
}
return bAllowDrop;
}
void FHierarchyItemViewModel::OnDroppedOnInternal(TSharedPtr<FHierarchyElementViewModel> DroppedItem, EItemDropZone ItemDropZone)
{
FScopedTransaction Transaction(LOCTEXT("Transaction_MovedItem", "Moved an item in the hierarchy"));
HierarchyViewModel->GetHierarchyRoot()->Modify();
bool bDropSucceeded = false;
if(ItemDropZone == EItemDropZone::AboveItem)
{
int32 IndexOfThis = Parent.Pin()->FindIndexOfDataChild(AsShared());
if(DroppedItem->IsForHierarchy() == false)
{
Parent.Pin()->DuplicateToThis(DroppedItem, FMath::Max(IndexOfThis, 0));
}
else
{
Parent.Pin()->ReparentToThis(DroppedItem, FMath::Max(IndexOfThis, 0));
}
bDropSucceeded = true;
}
else if(ItemDropZone == EItemDropZone::BelowItem)
{
int32 IndexOfThis = Parent.Pin()->FindIndexOfDataChild(AsShared());
if(DroppedItem->IsForHierarchy() == false)
{
Parent.Pin()->DuplicateToThis(DroppedItem, FMath::Min(IndexOfThis+1, Parent.Pin()->GetChildren().Num()));
}
else
{
Parent.Pin()->ReparentToThis(DroppedItem, FMath::Min(IndexOfThis+1, Parent.Pin()->GetChildren().Num()));
}
bDropSucceeded = true;
}
if(bDropSucceeded)
{
HierarchyViewModel->RefreshHierarchyView();
HierarchyViewModel->RefreshSourceView();
}
else
{
Transaction.Cancel();
}
}
void FHierarchyCategoryViewModel::OnDroppedOnInternal(TSharedPtr<FHierarchyElementViewModel> DroppedItem, EItemDropZone ItemDropZone)
{
FScopedTransaction Transaction(LOCTEXT("Transaction_OnCategoryDrop", "Dropped item on/above/below category"));
HierarchyViewModel->GetHierarchyRoot()->Modify();
if(UHierarchyCategory* Category = DroppedItem->GetDataMutable<UHierarchyCategory>())
{
// if we are dragging a category above/below another category and the new parent is going to be the root, we update its section to the active section
if(ItemDropZone != EItemDropZone::OntoItem)
{
if(Parent.IsValid() && Parent == HierarchyViewModel->GetHierarchyRootViewModel())
{
Category->SetSection(HierarchyViewModel->GetActiveHierarchySectionData());
// we null out any sections for all contained categories
TArray<UHierarchyCategory*> AllChildCategories;
Category->GetChildrenOfType<UHierarchyCategory>(AllChildCategories, true);
for(UHierarchyCategory* ChildCategory : AllChildCategories)
{
ChildCategory->SetSection(nullptr);
}
}
}
// if we are dragging a category onto another category, we null out its section instead
else
{
Category->SetSection(nullptr);
// we null out any sections for all contained categories
TArray<UHierarchyCategory*> AllChildCategories;
Category->GetChildrenOfType<UHierarchyCategory>(AllChildCategories, true);
for(UHierarchyCategory* ChildCategory : AllChildCategories)
{
ChildCategory->SetSection(nullptr);
}
}
}
// the actual moving of the item happens here
if(ItemDropZone == EItemDropZone::OntoItem)
{
if(DroppedItem->IsForHierarchy() == false)
{
DuplicateToThis(DroppedItem);
}
else
{
ReparentToThis(DroppedItem);
}
}
else if(ItemDropZone == EItemDropZone::AboveItem)
{
int32 IndexOfThis = Parent.Pin()->FindIndexOfDataChild(AsShared());
if(DroppedItem->IsForHierarchy() == false)
{
Parent.Pin()->DuplicateToThis(DroppedItem, FMath::Max(IndexOfThis, 0));
}
else
{
Parent.Pin()->ReparentToThis(DroppedItem, FMath::Max(IndexOfThis, 0));
}
}
else if(ItemDropZone == EItemDropZone::BelowItem)
{
int32 IndexOfThis = Parent.Pin()->FindIndexOfDataChild(AsShared());
if(DroppedItem->IsForHierarchy() == false)
{
Parent.Pin()->DuplicateToThis(DroppedItem, FMath::Min(IndexOfThis+1, Parent.Pin()->GetChildren().Num()));
}
else
{
Parent.Pin()->ReparentToThis(DroppedItem, FMath::Min(IndexOfThis+1, Parent.Pin()->GetChildren().Num()));
}
}
}
void UDataHierarchyViewModelBase::AddCategory(TSharedPtr<FHierarchyElementViewModel> CategoryParent) const
{
// If no category parent was specified, we add it to the root
if(CategoryParent == nullptr)
{
CategoryParent = GetHierarchyRootViewModel();
}
int32 HierarchyDepth = CategoryParent->GetHierarchyDepth();
if(HierarchyDepth > 15)
{
FNotificationInfo Info(LOCTEXT("TooManyNestedCategoriesToastText", "We currently only allow a hierarchy depth of 15."));
Info.ExpireDuration = 4.f;
FSlateNotificationManager::Get().AddNotification(Info);
return;
}
FText TransactionText = FText::FormatOrdered(LOCTEXT("Transaction_AddedItem", "Added new {0} to hierarchy"), FText::FromString(GetCategoryDataClass()->GetName()));
FScopedTransaction Transaction(TransactionText);
GetHierarchyRoot()->Modify();
UClass* CategoryClass = GetCategoryDataClass();
UHierarchyCategory* Category = Cast<UHierarchyCategory>(CategoryParent->AddChild(CategoryClass, UHierarchyCategory::ConstructIdentity()));
TSharedPtr<FHierarchyElementViewModel> ViewModel = CategoryParent->FindViewModelForChild(Category, false);
if(ensureMsgf(ViewModel.IsValid(), TEXT("Could not find view model for new category of type '%s'. Please ensure your 'CreateViewModelForData' function creates a view model."), *CategoryClass->GetName()))
{
TArray<UHierarchyCategory*> SiblingCategories;
Category->GetTypedOuter<UHierarchyElement>()->GetChildrenOfType<UHierarchyCategory>(SiblingCategories);
TSet<FName> CategoryNames;
for(const auto& SiblingCategory : SiblingCategories)
{
CategoryNames.Add(SiblingCategory->GetCategoryName());
}
Category->SetCategoryName(UE::DataHierarchyEditor::GetUniqueName(FName("New Category"), CategoryNames));
// we only set the section property if the current section isn't set to "All"
Category->SetSection(GetActiveHierarchySectionData());
RefreshHierarchyView();
OnElementAddedDelegate.ExecuteIfBound(ViewModel);
}
}
void UDataHierarchyViewModelBase::AddSection() const
{
TSharedPtr<FHierarchySectionViewModel> SectionViewModel = GetHierarchyRootViewModel()->AddSection();
OnElementAddedDelegate.ExecuteIfBound(SectionViewModel);
OnHierarchyChangedDelegate.Broadcast();
}
void UDataHierarchyViewModelBase::GenerateDynamicContextMenu(UToolMenu* ToolMenu)
{
UHierarchyMenuContext* HierarchyMenuContext = ToolMenu->FindContext<UHierarchyMenuContext>();
if(HierarchyMenuContext == nullptr || HierarchyMenuContext->HierarchyViewModel.IsValid() == false)
{
return;
}
UDataHierarchyViewModelBase* HierarchyViewModel = HierarchyMenuContext->HierarchyViewModel.Get();
HierarchyViewModel->GenerateDynamicContextMenuInternal(ToolMenu);
if(HierarchyMenuContext->MenuHierarchyElements.Num() == 1)
{
HierarchyMenuContext->MenuHierarchyElements[0]->AppendDynamicContextMenuForSingleElement(ToolMenu);
}
}
void UDataHierarchyViewModelBase::GenerateDynamicContextMenuInternal(UToolMenu* DynamicToolMenu) const
{
UHierarchyMenuContext* HierarchyMenuContext = DynamicToolMenu->FindContext<UHierarchyMenuContext>();
if(HierarchyMenuContext == nullptr || HierarchyMenuContext->HierarchyViewModel.IsValid() == false)
{
return;
}
UDataHierarchyViewModelBase* HierarchyViewModel = HierarchyMenuContext->HierarchyViewModel.Get();
DynamicToolMenu->AddMenuEntry("Dynamic", FToolMenuEntry::InitMenuEntryWithCommandList(FDataHierarchyEditorCommands::Get().FindInHierarchy, HierarchyViewModel->GetCommands(), TAttribute<FText>(), TAttribute<FText>(), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Find")));
DynamicToolMenu->AddMenuEntry("Dynamic", FToolMenuEntry::InitMenuEntryWithCommandList(FGenericCommands::Get().Rename, HierarchyViewModel->GetCommands()));
DynamicToolMenu->AddMenuEntry("Dynamic", FToolMenuEntry::InitMenuEntryWithCommandList(FGenericCommands::Get().Delete, HierarchyViewModel->GetCommands()));
}
UHierarchyElement* UDataHierarchyViewModelBase::AddElementUnderRoot(TSubclassOf<UHierarchyElement> NewChildClass, FHierarchyElementIdentity ChildIdentity)
{
FScopedTransaction Transaction(LOCTEXT("Transaction_AddItem", "Add hierarchy item"));
HierarchyRoot->Modify();
return GetHierarchyRootViewModel()->AddChild(NewChildClass, ChildIdentity);
}
void UDataHierarchyViewModelBase::DeleteElementWithIdentity(FHierarchyElementIdentity Identity)
{
if(Identity.IsValid() == false)
{
return;
}
FScopedTransaction Transaction(LOCTEXT("Transaction_DeleteItem", "Deleted hierarchy item"));
HierarchyRoot->Modify();
bool bItemDeleted = false;
if(TSharedPtr<FHierarchyElementViewModel> ViewModel = HierarchyRootViewModel->FindViewModelForChild(Identity, true))
{
if(ViewModel->CanDelete())
{
ViewModel->Delete();
bItemDeleted = true;
}
}
TArray<TSharedPtr<FHierarchySectionViewModel>> SectionViewModels = HierarchyRootViewModel->GetSectionViewModels();
for(TSharedPtr<FHierarchySectionViewModel> SectionViewModel : SectionViewModels)
{
if(SectionViewModel->GetData()->GetPersistentIdentity() == Identity && SectionViewModel->CanDelete())
{
SectionViewModel->Delete();
bItemDeleted = true;
}
}
if(bItemDeleted)
{
HierarchyRootViewModel->SyncViewModelsToData();
OnHierarchyChangedDelegate.Broadcast();
}
else
{
Transaction.Cancel();
}
}
void UDataHierarchyViewModelBase::DeleteElements(TArray<TSharedPtr<FHierarchyElementViewModel>> ViewModels) const
{
FScopedTransaction Transaction(LOCTEXT("Transaction_DeleteHierarchyElements", "Deleted hierarchy elements"));
HierarchyRoot->Modify();
bool bAnyItemsDeleted = false;
for(TSharedPtr<FHierarchyElementViewModel> ViewModel : ViewModels)
{
if(ViewModel->CanDelete())
{
ViewModel->Delete();
bAnyItemsDeleted = true;
}
}
if(bAnyItemsDeleted)
{
HierarchyRootViewModel->SyncViewModelsToData();
OnHierarchyChangedDelegate.Broadcast();
}
else
{
Transaction.Cancel();
}
}
void UDataHierarchyViewModelBase::NavigateToElementInHierarchy(const FHierarchyElementIdentity& HierarchyIdentity) const
{
OnNavigateToElementIdentityInHierarchyRequestedDelegate.ExecuteIfBound(HierarchyIdentity);
}
void UDataHierarchyViewModelBase::NavigateToElementInHierarchy(const TSharedRef<FHierarchyElementViewModel> HierarchyElement) const
{
OnNavigateToElementInHierarchyRequestedDelegate.ExecuteIfBound(HierarchyElement);
}
FHierarchyDragDropOp::FHierarchyDragDropOp(TSharedPtr<FHierarchyElementViewModel> InDraggedElementViewModel) : DraggedElement(InDraggedElementViewModel)
{
SetLabel(DraggedElement.Pin()->ToStringAsText());
}
TSharedPtr<SWidget> FHierarchyDragDropOp::GetDefaultDecorator() const
{
TSharedRef<SWidget> CustomDecorator = CreateCustomDecorator();
SVerticalBox::FSlot* CustomSlot;
TSharedPtr<SWidget> Decorator = SNew(SToolTip)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.Expose(CustomSlot).
AutoHeight()
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(2.f)
[
SNew(STextBlock)
.Text(this, &FHierarchyDragDropOp::GetLabel)
.TextStyle(&FAppStyle::GetWidgetStyle<FTextBlockStyle>("NormalText.Important"))
.Visibility_Lambda([this, CustomDecorator]()
{
return GetLabel().IsEmpty() || CustomDecorator != SNullWidget::NullWidget ? EVisibility::Collapsed : EVisibility::Visible;
})
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(2.f)
[
SNew(STextBlock)
.Text(this, &FHierarchyDragDropOp::GetDescription)
.TextStyle(&FAppStyle::GetWidgetStyle<FTextBlockStyle>("NormalText"))
.Visibility_Lambda([this]()
{
return GetDescription().IsEmpty() ? EVisibility::Collapsed : EVisibility::Visible;
})
]
];
if(CustomDecorator != SNullWidget::NullWidget)
{
CustomSlot->AttachWidget(CustomDecorator);
}
return Decorator;
}
TSharedRef<SWidget> FSectionDragDropOp::CreateCustomDecorator() const
{
return SNew(SCheckBox)
.Visibility(EVisibility::HitTestInvisible)
.Style(FAppStyle::Get(), "DetailsView.SectionButton")
.IsChecked(ECheckBoxState::Unchecked)
[
SNew(SInlineEditableTextBlock)
.Text(GetDraggedSection().Pin()->GetSectionNameAsText())
];
}
#undef LOCTEXT_NAMESPACE