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

584 lines
17 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SDetailsDiff.h"
#include "AsyncDetailViewDiff.h"
#include "Editor.h"
#include "Widgets/Layout/SSplitter.h"
#include "Widgets/SOverlay.h"
#include "SlateOptMacros.h"
#include "Widgets/Layout/SSpacer.h"
#include "Framework/MultiBox/MultiBoxDefs.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "Styling/AppStyle.h"
#include "K2Node_MathExpression.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "DetailsDiff.h"
#include "DetailTreeNode.h"
#include "HAL/PlatformApplicationMisc.h"
#include "Framework/Application/SlateApplication.h"
#include "SBlueprintDiff.h"
#include "DiffControl.h"
#include "IDetailsView.h"
#include "SDetailsSplitter.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#define LOCTEXT_NAMESPACE "SDetailsDif"
typedef TMap< FName, const FProperty* > FNamePropertyMap;
static const FName DetailsMode = FName(TEXT("DetailsMode"));
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SDetailsDiff::Construct( const FArguments& InArgs)
{
check(InArgs._OldAsset || InArgs._NewAsset);
PanelOld.Object = InArgs._OldAsset;
PanelNew.Object = InArgs._NewAsset;
PanelOld.RevisionInfo = InArgs._OldRevision;
PanelNew.RevisionInfo = InArgs._NewRevision;
// sometimes we want to clearly identify the assets being diffed (when it's
// not the same asset in each panel)
PanelOld.bShowAssetName = InArgs._ShowAssetNames;
PanelNew.bShowAssetName = InArgs._ShowAssetNames;
OnCustomizeDetailsWidget = InArgs._OnCustomizeDetailsWidget;
OnGenerateCustomDiffEntries = InArgs._OnGenerateCustomDiffEntries;
OnGenerateCustomDiffEntryWidget = InArgs._OnGenerateCustomDiffEntryWidget;
OnOrganizeDiffEntries = InArgs._OnOrganizeDiffEntries;
ShouldHighlightRow = InArgs._ShouldHighlightRow;
RowHighlightColor = InArgs._RowHighlightColor;
if (InArgs._ParentWindow.IsValid())
{
WeakParentWindow = InArgs._ParentWindow;
AssetEditorCloseDelegate = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->OnAssetEditorRequestClose().AddSP(this, &SDetailsDiff::OnCloseAssetEditor);
}
FCoreUObjectDelegates::OnObjectsReplaced.AddSP(this, &SDetailsDiff::OnObjectReplaced);
FToolBarBuilder NavToolBarBuilder(TSharedPtr< const FUICommandList >(), FMultiBoxCustomization::None);
NavToolBarBuilder.AddToolBarButton(
FUIAction(
FExecuteAction::CreateSP(this, &SDetailsDiff::PrevDiff),
FCanExecuteAction::CreateSP( this, &SDetailsDiff::HasPrevDiff)
)
, NAME_None
, LOCTEXT("PrevDiffLabel", "Prev")
, LOCTEXT("PrevDiffTooltip", "Go to previous difference")
, FSlateIcon(FAppStyle::GetAppStyleSetName(), "BlueprintDif.PrevDiff")
);
NavToolBarBuilder.AddToolBarButton(
FUIAction(
FExecuteAction::CreateSP(this, &SDetailsDiff::NextDiff),
FCanExecuteAction::CreateSP(this, &SDetailsDiff::HasNextDiff)
)
, NAME_None
, LOCTEXT("NextDiffLabel", "Next")
, LOCTEXT("NextDiffTooltip", "Go to next difference")
, FSlateIcon(FAppStyle::GetAppStyleSetName(), "BlueprintDif.NextDiff")
);
DifferencesTreeView = DiffTreeView::CreateTreeView(&PrimaryDifferencesList);
GenerateDifferencesList();
const auto TextBlock = [](FText Text) -> TSharedRef<SWidget>
{
return SNew(SBox)
.Padding(FMargin(4.0f,10.0f))
.VAlign(VAlign_Center)
.HAlign(HAlign_Left)
[
SNew(STextBlock)
.Visibility(EVisibility::HitTestInvisible)
.TextStyle(FAppStyle::Get(), "DetailsView.CategoryTextStyle")
.Text(Text)
];
};
TopRevisionInfoWidget =
SNew(SSplitter)
.Visibility(EVisibility::HitTestInvisible)
+ SSplitter::Slot()
.Value(.2f)
[
SNew(SBox)
]
+ SSplitter::Slot()
.Value(.8f)
[
SNew(SSplitter)
.PhysicalSplitterHandleSize(10.0f)
+ SSplitter::Slot()
.Value(.5f)
[
TextBlock(DiffViewUtils::GetPanelLabel(PanelOld.Object, PanelOld.RevisionInfo, FText()))
]
+ SSplitter::Slot()
.Value(.5f)
[
TextBlock(DiffViewUtils::GetPanelLabel(PanelNew.Object, PanelNew.RevisionInfo, FText()))
]
];
this->ChildSlot
[
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush( "Docking.Tab", ".ContentAreaBrush" ))
[
SNew(SOverlay)
+ SOverlay::Slot()
.VAlign(VAlign_Top)
[
TopRevisionInfoWidget.ToSharedRef()
]
+ SOverlay::Slot()
[
SNew(SVerticalBox)
+SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 2.0f, 0.0f, 2.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.Padding(4.f)
.AutoWidth()
[
NavToolBarBuilder.MakeWidget()
]
+ SHorizontalBox::Slot()
[
SNew(SSpacer)
]
]
+ SVerticalBox::Slot()
[
SNew(SSplitter)
+ SSplitter::Slot()
.Value(.2f)
[
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
[
DifferencesTreeView.ToSharedRef()
]
]
+ SSplitter::Slot()
.Value(.8f)
[
SAssignNew(ModeContents, SBox)
]
]
]
]
];
SetCurrentMode(DetailsMode);
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
SDetailsDiff::~SDetailsDiff()
{
if (AssetEditorCloseDelegate.IsValid())
{
GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->OnAssetEditorRequestClose().Remove(AssetEditorCloseDelegate);
}
}
void SDetailsDiff::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
ModePanels[CurrentMode].DiffControl->Tick();
}
void SDetailsDiff::OnCloseAssetEditor(UObject* Asset, EAssetEditorCloseReason CloseReason)
{
if (PanelOld.Object == Asset || PanelNew.Object == Asset || CloseReason == EAssetEditorCloseReason::CloseAllAssetEditors)
{
// Tell our window to close and set our selves to collapsed to try and stop it from ticking
SetVisibility(EVisibility::Collapsed);
if (AssetEditorCloseDelegate.IsValid())
{
GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->OnAssetEditorRequestClose().Remove(AssetEditorCloseDelegate);
}
if (WeakParentWindow.IsValid())
{
WeakParentWindow.Pin()->RequestDestroyWindow();
}
}
}
void SDetailsDiff::OnObjectReplaced(const FCoreUObjectDelegates::FReplacementObjectMap& Replacements)
{
bool bNeedsRegenerate = false;
auto Refresh = [](const UObject* Obj, const UObject* From, const UObject* To)
{
bool bNeedsRegenerate = false;
if (Obj == From)
{
Obj = To;
bNeedsRegenerate = true;
}
if (Obj == From->GetClass()->ClassGeneratedBy)
{
bNeedsRegenerate = true;
}
return bNeedsRegenerate;
};
// if any of the objects being displayed were reinstanced, refresh them
for (const auto &[From, To] : Replacements)
{
bNeedsRegenerate |= Refresh(OutputObject, From, To);
bNeedsRegenerate |= Refresh(PanelNew.Object, From, To);
bNeedsRegenerate |= Refresh(PanelOld.Object, From, To);
}
if (bNeedsRegenerate)
{
GenerateDifferencesList();
RefreshCurrentModePanel();
}
}
TSharedRef<SWidget> SDetailsDiff::DefaultEmptyPanel()
{
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(LOCTEXT("BlueprintDifGraphsToolTip", "Select Graph to Diff"))
];
}
TSharedRef<SDetailsDiff> SDetailsDiff::CreateDiffWindow(FText WindowTitle, const UObject* OldObject, const UObject* NewObject, const FRevisionInfo& OldRevision, const FRevisionInfo& NewRevision)
{
// sometimes we're comparing different revisions of one single asset (other
// times we're comparing two completely separate assets altogether)
const bool bIsSingleAsset = !NewObject || !OldObject || (NewObject->GetName() == OldObject->GetName());
TSharedPtr<SWindow> Window = SNew(SWindow)
.Title(WindowTitle)
.ClientSize(FVector2D(1000.f, 800.f));
TSharedRef<SDetailsDiff> DetailsDiff = SNew(SDetailsDiff)
.OldAsset(OldObject)
.NewAsset(NewObject)
.OldRevision(OldRevision)
.NewRevision(NewRevision)
.ShowAssetNames(!bIsSingleAsset)
.ParentWindow(Window);
Window->SetContent(DetailsDiff);
// Make this window a child of the modal window if we've been spawned while one is active.
const TSharedPtr<SWindow> ActiveModal = FSlateApplication::Get().GetActiveTopLevelWindow();
if (ActiveModal.IsValid())
{
FSlateApplication::Get().AddWindowAsNativeChild(Window.ToSharedRef(), ActiveModal.ToSharedRef());
}
else
{
FSlateApplication::Get().AddWindow(Window.ToSharedRef());
}
TWeakPtr<SDetailsDiff> SelfWeak = DetailsDiff.ToWeakPtr();
Window->SetOnWindowClosed(::FOnWindowClosed::CreateLambda([SelfWeak](const TSharedRef<SWindow>&)
{
if (const TSharedPtr<SDetailsDiff> Self = SelfWeak.Pin())
{
Self->OnWindowClosedEvent.Broadcast(Self.ToSharedRef());
}
}));
return DetailsDiff;
}
TSharedRef<SDetailsDiff> SDetailsDiff::CreateDiffWindow(const UObject* OldObject, const UObject* NewObject,
const FRevisionInfo& OldRevision, const FRevisionInfo& NewRevision, const UClass* ObjectClass)
{
check(OldObject || NewObject);
// sometimes we're comparing different revisions of one single asset (other
// times we're comparing two completely separate assets altogether)
const bool bIsSingleAsset = !OldObject || !NewObject || (NewObject->GetName() == OldObject->GetName());
FText WindowTitle = FText::Format(LOCTEXT("NamelessBlueprintDiff", "{0} Diff"), ObjectClass->GetDisplayNameText());
// if we're diffing one asset against itself
if (bIsSingleAsset)
{
// identify the assumed single asset in the window's title
const FString BPName = NewObject? NewObject->GetName() : OldObject->GetName();
WindowTitle = FText::Format(LOCTEXT("NamedBlueprintDiff", "{0} - {1} Diff"), FText::FromString(BPName), ObjectClass->GetDisplayNameText());
}
return CreateDiffWindow(WindowTitle, OldObject, NewObject, OldRevision, NewRevision);
}
void SDetailsDiff::SetOutputObject(UObject* InOutputObject)
{
OutputObject = InOutputObject;
OnOutputObjectSetEvent.Broadcast();
}
UObject* SDetailsDiff::GetOutputObject() const
{
return OutputObject;
}
bool SDetailsDiff::IsOutputEnabled() const
{
return OutputObject != nullptr;
}
void SDetailsDiff::ReportMergeConflicts(const TMap<FString, TMap<FPropertySoftPath, ETreeDiffResult>>& Conflicts)
{
MergeConflicts = Conflicts;
}
void SDetailsDiff::NextDiff()
{
DiffTreeView::HighlightNextDifference(DifferencesTreeView.ToSharedRef(), RealDifferences, PrimaryDifferencesList);
}
void SDetailsDiff::PrevDiff()
{
DiffTreeView::HighlightPrevDifference(DifferencesTreeView.ToSharedRef(), RealDifferences, PrimaryDifferencesList);
}
bool SDetailsDiff::HasNextDiff() const
{
return DiffTreeView::HasNextDifference(DifferencesTreeView.ToSharedRef(), RealDifferences);
}
bool SDetailsDiff::HasPrevDiff() const
{
return DiffTreeView::HasPrevDifference(DifferencesTreeView.ToSharedRef(), RealDifferences);
}
void SDetailsDiff::OnDiffListSelectionChanged(TSharedPtr<FDiffResultItem> TheDiff )
{
check( !TheDiff->Result.OwningObjectPath.IsEmpty() );
// TODO: What do I put here?
}
void SDetailsDiff::GenerateDifferencesList()
{
PrimaryDifferencesList.Empty();
RealDifferences.Empty();
ModePanels.Empty();
OnOutputObjectSetEvent.Clear(); // will be repopulated by ModePanel generation methods
// Now that we have done the diffs, create the panel widgets
// (we're currently only generating the details panel but we can add more as needed)
const auto GetBlueprintCDO = [](const UObject* Object)->const UObject*
{
return CastChecked<UBlueprint>(Object)->GeneratedClass->GetDefaultObject();
};
TFunction<const UObject*(const UObject*)> Redirector;
if ((!PanelOld.Object || PanelOld.Object->IsA<UBlueprint>()) && (!PanelNew.Object || PanelNew.Object->IsA<UBlueprint>()))
{
// Blueprints diff their GeneratedClass CDO in the details panel instead
Redirector = GetBlueprintCDO;
}
ModePanels.Add(DetailsMode, GenerateDetailsPanel(Redirector));
DifferencesTreeView->RebuildList();
}
SDetailsDiff::FDiffControl SDetailsDiff::GenerateDetailsPanel(const TFunction<const UObject*(const UObject*)>& Redirector)
{
const UObject* OldObject = Redirector ? Redirector(PanelOld.Object) : PanelOld.Object;
const UObject* NewObject = Redirector ? Redirector(PanelNew.Object) : PanelNew.Object;
const TSharedPtr<FDetailsDiffControl> NewDiffControl = MakeShared<FDetailsDiffControl>(OldObject, NewObject, FOnDiffEntryFocused::CreateRaw(this, &SDetailsDiff::SetCurrentMode, DetailsMode), true);
NewDiffControl->EnableComments(DifferencesTreeView.ToWeakPtr());
NewDiffControl->GenerateTreeEntries(PrimaryDifferencesList, RealDifferences);
NewDiffControl->GenerateCustomEntriesCallback = OnGenerateCustomDiffEntries;
NewDiffControl->GenerateCustomEntryWidgetCallback = OnGenerateCustomDiffEntryWidget;
NewDiffControl->OrganizeEntriesCallback = OnOrganizeDiffEntries;
if (OnCustomizeDetailsWidget.IsBound())
{
OnCustomizeDetailsWidget.Execute(NewDiffControl->GetDetailsWidget(OldObject));
OnCustomizeDetailsWidget.Execute(NewDiffControl->GetDetailsWidget(NewObject));
}
const TSharedRef<SDetailsSplitter> Splitter = SNew(SDetailsSplitter)
.ShouldHighlightRow(ShouldHighlightRow)
.RowHighlightColor(RowHighlightColor);
if (OldObject)
{
Splitter->AddSlot(
SDetailsSplitter::Slot()
.Value(0.5f)
.DetailsView(NewDiffControl->GetDetailsWidget(OldObject))
.DifferencesWithRightPanel(NewDiffControl.ToSharedRef(), &FDetailsDiffControl::GetDifferencesWithRight, Cast<UObject>(OldObject))
);
}
if (NewObject)
{
Splitter->AddSlot(
SDetailsSplitter::Slot()
.Value(0.5f)
.DetailsView(NewDiffControl->GetDetailsWidget(NewObject))
.DifferencesWithRightPanel(NewDiffControl.ToSharedRef(), &FDetailsDiffControl::GetDifferencesWithRight, Cast<UObject>(NewObject))
);
}
const TWeakPtr<SDetailsSplitter> WeakSplitter = Splitter;
const TWeakPtr<FDetailsDiffControl> WeakDiffControl = NewDiffControl;
OnOutputObjectSetEvent.AddLambda([WeakSplitter, WeakDiffControl, this, Redirector]()
{
const UObject* OutputDetailObject = Redirector ? Redirector(OutputObject) : OutputObject;
const TSharedPtr<SDetailsSplitter> Splitter = WeakSplitter.Pin();
const TSharedPtr<FDetailsDiffControl> DiffControl = WeakDiffControl.Pin();
if (Splitter && DiffControl)
{
// if output object is already in panel, don't insert a new one
TSharedPtr<IDetailsView> DetailsView = DiffControl->TryGetDetailsWidget(OutputDetailObject);
if (DetailsView)
{
// update readonly status in splitter so that property merge buttons appear
const int32 Index = DiffControl->IndexOfObject(OutputDetailObject);
Splitter->GetPanel(Index).IsReadonly = false;
}
else
{
DetailsView = DiffControl->InsertObject(OutputDetailObject, false, 1);
// insert the output object as a central panel
Splitter->AddSlot(
SDetailsSplitter::Slot()
.DetailsView(DetailsView)
.Value(0.5f)
.IsReadonly(false)
.DifferencesWithRightPanel(DiffControl.ToSharedRef(), &FDetailsDiffControl::GetDifferencesWithRight, (const UObject*)OutputDetailObject),
1 // insert between left and right panel (index 1)
);
}
// allow user to edit the output panel
DetailsView->SetIsPropertyEditingEnabledDelegate(FIsPropertyEditingEnabled::CreateStatic([]{return true; }));
// highlight merge conflicts
TMap<FString, TMap<FPropertySoftPath, FLinearColor>> Highlights;
for (auto& [ObjectPath, Properties] : MergeConflicts)
{
for (auto& [propertyPath, Diff] : Properties)
{
switch(Diff)
{
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
Highlights.FindOrAdd(ObjectPath).Add(propertyPath, FLinearColor(1.5f, 0.3f, 0.3f));
break;
default:; // ignore identical and invalid
}
}
}
Splitter->HighlightFromMergeResults(MergeConflicts);
}
});
if (OutputObject)
{
OnOutputObjectSetEvent.Broadcast();
}
SDetailsDiff::FDiffControl Ret;
Ret.DiffControl = NewDiffControl;
Ret.Widget = Splitter;
return Ret;
}
TSharedRef<SBox> SDetailsDiff::GenerateRevisionInfoWidgetForPanel(TSharedPtr<SWidget>& OutGeneratedWidget, const FText& InRevisionText) const
{
return SAssignNew(OutGeneratedWidget,SBox)
.Padding(FMargin(4.0f, 10.0f))
.VAlign(VAlign_Center)
.HAlign(HAlign_Left)
[
SNew(STextBlock)
.TextStyle(FAppStyle::Get(), "DetailsView.CategoryTextStyle")
.Text(InRevisionText)
.ShadowColorAndOpacity(FColor::Black)
.ShadowOffset(FVector2D(1.4,1.4))
];
}
void SDetailsDiff::SetCurrentMode(FName NewMode)
{
if (CurrentMode == NewMode)
{
return;
}
CurrentMode = NewMode;
FDiffControl* FoundControl = ModePanels.Find(NewMode);
if (FoundControl)
{
ModeContents->SetContent(FoundControl->Widget.ToSharedRef());
}
else
{
ensureMsgf(false, TEXT("Diff panel does not support mode %s"), *NewMode.ToString() );
}
OnModeChanged(NewMode);
}
void SDetailsDiff::RefreshCurrentModePanel()
{
FDiffControl* FoundControl = ModePanels.Find(CurrentMode);
if (FoundControl)
{
ModeContents->SetContent(FoundControl->Widget.ToSharedRef());
}
else
{
ensureMsgf(false, TEXT("Diff panel does not support mode %s"), *CurrentMode.ToString() );
}
OnModeChanged(CurrentMode);
}
void SDetailsDiff::UpdateTopSectionVisibility(const FName& InNewViewMode) const
{
SSplitter* TopRevisionInfoWidgetPtr = TopRevisionInfoWidget.Get();
if (!TopRevisionInfoWidgetPtr)
{
return;
}
TopRevisionInfoWidgetPtr->SetVisibility(EVisibility::HitTestInvisible);
}
void SDetailsDiff::OnModeChanged(const FName& InNewViewMode) const
{
UpdateTopSectionVisibility(InNewViewMode);
}
#undef LOCTEXT_NAMESPACE