// Copyright Epic Games, Inc. All Rights Reserved. #include "InstanceDataObjectFixupPanel.h" #include "AsyncDetailViewDiff.h" #include "DetailTreeNode.h" #include "Widgets/Layout/LinkableScrollBar.h" #include "InstanceDataObjectFixupDetailCustomization.h" #include "Modules/ModuleManager.h" #include "Editor.h" #include "Editor/PropertyEditor/Private/PropertyNode.h" #include "TedsAlerts.h" #include "UObject/PropertyBagRepository.h" #include "UObject/PropertyStateTracking.h" #include "Elements/Common/EditorDataStorageFeatures.h" #include "Elements/Interfaces/TypedElementDataStorageInterface.h" #include "Elements/Interfaces/TypedElementDataStorageCompatibilityInterface.h" #include "Serialization/ObjectReader.h" #include "Serialization/ObjectWriter.h" #include "UObject/OverriddenPropertySet.h" #include "UObject/OverridableManager.h" #include "UObject/TextProperty.h" #include "UObject/UObjectThreadContext.h" #define LOCTEXT_NAMESPACE "InstanceDataObjectFixupPanel" static const FName NAME_IsLooseMetadata(TEXT("IsLoose")); static const FName NAME_ContainsLoosePropertiesMetadata(ANSITEXTVIEW("ContainsLooseProperties")); FRedirectedPropertyNode::FRedirectedPropertyNode(const FRedirectedPropertyNode& Other) : PropertyName(Other.PropertyName) , Type(Other.Type) , ArrayIndex(Other.ArrayIndex) { // deep copy tree for (const TSharedPtr& Child : Other.Children) { Children.Add(MakeShared(*Child)); } } FRedirectedPropertyNode::FRedirectedPropertyNode(const FPropertyInfo& InInfo, const TWeakPtr& InParent) : PropertyName(InInfo.Property->GetFName()) , ArrayIndex(InInfo.ArrayIndex) , Parent(InParent) { UE::FPropertyTypeNameBuilder TypeBuilder; InInfo.Property->SaveTypeName(TypeBuilder); Type = TypeBuilder.Build(); } FRedirectedPropertyNode::FRedirectedPropertyNode(FName InPropertyName, const UE::FPropertyTypeName& InType, int32 InArrayIndex, const TWeakPtr& InParent) : PropertyName(InPropertyName) , Type(InType) , ArrayIndex(InArrayIndex) , Parent(InParent) { } TSharedPtr FRedirectedPropertyNode::FindOrAdd(const FPropertyPath& Path, int32 PathIndex) { check(PathIndex <= Path.GetNumProperties()); if (PathIndex == Path.GetNumProperties()) { return SharedThis(this); } const FPropertyInfo& ChildInfo = Path.GetPropertyInfo(PathIndex); const TSharedPtr Child = FindOrAdd(ChildInfo); return Child->FindOrAdd(Path, PathIndex + 1); } TSharedPtr FRedirectedPropertyNode::FindOrAdd(const FPropertyInfo& ChildInfo) { TSharedPtr Child = Find(ChildInfo); if (!Child) { Child = MakeShared(ChildInfo, SharedThis(this)); Children.Add(Child); } return Child; } TSharedPtr FRedirectedPropertyNode::FindOrAdd(FName ChildPropertyName, const UE::FPropertyTypeName& ChildType, int32 ChildArrayIndex) { TSharedPtr Child = Find(ChildPropertyName, ChildType, ChildArrayIndex); if (!Child) { Child = MakeShared(ChildPropertyName, ChildType, ChildArrayIndex, SharedThis(this)); Children.Add(Child); } return Child; } bool FRedirectedPropertyNode::Remove(const FPropertyPath& Path, int32 PathIndex) { if (TSharedPtr NodeToRemove = Find(Path, PathIndex)) { do { const TSharedPtr ParentNode = NodeToRemove->Parent.Pin(); if (ParentNode) { ParentNode->Remove(NodeToRemove->PropertyName, NodeToRemove->Type, NodeToRemove->ArrayIndex); } NodeToRemove = ParentNode; } while (NodeToRemove && NodeToRemove->Children.IsEmpty()); return true; } return false; } bool FRedirectedPropertyNode::Remove(const FPropertyInfo& ChildInfo) { const int32 Index = FindIndex(ChildInfo); if (Index != INDEX_NONE) { Children.RemoveAt(Index); return true; } return false; } bool FRedirectedPropertyNode::Remove(FName ChildPropertyName, const UE::FPropertyTypeName& ChildType, int32 ChildArrayIndex) { const int32 Index = FindIndex(ChildPropertyName, ChildType, ChildArrayIndex); if (Index != INDEX_NONE) { Children.RemoveAt(Index); return true; } return false; } TSharedPtr FRedirectedPropertyNode::Find(const FPropertyPath& Path, int32 PathIndex) const { check(PathIndex <= Path.GetNumProperties()); if (PathIndex == Path.GetNumProperties()) { return SharedThis(const_cast(this)); } const FPropertyInfo& ChildInfo = Path.GetPropertyInfo(PathIndex); if (const TSharedPtr Child = Find(ChildInfo)) { return Child->Find(Path, PathIndex + 1); } return {}; } TSharedPtr FRedirectedPropertyNode::Find(const FPropertyInfo& ChildInfo) const { const int32 Index = FindIndex(ChildInfo); if (Index != INDEX_NONE) { return Children[Index]; } return {}; } TSharedPtr FRedirectedPropertyNode::Find(FName ChildPropertyName, const UE::FPropertyTypeName& ChildType, int32 ChildArrayIndex) const { const int32 Index = FindIndex(ChildPropertyName, ChildType, ChildArrayIndex); if (Index != INDEX_NONE) { return Children[Index]; } return {}; } bool FRedirectedPropertyNode::Move(const FPropertyPath& FromPath, const FPropertyPath& ToPath) { if (TSharedPtr NodeToMove = Find(FromPath)) { const TSharedPtr Added = FindOrAdd(ToPath); // reparent children Added->Children = MoveTemp(NodeToMove->Children); for (const TSharedPtr& Child : Added->Children) { Child->Parent = Added; } do { const TSharedPtr ParentNode = NodeToMove->Parent.Pin(); if (ParentNode) { ParentNode->Remove(NodeToMove->PropertyName, NodeToMove->Type, NodeToMove->ArrayIndex); } NodeToMove = ParentNode; } while (NodeToMove && NodeToMove->Children.IsEmpty()); return true; } return false; } int32 FRedirectedPropertyNode::FindIndex(const FPropertyInfo& ChildInfo) const { UE::FPropertyTypeNameBuilder ChildTypeBuilder; ChildInfo.Property->SaveTypeName(ChildTypeBuilder); return FindIndex(ChildInfo.Property->GetFName(), ChildTypeBuilder.Build(), ChildInfo.ArrayIndex); } int32 FRedirectedPropertyNode::FindIndex(FName ChildPropertyName, const UE::FPropertyTypeName& ChildType, int32 ChildArrayIndex) const { return Children.IndexOfByPredicate([ChildPropertyName, ChildType, ChildArrayIndex](const TSharedPtr& Child) { if (Child->ArrayIndex != INDEX_NONE) { // a matching index will always match regardless of type and name return Child->ArrayIndex == ChildArrayIndex; } return Child->Type == ChildType && Child->PropertyName == ChildPropertyName; }); } FInstanceDataObjectFixupPanel::FInstanceDataObjectFixupPanel( TConstArrayView> InstanceDataObjects, TObjectPtr InstanceDataObjectsOwner, EViewFlags InViewFlags) : Instances(InstanceDataObjects) , InstancesOwner(InstanceDataObjectsOwner) , RedirectedPropertyTree(MakeShared()) , ViewFlags(InViewFlags) { InitRedirectedPropertyTree(); } static bool ObjectHasLoosePropertiesThatNeedFixup(UObject* Object) { bool bNeedsFixup = false; Object->GetClass()->Visit(Object, [&bNeedsFixup](const FPropertyVisitorContext& Context)->EPropertyVisitorControlFlow { const FPropertyVisitorPath& Path = Context.Path; const FPropertyVisitorData& Data = Context.Data; const FProperty* Property = Path.Top().Property; if (!Property->HasAnyPropertyFlags(CPF_SkipSerialization) && Property->GetBoolMetaData(NAME_IsLooseMetadata)) { bNeedsFixup = true; return EPropertyVisitorControlFlow::Stop; } if (!Property->GetBoolMetaData(NAME_ContainsLoosePropertiesMetadata)) { // if this sub-struct doesn't contain loose properties, it won't need fixup return EPropertyVisitorControlFlow::StepOver; } return EPropertyVisitorControlFlow::StepInto; }); return bNeedsFixup; } FInstanceDataObjectFixupPanel::~FInstanceDataObjectFixupPanel() { using namespace UE::Editor::DataStorage; ICoreProvider* DataStorage = GetMutableDataStorageFeature(StorageFeatureName); ICompatibilityProvider* DataStorageCompatibility = GetMutableDataStorageFeature(CompatibilityFeatureName); if (DataStorageCompatibility != nullptr && DataStorage != nullptr) { for (UObject* Instance : Instances) { if (!ObjectHasLoosePropertiesThatNeedFixup(Instance)) { static FName AlertName = FName("EntityLoosePropertiesErrorAlert"); UE::FPropertyBagRepository& Repository = UE::FPropertyBagRepository::Get(); Repository.MarkAsFixedUp(Repository.FindInstanceForDataObject(Instance)); // If a UObject isn't registered with TEDS, there's a chance its parent is registered and is the one // with the alert column on it, so search upward until the nearest registered parent is found. RowHandle Row = DataStorageCompatibility->FindRowWithCompatibleObject(InstancesOwner ? InstancesOwner.Get() : Instance); Alerts::RemoveAlert(*DataStorage, Row, AlertName); } } } else { for (UObject* Instance : Instances) { if (!ObjectHasLoosePropertiesThatNeedFixup(Instance)) { UE::FPropertyBagRepository& Repository = UE::FPropertyBagRepository::Get(); Repository.MarkAsFixedUp(Repository.FindInstanceForDataObject(Instance)); } } } } int32 FInstanceDataObjectFixupPanel::Find(UObject* Value) const { return Instances.Find(Value); } static bool RemoveCustomizationsWithLooseProperties(const FFieldVariant& FieldVariant, const TSharedPtr& DetailsView) { #if WITH_EDITORONLY_DATA if (FStructProperty* AsStructProperty = FieldVariant.Get()) { if (RemoveCustomizationsWithLooseProperties(AsStructProperty->Struct, DetailsView)) { return true; } } else if (FObjectProperty* AsObjectProperty = FieldVariant.Get()) { if (AsObjectProperty->HasAnyPropertyFlags(CPF_InstancedReference)) { if (RemoveCustomizationsWithLooseProperties(AsObjectProperty->PropertyClass, DetailsView)) { return true; } } } else if (const FArrayProperty* AsArrayProperty = FieldVariant.Get()) { if (RemoveCustomizationsWithLooseProperties(AsArrayProperty->Inner, DetailsView)) { return true; } } else if (const FSetProperty* AsSetProperty = FieldVariant.Get()) { if (RemoveCustomizationsWithLooseProperties(AsSetProperty->ElementProp, DetailsView)) { return true; } } else if (const FMapProperty* AsMapProperty = FieldVariant.Get()) { if (RemoveCustomizationsWithLooseProperties(AsMapProperty->KeyProp, DetailsView)) { return true; } if (RemoveCustomizationsWithLooseProperties(AsMapProperty->ValueProp, DetailsView)) { return true; } } else if (UStruct* AsStruct = FieldVariant.Get()) { bool result = false; for (const FProperty* Property : TFieldRange(AsStruct)) { if (RemoveCustomizationsWithLooseProperties(Property, DetailsView)) { result = true; } } if (result) { // register an empty delegate to override the global rule of displaying this type with customizations DetailsView->RegisterInstancedCustomPropertyTypeLayout(AsStruct->GetFName(), {}); } return result; } if (const FProperty* Property = FieldVariant.Get()) { if (Property->HasMetaData(NAME_IsLooseMetadata)) { return true; } } #endif return false; } TSharedPtr& FInstanceDataObjectFixupPanel::GenerateDetailsView(bool bScrollbarOnLeft) { FDetailsViewArgs DetailsViewArgs; DetailsViewArgs.bUpdatesFromSelection = false; DetailsViewArgs.bHideSelectionTip = true; DetailsViewArgs.ExternalScrollbar = SAssignNew(LinkableScrollBar, SLinkableScrollBar); DetailsViewArgs.ScrollbarAlignment = bScrollbarOnLeft ? HAlign_Left : HAlign_Right; DetailsViewArgs.DetailsNameWidgetOverrideCustomization = MakeShared(SharedThis(this)); DetailsViewArgs.bResolveInstanceDataObjects.Emplace(true); DetailsViewArgs.bShowLooseProperties = !HasViewFlag(EViewFlags::HideLooseProperties); if (HasViewFlag(EViewFlags::IncludeOnlySetBySerialization)) { DetailsViewArgs.ShouldForceHideProperty.BindLambda([this](const TSharedRef& PropertyNode)->bool { return !IsInRedirectedPropertyTree(*FPropertyNode::CreatePropertyPath(PropertyNode)); }); } FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked("PropertyEditor"); DetailsView = PropertyEditorModule.CreateDetailView(DetailsViewArgs); for (const UObject* Instance : Instances) { RemoveCustomizationsWithLooseProperties(Instance->GetClass(), DetailsView); } DetailsView->SetObjects(Instances, true); return DetailsView; } void FInstanceDataObjectFixupPanel::SetDiffAgainstLeft(const TSharedPtr& InDiffAgainstLeft) { DiffAgainstLeft = InDiffAgainstLeft; } void FInstanceDataObjectFixupPanel::SetDiffAgainstRight(const TSharedPtr& InDiffAgainstRight) { DiffAgainstRight = InDiffAgainstRight; } TSharedPtr FInstanceDataObjectFixupPanel::GetDiffAgainstLeft() const { return DiffAgainstLeft.Pin(); } TSharedPtr FInstanceDataObjectFixupPanel::GetDiffAgainstRight() const { return DiffAgainstRight.Pin(); } bool FInstanceDataObjectFixupPanel::ShouldSplitterIgnoreRow(const TWeakPtr& WeakDetailTreeNode) const { if (const TSharedPtr DetailTreeNode = WeakDetailTreeNode.Pin()) { if (const TSharedPtr Handle = DetailTreeNode->CreatePropertyHandle()) { if (MarkedForDelete.Contains(*Handle->CreateFPropertyPath())) { return true; } } } return false; } bool FInstanceDataObjectFixupPanel::AreAllConflictsRedirected() const { for (UObject* Instance : Instances) { if (ObjectHasLoosePropertiesThatNeedFixup(Instance)) { return false; } } return true; } void FInstanceDataObjectFixupPanel::AutoApplyMarkDeletedActions() { const TSharedPtr Diff = DiffAgainstRight.Pin(); if (!Diff) { return; } Diff->ForEach(ETreeTraverseOrder::PreOrder, [this] (const TUniquePtr& DiffNode)->ETreeTraverseControl { if (DiffNode->DiffResult == ETreeDiffResult::MissingFromTree2) { if (const TSharedPtr LeftTreeNode = DiffNode->ValueA.Pin()) { const FPropertyPath Path = LeftTreeNode->GetPropertyPath(); if (Path.IsValid()) { MarkForDelete(Path); } } } return ETreeTraverseControl::Continue; }); } bool FInstanceDataObjectFixupPanel::HasViewFlag(EViewFlags Flag) { return static_cast(Flag) & static_cast(ViewFlags); } static void* ResolvePath(const FPropertyPath& Path, void* Value) { const int32 LastPathIndex = Path.GetNumProperties() - 1; for(int32 PathIndex = 0; PathIndex < Path.GetNumProperties(); ++PathIndex) { const FPropertyInfo& PropertyInfo = Path.GetPropertyInfo(PathIndex); const FProperty* Property = PropertyInfo.Property.Get(); if (!Property) { return nullptr; } Value = Property->ContainerPtrToValuePtr(Value, PropertyInfo.ArrayIndex != INDEX_NONE ? PropertyInfo.ArrayIndex : 0); if (PathIndex < LastPathIndex) { if (const FObjectProperty* AsObjectProperty = CastField(Property)) { UObject* Object = AsObjectProperty->GetObjectPropertyValue(Value); UE::FPropertyBagRepository& PropertyBagRepository = UE::FPropertyBagRepository::Get(); if (UObject* Found = PropertyBagRepository.FindInstanceDataObject(Object)) { Object = Found; } Value = Object; } if (const FArrayProperty* AsArrayProperty = CastField(Property)) { FScriptArrayHelper Helper(AsArrayProperty, Value); Value = Helper.GetElementPtr(Path.GetPropertyInfo(++PathIndex).ArrayIndex); } if (const FSetProperty* AsSetProperty = CastField(Property)) { FScriptSetHelper Helper(AsSetProperty, Value); Value = Helper.FindNthElementPtr(Path.GetPropertyInfo(++PathIndex).ArrayIndex); } if (const FMapProperty* AsMapProperty = CastField(Property)) { FScriptMapHelper Helper(AsMapProperty, Value); Value = Helper.FindNthValuePtr(Path.GetPropertyInfo(++PathIndex).ArrayIndex); } } } return Value; } static FPropertyChangedEvent ConstructChangeEventForRedirect(const FPropertyPath& Path, FEditPropertyChain& OutChain, TMap& OutArrayIndices) { FPropertyChangedEvent OutEvent = FPropertyChangedEvent(Path.GetLeafMostProperty().Property.Get(), EPropertyChangeType::ValueSet); for (int32 I = 0; I < Path.GetNumProperties(); ++I) { const FPropertyInfo& Info = Path.GetPropertyInfo(I); OutChain.AddTail(Info.Property.Get()); // only the head is used in OverrideProperty if (Info.ArrayIndex != INDEX_NONE) { OutArrayIndices.Add(Info.Property->GetName(), Info.ArrayIndex); } if (Info.Property->IsA() || Info.Property->IsA() || Info.Property->IsA()) { if (++I < Path.GetNumProperties()) { OutArrayIndices.Add(Info.Property->GetName(), Path.GetPropertyInfo(I).ArrayIndex); } } } OutEvent.SetArrayIndexPerObject(MakeArrayView(&OutArrayIndices, 1)); return OutEvent; } void FInstanceDataObjectFixupPanel::FTypeConverter::Push(FProperty* SourceProperty, const void* SourceData, FProperty* DestinationProperty, void* DestinationData) { InstanceInfo.Push({SourceProperty, SourceData, DestinationProperty, DestinationData}); // check if warning was made more severe by this data Warning = FMath::Max(Warning, GenerateWarning(SourceProperty, SourceData, DestinationProperty)); } FInstanceDataObjectFixupPanel::FTypeConverter::operator bool() const { return Warning != EWarning::InvalidConversion; } void FInstanceDataObjectFixupPanel::FTypeConverter::operator()() const { check(Warning != EWarning::InvalidConversion); for (const FInstanceInfo& Info : InstanceInfo) { TryConvert(Info.SourceProperty, Info.SourceData, Info.DestinationProperty, Info.DestinationData); } } FText FInstanceDataObjectFixupPanel::FTypeConverter::GetWarning() const { switch(Warning) { case EWarning::NarrowingConversion: return LOCTEXT("NarrowingConversion", "This type conversion is a narrowing conversion. Likely data loss!"); case EWarning::NonInvertibleConversion: return LOCTEXT("NonInvertibleConversion", "This type conversion is not an invertable operation. Likely data loss!"); case EWarning::InvalidConversion: return LOCTEXT("InvalidConversion", "Invalid Conversion"); default: return FText::GetEmpty(); } } bool FInstanceDataObjectFixupPanel::FTypeConverter::TryConvert(FProperty* SourceProperty, const void* SourceData, FProperty* DestinationProperty, void* DestinationData) { FUObjectSerializeContext* SerializeContext = FUObjectThreadContext::Get().GetSerializeContext(); TGuardValue ScopedImpersonateProperties(SerializeContext->bImpersonateProperties, true); TArray Buffer; FObjectWriter ObjectWriter(Buffer); // using FObjectWriter so that object properties will serialize correctly FStructuredArchiveFromArchive StructuredWriter(ObjectWriter); // todo: handle static arrays FPropertyTag SourceTag(SourceProperty, 0, (uint8*)SourceData); SourceTag.SerializeTaggedProperty(StructuredWriter.GetSlot(), SourceProperty, (uint8*)SourceData, nullptr); FObjectReader ObjectReader(Buffer); FStructuredArchiveFromArchive StructuredReader(ObjectReader); // TODO: this breaks for static array elements. void* DestinationContainer = static_cast(DestinationData) - DestinationProperty->GetOffset_ForInternal(); bool bResult = false; switch(DestinationProperty->ConvertFromType(SourceTag, StructuredReader.GetSlot(), (uint8*)DestinationContainer, SourceProperty->GetOwnerStruct(), nullptr)) { case EConvertFromTypeResult::UseSerializeItem: if (SourceProperty->GetID() == DestinationProperty->GetID()) { SourceTag.SerializeTaggedProperty(StructuredReader.GetSlot(), DestinationProperty, (uint8*)DestinationData, nullptr); bResult = true; } break; case EConvertFromTypeResult::Serialized: bResult = true; break; case EConvertFromTypeResult::CannotConvert: break; case EConvertFromTypeResult::Converted: bResult = true; break; } if (!bResult) { bool bTryTextSerialize = false; const auto IsStringType = [](const FProperty* Property) { static FName VerseStringName = TEXT("VerseStringProperty"); return Property->IsA() || Property->IsA() || Property->IsA() || Property->GetID() == VerseStringName; }; if (IsStringType(SourceProperty) || IsStringType(DestinationProperty)) { // if either property is a string, text, or name, use text serialization bTryTextSerialize = true; } else if (FStructProperty* SourceAsStructProperty = CastField(SourceProperty)) { if (FStructProperty* DestinationAsStructProperty = CastField(DestinationProperty)) { if (!SourceAsStructProperty->Struct->UseNativeSerialization() && !DestinationAsStructProperty->Struct->UseNativeSerialization()) { // attempt to text serialize structs since ConvertFromType doesn't support them usually bTryTextSerialize = true; } } } // use ExportText_Direct and ImportText_Direct if (bTryTextSerialize) { FString StrBuffer; SourceProperty->ExportText_Direct(StrBuffer, SourceData, nullptr, nullptr, PPF_None); FStringOutputDevice ErrorOutput; DestinationProperty->ImportText_Direct(*StrBuffer, DestinationData, nullptr, PPF_None, &ErrorOutput); bResult = ErrorOutput.IsEmpty(); } } return bResult; } FInstanceDataObjectFixupPanel::FTypeConverter::EWarning FInstanceDataObjectFixupPanel::FTypeConverter::GenerateWarning(FProperty* SourceProperty, const void* SourceData, FProperty* DestinationProperty) { // convert from source to destination in a temp buffer to see if it's possible TArray> SourceToDest; SourceToDest.SetNumUninitialized(DestinationProperty->GetElementSize()); DestinationProperty->InitializeValue(SourceToDest.GetData()); if (!TryConvert(SourceProperty, SourceData, DestinationProperty, SourceToDest.GetData())) { return EWarning::InvalidConversion; } // convert from destination to source in a temp buffer to see if it's possible TArray> DestToSource; DestToSource.SetNumUninitialized(SourceProperty->GetElementSize()); SourceProperty->InitializeValue(DestToSource.GetData()); if (!TryConvert(DestinationProperty, SourceToDest.GetData(), SourceProperty, DestToSource.GetData())) { return EWarning::NonInvertibleConversion; } // check that the round trip result has the same value as source if (!SourceProperty->Identical(SourceData, DestToSource.GetData(), PPF_None)) { return EWarning::NarrowingConversion; } return EWarning::SafeConversion; } FInstanceDataObjectFixupPanel::FTypeConverter FInstanceDataObjectFixupPanel::CreateTypeConverter(const FPropertyPath& From, const FPropertyPath& To) { FTypeConverter Result; for (UObject* Instance : Instances) { FProperty* SourceProperty = From.GetLeafMostProperty().Property.Get(); const void* SourceData = ResolvePath(From, Instance); FProperty* DestinationProperty = To.GetLeafMostProperty().Property.Get(); void* DestinationData = ResolvePath(To, Instance); Result.Push(SourceProperty, SourceData, DestinationProperty, DestinationData); } return Result; } void FInstanceDataObjectFixupPanel::RedirectPropertyHelper(const FPropertyPath& From, const FPropertyPath& To, TOptional& FromRevertInfo, FRevertInfo*& ToRevertInfo) { UInstanceDataObjectFixupUndoHandler* Snapshot = NewObject(); Snapshot->Init(SharedThis(this)); GEditor->BeginTransaction(TEXT("InstanceDataObjectFixupTool"), FText::Format(LOCTEXT("RedirectPropertyTransaction","Redirect {0} to {1}"), FText::FromString(From.ToString()), FText::FromString(To.ToString())), nullptr); FProperty* SourceProperty = From.GetLeafMostProperty().Property.Get(); check(SourceProperty); FProperty* DestinationProperty = To.IsValid() ? To.GetLeafMostProperty().Property.Get() : nullptr; if (const FRevertInfo* Info = RevertInfo.Find(From)) { FromRevertInfo = *Info; if (DestinationProperty) { if (DestinationProperty->HasAnyPropertyFlags(CPF_SkipSerialization) != Info->bHadSkipSerialization) { // toggle CPF_SkipSerialization flag if needed DestinationProperty->PropertyFlags ^= CPF_SkipSerialization; } if (!Info->bWasHidden) { DestinationProperty->RemoveMetaData(TEXT("Hidden")); } DestinationProperty->RemoveMetaData(TEXT("Redirected")); } if (To.IsValid() && To != Info->OriginalPath) { TArray OriginalValue; ToRevertInfo = &RevertInfo.Add(To, { .OriginalPath = Info->OriginalPath, .bHadSkipSerialization = SourceProperty->HasAnyPropertyFlags(CPF_SkipSerialization), .bWasHidden = SourceProperty->HasMetaData(TEXT("Hidden")) }); } MarkedForDelete.Remove(Info->OriginalPath); RevertInfo.Remove(From); } else { if (To.IsValid()) { ToRevertInfo = &RevertInfo.Add(To, { .OriginalPath = From, .bHadSkipSerialization = SourceProperty->HasAnyPropertyFlags(CPF_SkipSerialization) }); MarkedForDelete.Remove(From); } } if (To != From) { auto OnHidden = [](FProperty* Property) { if (Property->HasMetaData(NAME_IsLooseMetadata)) { Property->PropertyFlags |= CPF_SkipSerialization; Property->SetMetaData(TEXT("Hidden"), TEXT("True")); Property->SetMetaData(TEXT("Redirected"), TEXT("True")); } }; if (To.IsValid()) { RedirectedPropertyTree->Move(From, To); for (FPropertyPath Path = From; Path.IsValid(); Path = Path.TrimPath(1).Get()) { // because RedirectedPropertyTree->Move could've removed multiple properties in the path, we need to check each of them if (!MarkedForDelete.Find(Path) && RedirectedPropertyTree->Find(Path)) { break; } OnHidden(Path.GetLeafMostProperty().Property.Get()); } } else { OnHidden(SourceProperty); } } Snapshot->OnRedirect(From, To); } void FInstanceDataObjectFixupPanel::RedirectProperty(const FPropertyPath& From, const FPropertyPath& To) { const FProperty* SourceProperty = From.GetLeafMostProperty().Property.Get(); check(SourceProperty); FProperty* DestinationProperty = To.IsValid() ? To.GetLeafMostProperty().Property.Get() : nullptr; TOptional FromRevertInfo; FRevertInfo* ToRevertInfo = nullptr; RedirectPropertyHelper(From, To, FromRevertInfo, ToRevertInfo); if (!DestinationProperty) // null destination is interpreted as a deletion { MarkedForDelete.Add(From); GEditor->EndTransaction(); DetailsView->ForceRefresh(); return; // delete actions don't need data copied } const uint8* FromRevertInfoItr = FromRevertInfo ? FromRevertInfo->OriginalValue.GetData() : nullptr; for (UObject* Instance : Instances) { void* Source = ResolvePath(From, Instance); void* Destination = ResolvePath(To, Instance); if (!ensure(Source && Destination)) { continue; } // construct change event FEditPropertyChain Chain; TMap ArrayIndices; FPropertyChangedEvent ChangeEvent = ConstructChangeEventForRedirect(To, Chain, ArrayIndices); FPropertyChangedChainEvent ChangedChainEvent(Chain, ChangeEvent); Instance->PreEditChange(Chain); if (ToRevertInfo) { // cache the destination value so it can be reverted later const int32 Size = DestinationProperty->ArrayDim * DestinationProperty->GetElementSize(); ToRevertInfo->OriginalValue.AddZeroed(Size); uint8* Buffer = ToRevertInfo->OriginalValue.GetData() + (ToRevertInfo->OriginalValue.Num() - Size); DestinationProperty->CopyCompleteValue(Buffer, Destination); } if (SourceProperty->SameType(DestinationProperty)) { SourceProperty->CopyCompleteValue(Destination, Source); FOverridableManager::Get().GetOverriddenProperties(Instance)->SetOverriddenPropertyOperation(EOverriddenPropertyOperation::Modified, nullptr, DestinationProperty); } else { FString ValueStr; SourceProperty->ExportText_Direct(ValueStr, Source, nullptr, Instance, PPF_Copy); DestinationProperty->ImportText_Direct(*ValueStr, Destination, Instance, PPF_Copy); } if (FromRevertInfo) { // apply FromRevertInfo to From SourceProperty->CopyCompleteValue(Source, FromRevertInfoItr); FromRevertInfoItr += DestinationProperty->ArrayDim * DestinationProperty->GetElementSize(); } Instance->PostEditChangeChainProperty(ChangedChainEvent); } GEditor->EndTransaction(); DetailsView->ForceRefresh(); } void FInstanceDataObjectFixupPanel::RedirectProperty(const FPropertyPath& From, const FPropertyPath& To, const FTypeConverter& TypeConversion) { const FProperty* SourceProperty = From.GetLeafMostProperty().Property.Get(); const FProperty* DestinationProperty = To.GetLeafMostProperty().Property.Get(); check(SourceProperty && DestinationProperty); TOptional FromRevertInfo; FRevertInfo* ToRevertInfo = nullptr; RedirectPropertyHelper(From, To, FromRevertInfo, ToRevertInfo); const uint8* FromRevertInfoItr = FromRevertInfo ? FromRevertInfo->OriginalValue.GetData() : nullptr; TArray ChangeEvents; TArray Chains; // call PreEditChange and set up undo handling for (UObject* Instance : Instances) { void* Source = ResolvePath(From, Instance); void* Destination = ResolvePath(To, Instance); if (!ensure(Source && Destination)) { continue; } // construct change event Chains.Emplace(); TMap ArrayIndices; ChangeEvents.Add(ConstructChangeEventForRedirect(To, Chains.Last(), ArrayIndices)); Instance->PreEditChange(Chains.Last()); if (ToRevertInfo) { // cache the destination value so it can be reverted later const int32 Size = DestinationProperty->ArrayDim * DestinationProperty->GetElementSize(); ToRevertInfo->OriginalValue.AddZeroed(Size); uint8* Buffer = ToRevertInfo->OriginalValue.GetData() + (ToRevertInfo->OriginalValue.Num() - Size); DestinationProperty->CopyCompleteValue(Buffer, Destination); } } // applied to all instances at once TypeConversion(); // call post edit change and apply undo handling for (int32 I = 0; I < Instances.Num(); ++I) { UObject* Instance = Instances[I]; void* Source = ResolvePath(From, Instance); if (FromRevertInfo) { // apply FromRevertInfo to From SourceProperty->CopyCompleteValue(Source, FromRevertInfoItr); FromRevertInfoItr += DestinationProperty->ArrayDim * DestinationProperty->GetElementSize(); } FPropertyChangedChainEvent ChangedChainEvent(Chains[I], ChangeEvents[I]); Instance->PostEditChangeChainProperty(ChangedChainEvent); } GEditor->EndTransaction(); DetailsView->ForceRefresh(); } void FInstanceDataObjectFixupPanel::OnRedirectProperty(FPropertyPath From, FPropertyPath To) { RedirectProperty(From, To); } void FInstanceDataObjectFixupPanel::OnRedirectProperty(FPropertyPath From, FPropertyPath To, FTypeConverter TypeConversion) { RedirectProperty(From, To, TypeConversion); } static void InitRedirectedPropertyTreeRec(const TSharedPtr& Node, FProperty* Property, void* Value, TSet& EnteredObjects); static void InitRedirectedPropertyTreeRec(const TSharedPtr& Node, UStruct* Struct, void* StructValue, TSet& EnteredObjects) { const UE::FSerializedPropertyValueState SerializedState(Struct, StructValue); for (FProperty* Property : TFieldRange(Struct)) { for (int32 StaticArrayIndex = 0; StaticArrayIndex < Property->ArrayDim; ++StaticArrayIndex) { if (SerializedState.IsSet(Property, StaticArrayIndex)) { FPropertyInfo PropertyInfo( Property, Property->ArrayDim > 1 ? StaticArrayIndex : INDEX_NONE // use INDEX_NONE for properties that aren't in arrays ); const TSharedPtr& ChildNode = Node->FindOrAdd(PropertyInfo); void* Value = Property->ContainerPtrToValuePtr(StructValue, StaticArrayIndex); InitRedirectedPropertyTreeRec(ChildNode, Property, Value, EnteredObjects); } } } } static void InitRedirectedPropertyTreeRec(const TSharedPtr& Node, FProperty* Property, void* Value, TSet& EnteredObjects) { if (const FStructProperty* AsStructProperty = CastField(Property)) { InitRedirectedPropertyTreeRec(Node, AsStructProperty->Struct, Value, EnteredObjects); } else if (const FObjectProperty* AsObjectProperty = CastField(Property)) { if (AsObjectProperty->HasAnyPropertyFlags(CPF_InstancedReference)) { if (UObject* Object = AsObjectProperty->GetObjectPropertyValue(Value)) { UE::FPropertyBagRepository& PropertyBagRepository = UE::FPropertyBagRepository::Get(); if (UObject* Found = PropertyBagRepository.FindInstanceDataObject(Object)) { Object = Found; } // check for circular references to avoid infinite recursion if (!EnteredObjects.Contains(Object)) { EnteredObjects.Add(Object); InitRedirectedPropertyTreeRec(Node, Object->GetClass(), Object, EnteredObjects); EnteredObjects.Remove(Object); } } } } else if (const FArrayProperty* AsArrayProperty = CastField(Property)) { FScriptArrayHelper Array(AsArrayProperty, Value); for (int32 ArrayIndex = 0; ArrayIndex < Array.Num(); ++ArrayIndex) { const TSharedPtr& ChildNode = Node->FindOrAdd(FPropertyInfo(AsArrayProperty->Inner, ArrayIndex)); InitRedirectedPropertyTreeRec(ChildNode, AsArrayProperty->Inner, Array.GetElementPtr(ArrayIndex), EnteredObjects); } } else if (const FSetProperty* AsSetProperty = CastField(Property)) { FScriptSetHelper Set(AsSetProperty, Value); for (FScriptSetHelper::FIterator Itr = Set.CreateIterator(); Itr; ++Itr) { const TSharedPtr& ChildNode = Node->FindOrAdd(FPropertyInfo(AsSetProperty->ElementProp, Itr.GetLogicalIndex())); InitRedirectedPropertyTreeRec(ChildNode, AsSetProperty->ElementProp, Set.GetElementPtr(Itr), EnteredObjects); } } else if (const FMapProperty* AsMapProperty = CastField(Property)) { FScriptMapHelper Map(AsMapProperty, Value); for (FScriptMapHelper::FIterator Itr = Map.CreateIterator(); Itr; ++Itr) { const TSharedPtr& KeyNode = Node->FindOrAdd(FPropertyInfo(AsMapProperty->KeyProp, Itr.GetLogicalIndex())); InitRedirectedPropertyTreeRec(KeyNode, AsMapProperty->KeyProp, Map.GetKeyPtr(Itr), EnteredObjects); const TSharedPtr& ValNode = Node->FindOrAdd(FPropertyInfo(AsMapProperty->ValueProp, Itr.GetLogicalIndex())); InitRedirectedPropertyTreeRec(ValNode, AsMapProperty->ValueProp, Map.GetValuePtr(Itr), EnteredObjects); } } } void FInstanceDataObjectFixupPanel::InitRedirectedPropertyTree() { TSet EnteredObjects = {Instances[0]}; InitRedirectedPropertyTreeRec(RedirectedPropertyTree, Instances[0]->GetClass(), Instances[0], EnteredObjects); EnteredObjects.Remove(Instances[0]); check(EnteredObjects.IsEmpty()); } void UInstanceDataObjectFixupUndoHandler::Init(const TSharedRef& Panel) { InstanceDataObjectPanel = Panel; RevertInfo = Panel->RevertInfo; MarkedForDelete = Panel->MarkedForDelete; SetFlags(RF_Transactional); } void UInstanceDataObjectFixupUndoHandler::OnRedirect(const FPropertyPath& From, const FPropertyPath& To) { if (const TSharedPtr Panel = InstanceDataObjectPanel.Pin()) { RedirectFrom = From; RedirectTo = To; ++ChangeNum; } Modify(); } void UInstanceDataObjectFixupUndoHandler::PostEditUndo() { if (const TSharedPtr Panel = InstanceDataObjectPanel.Pin()) { if (RedirectTo != RedirectFrom) { if (RedirectTo.IsValid() && RedirectFrom.IsValid()) { Panel->RedirectedPropertyTree->Move(RedirectTo, RedirectFrom); } Swap(RedirectTo, RedirectFrom); } Swap(Panel->RevertInfo, RevertInfo); Swap(Panel->MarkedForDelete, MarkedForDelete); Panel->DetailsView->ForceRefresh(); } } bool FInstanceDataObjectFixupPanel::IsInRedirectedPropertyTree(const FPropertyPath& Path) const { return RedirectedPropertyTree->Find(Path).IsValid(); } const FPropertyPath& FInstanceDataObjectFixupPanel::GetOriginalPath(const FPropertyPath& Path) const { if (const FRevertInfo* Found = RevertInfo.Find(Path)) { return Found->OriginalPath; } return Path; } void FInstanceDataObjectFixupPanel::MarkForDelete(const FPropertyPath& CurrentPath) { // undo any existing redirection on this node if (const FRevertInfo* Found = RevertInfo.Find(CurrentPath)) { // move this property back to it's original location before marking it for delete const FPropertyPath PathCopy = Found->OriginalPath; // RedirectProperty will invalidate pointers. copy path by value so it doesn't get destroyed. RedirectProperty(CurrentPath, PathCopy); RedirectProperty(PathCopy, {}); } else { RedirectProperty(CurrentPath, {}); } } void FInstanceDataObjectFixupPanel::OnMarkForDelete(FPropertyPath Path) { MarkForDelete(Path); } #undef LOCTEXT_NAMESPACE