// Copyright Epic Games, Inc. All Rights Reserved. #include "DiffUtils.h" #include "AssetDefinitionRegistry.h" #include "Components/ActorComponent.h" #include "Containers/BitArray.h" #include "EditorCategoryUtils.h" #include "IAssetTools.h" #include "Engine/Blueprint.h" #include "Engine/Level.h" #include "IAssetTypeActions.h" #include "ISourceControlModule.h" #include "ISourceControlProvider.h" #include "ISourceControlRevision.h" #include "Internationalization/Internationalization.h" #include "Math/UnrealMathSSE.h" #include "Misc/Attribute.h" #include "Misc/CString.h" #include "Misc/DateTime.h" #include "Misc/Optional.h" #include "ObjectEditorUtils.h" #include "SlotBase.h" #include "Styling/AppStyle.h" #include "Styling/SlateColor.h" #include "Templates/SubclassOf.h" #include "Templates/UnrealTemplate.h" #include "Types/SlateConstants.h" #include "Types/SlateEnums.h" #include "UObject/Class.h" #include "UObject/Field.h" #include "UObject/Object.h" #include "UObject/ObjectMacros.h" #include "UObject/PropertyPortFlags.h" #include "UObject/SavePackage.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Images/SImage.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Views/STableRow.h" #include "Widgets/Views/STableViewBase.h" #include "Widgets/Views/STreeView.h" #include "HAL/PlatformFileManager.h" #include "UnrealEngine.h" #include "UObject/Linker.h" class ITableRow; class SWidget; namespace UEDiffUtils_Private { static FProperty* Resolve( const UStruct* Class, FName PropertyName ) { if(Class == nullptr ) { return nullptr; } for (FProperty* Prop : TFieldRange(Class)) { if( Prop->GetFName() == PropertyName ) { return Prop; } } return nullptr; } static FPropertySoftPathSet GetPropertyNameSet(const UStruct* ForStruct) { return FPropertySoftPathSet(DiffUtils::GetVisiblePropertiesInOrderDeclared(ForStruct)); } static const FString DiffSyntaxHelp = TEXT("format: 'diff "); static const FString MergeSyntaxHelp = TEXT("format: 'merge [-o out_path]' or 'merge [-o out_path]'"); static void RunDiffCommand(const TArray& Args); static void RunMergeCommand(const TArray& Args); FAutoConsoleCommand MergeConsoleCommand( TEXT("merge"), *FString::Format(TEXT("Either merge three assets or a single conflicted asset.\n{0}"), {MergeSyntaxHelp}), FConsoleCommandWithArgsDelegate::CreateStatic(&RunMergeCommand), ECVF_Default ); FAutoConsoleCommand DiffConsoleCommand( TEXT("diff"), *FString::Format(TEXT("diff two assets against one another.\n{0}"), {DiffSyntaxHelp}), FConsoleCommandWithArgsDelegate::CreateStatic(&RunDiffCommand), ECVF_Default ); } static UObject* LoadAssetFromExternalPath(FString Path) { FPackagePath PackagePath; if (!FPackagePath::TryFromPackageName(Path, PackagePath)) { // copy to the temp directory so it can be loaded properly FString File = FPaths::GetBaseFilename(Path) + TEXT("-"); for (const ANSICHAR Char : "#(){}[].") { File.ReplaceCharInline(Char, '-'); } const FString Extension = TEXT(".") + FPaths::GetExtension(Path); const FString SourcePath = Path; FPlatformFileManager::Get().GetPlatformFile().CreateDirectory(*FPaths::DiffDir()); Path = FPaths::CreateTempFilename(*FPaths::DiffDir(), *File, *Extension); Path = FPaths::ConvertRelativePathToFull(Path); if (!FPlatformFileManager::Get().GetPlatformFile().CopyFile(*Path, *SourcePath)) { UE_LOG(LogEngine, Display, TEXT("Failed to Copy %s"), *SourcePath); return nullptr; } // load the temp package PackagePath = FPackagePath::FromLocalPath(Path); } if (PackagePath.IsEmpty()) { UE_LOG(LogEngine, Display, TEXT("Invalid Path: %s"), *Path); return nullptr; } if (const UPackage* TempPackage = DiffUtils::LoadPackageForDiff(PackagePath, {})) { if (UObject* Object = TempPackage->FindAssetInPackage()) { return Object; } } UE_LOG(LogEngine, Display, TEXT("Failed to load: %s"), *Path); return nullptr; } static void UEDiffUtils_Private::RunDiffCommand(const TArray& Args) { if (Args.Num() != 2) { UE_LOG(LogEngine, Display, TEXT("%s"), *DiffSyntaxHelp); return; } UObject* LHS = LoadAssetFromExternalPath(Args[0]); UObject* RHS = LoadAssetFromExternalPath(Args[1]); if (LHS && RHS) { IAssetTools::Get().DiffAssets(LHS, RHS, {}, {}); } } namespace UE::CmdLink { // CmdLinkServerModule will set these methods if loaded. // they're used by the merge command because we need to keep the CmdLink client running until the user closes the merge window and saves the output UNREALED_API void(*GBeginAsyncCommand)(const FString&, const TArray&) = [](const FString&, const TArray&){}; UNREALED_API void(*GEndAsyncCommand)(const FString&, const TArray&) = [](const FString&, const TArray&){}; } static void UEDiffUtils_Private::RunMergeCommand(const TArray& Args) { UObject* Local = nullptr; UObject* Base = nullptr; UObject* Remote = nullptr; FString OutDirectory; bool bThreeWayMerge = false; bool bInvalidSyntax = false; switch (Args.Num()) { case 1: // merge Local = LoadAssetFromExternalPath(Args[0]); bThreeWayMerge = false; break; case 3: if (Args[1] == TEXT("-o")) // merge -o { Local = LoadAssetFromExternalPath(Args[0]); OutDirectory = Args[2]; bThreeWayMerge = false; } else // merge { Remote = LoadAssetFromExternalPath(Args[0]); Local = LoadAssetFromExternalPath(Args[1]); Base = LoadAssetFromExternalPath(Args[2]); bThreeWayMerge = true; } break; case 5: // merge -o if (Args[3] == TEXT("-o")) { Remote = LoadAssetFromExternalPath(Args[0]); Local = LoadAssetFromExternalPath(Args[1]); Base = LoadAssetFromExternalPath(Args[2]); OutDirectory = Args[4]; bThreeWayMerge = true; break; } // 5 parameters requires output file at the end bInvalidSyntax = true; break; default: // unsupported parameter count bInvalidSyntax = true; break; } if (bInvalidSyntax) { // invalid syntax. display help. UE_LOG(LogEngine, Display, TEXT("%s"), *MergeSyntaxHelp); return; } const FOnAssetMergeResolved ResolutionCallback = FOnAssetMergeResolved::CreateLambda([Args, Local, OutDirectory](const FAssetMergeResults& Results) { if (!OutDirectory.IsEmpty() && Results.Result == EAssetMergeResult::Completed) { // save a copy of the asset to the output directory FSavePackageArgs SaveArgs; SaveArgs.TopLevelFlags = RF_Standalone; SaveArgs.Error = GLog; UPackage::SavePackage(Results.MergedPackage, Local, *OutDirectory, SaveArgs); ResetLoaders(Results.MergedPackage); } UE::CmdLink::GEndAsyncCommand(TEXT("merge"), Args); }); if (bThreeWayMerge) { FAssetManualMergeArgs MergeArgs; MergeArgs.LocalAsset = Local; MergeArgs.BaseAsset = Base; MergeArgs.RemoteAsset = Remote; MergeArgs.ResolutionCallback = ResolutionCallback; MergeArgs.Flags = MF_NONE; if (MergeArgs.LocalAsset && MergeArgs.BaseAsset && MergeArgs.RemoteAsset) { const UAssetDefinition* AssetDefinition = UAssetDefinitionRegistry::Get()->GetAssetDefinitionForClass(Local->GetClass()); if (!AssetDefinition->CanMerge()) { UE_LOG(LogEngine, Error, TEXT("%s of class type %s does not support merging"), *Local->GetName(), *Local->GetClass()->GetName()); return; } UE::CmdLink::GBeginAsyncCommand(TEXT("merge"), Args); AssetDefinition->Merge(MergeArgs); } } else { FAssetAutomaticMergeArgs MergeArgs; MergeArgs.LocalAsset = Local; MergeArgs.ResolutionCallback = ResolutionCallback; MergeArgs.Flags = MF_NONE; if (MergeArgs.LocalAsset) { const UAssetDefinition* AssetDefinition = UAssetDefinitionRegistry::Get()->GetAssetDefinitionForClass(Local->GetClass()); if (!AssetDefinition->CanMerge()) { UE_LOG(LogEngine, Error, TEXT("%s does not support merging"), *Local->GetName()); return; } UE::CmdLink::GBeginAsyncCommand(TEXT("merge"), Args); AssetDefinition->Merge(MergeArgs); } } } FPropertySoftPath::FPropertySoftPath() : RootTypeHint(nullptr) { } FPropertySoftPath::FPropertySoftPath(const FProperty* Property) : FPropertySoftPath() { PropertyChain.Push(FChainElement(Property)); } FPropertySoftPath::FPropertySoftPath(TArray InPropertyChain) : FPropertySoftPath() { for (FName PropertyName : InPropertyChain) { PropertyChain.Push(FChainElement(PropertyName)); } } FPropertySoftPath::FPropertySoftPath(FPropertyPath InPropertyPath) : FPropertySoftPath() { for (int32 PropertyIndex = 0, end = InPropertyPath.GetNumProperties(); PropertyIndex != end; ++PropertyIndex) { const FPropertyInfo& Info = InPropertyPath.GetPropertyInfo(PropertyIndex); if (Info.ArrayIndex != INDEX_NONE) { PropertyChain.Push(FName(*FString::FromInt(Info.ArrayIndex))); } else { PropertyChain.Push(FChainElement(Info.Property.Get())); } } if (InPropertyPath.GetNumProperties() > 0) { const FProperty* RootProperty = InPropertyPath.GetPropertyInfo(0).Property.Get(); if(RootProperty) { RootTypeHint = RootProperty->GetOwnerStruct(); } } } FPropertySoftPath::FPropertySoftPath(const FPropertySoftPath& MainPropertyPath, const FPropertySoftPath& SubPropertyPath) : PropertyChain(MainPropertyPath.PropertyChain) , RootTypeHint(MainPropertyPath.RootTypeHint) { PropertyChain.Append(SubPropertyPath.PropertyChain); } FPropertySoftPath::FPropertySoftPath(const FPropertySoftPath& SubPropertyPath, const FProperty* LeafProperty) : PropertyChain(SubPropertyPath.PropertyChain) , RootTypeHint(SubPropertyPath.RootTypeHint) { PropertyChain.Push(FChainElement(LeafProperty)); } FPropertySoftPath::FPropertySoftPath(const FPropertySoftPath& SubPropertyPath, int32 ContainerIndex) : PropertyChain(SubPropertyPath.PropertyChain) , RootTypeHint(SubPropertyPath.RootTypeHint) { PropertyChain.Push(FName(*FString::FromInt(ContainerIndex))); } FResolvedProperty FPropertySoftPath::Resolve(const UObject* Object) const { FResolvedProperty ResolvedProperty = Resolve(Object->GetClass(), Object); return ResolvedProperty; } int32 FPropertySoftPath::TryReadIndex(const TArray& LocalPropertyChain, int32& OutIndex) { if(OutIndex + 1 < LocalPropertyChain.Num()) { FString AsString = LocalPropertyChain[OutIndex + 1].DisplayString; if(AsString.IsNumeric()) { ++OutIndex; return FCString::Atoi(*AsString); } } return INDEX_NONE; }; FResolvedProperty FPropertySoftPath::Resolve(const UStruct* Struct, const void* StructData) const { if (RootTypeHint && RootTypeHint != Struct) { if (const UClass* AsClass = Cast(Struct)) { const UScriptStruct* SparseClassDataStruct = AsClass->GetSparseClassDataStruct(); if (SparseClassDataStruct && SparseClassDataStruct->IsChildOf(RootTypeHint)) { if (const void* SparseClassData = const_cast(AsClass)->GetSparseClassData(EGetSparseClassDataMethod::ReturnIfNull)) { return Resolve(RootTypeHint, SparseClassData); } } } } // dig into the object, finding nested objects, etc: const void* CurrentBlock = StructData; const UStruct* NextClass = Struct; const void* NextBlock = CurrentBlock; const FProperty* Property = nullptr; for (int32 i = 0; i < PropertyChain.Num(); ++i) { CurrentBlock = NextBlock; const FProperty* NextProperty = UEDiffUtils_Private::Resolve(NextClass, PropertyChain[i].PropertyName); if (!NextProperty) { if (const FStructProperty* StructProperty = CastField(Property)) { StructProperty->FindInnerPropertyInstance(PropertyChain[i].PropertyName, CurrentBlock, NextProperty, NextBlock); } } CurrentBlock = NextBlock; // if an index was provided, resolve it const int32 PropertyIndex = TryReadIndex(PropertyChain, i); if (NextProperty && PropertyIndex != INDEX_NONE) { Property = NextProperty; if (const FArrayProperty* ArrayProperty = CastField(Property)) { FScriptArrayHelper ArrayHelper(ArrayProperty, Property->ContainerPtrToValuePtr(CurrentBlock)); if (ArrayHelper.IsValidIndex(PropertyIndex)) { NextProperty = ArrayProperty->Inner; NextBlock = ArrayHelper.GetRawPtr(PropertyIndex); } } else if( const FSetProperty* SetProperty = CastField(Property) ) { FScriptSetHelper SetHelper(SetProperty, Property->ContainerPtrToValuePtr(CurrentBlock)); NextProperty = SetHelper.GetElementProperty(); NextBlock = SetHelper.FindNthElementPtr(PropertyIndex); } else if( const FMapProperty* MapProperty = CastField(Property) ) { FScriptMapHelper MapHelper(MapProperty, Property->ContainerPtrToValuePtr(CurrentBlock)); NextProperty = MapHelper.GetValueProperty(); NextBlock = MapHelper.FindNthPairPtr(PropertyIndex); } } CurrentBlock = NextBlock; if (NextProperty) { Property = NextProperty; if (const FObjectProperty* ObjectProperty = CastField(Property)) { const UObject* NextObject = ObjectProperty->GetObjectPropertyValue(Property->ContainerPtrToValuePtr(CurrentBlock)); NextBlock = NextObject; NextClass = NextObject ? NextObject->GetClass() : nullptr; } else if (const FStructProperty* StructProperty = CastField(Property)) { NextBlock = StructProperty->ContainerPtrToValuePtr(CurrentBlock); NextClass = StructProperty->Struct; } else { break; } } else { break; } } return FResolvedProperty(CurrentBlock, Property); } FPropertyPath FPropertySoftPath::ResolvePath(const UObject* Object) const { auto UpdateContainerAddress = [](const FProperty* Property, const void* Instance, const void*& OutContainerAddress, const UStruct*& OutContainerStruct) { if( ensure(Instance) ) { if(const FObjectProperty* ObjectProperty = CastField(Property)) { const UObject* const* InstanceObject = reinterpret_cast(Instance); if( *InstanceObject) { OutContainerAddress = *InstanceObject; OutContainerStruct = (*InstanceObject)->GetClass(); } } else if(const FStructProperty* StructProperty = CastField(Property)) { OutContainerAddress = Instance; OutContainerStruct = StructProperty->Struct; } } }; const void* ContainerAddress = Object; const UStruct* ContainerStruct = (Object ? Object->GetClass() : nullptr); FPropertyPath Ret; for( int32 I = 0; I < PropertyChain.Num(); ++I ) { FName PropertyIdentifier = PropertyChain[I].PropertyName; FProperty* ResolvedProperty = UEDiffUtils_Private::Resolve(ContainerStruct, PropertyIdentifier); // if property wasn't found in ContainerStruct, check for it in SparseClassData if (!ResolvedProperty && RootTypeHint && RootTypeHint != ContainerStruct) { if (const UClass* AsClass = Cast(ContainerStruct)) { const UScriptStruct* SparseClassDataStruct = AsClass->GetSparseClassDataStruct(); if (SparseClassDataStruct && SparseClassDataStruct->IsChildOf(RootTypeHint)) { ResolvedProperty = UEDiffUtils_Private::Resolve(RootTypeHint, PropertyIdentifier); // return if null won't mutate... ContainerAddress = const_cast(AsClass)->GetSparseClassData(EGetSparseClassDataMethod::ReturnIfNull); } } } // If the property didn't exist inside the container, return an invalid property if (!ResolvedProperty) { return FPropertyPath(); } FPropertyInfo Info(ResolvedProperty, INDEX_NONE); Ret.AddProperty(Info); int32 PropertyIndex = TryReadIndex(PropertyChain, I); // calculate offset so we can continue resolving object properties/structproperties: if( const FArrayProperty* ArrayProperty = CastField(ResolvedProperty) ) { FScriptArrayHelper ArrayHelper(ArrayProperty, ArrayProperty->ContainerPtrToValuePtr( ContainerAddress )); if (ArrayHelper.IsValidIndex(PropertyIndex)) { UpdateContainerAddress( ArrayProperty->Inner, ArrayHelper.GetRawPtr(PropertyIndex), ContainerAddress, ContainerStruct ); FPropertyInfo ArrayInfo(ArrayProperty->Inner, PropertyIndex); Ret.AddProperty(ArrayInfo); } } else if( const FSetProperty* SetProperty = CastField(ResolvedProperty) ) { FScriptSetHelper SetHelper(SetProperty, SetProperty->ContainerPtrToValuePtr( ContainerAddress )); if (SetHelper.IsValidIndex(PropertyIndex)) { const int32 InternalIndex = SetHelper.FindInternalIndex(PropertyIndex); UpdateContainerAddress( SetProperty->ElementProp, SetHelper.GetElementPtr(InternalIndex), ContainerAddress, ContainerStruct ); FPropertyInfo SetInfo(SetProperty->ElementProp, PropertyIndex); Ret.AddProperty(SetInfo); } } else if( const FMapProperty* MapProperty = CastField(ResolvedProperty) ) { FScriptMapHelper MapHelper(MapProperty, MapProperty->ContainerPtrToValuePtr( ContainerAddress )); if (MapHelper.IsValidIndex(PropertyIndex)) { const int32 InternalIndex = MapHelper.FindInternalIndex(PropertyIndex); // we have an index, but are we looking into a key or value? Peek ahead to find out: if(ensure((I + 1 < PropertyChain.Num()))) { if(PropertyChain[I+1].PropertyName == MapProperty->KeyProp->GetFName()) { ++I; UpdateContainerAddress( MapProperty->KeyProp, MapHelper.GetKeyPtr(InternalIndex), ContainerAddress, ContainerStruct ); FPropertyInfo MakKeyInfo(MapProperty->KeyProp, PropertyIndex); Ret.AddProperty(MakKeyInfo); } else if(ensure( PropertyChain[I+1].PropertyName == MapProperty->ValueProp->GetFName() )) { ++I; UpdateContainerAddress( MapProperty->ValueProp, MapHelper.GetValuePtr(InternalIndex), ContainerAddress, ContainerStruct ); FPropertyInfo MapValueInfo(MapProperty->ValueProp, PropertyIndex); Ret.AddProperty(MapValueInfo); } } } } else if (const FObjectProperty* ObjectProperty = CastField(ResolvedProperty)) { UpdateContainerAddress( ObjectProperty, ObjectProperty->ContainerPtrToValuePtr( ContainerAddress, FMath::Max(PropertyIndex, 0) ), ContainerAddress, ContainerStruct ); // handle static arrays: if(PropertyIndex != INDEX_NONE ) { FPropertyInfo ObjectInfo(ResolvedProperty, PropertyIndex); Ret.AddProperty(ObjectInfo); } } else if( const FStructProperty* StructProperty = CastField(ResolvedProperty) ) { UpdateContainerAddress( StructProperty, StructProperty->ContainerPtrToValuePtr( ContainerAddress, FMath::Max(PropertyIndex, 0) ), ContainerAddress, ContainerStruct ); // handle static arrays: if(PropertyIndex != INDEX_NONE ) { FPropertyInfo StructInfo(ResolvedProperty, PropertyIndex); Ret.AddProperty(StructInfo); } } else { // handle static arrays: if(PropertyIndex != INDEX_NONE ) { FPropertyInfo StaticArrayInfo(ResolvedProperty, PropertyIndex); Ret.AddProperty(StaticArrayInfo); } } } return Ret; } FString FPropertySoftPath::ToDisplayName(const int32 NumberOfElements /*= INDEX_NONE*/) const { FString Ret; const int32 Count = NumberOfElements != INDEX_NONE ? FMath::Clamp(NumberOfElements, 1, PropertyChain.Num()) : PropertyChain.Num(); for (int32 i = PropertyChain.Num() - Count; i < PropertyChain.Num(); i++) { FString PropertyAsString = PropertyChain[i].DisplayString; if (Ret.IsEmpty()) { Ret.Append(PropertyAsString); } else if (PropertyAsString.IsNumeric()) { Ret.AppendChar('['); Ret.Append(PropertyAsString); Ret.AppendChar(']'); } else { Ret.AppendChar(' '); Ret.Append(PropertyAsString); } } return Ret; } FPropertySoftPath FPropertySoftPath::GetRootProperty(int32 Depth) const { FPropertySoftPath RootPath; Depth = FMath::Clamp(Depth, 1, PropertyChain.Num()); for (int32 ElementIndex = 0; ElementIndex < Depth; ElementIndex++) { RootPath.PropertyChain.Push(PropertyChain[ElementIndex]); } return RootPath; } int32 FPropertySoftPath::TryReadIndex(int32 Index) const { return TryReadIndex(PropertyChain, Index); }; const UObject* DiffUtils::GetCDO(const UBlueprint* ForBlueprint) { if (!ForBlueprint || !ForBlueprint->GeneratedClass) { return NULL; } return ForBlueprint->GeneratedClass->GetDefaultObject(false); } void DiffUtils::CompareUnrelatedStructs(const UStruct* StructA, const void* A, const UStruct* StructB, const void* B, TArray& OutDifferingProperties) { CompareUnrelatedStructs(StructA, A, nullptr, StructB, B, nullptr, OutDifferingProperties); } void DiffUtils::CompareUnrelatedStructs(const UStruct* StructA, const void* A, const UObject* OwningOuterA, const UStruct* StructB, const void* B, const UObject* OwningOuterB, TArray& OutDifferingProperties) { FPropertySoftPathSet PropertiesInA = UEDiffUtils_Private::GetPropertyNameSet(StructA); FPropertySoftPathSet PropertiesInB = UEDiffUtils_Private::GetPropertyNameSet(StructB); // any properties in A that aren't in B are differing: auto AddedToA = PropertiesInA.Difference(PropertiesInB).Array(); for (const auto& Entry : AddedToA) { OutDifferingProperties.Push(FSingleObjectDiffEntry(Entry, EPropertyDiffType::PropertyAddedToA)); } // and the converse: auto AddedToB = PropertiesInB.Difference(PropertiesInA).Array(); for (const auto& Entry : AddedToB) { OutDifferingProperties.Push(FSingleObjectDiffEntry(Entry, EPropertyDiffType::PropertyAddedToB)); } // for properties in common, dig out the uproperties and determine if they're identical: if (A && B) { FPropertySoftPathSet Common = PropertiesInA.Intersect(PropertiesInB); for (const auto& PropertyName : Common) { FResolvedProperty AProp = PropertyName.Resolve(StructA, A); FResolvedProperty BProp = PropertyName.Resolve(StructB, B); check(AProp != FResolvedProperty() && BProp != FResolvedProperty()); TArray DifferingSubProperties; if (!DiffUtils::Identical(AProp, BProp, OwningOuterA, OwningOuterB, FDiffParameters(PropertyName), DifferingSubProperties)) { for (int DifferingIndex = 0; DifferingIndex < DifferingSubProperties.Num(); DifferingIndex++) { OutDifferingProperties.Push(FSingleObjectDiffEntry(DifferingSubProperties[DifferingIndex], EPropertyDiffType::PropertyValueChanged)); } } } } } void DiffUtils::CompareUnrelatedObjects(const UObject* A, const UObject* B, TArray& OutDifferingProperties) { if (A && B) { return CompareUnrelatedStructs(A->GetClass(), A, A->GetPackage(), B->GetClass(), B, B->GetPackage(), OutDifferingProperties); } } void DiffUtils::CompareUnrelatedSCS(const UBlueprint* Old, const TArray< FSCSResolvedIdentifier >& OldHierarchy, const UBlueprint* New, const TArray< FSCSResolvedIdentifier >& NewHierarchy, FSCSDiffRoot& OutDifferingEntries ) { const auto FindEntry = [](TArray< FSCSResolvedIdentifier > const& InArray, const FSCSIdentifier* Value) -> const FSCSResolvedIdentifier* { const FSCSResolvedIdentifier* BestMatch = nullptr; for (const auto& Node : InArray) { if (Node.Identifier.Name == Value->Name) { if (Node.Identifier.TreeLocation == Value->TreeLocation) { return &Node; } else if (BestMatch == nullptr) { BestMatch = &Node; } } } return BestMatch; }; for (const auto& OldNode : OldHierarchy) { const FSCSResolvedIdentifier* NewEntry = FindEntry(NewHierarchy, &OldNode.Identifier); if (NewEntry != nullptr) { bool bShouldDiffProperties = true; // did it change class? const bool bObjectTypesDiffer = OldNode.Object != nullptr && NewEntry->Object != nullptr && OldNode.Object->GetClass() != NewEntry->Object->GetClass(); if (bObjectTypesDiffer) { FSCSDiffEntry Diff = { OldNode.Identifier, ETreeDiffType::NODE_TYPE_CHANGED, FSingleObjectDiffEntry() }; OutDifferingEntries.Entries.Push(Diff); // Only diff properties if we're still within the same class inheritance hierarchy. bShouldDiffProperties = OldNode.Object->GetClass()->IsChildOf(NewEntry->Object->GetClass()) || NewEntry->Object->GetClass()->IsChildOf(OldNode.Object->GetClass()); } // did a property change? if(bShouldDiffProperties) { TArray DifferingProperties; DiffUtils::CompareUnrelatedObjects(OldNode.Object, NewEntry->Object, DifferingProperties); for (const auto& Property : DifferingProperties) { // Only include property value change entries if the object types differ. if (!bObjectTypesDiffer || Property.DiffType == EPropertyDiffType::PropertyValueChanged) { FSCSDiffEntry Diff = { OldNode.Identifier, ETreeDiffType::NODE_PROPERTY_CHANGED, Property }; OutDifferingEntries.Entries.Push(Diff); } } } // did it move? if( NewEntry->Identifier.TreeLocation != OldNode.Identifier.TreeLocation ) { FSCSDiffEntry Diff = { OldNode.Identifier, ETreeDiffType::NODE_MOVED, FSingleObjectDiffEntry() }; OutDifferingEntries.Entries.Push(Diff); } // did it become corrupted? or stop being corrupted? const bool bNewIsCorrupt = New->GeneratedClass && NewEntry->Object && !NewEntry->Object->IsIn(New->GeneratedClass); const bool bOldIsCorrupt = Old->GeneratedClass && OldNode.Object && !OldNode.Object->IsIn(Old->GeneratedClass); if (bNewIsCorrupt != bOldIsCorrupt) { if (bNewIsCorrupt) { FSCSDiffEntry Diff = { NewEntry->Identifier, ETreeDiffType::NODE_CORRUPTED, FSingleObjectDiffEntry() }; OutDifferingEntries.Entries.Push(Diff); } else { FSCSDiffEntry Diff = { OldNode.Identifier, ETreeDiffType::NODE_FIXED, FSingleObjectDiffEntry() }; OutDifferingEntries.Entries.Push(Diff); } } // no change! Do nothing. } else { // not found in the new data, must have been deleted: FSCSDiffEntry Entry = { OldNode.Identifier, ETreeDiffType::NODE_REMOVED, FSingleObjectDiffEntry() }; OutDifferingEntries.Entries.Push( Entry ); } } for (const auto& NewNode : NewHierarchy) { const FSCSResolvedIdentifier* OldEntry = FindEntry(OldHierarchy, &NewNode.Identifier); if (OldEntry == nullptr) { FSCSDiffEntry Entry = { NewNode.Identifier, ETreeDiffType::NODE_ADDED, FSingleObjectDiffEntry() }; OutDifferingEntries.Entries.Push( Entry ); } } } static void IdenticalHelper(const FProperty* AProperty, const FProperty* BProperty, const void* AValue, const void* BValue, const UObject* OwningOuterA, const UObject* OwningOuterB, DiffUtils::FDiffParameters DiffParameters, TArray& DifferingSubProperties) { if (AProperty == nullptr || BProperty == nullptr || AProperty->ArrayDim != BProperty->ArrayDim || AProperty->GetClass() != BProperty->GetClass()) { DifferingSubProperties.Push(DiffParameters.RootPath); return; } if (AValue == nullptr || BValue == nullptr) { if (AValue != BValue) { DifferingSubProperties.Push(DiffParameters.RootPath); } return; } if (DiffParameters.ShouldIgnorePropertyPredicate && DiffParameters.ShouldIgnorePropertyPredicate(*AProperty)) { return; } // Keep copy of the initial RootPath since we reuse DiffParameters with // an updated path to nested calls to 'IdenticalHelper' const FPropertySoftPath RootPath = DiffParameters.RootPath; if (DiffParameters.bShouldDiffArrayElements && AProperty->ArrayDim != 1) { // Identical does not handle static array case automatically and we have to do the offset calculation ourself because // our container (e.g. the struct or class or dynamic array) has already done the initial offset calculation: for (int32 I = 0; I < AProperty->ArrayDim; ++I) { int32 Offset = AProperty->GetElementSize() * I; DiffParameters.RootPath = FPropertySoftPath(RootPath, I); DiffParameters.bShouldDiffArrayElements = false; const void* CurAValue = reinterpret_cast(reinterpret_cast(AValue) + Offset); const void* CurBValue = reinterpret_cast(reinterpret_cast(BValue) + Offset); IdenticalHelper(AProperty, BProperty, CurAValue, CurBValue, OwningOuterA, OwningOuterB, DiffParameters, DifferingSubProperties); } return; } const FStructProperty* APropAsStruct = CastField(AProperty); const FArrayProperty* APropAsArray = CastField(AProperty); const FSetProperty* APropAsSet = CastField(AProperty); const FMapProperty* APropAsMap = CastField(AProperty); const FObjectProperty* APropAsObject = CastField(AProperty); if (APropAsStruct != nullptr) { const FStructProperty* BPropAsStruct = CastFieldChecked(const_cast(BProperty)); if (BPropAsStruct->Struct == APropAsStruct->Struct) { if (APropAsStruct->Struct->StructFlags & STRUCT_IdenticalNative) { // If the struct uses CPP identical tests then we need to honor that if (!AProperty->Identical(AValue, BValue, PPF_DeepComparison)) { DifferingSubProperties.Push(RootPath); } } else { // Compare sub-properties to detect more granular changes for (TFieldIterator PropertyIt(APropAsStruct->Struct); PropertyIt; ++PropertyIt) { const FProperty* StructProp = *PropertyIt; DiffParameters.RootPath = FPropertySoftPath(RootPath, StructProp); const void* SubValueA = StructProp->ContainerPtrToValuePtr(AValue, 0); const void* SubValueB = StructProp->ContainerPtrToValuePtr(BValue, 0); IdenticalHelper(StructProp, StructProp, SubValueA, SubValueB, OwningOuterA, OwningOuterB, DiffParameters, DifferingSubProperties); } } } else { DifferingSubProperties.Push(RootPath); } } else if (APropAsArray != nullptr) { const FArrayProperty* BPropAsArray = CastFieldChecked(BProperty); if(BPropAsArray->Inner->GetClass() == APropAsArray->Inner->GetClass()) { FScriptArrayHelper ArrayHelperA(APropAsArray, AValue); FScriptArrayHelper ArrayHelperB(BPropAsArray, BValue); // note any differences in contained types: for (int32 ArrayIndex = 0; ArrayIndex < ArrayHelperA.Num() && ArrayIndex < ArrayHelperB.Num(); ArrayIndex++) { DiffParameters.RootPath = FPropertySoftPath(RootPath, ArrayIndex); const void* SubValueA = ArrayHelperA.GetRawPtr(ArrayIndex); const void* SubValueB = ArrayHelperB.GetRawPtr(ArrayIndex); IdenticalHelper(APropAsArray->Inner, BPropAsArray->Inner, SubValueA, SubValueB, OwningOuterA, OwningOuterB, DiffParameters, DifferingSubProperties); } // note any size difference: if (ArrayHelperA.Num() != ArrayHelperB.Num()) { DifferingSubProperties.Push(RootPath); } } else { DifferingSubProperties.Push(RootPath); } } else if(APropAsSet != nullptr) { const FSetProperty* BPropAsSet = CastFieldChecked(BProperty); if(BPropAsSet->ElementProp->GetClass() == APropAsSet->ElementProp->GetClass()) { FScriptSetHelper SetHelperA(APropAsSet, AValue); FScriptSetHelper SetHelperB(BPropAsSet, BValue); if (SetHelperA.Num() != SetHelperB.Num()) { // API not robust enough to indicate changes made to # of set elements, would // need to return something more detailed than DifferingSubProperties array: DifferingSubProperties.Push(RootPath); } // note any differences in contained elements: FScriptSetHelper::FIterator IteratorA(SetHelperA); FScriptSetHelper::FIterator IteratorB(SetHelperB); for (; IteratorA && IteratorB; ++IteratorA, ++IteratorB) { DiffParameters.RootPath = FPropertySoftPath(RootPath, IteratorA.GetLogicalIndex()); const void* SubValueA = SetHelperA.GetElementPtr(IteratorA); const void* SubValueB = SetHelperB.GetElementPtr(IteratorB); IdenticalHelper(APropAsSet->ElementProp, BPropAsSet->ElementProp, SubValueA, SubValueB, OwningOuterA, OwningOuterB, DiffParameters, DifferingSubProperties); } } else { DifferingSubProperties.Push(RootPath); } } else if(APropAsMap != nullptr) { const FMapProperty* BPropAsMap = CastFieldChecked(BProperty); if(APropAsMap->KeyProp->GetClass() == BPropAsMap->KeyProp->GetClass() && APropAsMap->ValueProp->GetClass() == BPropAsMap->ValueProp->GetClass()) { FScriptMapHelper MapHelperA(APropAsMap, AValue); FScriptMapHelper MapHelperB(BPropAsMap, BValue); if (MapHelperA.Num() != MapHelperB.Num()) { // API not robust enough to indicate changes made to # of set elements, would // need to return something more detailed than DifferingSubProperties array: DifferingSubProperties.Push(RootPath); } FScriptMapHelper::FIterator IteratorA(MapHelperA); FScriptMapHelper::FIterator IteratorB(MapHelperB); for (; IteratorA && IteratorB; ++IteratorA, ++IteratorB) { const FPropertySoftPath EntryIndexPath = FPropertySoftPath(RootPath, IteratorA.GetLogicalIndex()); DiffParameters.RootPath = FPropertySoftPath(EntryIndexPath, BPropAsMap->KeyProp); IdenticalHelper(APropAsMap->KeyProp, BPropAsMap->KeyProp, MapHelperA.GetKeyPtr(IteratorA), MapHelperB.GetKeyPtr(IteratorB), OwningOuterA, OwningOuterB, DiffParameters, DifferingSubProperties); DiffParameters.RootPath = FPropertySoftPath(EntryIndexPath, BPropAsMap->ValueProp); IdenticalHelper(APropAsMap->ValueProp, BPropAsMap->ValueProp, MapHelperA.GetValuePtr(IteratorA), MapHelperB.GetValuePtr(IteratorB), OwningOuterA, OwningOuterB, DiffParameters, DifferingSubProperties); } } else { DifferingSubProperties.Push(RootPath); } } else if(APropAsObject != nullptr) { // Past container check, do a normal identical check now before going into components if (AProperty->Identical(AValue, BValue, PPF_DeepComparison)) { return; } const FObjectProperty* BPropAsObject = CastFieldChecked(BProperty); const UObject* A = *((const UObject* const*)AValue); const UObject* B = *((const UObject* const*)BValue); // dig into the objects if they are in the same package as our initial object if (A && B && A->GetClass() == B->GetClass() && OwningOuterA && OwningOuterB && A->IsIn(OwningOuterA) && B->IsIn(OwningOuterB)) { const UClass* AClass = A->GetClass(); // BClass and AClass are identical! // We only want to recurse if this is EditInlineNew and not a component // Other instanced refs are likely to form a type-specific web so recursion doesn't make sense and won't be displayed properly in the details pane if (AClass->HasAnyClassFlags(CLASS_EditInlineNew) && !AClass->IsChildOf(UActorComponent::StaticClass())) { for (TFieldIterator PropertyIt(AClass); PropertyIt; ++PropertyIt) { const FProperty* ClassProp = *PropertyIt; DiffParameters.RootPath = FPropertySoftPath(RootPath, ClassProp); const void* SubValueA = ClassProp->ContainerPtrToValuePtr(A, 0); const void* SubValueB = ClassProp->ContainerPtrToValuePtr(B, 0); IdenticalHelper(ClassProp, ClassProp, SubValueA, SubValueB, OwningOuterA, OwningOuterB, DiffParameters, DifferingSubProperties); } } else { const FString PathA = A->GetPathName(OwningOuterA); const FString PathB = B->GetPathName(OwningOuterB); if (PathA != PathB) { DifferingSubProperties.Push(RootPath); } } } else { DifferingSubProperties.Push(RootPath); } } else { // Passed all container tests that would check for nested properties being wrong if (AProperty->Identical(AValue, BValue, PPF_DeepComparison)) { return; } DifferingSubProperties.Push(RootPath); } } bool DiffUtils::Identical(const FResolvedProperty& AProp, const FResolvedProperty& BProp, const FPropertySoftPath& RootPath, TArray& DifferingProperties) { if( AProp.Property == nullptr && BProp.Property == nullptr ) { return true; } else if( AProp.Property == nullptr || BProp.Property == nullptr ) { return false; } const void* AValue = AProp.Property->ContainerPtrToValuePtr(AProp.Object); const void* BValue = BProp.Property->ContainerPtrToValuePtr(BProp.Object); // We _could_ just ask the property for comparison but that would make the "identical" functions significantly more complex. // Instead let's write a new function, specific to DiffUtils, that handles the sub properties // NOTE: For Static Arrays, AValue and BValue were, and are, only references to the value at index 0. So changes to values past index 0 didn't show up before and // won't show up now. Changes to index 0 will show up as a change to the entire array. IdenticalHelper(AProp.Property, BProp.Property, AValue, BValue, nullptr, nullptr, FDiffParameters(RootPath), DifferingProperties); return DifferingProperties.Num() == 0; } bool DiffUtils::Identical(const FResolvedProperty& AProp, const FResolvedProperty& BProp, const UObject* OwningOuterA, const UObject* OwningOuterB) { TArray DifferingProperties; return Identical(AProp, BProp, OwningOuterA, OwningOuterB, FDiffParameters{}, DifferingProperties); } bool DiffUtils::Identical(const FResolvedProperty& AProp, const FResolvedProperty& BProp, const UObject* OwningOuterA, const UObject* OwningOuterB, const FPropertySoftPath& RootPath, TArray& DifferingProperties) { return Identical(AProp, BProp, OwningOuterA, OwningOuterB, FDiffParameters(RootPath), DifferingProperties); } bool DiffUtils::Identical(const FResolvedProperty& AProp, const FResolvedProperty& BProp, const UObject* OwningOuterA, const UObject* OwningOuterB, FDiffParameters DiffParameters, TArray& DifferingProperties) { if( AProp.Property == nullptr && BProp.Property == nullptr ) { return true; } else if( AProp.Property == nullptr || BProp.Property == nullptr ) { return false; } const void* AValue = AProp.Property->ContainerPtrToValuePtr(AProp.Object); const void* BValue = BProp.Property->ContainerPtrToValuePtr(BProp.Object); // note that we're not directly calling FProperty::Identical because sub-object properties should be weakly compared based on // their paths instead of their pointers or data IdenticalHelper(AProp.Property, BProp.Property, AValue, BValue, OwningOuterA, OwningOuterB, DiffParameters, DifferingProperties); return DifferingProperties.Num() == 0; } bool DiffUtils::Identical(const TSharedPtr& PropertyHandleA, const TSharedPtr& PropertyHandleB, const TArray>& OwningOutersA, const TArray>& OwningOutersB) { TArray DifferingProperties; return Identical(DifferingProperties, PropertyHandleA, PropertyHandleB, OwningOutersA, OwningOutersB); } bool DiffUtils::Identical( TArray& OutDifferingProperties, const TSharedPtr& PropertyHandleA, const TSharedPtr& PropertyHandleB, const TArray>& OwningOutersA, const TArray>& OwningOutersB ) { TArray ValuesA; TArray ValuesB; PropertyHandleA->AccessRawData(ValuesA); PropertyHandleB->AccessRawData(ValuesB); // if OwningOuters weren't provided, fallback to using the property handles to find them TArray HandleOutersA; TArray HandleOutersB; if (OwningOutersA.IsEmpty()) { PropertyHandleA->GetOuterObjects(HandleOutersA); } if (OwningOutersB.IsEmpty()) { PropertyHandleB->GetOuterObjects(HandleOutersB); } if (!ensure(ValuesA.Num() == OwningOutersA.Num())) { // Outer count mismatch return false; } if (!ensure(ValuesB.Num() == OwningOutersB.Num())) { // Outer count mismatch return false; } auto IsIdenticalAtIndex = [&](int32 IndexA, int32 IndexB) { const void* ValueA = ValuesA[IndexA]; const void* ValueB = ValuesB[IndexB]; const UObject* OwningOuterA = OwningOutersA.IsEmpty() ? HandleOutersA[IndexA] : OwningOutersA[IndexA].Get(); const UObject* OwningOuterB = OwningOutersB.IsEmpty() ? HandleOutersB[IndexB] : OwningOutersB[IndexB].Get(); if (!OwningOuterA || !OwningOuterB) { // objects were Garbage Collected! return !OwningOuterA && !OwningOuterB; } // note that we're not directly calling FProperty::Identical because sub-object properties should be weakly compared based on // their paths instead of their pointers or data FDiffParameters DiffParameters; DiffParameters.bShouldDiffArrayElements = false; IdenticalHelper(PropertyHandleA->GetProperty(), PropertyHandleB->GetProperty(), ValueA, ValueB, OwningOuterA, OwningOuterB, DiffParameters, OutDifferingProperties); return OutDifferingProperties.IsEmpty(); }; if (ValuesA.Num() == ValuesB.Num()) { // compare AValues[I] with BValues[I] for (int32 I = 0; I < ValuesA.Num(); ++I) { if (!IsIdenticalAtIndex(I,I)) { return false; } } } else if (ValuesA.Num() == 1) { // compare AValues[0] with BValues[0...N] for (int32 I = 0; I < ValuesB.Num(); ++I) { if (!IsIdenticalAtIndex(0,I)) { return false; } } } else if (ValuesB.Num() == 1) { // compare BValues[0] with AValues[0...N] for (int32 I = 0; I < ValuesA.Num(); ++I) { if (!IsIdenticalAtIndex(I,0)) { return false; } } } else { // number of values doesn't match... this cannot be compared return ensure(false); } return true; } TArray DiffUtils::GetVisiblePropertiesInOrderDeclared(const UStruct* ForStruct, const FPropertySoftPath& Scope /*= TArray()*/) { TArray Ret; if (ForStruct) { TSet HiddenCategories = FEditorCategoryUtils::GetHiddenCategories(ForStruct); for (TFieldIterator PropertyIt(ForStruct); PropertyIt; ++PropertyIt) { FName CategoryName = FObjectEditorUtils::GetCategoryFName(*PropertyIt); if (!HiddenCategories.Contains(CategoryName.ToString())) { if (PropertyIt->PropertyFlags&CPF_Edit) { // We don't need to recurse into objects/structs as those will be picked up in the Identical check later FPropertySoftPath NewPath(Scope, *PropertyIt); Ret.Push(NewPath); } } } } return Ret; } TArray DiffUtils::ResolveAll(const UObject* Object, const TArray& InSoftProperties) { TArray< FPropertyPath > Ret; for (const auto& Path : InSoftProperties) { Ret.Push(Path.ResolvePath(Object)); } return Ret; } TArray DiffUtils::ResolveAll(const UObject* Object, const TArray& InDifferences) { TArray< FPropertyPath > Ret; for (const auto& Difference : InDifferences) { Ret.Push(Difference.Identifier.ResolvePath(Object)); } return Ret; } UPackage* DiffUtils::LoadPackageForDiff(const FPackagePath& InTempPackagePath, const FPackagePath& InOriginalPackagePath) { // if this is a local asset, load it normally if (!FPackageName::IsTempPackage(InTempPackagePath.GetPackageName())) { return LoadPackage(nullptr, *InTempPackagePath.GetPackageName(), LOAD_None); } // set up instancing context FLinkerInstancingContext Context; if (!InOriginalPackagePath.GetLocalFullPath().IsEmpty()) { Context.AddTag(ULevel::DontLoadExternalObjectsTag); Context.AddPackageMapping(InOriginalPackagePath.GetPackageFName(), InTempPackagePath.GetPackageFName()); } return LoadPackage(nullptr, *InTempPackagePath.GetPackageName(), LOAD_ForDiff | LOAD_DisableCompileOnLoad | LOAD_DisableEngineVersionChecks, nullptr, &Context); } UPackage* DiffUtils::LoadPackageForDiff(TSharedPtr Revision) { FString TempFileName; if(Revision->Get(TempFileName)) { // Try and load that package const FPackagePath TempPackagePath = FPackagePath::FromLocalPath(TempFileName); const FPackagePath OriginalPackagePath = FPackagePath::FromLocalPath(Revision->GetFilename()); return LoadPackageForDiff(TempPackagePath, OriginalPackagePath); } return nullptr; } TSharedPtr FBlueprintDifferenceTreeEntry::NoDifferencesEntry() { // This just generates a widget that tells the user that no differences were detected. Without this // the treeview displaying differences is confusing when no differences are present because it is not obvious // that the control is a treeview (a treeview with no children looks like a listview). const auto GenerateWidget = []() -> TSharedRef { return SNew(STextBlock) .ColorAndOpacity(FLinearColor(.7f, .7f, .7f)) .TextStyle(FAppStyle::Get(), TEXT("BlueprintDif.ItalicText")) .Text(NSLOCTEXT("FBlueprintDifferenceTreeEntry", "NoDifferencesLabel", "No differences detected...")); }; return TSharedPtr(new FBlueprintDifferenceTreeEntry( FOnDiffEntryFocused() , FGenerateDiffEntryWidget::CreateStatic(GenerateWidget) , TArray< TSharedPtr >() ) ); } TSharedPtr FBlueprintDifferenceTreeEntry::UnknownDifferencesEntry() { // Warn about there being unknown differences const auto GenerateWidget = []() -> TSharedRef { return SNew(STextBlock) .ColorAndOpacity(FLinearColor(.7f, .7f, .7f)) .TextStyle(FAppStyle::Get(), TEXT("BlueprintDif.ItalicText")) .Text(NSLOCTEXT("FBlueprintDifferenceTreeEntry", "BlueprintTypeNotSupported", "Warning: Detecting differences in this Blueprint type specific data is not yet supported...")); }; return TSharedPtr(new FBlueprintDifferenceTreeEntry( FOnDiffEntryFocused() , FGenerateDiffEntryWidget::CreateStatic(GenerateWidget) , TArray< TSharedPtr >() )); } TSharedPtr FBlueprintDifferenceTreeEntry::CreateCategoryEntry(const FText& LabelText, const FText& ToolTipText, FOnDiffEntryFocused FocusCallback, const TArray< TSharedPtr >& Children, bool bHasDifferences) { const auto CreateDefaultsRootEntry = [](FText LabelText, FText ToolTipText, FLinearColor Color) -> TSharedRef { return SNew(STextBlock) .ToolTipText(ToolTipText) .ColorAndOpacity(Color) .Text(LabelText); }; return TSharedPtr(new FBlueprintDifferenceTreeEntry( FocusCallback , FGenerateDiffEntryWidget::CreateStatic(CreateDefaultsRootEntry, LabelText, ToolTipText, DiffViewUtils::LookupColor(bHasDifferences)) , Children )); } TSharedPtr FBlueprintDifferenceTreeEntry::CreateCategoryEntryForMerge(const FText& LabelText, const FText& ToolTipText, FOnDiffEntryFocused FocusCallback, const TArray< TSharedPtr >& Children, bool bHasRemoteDifferences, bool bHasLocalDifferences, bool bHasConflicts) { const auto CreateDefaultsRootEntry = [](FText LabelText, FText ToolTipText, bool bInHasRemoteDifferences, bool bInHasLocalDifferences, bool bInHasConflicts) -> TSharedRef { const FLinearColor BaseColor = DiffViewUtils::LookupColor(bInHasRemoteDifferences || bInHasLocalDifferences, bInHasConflicts); return SNew(SHorizontalBox) + SHorizontalBox::Slot() [ SNew(STextBlock) .ToolTipText(ToolTipText) .ColorAndOpacity(BaseColor) .Text(LabelText) ] + DiffViewUtils::Box(true, DiffViewUtils::LookupColor(bInHasRemoteDifferences, bInHasConflicts)) + DiffViewUtils::Box(true, BaseColor) + DiffViewUtils::Box(true, DiffViewUtils::LookupColor(bInHasLocalDifferences, bInHasConflicts)); }; return TSharedPtr(new FBlueprintDifferenceTreeEntry( FocusCallback , FGenerateDiffEntryWidget::CreateStatic(CreateDefaultsRootEntry, LabelText, ToolTipText, bHasRemoteDifferences, bHasLocalDifferences, bHasConflicts) , Children )); } TSharedRef< STreeView > > DiffTreeView::CreateTreeView(TArray< TSharedPtr >* DifferencesList) { const auto RowGenerator = [](TSharedPtr< FBlueprintDifferenceTreeEntry > Entry, const TSharedRef& Owner) -> TSharedRef< ITableRow > { return SNew(STableRow >, Owner) [ Entry->GenerateWidget.Execute() ]; }; const auto ChildrenAccessor = [](TSharedPtr InTreeItem, TArray< TSharedPtr< FBlueprintDifferenceTreeEntry > >& OutChildren) { OutChildren = InTreeItem->Children; }; const auto Selector = [](TSharedPtr InTreeItem, ESelectInfo::Type Type) { if (InTreeItem.IsValid()) { InTreeItem->OnFocus.ExecuteIfBound(); } }; return SNew(STreeView< TSharedPtr< FBlueprintDifferenceTreeEntry > >) .OnGenerateRow(STreeView< TSharedPtr< FBlueprintDifferenceTreeEntry > >::FOnGenerateRow::CreateStatic(RowGenerator)) .OnGetChildren(STreeView< TSharedPtr< FBlueprintDifferenceTreeEntry > >::FOnGetChildren::CreateStatic(ChildrenAccessor)) .OnSelectionChanged(STreeView< TSharedPtr< FBlueprintDifferenceTreeEntry > >::FOnSelectionChanged::CreateStatic(Selector)) .TreeItemsSource(DifferencesList); } int32 DiffTreeView::CurrentDifference(TSharedRef< STreeView > > TreeView, const TArray< TSharedPtr >& Differences) { auto SelectedItems = TreeView->GetSelectedItems(); if (SelectedItems.Num() == 0) { return INDEX_NONE; } for (int32 Iter = 0; Iter < SelectedItems.Num(); ++Iter) { int32 Index = Differences.Find(SelectedItems[Iter]); if (Index != INDEX_NONE) { return Index; } } return INDEX_NONE; } void DiffTreeView::HighlightNextDifference(TSharedRef< STreeView > > TreeView, const TArray< TSharedPtr >& Differences, const TArray< TSharedPtr >& RootDifferences) { int32 CurrentIndex = CurrentDifference(TreeView, Differences); auto Next = Differences[CurrentIndex + 1]; // we have to manually expand our parent: for (auto& Test : RootDifferences) { if (Test->Children.Contains(Next)) { TreeView->SetItemExpansion(Test, true); break; } } TreeView->SetSelection(Next); TreeView->RequestScrollIntoView(Next); } void DiffTreeView::HighlightPrevDifference(TSharedRef< STreeView > > TreeView, const TArray< TSharedPtr >& Differences, const TArray< TSharedPtr >& RootDifferences) { int32 CurrentIndex = CurrentDifference(TreeView, Differences); auto Prev = Differences[CurrentIndex - 1]; // we have to manually expand our parent: for (auto& Test : RootDifferences) { if (Test->Children.Contains(Prev)) { TreeView->SetItemExpansion(Test, true); break; } } TreeView->SetSelection(Prev); TreeView->RequestScrollIntoView(Prev); } bool DiffTreeView::HasNextDifference(TSharedRef< STreeView > > TreeView, const TArray< TSharedPtr >& Differences) { int32 CurrentIndex = CurrentDifference(TreeView, Differences); return Differences.IsValidIndex(CurrentIndex + 1); } bool DiffTreeView::HasPrevDifference(TSharedRef< STreeView > > TreeView, const TArray< TSharedPtr >& Differences) { int32 CurrentIndex = CurrentDifference(TreeView, Differences); return Differences.IsValidIndex(CurrentIndex - 1); } FLinearColor DiffViewUtils::LookupColor(bool bDiffers, bool bConflicts) { if( bConflicts ) { return DiffViewUtils::Conflicting(); } else if( bDiffers ) { return DiffViewUtils::Differs(); } else { return DiffViewUtils::Identical(); } } FLinearColor DiffViewUtils::Differs() { // yellow color return FLinearColor(0.85f,0.71f,0.25f); } FLinearColor DiffViewUtils::Identical() { const static FLinearColor ForegroundColor = FAppStyle::GetColor("Graph.ForegroundColor"); return ForegroundColor; } FLinearColor DiffViewUtils::Missing() { // blue color return FLinearColor(0.3f,0.3f,1.f); } FLinearColor DiffViewUtils::Conflicting() { // red color return FLinearColor(1.0f,0.2f,0.3f); } FText DiffViewUtils::PropertyDiffMessage(FSingleObjectDiffEntry Difference, FText ObjectName) { FText Message; FString PropertyName = Difference.Identifier.ToDisplayName(); switch (Difference.DiffType) { case EPropertyDiffType::PropertyAddedToA: Message = FText::Format(NSLOCTEXT("DiffViewUtils", "PropertyValueChange_Removed", "{0} removed from {1}"), FText::FromString(PropertyName), ObjectName); break; case EPropertyDiffType::PropertyAddedToB: Message = FText::Format(NSLOCTEXT("DiffViewUtils", "PropertyValueChange_Added", "{0} added to {1}"), FText::FromString(PropertyName), ObjectName); break; case EPropertyDiffType::PropertyValueChanged: Message = FText::Format(NSLOCTEXT("DiffViewUtils", "PropertyValueChange", "{0} changed value in {1}"), FText::FromString(PropertyName), ObjectName); break; } return Message; } FText DiffViewUtils::SCSDiffMessage(const FSCSDiffEntry& Difference, FText ObjectName) { const FText NodeName = FText::FromName(Difference.TreeIdentifier.Name); FText Text; switch (Difference.DiffType) { case ETreeDiffType::NODE_ADDED: Text = FText::Format(NSLOCTEXT("DiffViewUtils", "NodeAdded", "Added Node {0} to {1}"), NodeName, ObjectName); break; case ETreeDiffType::NODE_REMOVED: Text = FText::Format(NSLOCTEXT("DiffViewUtils", "NodeRemoved", "Removed Node {0} from {1}"), NodeName, ObjectName); break; case ETreeDiffType::NODE_TYPE_CHANGED: Text = FText::Format(NSLOCTEXT("DiffViewUtils", "NodeTypeChanged", "Node {0} changed type in {1}"), NodeName, ObjectName); break; case ETreeDiffType::NODE_PROPERTY_CHANGED: Text = FText::Format(NSLOCTEXT("DiffViewUtils", "NodePropertyChanged", "{0} on {1}"), DiffViewUtils::PropertyDiffMessage(Difference.PropertyDiff, NodeName), ObjectName); break; case ETreeDiffType::NODE_MOVED: Text = FText::Format(NSLOCTEXT("DiffViewUtils", "NodeMoved", "Moved Node {0} in {1}"), NodeName, ObjectName); break; case ETreeDiffType::NODE_CORRUPTED: Text = FText::Format(NSLOCTEXT("DiffViewUtils", "NodeCorrupted", "Node {0} in {1} has corrupt outer - must be recreated"), NodeName, ObjectName); break; case ETreeDiffType::NODE_FIXED: Text = FText::Format(NSLOCTEXT("DiffViewUtils", "NodeFixed", "Node {0} in {1} has been recreated to correct outer"), NodeName, ObjectName); break; } return Text; } FText DiffViewUtils::GetPanelLabel(const UObject* Asset, const FRevisionInfo& Revision, FText Label) { if( !Asset ) { return NSLOCTEXT("DiffViewUtils", "NoBlueprint", "None" ); } if( !Revision.Revision.IsEmpty() ) { FText RevisionData; if(ISourceControlModule::Get().GetProvider().UsesChangelists()) { RevisionData = FText::Format(NSLOCTEXT("DiffViewUtils", "RevisionData", "Revision {0} - CL {1} - {2}") , FText::FromString(Revision.Revision) , FText::AsNumber(Revision.Changelist, &FNumberFormattingOptions::DefaultNoGrouping()) , FText::AsDateTime(Revision.Date)); } else { RevisionData = FText::Format(NSLOCTEXT("DiffViewUtils", "RevisionDataNoChangelist", "Revision {0} - {1}") , FText::FromString(Revision.Revision) , FText::AsDateTime(Revision.Date)); } if (Label.IsEmpty()) { return FText::Format(NSLOCTEXT("DiffViewUtils", "RevisionLabelTwoLines", "{0}\n{1}") , FText::FromString(Asset->GetName()) , RevisionData); } else { return FText::Format(NSLOCTEXT("DiffViewUtils", "RevisionLabel", "{0}\n{1}\n{2}") , Label , FText::FromString(Asset->GetName()) , RevisionData); } } else { if (Label.IsEmpty()) { return FText::Format(NSLOCTEXT("DiffViewUtils", "RevisionLabelTwoLines", "{0}\n{1}") , FText::FromString(Asset->GetName()) , NSLOCTEXT("DiffViewUtils", "LocalRevisionLabel", "Local Revision")); } else { return FText::Format(NSLOCTEXT("DiffViewUtils", "RevisionLabel", "{0}\n{1}\n{2}") , Label , FText::FromString(Asset->GetName()) , NSLOCTEXT("DiffViewUtils", "LocalRevisionLabel", "Local Revision")); } } } SHorizontalBox::FSlot::FSlotArguments DiffViewUtils::Box(bool bIsPresent, FLinearColor Color) { return MoveTemp(SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Right) .VAlign(VAlign_Center) .Padding(0.5f, 0.f) [ SNew(SImage) .ColorAndOpacity(Color) .Image(bIsPresent ? FAppStyle::GetBrush("BlueprintDif.HasGraph") : FAppStyle::GetBrush("BlueprintDif.MissingGraph")) ]); };