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

1006 lines
34 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SDetailsSplitter.h"
#include "AsyncDetailViewDiff.h"
#include "DetailTreeNode.h"
#include "Editor.h"
#include "IDetailsViewPrivate.h"
#include "PropertyNode.h"
#include "Styling/StyleColors.h"
#include "Framework/Application/SlateApplication.h"
#include "Serialization/ObjectWriter.h"
#define LOCTEXT_NAMESPACE "DetailsSplitter"
namespace DetailsSplitterHelpers
{
const TArray<TWeakObjectPtr<UObject>>* GetObjects(const TSharedPtr<FDetailTreeNode>& TreeNode)
{
if (TSharedPtr<const IDetailsViewPrivate> DetailsView = TreeNode->GetDetailsViewSharedPtr())
{
return &DetailsView->GetSelectedObjects();
}
return nullptr;
}
// converts from a container helper like FScriptArrayHelper to a property like FArrayProperty
template<typename HelperType>
using TContainerPropertyType =
std::conditional_t<std::is_same_v<HelperType, FScriptArrayHelper>, FArrayProperty,
std::conditional_t<std::is_same_v<HelperType, FScriptMapHelper>, FMapProperty,
std::conditional_t<std::is_same_v<HelperType, FScriptSetHelper>, FSetProperty,
void>>>;
template<typename ContainerPropertyType>
bool TryGetSourceContainer(TSharedPtr<FDetailTreeNode> DetailsNode, FPropertyNode*& OutPropNode)
{
if ((OutPropNode = DetailsNode->GetPropertyNode().Get()) == nullptr)
{
return false;
}
if ((OutPropNode = OutPropNode->GetParentNode()) == nullptr)
{
return false;
}
if (CastField<ContainerPropertyType>(OutPropNode->GetProperty()) != nullptr)
{
return true;
}
return false;
}
template<typename ContainerPropertyType>
bool TryGetDestinationContainer(TSharedPtr<FDetailTreeNode> DetailsNode, FPropertyNode*& OutPropNode, int32& OutInsertIndex)
{
if ((OutPropNode = DetailsNode->GetPropertyNode().Get()) == nullptr)
{
return false;
}
if (CastField<ContainerPropertyType>(OutPropNode->GetProperty()) != nullptr)
{
OutInsertIndex = 0;
return true;
}
OutInsertIndex = OutPropNode->GetArrayIndex() + 1;
while((OutPropNode = OutPropNode->GetParentNode()) != nullptr)
{
if (CastField<ContainerPropertyType>(OutPropNode->GetProperty()) != nullptr)
{
return true;
}
OutInsertIndex = OutPropNode->GetArrayIndex() + 1;
}
return false;
}
template<typename HelperType>
bool TryGetSourceContainer(TSharedPtr<FDetailTreeNode> DetailsNode, FPropertyNode*& OutPropNode, TArray<TUniquePtr<HelperType>>& OutContainerHelper)
{
using ContainerPropertyType = TContainerPropertyType<HelperType>;
if (TryGetSourceContainer<ContainerPropertyType>(DetailsNode, OutPropNode))
{
const FPropertySoftPath SoftPropertyPath(FPropertyNode::CreatePropertyPath(OutPropNode->AsShared()).Get());
if (const TArray<TWeakObjectPtr<UObject>>* Objects = DetailsSplitterHelpers::GetObjects(DetailsNode))
{
for (const TWeakObjectPtr<UObject> WeakObject : *Objects)
{
if (const UObject* Object = WeakObject.Get())
{
const FResolvedProperty Resolved = SoftPropertyPath.Resolve(Object);
const ContainerPropertyType* ContainerProperty = CastFieldChecked<ContainerPropertyType>(Resolved.Property);
OutContainerHelper.Add(MakeUnique<HelperType>(ContainerProperty, ContainerProperty->template ContainerPtrToValuePtr<UObject*>(Resolved.Object)));
}
}
}
return true;
}
return false;
}
template<typename HelperType>
bool TryGetDestinationContainer(TSharedPtr<FDetailTreeNode> DetailsNode, FPropertyNode*& OutPropNode, TArray<TUniquePtr<HelperType>>& OutContainerHelper, int32& OutInsertIndex)
{
using ContainerPropertyType = TContainerPropertyType<HelperType>;
if (TryGetDestinationContainer<ContainerPropertyType>(DetailsNode, OutPropNode, OutInsertIndex))
{
const FPropertySoftPath SoftPropertyPath(FPropertyNode::CreatePropertyPath(OutPropNode->AsShared()).Get());
if (const TArray<TWeakObjectPtr<UObject>>* Objects = DetailsSplitterHelpers::GetObjects(DetailsNode))
{
for (const TWeakObjectPtr<UObject> WeakObject : *Objects)
{
if (const UObject* Object = WeakObject.Get())
{
const FResolvedProperty Resolved = SoftPropertyPath.Resolve(Object);
const ContainerPropertyType* ContainerProperty = CastFieldChecked<ContainerPropertyType>(Resolved.Property);
OutContainerHelper.Add(MakeUnique<HelperType>(ContainerProperty, ContainerProperty->template ContainerPtrToValuePtr<UObject*>(Resolved.Object)));
}
}
}
return true;
}
return false;
}
void CopyPropertyValueForInsert(const TSharedPtr<FDetailTreeNode>& SourceDetailsNode, const TSharedPtr<FDetailTreeNode>& DestinationDetailsNode)
{
const TSharedPtr<IPropertyHandle> DestinationHandle = DestinationDetailsNode->CreatePropertyHandle();
const TSharedPtr<IPropertyHandle> SourceHandle = SourceDetailsNode->CreatePropertyHandle();
// Array
TArray<TUniquePtr<FScriptArrayHelper>> SourceArrays;
FPropertyNode* SourceArrayPropertyNode;
if (TryGetSourceContainer(SourceDetailsNode, SourceArrayPropertyNode, SourceArrays))
{
int32 InsertIndex;
TArray<TUniquePtr<FScriptArrayHelper>> DestinationArrays;
FPropertyNode* DestinationArrayPropertyNode;
if (ensure(TryGetDestinationContainer(DestinationDetailsNode, DestinationArrayPropertyNode, DestinationArrays, InsertIndex)))
{
ensure(SourceArrays.Num() == DestinationArrays.Num());
GEditor->BeginTransaction(TEXT("DetailsSplitter"), FText::Format(LOCTEXT("InsertPropertyValueTransaction","Insert {0}"), SourceHandle->GetPropertyDisplayName()), nullptr);
DestinationHandle->NotifyPreChange();
for (int32 ArrayNum = 0; ArrayNum < SourceArrays.Num(); ++ ArrayNum)
{
DestinationArrays[ArrayNum]->InsertValues(InsertIndex, 1);
const void* SourceData = SourceArrays[ArrayNum]->GetElementPtr(SourceDetailsNode->GetPropertyNode()->GetArrayIndex());
void* DestinationData = DestinationArrays[ArrayNum]->GetElementPtr(InsertIndex);
const FArrayProperty* ArrayProperty = CastFieldChecked<FArrayProperty>(DestinationArrayPropertyNode->GetProperty());
const FProperty* ElementProperty = ArrayProperty->Inner;
ElementProperty->CopySingleValue(DestinationData, SourceData);
}
DestinationHandle->NotifyPostChange(EPropertyChangeType::ArrayAdd);
DestinationHandle->NotifyFinishedChangingProperties();
GEditor->EndTransaction();
}
return;
}
// Set
TArray<TUniquePtr<FScriptSetHelper>> SourceSets;
FPropertyNode* SourceSetPropertyNode;
if (TryGetSourceContainer(SourceDetailsNode, SourceSetPropertyNode, SourceSets))
{
int32 InsertIndex;
TArray<TUniquePtr<FScriptSetHelper>> DestinationSets;
FPropertyNode* DestinationPropertyNode;
if (ensure(TryGetDestinationContainer(DestinationDetailsNode, DestinationPropertyNode, DestinationSets, InsertIndex)))
{
ensure(SourceSets.Num() == DestinationSets.Num());
GEditor->BeginTransaction(TEXT("DetailsSplitter"), FText::Format(LOCTEXT("InsertPropertyValueTransaction","Insert {0}"), SourceHandle->GetPropertyDisplayName()), nullptr);
DestinationHandle->NotifyPreChange();
for (int32 SetNum = 0; SetNum < SourceSets.Num(); ++ SetNum)
{
const void* SourceData = SourceSets[SetNum]->FindNthElementPtr(SourceDetailsNode->GetPropertyNode()->GetArrayIndex());
DestinationSets[SetNum]->AddElement(SourceData);
}
DestinationHandle->NotifyPostChange(EPropertyChangeType::ArrayAdd);
DestinationHandle->NotifyFinishedChangingProperties();
GEditor->EndTransaction();
}
return;
}
// Map
TArray<TUniquePtr<FScriptMapHelper>> SourceMaps;
FPropertyNode* SourceMapPropertyNode;
if (TryGetSourceContainer(SourceDetailsNode, SourceMapPropertyNode, SourceMaps))
{
int32 InsertIndex;
TArray<TUniquePtr<FScriptMapHelper>> DestinationMaps;
FPropertyNode* DestinationPropertyNode;
if (ensure(TryGetDestinationContainer(DestinationDetailsNode, DestinationPropertyNode, DestinationMaps, InsertIndex)))
{
ensure(SourceMaps.Num() == DestinationMaps.Num());
GEditor->BeginTransaction(TEXT("DetailsSplitter"), FText::Format(LOCTEXT("InsertPropertyValueTransaction","Insert {0}"), SourceHandle->GetPropertyDisplayName()), nullptr);
DestinationHandle->NotifyPreChange();
for (int32 MapNum = 0; MapNum < SourceMaps.Num(); ++ MapNum)
{
const int32 Index = SourceMaps[MapNum]->FindInternalIndex(SourceDetailsNode->GetPropertyNode()->GetArrayIndex());
const void* SourceKey = SourceMaps[MapNum]->GetKeyPtr(Index);
const void* SourceVal = SourceMaps[MapNum]->GetValuePtr(Index);
DestinationMaps[MapNum]->AddPair(SourceKey, SourceVal);
}
DestinationHandle->NotifyPostChange(EPropertyChangeType::ArrayAdd);
DestinationHandle->NotifyFinishedChangingProperties();
GEditor->EndTransaction();
}
return;
}
}
void AssignPropertyValue(const TSharedPtr<IPropertyHandle>& SourceHandle, const TSharedPtr<IPropertyHandle>& DestinationHandle)
{
TArray<FString> SourceValues;
SourceHandle->GetPerObjectValues(SourceValues);
DestinationHandle->SetPerObjectValues(SourceValues);
}
void CopyPropertyValue(const TSharedPtr<FDetailTreeNode>& SourceDetailsNode, const TSharedPtr<FDetailTreeNode>& DestinationDetailsNode, ETreeDiffResult Diff)
{
switch(Diff)
{
// traditional copy
case ETreeDiffResult::DifferentValues:
break;
// insert
case ETreeDiffResult::MissingFromTree1:
case ETreeDiffResult::MissingFromTree2:
CopyPropertyValueForInsert(SourceDetailsNode, DestinationDetailsNode);
return;
// no difference
case ETreeDiffResult::Invalid:
case ETreeDiffResult::Identical:
default:
return;
}
const TSharedPtr<IPropertyHandle> SourceHandle = SourceDetailsNode->CreatePropertyHandle();
const TSharedPtr<IPropertyHandle> DestinationHandle = DestinationDetailsNode->CreatePropertyHandle();
if (!ensure(SourceHandle && DestinationHandle))
{
return;
}
if (!SourceHandle->GetProperty()->SameType(DestinationHandle->GetProperty()))
{
// convert types by assigning via text serialization
AssignPropertyValue(SourceHandle, DestinationHandle);
return;
}
TArray<void*> SourceData;
TArray<void*> DestinationData;
SourceHandle->AccessRawData(SourceData);
DestinationHandle->AccessRawData(DestinationData);
if (!ensure(SourceData.Num() == DestinationData.Num()))
{
return;
}
GEditor->BeginTransaction(TEXT("DetailsSplitter"), FText::Format(LOCTEXT("CopyPropertyValueTransaction","Copy {0}"), SourceHandle->GetPropertyDisplayName()), nullptr);
DestinationHandle->NotifyPreChange();
for (int32 I = 0; I < SourceData.Num(); ++I)
{
if (DestinationHandle->GetArrayIndex() != INDEX_NONE)
{
DestinationHandle->GetProperty()->CopySingleValue(DestinationData[I], SourceData[I]);
}
else
{
DestinationHandle->GetProperty()->CopyCompleteValue(DestinationData[I], SourceData[I]);
}
}
DestinationHandle->NotifyPostChange(EPropertyChangeType::ValueSet);
DestinationHandle->NotifyFinishedChangingProperties();
GEditor->EndTransaction();
}
// note: DestinationDetailsNode is the node before the position in the tree you wish to insert
bool CanCopyPropertyValueForInsert(const TSharedPtr<FDetailTreeNode>& SourceDetailsNode, const TSharedPtr<FDetailTreeNode>& DestinationDetailsNode)
{
FPropertyNode* SourceArrayPropertyNode;
if (TryGetSourceContainer<FArrayProperty>(SourceDetailsNode, SourceArrayPropertyNode))
{
FPropertyNode* DestinationArrayPropertyNode;
int32 InsertIndex;
if (TryGetDestinationContainer<FArrayProperty>(DestinationDetailsNode, DestinationArrayPropertyNode, InsertIndex))
{
return true;
}
return false;
}
FPropertyNode* SourceSetPropertyNode;
if (TryGetSourceContainer<FSetProperty>(SourceDetailsNode, SourceSetPropertyNode))
{
FPropertyNode* DestinationSetPropertyNode;
int32 InsertIndex;
if (TryGetDestinationContainer<FSetProperty>(DestinationDetailsNode, DestinationSetPropertyNode, InsertIndex))
{
return true;
}
return false;
}
FPropertyNode* SourceMapPropertyNode;
if (TryGetSourceContainer<FMapProperty>(SourceDetailsNode, SourceMapPropertyNode))
{
FPropertyNode* DestinationMapPropertyNode;
int32 InsertIndex;
if (TryGetDestinationContainer<FMapProperty>(DestinationDetailsNode, DestinationMapPropertyNode, InsertIndex))
{
return true;
}
return false;
}
// you can only insert into containers
return false;
}
bool CanAssignPropertyValue(const TSharedPtr<IPropertyHandle>& SourceHandle, const TSharedPtr<IPropertyHandle>& DestinationHandle)
{
TArray<FString> SourceValues;
TArray<FString> OldDestinationValues;
SourceHandle->GetPerObjectValues(SourceValues);
DestinationHandle->GetPerObjectValues(OldDestinationValues);
FPropertyAccess::Result Result = DestinationHandle->SetPerObjectValues(SourceValues, EPropertyValueSetFlags::NotTransactable);
TArray<FString> ChangedDestinationValues;
DestinationHandle->GetPerObjectValues(ChangedDestinationValues);
// revert changes and query whether anything changed
DestinationHandle->SetPerObjectValues(OldDestinationValues, EPropertyValueSetFlags::NotTransactable);
return Result != FPropertyAccess::Fail && OldDestinationValues != ChangedDestinationValues;
}
bool CanCopyPropertyValue(const TSharedPtr<FDetailTreeNode>& SourceDetailsNode, const TSharedPtr<FDetailTreeNode>& DestinationDetailsNode, ETreeDiffResult Diff)
{
if (!SourceDetailsNode || !DestinationDetailsNode)
{
return false;
}
// in order to copy properties there needs to be the same number of objects in each panel
const TArray<TWeakObjectPtr<UObject>>* SourceObjects = DetailsSplitterHelpers::GetObjects(SourceDetailsNode);
const TArray<TWeakObjectPtr<UObject>>* DestinationObjects = DetailsSplitterHelpers::GetObjects(DestinationDetailsNode);
if (!SourceObjects || !DestinationObjects || SourceObjects->Num() != DestinationObjects->Num())
{
return false;
}
switch (Diff)
{
// traditional copy
case ETreeDiffResult::DifferentValues:
{
const TSharedPtr<IPropertyHandle> SourceHandle = SourceDetailsNode->CreatePropertyHandle();
const TSharedPtr<IPropertyHandle> DestinationHandle = DestinationDetailsNode->CreatePropertyHandle();
if (SourceHandle && DestinationHandle && SourceHandle->GetProperty() && DestinationHandle->GetProperty())
{
TArray<void*> SourceData;
TArray<void*> DestinationData;
SourceHandle->AccessRawData(SourceData);
DestinationHandle->AccessRawData(DestinationData);
if (SourceData == DestinationData && SourceHandle->GetProperty() == DestinationHandle->GetProperty())
{
// disable copying a value to itself since it's a no-op
return false;
}
if (SourceHandle->GetProperty()->SameType(DestinationHandle->GetProperty()))
{
return true;
}
if (CanAssignPropertyValue(SourceHandle, DestinationHandle))
{
return true;
}
}
return false;
}
// insert
case ETreeDiffResult::MissingFromTree1:
case ETreeDiffResult::MissingFromTree2:
return CanCopyPropertyValueForInsert(SourceDetailsNode, DestinationDetailsNode);
// no difference
case ETreeDiffResult::Invalid:
case ETreeDiffResult::Identical:
default:
return false;
}
}
}
void SDetailsSplitter::Construct(const FArguments& InArgs)
{
Splitter = SNew(SSplitter).PhysicalSplitterHandleSize(5.f);
GetRowHighlightColor = InArgs._RowHighlightColor.IsBound() ? InArgs._RowHighlightColor : FRowHighlightColor::CreateStatic(
[](const TUniquePtr<FAsyncDetailViewDiff::DiffNodeType>&)
{
return FLinearColor(0.f, 1.f, 1.f, .7f);
});
GetShouldHighlightRow = InArgs._ShouldHighlightRow.IsBound() ? InArgs._ShouldHighlightRow : FShouldHighlightRow::CreateStatic(
[](const TUniquePtr<FAsyncDetailViewDiff::DiffNodeType>& DiffNode)
{
return DiffNode->DiffResult != ETreeDiffResult::Identical;
});
for(const FSlot::FSlotArguments& SlotArgs : InArgs._Slots)
{
AddSlot(SlotArgs);
}
ChildSlot
[
Splitter.ToSharedRef()
];
}
void SDetailsSplitter::AddSlot(const FSlot::FSlotArguments& SlotArgs, int32 Index)
{
if (Index == INDEX_NONE)
{
Index = Panels.Num();
}
Splitter->AddSlot(Index)
.Value(SlotArgs._Value)
[
SNew(SBox).Padding(15.f,0.f, 15.f,0.f)
[
SlotArgs._DetailsView ? SlotArgs._DetailsView.ToSharedRef() : SNullWidget::NullWidget
]
];
Panels.Insert({
SlotArgs._DetailsView,
SlotArgs._IsReadonly,
SlotArgs._DifferencesWithRightPanel,
SlotArgs._ShouldIgnoreRow.IsBound() ? SlotArgs._ShouldIgnoreRow : FShouldIgnoreRow::CreateStatic([](const TWeakPtr<FDetailTreeNode>&){return false;})
}, Index);
}
SDetailsSplitter::FPanel& SDetailsSplitter::GetPanel(int32 Index)
{
return Panels[Index];
}
// find the inner-most object in the property path and shorten the path relative to that object
static void ShortenToPathFromLastObject(const UObject*& InOutObject, FPropertyPath& InOutPath)
{
TArray<FPropertyInfo> PathFromSubObjectReversed;
PathFromSubObjectReversed.Add(InOutPath.GetLeafMostProperty());
InOutPath = *InOutPath.TrimPath(1);
while (InOutPath.IsValid())
{
// Note that if we have perf issues, this could be made faster by writing a custom resolve function that stops
// at the last FObjectProperty. that way we wouldn't need to resolve multiple times
const FResolvedProperty Resolved = FPropertySoftPath(InOutPath).Resolve(InOutObject);
if (const FObjectProperty* ObjectProperty = CastField<FObjectProperty>(Resolved.Property))
{
InOutObject = ObjectProperty->GetObjectPropertyValue(ObjectProperty->ContainerPtrToValuePtr<void>(Resolved.Object));
break;
}
PathFromSubObjectReversed.Add(InOutPath.GetLeafMostProperty());
InOutPath = *InOutPath.TrimPath(1);
}
InOutPath = {};
while(!PathFromSubObjectReversed.IsEmpty())
{
InOutPath.AddProperty(PathFromSubObjectReversed.Pop());
}
}
void SDetailsSplitter::HighlightFromMergeResults(const TMap<FString, TMap<FPropertySoftPath, ETreeDiffResult>>& CustomHighlights)
{
GetRowHighlightColor = FRowHighlightColor::CreateLambda([CustomHighlights](const TUniquePtr<FAsyncDetailViewDiff::DiffNodeType>& DiffNode)
{
const TSharedPtr<FDetailTreeNode> DetailNode = DiffNode->ValueA.IsValid() ? DiffNode->ValueA.Pin() : DiffNode->ValueB.Pin();
FPropertyPath Path = DetailNode->GetPropertyPath();
const UObject* OwningObject = DetailNode->GetDetailsViewSharedPtr()->GetSelectedObjects()[0].Get();
if (OwningObject && Path.IsValid())
{
FPropertyPath PathFromSubObject;
ShortenToPathFromLastObject(OwningObject, Path);
if (OwningObject)
{
if (const TMap<FPropertySoftPath, ETreeDiffResult>* Highlights = CustomHighlights.Find(OwningObject->GetPathName(OwningObject->GetPackage())))
{
if (const ETreeDiffResult* DiffResult = Highlights->Find(FPropertySoftPath(Path)))
{
switch(*DiffResult)
{
case ETreeDiffResult::MissingFromTree1: // fall through
case ETreeDiffResult::MissingFromTree2: // fall through
case ETreeDiffResult::DifferentValues:
// color is intentionally using values greater than 1 so that it stays very saturated
return FLinearColor(1.5f, 0.3f, 0.3f);
default:; // ignore identical and invalid
}
}
}
}
}
return FLinearColor(0.f, 1.f, 1.f, .7f);
});
}
void SDetailsSplitter::SetRowHighlightColorDelegate(const FRowHighlightColor& Delegate)
{
GetRowHighlightColor = Delegate;
}
SDetailsSplitter::FSlot::FSlotArguments SDetailsSplitter::Slot()
{
return FSlot::FSlotArguments(MakeUnique<FSlot>());
}
int32 SDetailsSplitter::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId,
const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
int32 MaxLayerId = LayerId;
MaxLayerId = Splitter->Paint(Args,AllottedGeometry,MyCullingRect,OutDrawElements,MaxLayerId,InWidgetStyle,bParentEnabled);
++MaxLayerId;
TMap<FSlateRect, FLinearColor> RowHighlights;
for (int32 LeftIndex = 0; LeftIndex < Panels.Num(); ++LeftIndex)
{
const FPanel& LeftPanel = Panels[LeftIndex];
if (!LeftPanel.DiffRight.IsSet())
{
continue;
}
if (const TSharedPtr<FAsyncDetailViewDiff> Diff = LeftPanel.DiffRight.Get())
{
FSlateRect PrevLeftPropertyRect;
FSlateRect PrevRightPropertyRect;
TSharedPtr<FDetailTreeNode> LastSeenRightDetailsNode;
TSharedPtr<FDetailTreeNode> LastSeenLeftDetailsNode;
Diff->ForEachRow([&](const TUniquePtr<FAsyncDetailViewDiff::DiffNodeType>& DiffNode, int32, int32)->ETreeTraverseControl
{
const bool bShouldHighlightRow = GetShouldHighlightRow.Execute(DiffNode);
const FLinearColor Color = GetRowHighlightColor.Execute(DiffNode);
FSlateRect LeftPropertyRect;
if (const TSharedPtr<FDetailTreeNode> LeftDetailNode = DiffNode->ValueA.Pin())
{
LastSeenLeftDetailsNode = LeftDetailNode;
// if the other tree doesn't have a matching node, treat this and all it's children as a single group
const bool bIncludeChildren = !DiffNode->ValueB.IsValid();
LeftPropertyRect = LeftPanel.DetailsView->GetPaintSpacePropertyBounds(LeftDetailNode.ToSharedRef(), bIncludeChildren);
}
if (!LeftPropertyRect.IsValid() && PrevLeftPropertyRect.IsValid())
{
LeftPropertyRect = PrevLeftPropertyRect;
LeftPropertyRect.Top = LeftPropertyRect.Bottom;
}
if (LeftPropertyRect.IsValid() && bShouldHighlightRow)
{
if (!LeftPanel.ShouldIgnoreRow.Execute(DiffNode->ValueA))
{
RowHighlights.Add(LeftPropertyRect, Color);
}
}
const int32 RightIndex = LeftIndex + 1;
FSlateRect RightPropertyRect;
if (Panels.IsValidIndex(RightIndex))
{
const FPanel& RightPanel = Panels[RightIndex];
if (const TSharedPtr<FDetailTreeNode> RightDetailNode = DiffNode->ValueB.Pin())
{
LastSeenRightDetailsNode = RightDetailNode;
// if the other tree doesn't have a matching node, treat this and all it's children as a single group
const bool bIncludeChildren = !DiffNode->ValueA.IsValid();
RightPropertyRect = RightPanel.DetailsView->GetPaintSpacePropertyBounds(RightDetailNode.ToSharedRef(), bIncludeChildren);
}
if (!RightPropertyRect.IsValid() && PrevRightPropertyRect.IsValid())
{
RightPropertyRect = PrevRightPropertyRect;
RightPropertyRect.Top = RightPropertyRect.Bottom;
}
if (RightPropertyRect.IsValid() && bShouldHighlightRow)
{
if (!RightPanel.ShouldIgnoreRow.Execute(DiffNode->ValueB))
{
RowHighlights.Add(RightPropertyRect, Color);
}
}
if (LeftPropertyRect.IsValid() && RightPropertyRect.IsValid() && bShouldHighlightRow)
{
if (!LeftPanel.ShouldIgnoreRow.Execute(DiffNode->ValueA) && !RightPanel.ShouldIgnoreRow.Execute(DiffNode->ValueB))
{
FLinearColor FillColor = Color.Desaturate(.3f) * FLinearColor(0.053f,0.053f,0.053f);
FillColor.A = 0.43f;
const FLinearColor OutlineColor = Color;
PaintPropertyConnector(OutDrawElements, MaxLayerId, LeftPropertyRect, RightPropertyRect, FillColor, OutlineColor);
++MaxLayerId;
if (!RightPanel.IsReadonly.Get(true) && DetailsSplitterHelpers::CanCopyPropertyValue(LastSeenLeftDetailsNode, LastSeenRightDetailsNode, DiffNode->DiffResult))
{
PaintCopyPropertyButton(OutDrawElements, MaxLayerId, DiffNode, LeftPropertyRect, RightPropertyRect, EPropertyCopyDirection::CopyLeftToRight);
}
if (!LeftPanel.IsReadonly.Get(true) && DetailsSplitterHelpers::CanCopyPropertyValue(LastSeenRightDetailsNode, LastSeenLeftDetailsNode, DiffNode->DiffResult))
{
PaintCopyPropertyButton(OutDrawElements, MaxLayerId, DiffNode, LeftPropertyRect, RightPropertyRect, EPropertyCopyDirection::CopyRightToLeft);
}
++MaxLayerId;
}
}
}
PrevLeftPropertyRect = MoveTemp(LeftPropertyRect);
PrevRightPropertyRect = MoveTemp(RightPropertyRect);
// only traverse children if both trees have them
if (!DiffNode->ValueA.IsValid() || !DiffNode->ValueB.IsValid())
{
return ETreeTraverseControl::SkipChildren;
}
return ETreeTraverseControl::Continue;
});
}
}
for (const auto& [PropertyRect, Color] : RowHighlights)
{
FLinearColor FillColor = Color.Desaturate(.3f) * FLinearColor(0.053f,0.053f,0.053f);
FillColor.A = 0.43f;
FPaintGeometry Geometry(
PropertyRect.GetTopLeft() + FVector2D{0.f,2.f},
PropertyRect.GetSize() - FVector2D{0.f,4.f},
1.f
);
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerId + 8, // draw in front of background but behind text, buttons, etc
Geometry,
FAppStyle::GetBrush("WhiteBrush"),
ESlateDrawEffect::None,
FillColor
);
}
return MaxLayerId;
}
FReply SDetailsSplitter::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
HoveredCopyButton = {};
const FVector2D MousePosition = MouseEvent.GetScreenSpacePosition();
for (int32 LeftIndex = 0; LeftIndex < Panels.Num() - 1; ++LeftIndex)
{
const FPanel& LeftPanel = Panels[LeftIndex];
if (!LeftPanel.DiffRight.IsSet())
{
continue;
}
const int32 RightIndex = LeftIndex + 1;
const FPanel& RightPanel = Panels[RightIndex];
if (LeftPanel.IsReadonly.Get(true) && RightPanel.IsReadonly.Get(true))
{
continue;
}
if (const TSharedPtr<FAsyncDetailViewDiff> Diff = LeftPanel.DiffRight.Get())
{
TSharedPtr<FDetailTreeNode> LastSeenRightDetailsNode;
TSharedPtr<FDetailTreeNode> LastSeenLeftDetailsNode;
Diff->ForEachRow([&LastSeenLeftDetailsNode, &LastSeenRightDetailsNode, &LeftPanel, &RightPanel, &MousePosition, &HoveredCopyButton = HoveredCopyButton]
(const TUniquePtr<FAsyncDetailViewDiff::DiffNodeType>& DiffNode, int32, int32)->ETreeTraverseControl
{
FSlateRect LeftPropertyRect;
if (const TSharedPtr<FDetailTreeNode> LeftDetailNode = DiffNode->ValueA.Pin())
{
LastSeenLeftDetailsNode = LeftDetailNode;
const bool bIncludeChildren = !DiffNode->ValueB.IsValid();
LeftPropertyRect = LeftPanel.DetailsView->GetTickSpacePropertyBounds(LeftDetailNode.ToSharedRef(), bIncludeChildren);
}
FSlateRect RightPropertyRect;
if (const TSharedPtr<FDetailTreeNode> RightDetailNode = DiffNode->ValueB.Pin())
{
LastSeenRightDetailsNode = RightDetailNode;
const bool bIncludeChildren = !DiffNode->ValueA.IsValid();
RightPropertyRect = RightPanel.DetailsView->GetTickSpacePropertyBounds(RightDetailNode.ToSharedRef(), bIncludeChildren);
}
if (DiffNode->DiffResult == ETreeDiffResult::Identical)
{
return ETreeTraverseControl::Continue;
}
if (LeftPropertyRect.IsValid() && !RightPanel.IsReadonly.Get(true))
{
const FSlateRect CopyButtonZoneLeftToRight = FSlateRect(
LeftPropertyRect.Right,
LeftPropertyRect.Top,
LeftPropertyRect.Right + 15.f,
LeftPropertyRect.Bottom
);
if (CopyButtonZoneLeftToRight.ContainsPoint(MousePosition))
{
if (DetailsSplitterHelpers::CanCopyPropertyValue(LastSeenLeftDetailsNode, LastSeenRightDetailsNode, DiffNode->DiffResult))
{
HoveredCopyButton = {
LastSeenLeftDetailsNode,
LastSeenRightDetailsNode,
DiffNode->DiffResult,
EPropertyCopyDirection::CopyLeftToRight
};
}
return ETreeTraverseControl::Break;
}
}
if (RightPropertyRect.IsValid() && !LeftPanel.IsReadonly.Get(true))
{
const FSlateRect CopyButtonZoneRightToLeft = FSlateRect(
RightPropertyRect.Left - 15.f,
RightPropertyRect.Top,
RightPropertyRect.Left,
RightPropertyRect.Bottom
);
if (CopyButtonZoneRightToLeft.ContainsPoint(MousePosition))
{
if (DetailsSplitterHelpers::CanCopyPropertyValue(LastSeenRightDetailsNode, LastSeenLeftDetailsNode, DiffNode->DiffResult))
{
HoveredCopyButton = {
LastSeenRightDetailsNode,
LastSeenLeftDetailsNode,
DiffNode->DiffResult,
EPropertyCopyDirection::CopyRightToLeft
};
}
return ETreeTraverseControl::Break;
}
}
return ETreeTraverseControl::Continue;
});
if (HoveredCopyButton.CopyDirection != EPropertyCopyDirection::Copy_None)
{
break;
}
}
}
return SCompoundWidget::OnMouseMove(MyGeometry, MouseEvent);
}
void SDetailsSplitter::OnMouseLeave(const FPointerEvent& MouseEvent)
{
HoveredCopyButton = {};
SCompoundWidget::OnMouseLeave(MouseEvent);
}
FReply SDetailsSplitter::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
if (MouseEvent.GetEffectingButton() != EKeys::LeftMouseButton)
{
return FReply::Unhandled();
}
if (HoveredCopyButton.CopyDirection == EPropertyCopyDirection::Copy_None)
{
return FReply::Unhandled();
}
DetailsSplitterHelpers::CopyPropertyValue(
HoveredCopyButton.SourceDetailsNode.Pin(),
HoveredCopyButton.DestinationDetailsNode.Pin(),
HoveredCopyButton.DiffResult);
return FReply::Handled();
}
void SDetailsSplitter::PaintPropertyConnector(FSlateWindowElementList& OutDrawElements, int32 LayerId, const FSlateRect& LeftPropertyRect,
const FSlateRect& RightPropertyRect, const FLinearColor& FillColor, const FLinearColor& OutlineColor) const
{
FVector2D TopLeft = LeftPropertyRect.GetTopRight();
FVector2D BottomLeft = LeftPropertyRect.GetBottomRight();
FVector2D TopRight = RightPropertyRect.GetTopLeft();
FVector2D BottomRight = RightPropertyRect.GetBottomLeft();
{
constexpr float YPadding = 2.f;
if (BottomLeft.Y - TopLeft.Y > YPadding * 2.f)
{
BottomLeft.Y -= YPadding;
TopLeft.Y += YPadding;
}
if (BottomRight.Y - TopRight.Y > YPadding * 2.f)
{
BottomRight.Y -= YPadding;
TopRight.Y += YPadding;
}
}
TArray<FSlateVertex> FillVerts;
TArray<SlateIndex> FillIndices;
TArray<FVector2D> TopBoarderLine;
TArray<FVector2D> BottomBoarderLine;
auto AddVert = [&FillVerts, &FillIndices](const FVector2D& Position, const FColor& VertColor)
{
FillVerts.Add(FSlateVertex::Make<ESlateVertexRounding::Disabled>(
FSlateRenderTransform(),
FVector2f(Position),
{0.f,0.f},
VertColor
));
if (FillVerts.Num() >= 3)
{
FillIndices.Add(FillVerts.Num()-3);
FillIndices.Add(FillVerts.Num()-2);
FillIndices.Add(FillVerts.Num()-1);
}
};
// interpolate between left and right corners and add vertices to the mesh
constexpr float StepSize = 1.f / 30.f;
constexpr float InterpBoarder = .3f; // make room for buttons
FillVerts.Empty();
FillIndices.Empty();
TopBoarderLine.Empty();
BottomBoarderLine.Empty();
float Alpha = 0.f;
while (true)
{
constexpr float Exp = 3.f;
const float TopAlpha = FMath::Clamp(Alpha, 0.f, 1.f);
const float BottomAlpha = FMath::Clamp(Alpha, 0.f, 1.f);
const double TopX = FMath::Lerp(TopLeft.X, TopRight.X, TopAlpha);
const double BottomX = FMath::Lerp(TopLeft.X, TopRight.X, BottomAlpha);
constexpr float InterpolationRange = 1.f - 2.f * InterpBoarder;
const float TopTransformedAlpha = FMath::Clamp((TopAlpha - InterpBoarder) / InterpolationRange, 0.f, 1.f);
const float BottomTransformedAlpha = FMath::Clamp((BottomAlpha - InterpBoarder) / InterpolationRange, 0.f, 1.f);
const double TopY = FMath::InterpEaseInOut(TopLeft.Y, TopRight.Y, TopTransformedAlpha, Exp);
const double BottomY = FMath::InterpEaseInOut(BottomLeft.Y, BottomRight.Y, BottomTransformedAlpha, Exp);
FLinearColor ColumnColor = FillColor;
if (Alpha <= 0.5f)
{
ColumnColor.A = FMath::InterpEaseOut(FillColor.A, 1.f, Alpha * 2.f, 2.f);
}
else
{
ColumnColor.A = FMath::InterpEaseIn( 1.f,FillColor.A, (Alpha - 0.5f) * 2.f, 2.f);
}
TopBoarderLine.Emplace(TopX,TopY);
FLinearColor TopColor = ColumnColor.Desaturate(0.5f);
AddVert(TopBoarderLine.Last(), TopColor.ToFColorSRGB());
BottomBoarderLine.Emplace(BottomX,BottomY);
FLinearColor BottomColor = ColumnColor;
AddVert(BottomBoarderLine.Last(), BottomColor.ToFColorSRGB());
if (Alpha >= 1.f)
{
break;
}
Alpha = FMath::Clamp(Alpha + StepSize, 0.f, 1.f);
}
const FSlateResourceHandle ResourceHandle = FSlateApplication::Get().GetRenderer()->GetResourceHandle(*FAppStyle::GetBrush("WhiteBrush"));
FSlateDrawElement::MakeCustomVerts(
OutDrawElements,
LayerId,
ResourceHandle,
FillVerts,
FillIndices,
nullptr,
0,
0,
ESlateDrawEffect::None
);
FSlateDrawElement::MakeLines(
OutDrawElements,
LayerId,
FPaintGeometry(),
TopBoarderLine,
ESlateDrawEffect::None,
OutlineColor,
true,
0.5f
);
FSlateDrawElement::MakeLines(
OutDrawElements,
LayerId,
FPaintGeometry(),
BottomBoarderLine,
ESlateDrawEffect::None,
OutlineColor,
true,
0.5f
);
}
void SDetailsSplitter::PaintCopyPropertyButton(FSlateWindowElementList& OutDrawElements, int32 LayerId, const TUniquePtr<FAsyncDetailViewDiff::DiffNodeType>& DiffNode,
const FSlateRect& LeftPropertyRect, const FSlateRect& RightPropertyRect, EPropertyCopyDirection CopyDirection) const
{
constexpr float ButtonScale = 15.f;
FPaintGeometry Geometry;
const FSlateBrush* Brush = nullptr;
FLinearColor ButtonColor = FStyleColors::Foreground.GetSpecifiedColor();
switch (CopyDirection)
{
case EPropertyCopyDirection::CopyLeftToRight:
if (LeftPropertyRect.GetSize().Y < UE_SMALL_NUMBER)
{
// space is too small to fit a button
return;
}
Geometry = FPaintGeometry(
FVector2D(LeftPropertyRect.Right, LeftPropertyRect.GetCenter().Y - 0.5f * ButtonScale),
FVector2D(ButtonScale),
1.f
);
Brush = FAppStyle::GetBrush("BlueprintDif.CopyPropertyRight");
if (HoveredCopyButton.CopyDirection == EPropertyCopyDirection::CopyLeftToRight)
{
if (HoveredCopyButton.SourceDetailsNode == DiffNode->ValueA)
{
ButtonColor = FStyleColors::ForegroundHover.GetSpecifiedColor();
}
}
break;
case EPropertyCopyDirection::CopyRightToLeft:
if (RightPropertyRect.GetSize().Y < UE_SMALL_NUMBER)
{
// space is too small to fit a button
return;
}
Geometry = FPaintGeometry(
FVector2D(RightPropertyRect.Left - ButtonScale, RightPropertyRect.GetCenter().Y - 0.5f * ButtonScale),
FVector2D(ButtonScale),
1.f
);
Brush = FAppStyle::GetBrush("BlueprintDif.CopyPropertyLeft");
if (HoveredCopyButton.CopyDirection == EPropertyCopyDirection::CopyRightToLeft)
{
if (HoveredCopyButton.SourceDetailsNode == DiffNode->ValueB)
{
ButtonColor = FStyleColors::ForegroundHover.GetSpecifiedColor();
}
}
break;
default:
return;
}
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerId, // draw in front of background but behind text, buttons, etc
Geometry,
Brush,
ESlateDrawEffect::None,
ButtonColor
);
}
#undef LOCTEXT_NAMESPACE