// Copyright Epic Games, Inc. All Rights Reserved. #include "AsyncDetailViewDiff.h" #include "DetailTreeNode.h" #include "IDetailsViewPrivate.h" #include "DiffUtils.h" #include "PropertyHandleImpl.h" namespace AsyncDetailViewDiffHelpers { TArray> GetObjects(const TSharedPtr& TreeNode) { TArray Result; if (TSharedPtr DetailsView = TreeNode->GetDetailsViewSharedPtr()) { return DetailsView->GetSelectedObjects(); } return {}; } } bool TTreeDiffSpecification>::AreValuesEqual(const TWeakPtr& TreeNodeA, const TWeakPtr& TreeNodeB, TArray*) const { return AreValuesEqual(TreeNodeA, TreeNodeB); } bool TTreeDiffSpecification>::AreValuesEqual(const TWeakPtr& TreeNodeA, const TWeakPtr& TreeNodeB) const { const TSharedPtr PinnedTreeNodeA = TreeNodeA.Pin(); const TSharedPtr PinnedTreeNodeB = TreeNodeB.Pin(); if (!PinnedTreeNodeA || !PinnedTreeNodeB) { return PinnedTreeNodeA == PinnedTreeNodeB; } const TSharedPtr PropertyHandleA = PinnedTreeNodeA->CreatePropertyHandle(); const TSharedPtr PropertyHandleB = PinnedTreeNodeB->CreatePropertyHandle(); if (!PropertyHandleA || !PropertyHandleB) { // category nodes return PinnedTreeNodeA->GetNodeName() == PinnedTreeNodeB->GetNodeName(); } const TSharedPtr& PropertyNodeA = StaticCastSharedPtr(PropertyHandleA)->GetPropertyNode(); const TSharedPtr& PropertyNodeB = StaticCastSharedPtr(PropertyHandleB)->GetPropertyNode(); if (!PropertyNodeA || !PropertyNodeB) { return PinnedTreeNodeA->GetNodeName() == PinnedTreeNodeB->GetNodeName(); } TArray DataValuesA; TArray DataValuesB; PropertyHandleA->AccessRawData(DataValuesA); PropertyHandleB->AccessRawData(DataValuesB); if(DataValuesA.IsEmpty() || DataValuesB.IsEmpty()) { return true; } const TArray> OwningObjectsA = AsyncDetailViewDiffHelpers::GetObjects(PinnedTreeNodeA); const TArray> OwningObjectsB = AsyncDetailViewDiffHelpers::GetObjects(PinnedTreeNodeB); if (!ensure(OwningObjectsA.Num() == DataValuesA.Num() && OwningObjectsB.Num() == DataValuesB.Num())) { return true; } return DiffUtils::Identical(PropertyHandleA, PropertyHandleB, OwningObjectsA, OwningObjectsB); } bool TTreeDiffSpecification>::AreMatching(const TWeakPtr& TreeNodeA, const TWeakPtr& TreeNodeB, TArray*) const { return AreMatching(TreeNodeA, TreeNodeB); } bool TTreeDiffSpecification>::AreMatching(const TWeakPtr& TreeNodeA, const TWeakPtr& TreeNodeB) const { const TSharedPtr PinnedTreeNodeA = TreeNodeA.Pin(); const TSharedPtr PinnedTreeNodeB = TreeNodeB.Pin(); if (!PinnedTreeNodeA || !PinnedTreeNodeB) { return PinnedTreeNodeA == PinnedTreeNodeB; } const TSharedPtr PropertyHandleA = PinnedTreeNodeA->CreatePropertyHandle(); const TSharedPtr PropertyHandleB = PinnedTreeNodeB->CreatePropertyHandle(); if (!PropertyHandleA || !PropertyHandleB) { // category nodes return PinnedTreeNodeA->GetNodeName() == PinnedTreeNodeB->GetNodeName(); } const int32 ArrayIndexA = PropertyHandleA->GetArrayIndex(); const int32 ArrayIndexB = PropertyHandleB->GetArrayIndex(); if (ArrayIndexA != INDEX_NONE && ArrayIndexB != INDEX_NONE) { const TSharedPtr KeyHandleA = PropertyHandleA->GetKeyHandle(); const TSharedPtr KeyHandleB = PropertyHandleB->GetKeyHandle(); if (PropertyHandleA->GetKeyHandle() && PropertyHandleB->GetKeyHandle()) { const TArray> OwningObjectsA = AsyncDetailViewDiffHelpers::GetObjects(PinnedTreeNodeA); const TArray> OwningObjectsB = AsyncDetailViewDiffHelpers::GetObjects(PinnedTreeNodeB); return DiffUtils::Identical(KeyHandleA, KeyHandleB, OwningObjectsA, OwningObjectsB); } const TSharedPtr SetHandleA = PropertyHandleA->GetParentHandle()->AsSet(); const TSharedPtr SetHandleB = PropertyHandleB->GetParentHandle()->AsSet(); if (SetHandleA && SetHandleB) { // match set elements by value const TArray> OwningObjectsA = AsyncDetailViewDiffHelpers::GetObjects(PinnedTreeNodeA); const TArray> OwningObjectsB = AsyncDetailViewDiffHelpers::GetObjects(PinnedTreeNodeB); return DiffUtils::Identical(PropertyHandleA, PropertyHandleB, OwningObjectsA, OwningObjectsB); } return ArrayIndexA == ArrayIndexB; } return true; } void TTreeDiffSpecification>::GetChildren(const TWeakPtr& InParent, TArray>& OutChildren) const { const TSharedPtr PinnedParent = InParent.Pin(); if (PinnedParent) { TArray> Children; PinnedParent->GetChildren(Children); for (TSharedRef Child : Children) { OutChildren.Add(Child); } } } bool TTreeDiffSpecification>::ShouldMatchByValue(const TWeakPtr& TreeNode) const { const TSharedPtr PinnedTreeNode = TreeNode.Pin(); if (!PinnedTreeNode) { return false; } const TSharedPtr PropertyNodeA = PinnedTreeNode->GetPropertyNode(); if (!PropertyNodeA || !PropertyNodeA->GetParentNode()) { return false; } const int32 ArrayIndex = PropertyNodeA->GetArrayIndex(); const FArrayProperty* ParentArrayProperty = CastField(PropertyNodeA->GetParentNode()->GetProperty()); // match array elements by value rather than by index return ParentArrayProperty && ArrayIndex != INDEX_NONE; } bool TTreeDiffSpecification>::ShouldInheritEqualFromChildren( const TWeakPtr& TreeNodeA, const TWeakPtr& TreeNodeB) const { return false; // this theoretically could return true, but leaving it off has helped find false positives and false negatives } FAsyncDetailViewDiff::FAsyncDetailViewDiff(TSharedRef InLeftView, TSharedRef InRightView) : TAsyncTreeDifferences(RootNodesAttribute(InLeftView), RootNodesAttribute(InRightView)) , LeftView(InLeftView) , RightView(InRightView) {} void FAsyncDetailViewDiff::GetPropertyDifferences(TArray& OutDiffEntries) const { ForEach(ETreeTraverseOrder::PreOrder, [&](const TUniquePtr& Node)->ETreeTraverseControl { FPropertyPath PropertyPath; FPropertyPath RightPropertyPath; if (const TSharedPtr LeftTreeNode = Node->ValueA.Pin()) { PropertyPath = LeftTreeNode->GetPropertyPath(); } else if (const TSharedPtr RightTreeNode = Node->ValueB.Pin()) { PropertyPath = RightTreeNode->GetPropertyPath(); } // only include tree nodes with properties if (!PropertyPath.IsValid()) { return ETreeTraverseControl::Continue; } EPropertyDiffType::Type PropertyDiffType; switch (Node->DiffResult) { case ETreeDiffResult::MissingFromTree1: PropertyDiffType = EPropertyDiffType::PropertyAddedToB; break; case ETreeDiffResult::MissingFromTree2: PropertyDiffType = EPropertyDiffType::PropertyAddedToA; break; case ETreeDiffResult::DifferentValues: PropertyDiffType = EPropertyDiffType::PropertyValueChanged; break; default: // only include changes return ETreeTraverseControl::Continue; } OutDiffEntries.Add(FSingleObjectDiffEntry(FPropertySoftPath(PropertyPath), PropertyDiffType)); // only include top-most properties for adds and removes return (Node->DiffResult == ETreeDiffResult::DifferentValues) ? ETreeTraverseControl::Continue : ETreeTraverseControl::SkipChildren; }); } TPair FAsyncDetailViewDiff::ForEachRow(const TFunction&, int32, int32)>& Method) const { const TSharedPtr LeftDetailsView = LeftView.Pin(); const TSharedPtr RightDetailsView = RightView.Pin(); if (!LeftDetailsView || !RightDetailsView) { return {0,0}; } int32 LeftRowNum = 0; int32 RightRowNum = 0; ForEach( ETreeTraverseOrder::PreOrder, [&LeftDetailsView,&RightDetailsView,&Method,&LeftRowNum,&RightRowNum](const TUniquePtr& DiffNode)->ETreeTraverseControl { bool bFoundLeftRow = false; if (const TSharedPtr LeftTreeNode = DiffNode->ValueA.Pin()) { if (!LeftDetailsView->IsAncestorCollapsed(LeftTreeNode.ToSharedRef())) { bFoundLeftRow = true; } } bool bFoundRightRow = false; if (const TSharedPtr RightTreeNode = DiffNode->ValueB.Pin()) { if (!RightDetailsView->IsAncestorCollapsed(RightTreeNode.ToSharedRef())) { bFoundRightRow = true; } } ETreeTraverseControl Control = ETreeTraverseControl::SkipChildren; if (bFoundRightRow || bFoundLeftRow) { Control = Method(DiffNode, LeftRowNum, RightRowNum); } if (bFoundLeftRow) { ++LeftRowNum; } if (bFoundRightRow) { ++RightRowNum; } return Control; } ); return {LeftRowNum, RightRowNum}; } TArray FAsyncDetailViewDiff::GenerateScrollSyncRate() const { TArray MatchingRows; // iterate matching rows of both details panels simultaneously auto [LeftRowCount, RightRowCount] = ForEachRow( [&MatchingRows](const TUniquePtr& DiffNode, int32 LeftRow, int32 RightRow)->ETreeTraverseControl { // if both trees share this row, sync scrolling here if (DiffNode->ValueA.IsValid() && DiffNode->ValueB.IsValid()) { if (MatchingRows.IsEmpty() || (MatchingRows.Last().X != LeftRow && MatchingRows.Last().Y != RightRow)) { MatchingRows.Emplace(LeftRow, RightRow); } } return ETreeTraverseControl::Continue; } ); TArray FixedPoints; FixedPoints.Emplace(0.f, 0.f); // normalize fixed points for (FIntVector2& MatchingRow : MatchingRows) { FixedPoints.Emplace( StaticCast(MatchingRow.X) / LeftRowCount, StaticCast(MatchingRow.Y) / RightRowCount ); } FixedPoints.Emplace(1.f, 1.f); return FixedPoints; } TAttribute>> FAsyncDetailViewDiff::RootNodesAttribute(TWeakPtr DetailsView) { return TAttribute>>::CreateLambda([DetailsView]() { TArray> Result; if (const TSharedPtr Details = StaticCastSharedPtr(DetailsView.Pin())) { Details->GetHeadNodes(Result); } return Result; }); }