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

378 lines
13 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "DetailsDiff.h"
#include "BlueprintDetailsCustomization.h"
#include "BlueprintEditorLibrary.h"
#include "DetailCategoryBuilder.h"
#include "DetailLayoutBuilder.h"
#include "Algo/Copy.h"
#include "Algo/Transform.h"
#include "Containers/Set.h"
#include "DetailsViewArgs.h"
#include "DetailWidgetRow.h"
#include "HAL/PlatformCrt.h"
#include "IDetailsView.h"
#include "Misc/AssertionMacros.h"
#include "Modules/ModuleManager.h"
#include "PropertyEditorDelegates.h"
#include "PropertyEditorModule.h"
#include "PropertyPath.h"
#include "UObject/WeakObjectPtrTemplates.h"
#include "Engine/MemberReference.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Widgets/Layout/LinkableScrollBar.h"
#include "Widgets/Input/SComboButton.h"
#include "Engine/BlueprintGeneratedClass.h"
class UObject;
#define LOCTEXT_NAMESPACE "DetailsDif"
FDetailsDiff::FDetailsDiff(const UObject* InObject, bool bScrollbarOnLeft )
: DisplayedObject( InObject )
{
ScrollBar = SNew(SLinkableScrollBar);
DetailsView = CreateDetailsView(InObject, ScrollBar, bScrollbarOnLeft);
DisplayedProperties = DetailsView->GetPropertiesInOrderDisplayed();
}
PRAGMA_DISABLE_DEPRECATION_WARNINGS
FDetailsDiff::FDetailsDiff( const UObject* InObject, FDetailsDiff::FOnDisplayedPropertiesChanged InOnDisplayedPropertiesChanged, bool bScrollbarOnLeft )
: FDetailsDiff(InObject, bScrollbarOnLeft)
{
}
PRAGMA_ENABLE_DEPRECATION_WARNINGS
class DOBPDetailsCustomization : public IDetailCustomization
{
public:
DOBPDetailsCustomization(UBlueprint* InBlueprint) : Blueprint(InBlueprint)
{}
static TSharedRef<IDetailCustomization> Make(UBlueprint* Blueprint)
{
return MakeShared<DOBPDetailsCustomization>(Blueprint);
}
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override
{
if (!Blueprint.IsValid())
{
return;
}
IDetailCategoryBuilder& Category = DetailLayout.EditCategory("ClassOptions", LOCTEXT("ClassOptions", "Class Options"));
// put the ClassOptions first
Category.SetSortOrder(0);
// ParentClass is a hidden property so we have to add it to the property map manually to use it
const TSharedPtr<IPropertyHandle> ParentClassProperty = DetailLayout.AddObjectPropertyData({Blueprint.Get()}, TEXT("ParentClass"));
Category.AddCustomRow( LOCTEXT("ClassOptions", "Class Options") )
.NameContent()
[
SNew(STextBlock)
.Text(LOCTEXT("BlueprintDetails_ParentClass", "Parent Class"))
.Font(IDetailLayoutBuilder::GetDetailFont())
]
.ValueContent()
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.FillWidth(1.0f)
[
SAssignNew(ParentClassComboButton, SComboButton)
.IsEnabled(this, &DOBPDetailsCustomization::CanReparent)
.OnGetMenuContent(this, &DOBPDetailsCustomization::GetParentClassMenuContent)
.ButtonContent()
[
SNew(STextBlock)
.Text(this, &DOBPDetailsCustomization::GetParentClassName)
.Font(IDetailLayoutBuilder::GetDetailFont())
]
]
]
.PropertyHandleList({ParentClassProperty});
}
bool CanReparent() const
{
// Don't show the reparent option if it's an Interface or we're not in editing mode
return Blueprint.IsValid() && !FBlueprintEditorUtils::IsInterfaceBlueprint(Blueprint.Get()) && (BPTYPE_FunctionLibrary != Blueprint->BlueprintType);
}
TSharedRef<SWidget> GetParentClassMenuContent() const
{
if (!Blueprint.IsValid())
{
return SNew(SBox);
}
TArray<UBlueprint*> Blueprints;
Blueprints.Add(Blueprint.Get());
const TSharedRef<SWidget> ClassPicker = FBlueprintEditorUtils::ConstructBlueprintParentClassPicker(Blueprints, FOnClassPicked::CreateSP(this, &DOBPDetailsCustomization::OnClassPicked));
// Achieving fixed width by nesting items within a fixed width box.
return SNew(SBox)
.WidthOverride(350.0f)
[
SNew(SVerticalBox)
+SVerticalBox::Slot()
.MaxHeight(400.0f)
.AutoHeight()
[
ClassPicker
]
];
}
FText GetParentClassName() const
{
const UClass* ParentClass = Blueprint.IsValid() ? Blueprint->ParentClass : nullptr;
return ParentClass ? ParentClass->GetDisplayNameText() : FText::FromName(NAME_None);
}
void OnClassPicked(UClass* PickedClass) const
{
ParentClassComboButton->SetIsOpen(false);
UBlueprintEditorLibrary::ReparentBlueprint(Blueprint.Get(), PickedClass);
}
private:
TWeakObjectPtr<UBlueprint> Blueprint;
/** Combo button used to choose a parent class */
TSharedPtr<SComboButton> ParentClassComboButton;
};
TSharedRef<IDetailsView> FDetailsDiff::CreateDetailsView(const UObject* InObject, TSharedPtr<SScrollBar> ExternalScrollbar, bool bScrollbarOnLeft)
{
FDetailsViewArgs DetailsViewArgs;
DetailsViewArgs.bShowDifferingPropertiesOption = true;
DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea;
DetailsViewArgs.ExternalScrollbar = ExternalScrollbar;
DetailsViewArgs.ScrollbarAlignment = bScrollbarOnLeft ? HAlign_Left : HAlign_Right;
FPropertyEditorModule& EditModule = FModuleManager::Get().GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
TSharedRef<IDetailsView> DetailsView = EditModule.CreateDetailView(DetailsViewArgs);
DetailsView->SetIsPropertyEditingEnabledDelegate(FIsPropertyEditingEnabled::CreateStatic([]{return false; }));
if (InObject && InObject->IsA<UBlueprint>())
{
TWeakObjectPtr<UBlueprint> ForCapture = const_cast<UBlueprint*>(Cast<UBlueprint>(InObject));
// create a custom property layout so that sections like interfaces will be included in the diff view
const FOnGetDetailCustomizationInstance LayoutOptionDetails = FOnGetDetailCustomizationInstance::CreateStatic(
&FBlueprintGlobalOptionsDetails::MakeInstanceForDiff,
ForCapture
);
DetailsView->RegisterInstancedCustomPropertyLayout(UBlueprint::StaticClass(), LayoutOptionDetails);
}
// if InObject is a BP-CDO, add a "Parent Class" Combo button
if (InObject && Cast<UObject>(InObject->GetClass())->IsA<UBlueprintGeneratedClass>() )
{
UBlueprintGeneratedClass* Class = Cast<UBlueprintGeneratedClass>(InObject->GetClass());
if (Class->GetDefaultObject() == InObject)
{
const FOnGetDetailCustomizationInstance LayoutOptionDetails = FOnGetDetailCustomizationInstance::CreateStatic(
&DOBPDetailsCustomization::Make,
Cast<UBlueprint>(Class->ClassGeneratedBy)
);
DetailsView->RegisterInstancedCustomPropertyLayout(Class, LayoutOptionDetails);
}
}
// Forcing all advanced properties to be displayed for now, the logic to show changes made to advance properties
// conditionally is fragile and low priority for now:
DetailsView->ShowAllAdvancedProperties();
// This is a read only details view (see the property editing delegate), but const correctness at the details view
// level would stress the type system a little:
DetailsView->SetObject(const_cast<UObject*>(InObject));
return DetailsView;
}
void FDetailsDiff::HighlightProperty(const FPropertySoftPath& PropertyName)
{
// resolve the property soft path:
const FPropertyPath ResolvedProperty = PropertyName.ResolvePath(DisplayedObject);
DetailsView->HighlightProperty(ResolvedProperty);
}
TSharedRef< IDetailsView > FDetailsDiff::DetailsWidget() const
{
return DetailsView.ToSharedRef();
}
TArray<FPropertySoftPath> FDetailsDiff::GetDisplayedProperties() const
{
TArray<FPropertySoftPath> Ret;
Algo::Copy(DisplayedProperties, Ret);
return Ret;
}
void FDetailsDiff::DiffAgainst(const FDetailsDiff& Newer, TArray< FSingleObjectDiffEntry > &OutDifferences, bool bSortByDisplayOrder) const
{
if (!DisplayedObject || !Newer.DisplayedObject)
{
return;
}
TSharedPtr< class IDetailsView > OldDetailsView = DetailsView;
TSharedPtr< class IDetailsView > NewDetailsView = Newer.DetailsView;
const TArray<TWeakObjectPtr<UObject>>& OldSelectedObjects = OldDetailsView->GetSelectedObjects();
const TArray<TWeakObjectPtr<UObject>>& NewSelectedObjects = NewDetailsView->GetSelectedObjects();
const TArray<FPropertyPath> OldProperties = OldDetailsView->GetPropertiesInOrderDisplayed();
const TArray<FPropertyPath> NewProperties = NewDetailsView->GetPropertiesInOrderDisplayed();
TSet<FPropertySoftPath> OldPropertiesSet;
TSet<FPropertySoftPath> NewPropertiesSet;
Algo::Transform(OldProperties, OldPropertiesSet, [](const FPropertyPath& Entry) { return FPropertySoftPath(Entry); });
Algo::Transform(NewProperties, NewPropertiesSet, [](const FPropertyPath& Entry) { return FPropertySoftPath(Entry); });
// detect removed properties:
const TSet<FPropertySoftPath> RemovedProperties = OldPropertiesSet.Difference(NewPropertiesSet);
for (const FPropertySoftPath& RemovedProperty : RemovedProperties)
{
// @todo: (doc) label these as removed, rather than added to a
FSingleObjectDiffEntry Entry(RemovedProperty, EPropertyDiffType::PropertyAddedToA);
OutDifferences.Push(Entry);
}
// detect added properties:
const TSet<FPropertySoftPath> AddedProperties = NewPropertiesSet.Difference(OldPropertiesSet);
for (const FPropertySoftPath& AddedProperty : AddedProperties)
{
FSingleObjectDiffEntry Entry(AddedProperty, EPropertyDiffType::PropertyAddedToB);
OutDifferences.Push(Entry);
}
// check for changed properties
const TSet<FPropertySoftPath> CommonProperties = NewPropertiesSet.Intersect(OldPropertiesSet);
for (const FPropertySoftPath& CommonProperty : CommonProperties)
{
// get value, diff:
check(NewSelectedObjects.Num() == 1);
FResolvedProperty OldProperty = CommonProperty.Resolve(OldSelectedObjects[0].Get());
FResolvedProperty NewProperty = CommonProperty.Resolve(NewSelectedObjects[0].Get());
const UPackage* OldPackage = OldSelectedObjects[0]->GetPackage();
const UPackage* NewPackage = NewSelectedObjects[0]->GetPackage();
TArray<FPropertySoftPath> DifferingSubProperties;
if (!DiffUtils::Identical(OldProperty, NewProperty, OldPackage, NewPackage, DiffUtils::FDiffParameters(CommonProperty), DifferingSubProperties))
{
for (const FPropertySoftPath& DifferingSubProperty : DifferingSubProperties)
{
OutDifferences.Push(FSingleObjectDiffEntry(DifferingSubProperty, EPropertyDiffType::PropertyValueChanged));
}
}
}
if (bSortByDisplayOrder)
{
TArray<FSingleObjectDiffEntry> AllDifferingProperties = OutDifferences;
OutDifferences.Reset();
// OrderedProperties will contain differences in the order they are displayed:
TArray< const FSingleObjectDiffEntry* > OrderedProperties;
// create differing properties list based on what is displayed by the old properties..
TArray<FPropertySoftPath> SoftOldProperties = GetDisplayedProperties();
TArray<FPropertySoftPath> SoftNewProperties = Newer.GetDisplayedProperties();
const auto FindAndPushDiff = [&OrderedProperties, &AllDifferingProperties](const FPropertySoftPath& PropertyIdentifier) -> bool
{
bool bDiffers = false;
for (const auto& Difference : AllDifferingProperties)
{
if (Difference.Identifier == PropertyIdentifier)
{
bDiffers = true;
// if there are any nested differences associated with PropertyIdentifier, add those
// as well:
OrderedProperties.AddUnique(&Difference);
}
else if (Difference.Identifier.IsSubPropertyMatch(PropertyIdentifier))
{
bDiffers = true;
OrderedProperties.AddUnique(&Difference);
}
}
return bDiffers;
};
// zip the two sets of properties, zip iterators are not trivial to write in c++,
// so this procedural stuff will have to do:
int IterOld = 0;
int IterNew = 0;
while (IterOld < SoftOldProperties.Num() || IterNew < SoftNewProperties.Num())
{
const bool bOldIterValid = IterOld < SoftOldProperties.Num();
const bool bNewIterValid = IterNew < SoftNewProperties.Num();
// We've reached the end of the new list, but still have properties in the old list.
// Continue over the old list to catch any remaining diffs.
if (bOldIterValid && !bNewIterValid)
{
FindAndPushDiff(SoftOldProperties[IterOld]);
++IterOld;
}
// We've reached the end of the old list, but still have properties in the new list.
// Continue over the new list to catch any remaining diffs.
else if (!bOldIterValid && bNewIterValid)
{
FindAndPushDiff(SoftNewProperties[IterNew]);
++IterNew;
}
else
{
// If both properties have the same path, check to ensure the property hasn't changed.
if (SoftOldProperties[IterOld] == SoftNewProperties[IterNew])
{
FindAndPushDiff(SoftOldProperties[IterOld]);
++IterNew;
++IterOld;
}
else
{
// If the old property is different, add it to the list and increment the old iter.
// This indicates the property was removed.
if (FindAndPushDiff(SoftOldProperties[IterOld]))
{
++IterOld;
}
// If the new property is different, add it to the list and increment the new iter.
// This indicates the property was added.
else if (FindAndPushDiff(SoftNewProperties[IterNew]))
{
++IterNew;
}
// Neither property was different.
// This indicates the iterators were just out of step from a previous addition or removal.
else
{
++IterOld;
++IterNew;
}
}
}
}
// Readd to OutDifferences
for (const FSingleObjectDiffEntry* Difference : OrderedProperties)
{
OutDifferences.Add(*Difference);
}
}
}
void FDetailsDiff::LinkScrolling(FDetailsDiff& LeftPanel, FDetailsDiff& RightPanel,
const TAttribute<TArray<FVector2f>>& ScrollRate)
{
SLinkableScrollBar::LinkScrollBars(LeftPanel.ScrollBar.ToSharedRef(), RightPanel.ScrollBar.ToSharedRef(), ScrollRate);
}
#undef LOCTEXT_NAMESPACE