// 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()->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 { 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()->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()->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 SDetailsDiff::DefaultEmptyPanel() { return SNew(SHorizontalBox) + SHorizontalBox::Slot() .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(LOCTEXT("BlueprintDifGraphsToolTip", "Select Graph to Diff")) ]; } TSharedRef 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 Window = SNew(SWindow) .Title(WindowTitle) .ClientSize(FVector2D(1000.f, 800.f)); TSharedRef 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 ActiveModal = FSlateApplication::Get().GetActiveTopLevelWindow(); if (ActiveModal.IsValid()) { FSlateApplication::Get().AddWindowAsNativeChild(Window.ToSharedRef(), ActiveModal.ToSharedRef()); } else { FSlateApplication::Get().AddWindow(Window.ToSharedRef()); } TWeakPtr SelfWeak = DetailsDiff.ToWeakPtr(); Window->SetOnWindowClosed(::FOnWindowClosed::CreateLambda([SelfWeak](const TSharedRef&) { if (const TSharedPtr Self = SelfWeak.Pin()) { Self->OnWindowClosedEvent.Broadcast(Self.ToSharedRef()); } })); return DetailsDiff; } TSharedRef 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>& 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 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(Object)->GeneratedClass->GetDefaultObject(); }; TFunction Redirector; if ((!PanelOld.Object || PanelOld.Object->IsA()) && (!PanelNew.Object || PanelNew.Object->IsA())) { // 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& Redirector) { const UObject* OldObject = Redirector ? Redirector(PanelOld.Object) : PanelOld.Object; const UObject* NewObject = Redirector ? Redirector(PanelNew.Object) : PanelNew.Object; const TSharedPtr NewDiffControl = MakeShared(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 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(OldObject)) ); } if (NewObject) { Splitter->AddSlot( SDetailsSplitter::Slot() .Value(0.5f) .DetailsView(NewDiffControl->GetDetailsWidget(NewObject)) .DifferencesWithRightPanel(NewDiffControl.ToSharedRef(), &FDetailsDiffControl::GetDifferencesWithRight, Cast(NewObject)) ); } const TWeakPtr WeakSplitter = Splitter; const TWeakPtr WeakDiffControl = NewDiffControl; OnOutputObjectSetEvent.AddLambda([WeakSplitter, WeakDiffControl, this, Redirector]() { const UObject* OutputDetailObject = Redirector ? Redirector(OutputObject) : OutputObject; const TSharedPtr Splitter = WeakSplitter.Pin(); const TSharedPtr DiffControl = WeakDiffControl.Pin(); if (Splitter && DiffControl) { // if output object is already in panel, don't insert a new one TSharedPtr 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> 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 SDetailsDiff::GenerateRevisionInfoWidgetForPanel(TSharedPtr& 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