// Copyright Epic Games, Inc. All Rights Reserved. #include "SStateTreeDiff.h" #include "Customizations/StateTreeBindingExtension.h" #include "DetailsDiff.h" #include "DetailTreeNode.h" #include "Editor.h" #include "Framework/Application/SlateApplication.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "IDetailsView.h" #include "SDetailsDiff.h" #include "SlateOptMacros.h" #include "SStateTreeSplitter.h" #include "StateTreeDiffControl.h" #include "StateTreeDiffHelper.h" #include "StateTreeState.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SSpacer.h" #define LOCTEXT_NAMESPACE "SStateTreeDif" namespace UE::StateTree::Diff { SDiffWidget::SDiffWidget() { } SDiffWidget::~SDiffWidget() { if (AssetEditorCloseHandle.IsValid()) { GEditor->GetEditorSubsystem()->OnAssetEditorRequestClose().Remove(AssetEditorCloseHandle); } } BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void SDiffWidget::Construct(const FArguments& InArgs) { check(InArgs._OldAsset || InArgs._NewAsset); OldAssetPanel.StateTree = TStrongObjectPtr(InArgs._OldAsset); NewAssetPanel.StateTree = TStrongObjectPtr(InArgs._NewAsset); OldAssetPanel.RevisionInfo = InArgs._OldRevision; NewAssetPanel.RevisionInfo = InArgs._NewRevision; // sometimes we want to clearly identify the assets being diffed (when it's // not the same asset in each panel) OldAssetPanel.bShowAssetName = InArgs._ShowAssetNames; NewAssetPanel.bShowAssetName = InArgs._ShowAssetNames; if (InArgs._ParentWindow.IsValid()) { WeakParentWindow = InArgs._ParentWindow; AssetEditorCloseHandle = GEditor->GetEditorSubsystem()->OnAssetEditorRequestClose().AddSP(this, &SDiffWidget::HandleAssetEditorRequestClose); } FToolBarBuilder NavToolBarBuilder(TSharedPtr(), FMultiBoxCustomization::None); NavToolBarBuilder.AddToolBarButton( FUIAction( FExecuteAction::CreateSP(this, &SDiffWidget::PrevDiff), FCanExecuteAction::CreateSP(this, &SDiffWidget::HasPrevDiff)), NAME_None, LOCTEXT("PrevDiffLabel", "Prev"), LOCTEXT("PrevDiffTooltip", "Go to previous difference"), FSlateIcon(FAppStyle::GetAppStyleSetName(), "BlueprintDif.PrevDiff")); NavToolBarBuilder.AddToolBarButton( FUIAction( FExecuteAction::CreateSP(this, &SDiffWidget::NextDiff), FCanExecuteAction::CreateSP(this, &SDiffWidget::HasNextDiff)), NAME_None, LOCTEXT("NextDiffLabel", "Next"), LOCTEXT("NextDiffTooltip", "Go to next difference"), FSlateIcon(FAppStyle::GetAppStyleSetName(), "BlueprintDif.NextDiff")); DifferencesTreeView = DiffTreeView::CreateTreeView(&Differences); GenerateDifferencesList(); const auto TextBlock = [](const 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(OldAssetPanel.StateTree.Get(), OldAssetPanel.RevisionInfo, FText())) ] + SSplitter::Slot() .Value(.5f) [ TextBlock(DiffViewUtils::GetPanelLabel(NewAssetPanel.StateTree.Get(), NewAssetPanel.RevisionInfo, FText())) ] ]; this->ChildSlot [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("Docking.Tab", ".ContentAreaBrush")) [ SNew(SOverlay) + SOverlay::Slot() .VAlign(VAlign_Top) [ TopRevisionInfoWidget.ToSharedRef() ] + SOverlay::Slot() [ SNew(SSplitter) .Orientation(EOrientation::Orient_Vertical) + SSplitter::Slot() .Value(.55f) [ 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) [ StateTreePanel.Splitter.ToSharedRef() ] ] ] + SSplitter::Slot() .Value(.45f) [ SAssignNew(DetailsViewContents, SBox) ] ] ] ]; SetDetailsDiff(); } END_SLATE_FUNCTION_BUILD_OPTIMIZATION void SDiffWidget::HandleAssetEditorRequestClose(UObject* Asset, const EAssetEditorCloseReason CloseReason) { if (OldAssetPanel.StateTree.Get()== Asset || NewAssetPanel.StateTree.Get() == 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 (AssetEditorCloseHandle.IsValid()) { GEditor->GetEditorSubsystem()->OnAssetEditorRequestClose().Remove(AssetEditorCloseHandle); } if (const TSharedPtr ParentWindow = WeakParentWindow.Pin()) { ParentWindow->RequestDestroyWindow(); } } } TSharedRef SDiffWidget::CreateDiffWindow(const FText WindowTitle, TNotNull OldStateTree, TNotNull NewStateTree, 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 = (NewStateTree->GetName() == OldStateTree->GetName()); const TSharedPtr Window = SNew(SWindow) .Title(WindowTitle) .ClientSize(FVector2D(1000.f, 800.f)); TSharedRef StateTreeDiff = SNew(SDiffWidget) .OldAsset(OldStateTree) .NewAsset(NewStateTree) .OldRevision(OldRevision) .NewRevision(NewRevision) .ShowAssetNames(!bIsSingleAsset) .ParentWindow(Window); Window->SetContent(StateTreeDiff); // 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()); } return StateTreeDiff; } TSharedRef SDiffWidget::CreateDiffWindow(TNotNull OldStateTree, TNotNull NewStateTree, const FRevisionInfo& OldRevision, const FRevisionInfo& NewRevision, const UClass* StateTreeClass) { // sometimes we're comparing different revisions of one single asset (other // times we're comparing two completely separate assets altogether) //@TODO use pathname instead of asset name. const bool bIsSingleAsset = NewStateTree->GetFName() == OldStateTree->GetFName(); FText WindowTitle = FText::Format(LOCTEXT("NamelessStateTreeDiff", "{0} Diff (experimental)"), StateTreeClass->GetDisplayNameText()); // if we're diffing one asset against itself if (bIsSingleAsset) { // identify the assumed single asset in the window's title const FString STName = NewStateTree->GetName(); WindowTitle = FText::Format(LOCTEXT("NamedStateTreeDiff", "{0} - {1} Diff (experimental)"), FText::FromString(STName), StateTreeClass->GetDisplayNameText()); } return CreateDiffWindow(WindowTitle, OldStateTree, NewStateTree, OldRevision, NewRevision); } void SDiffWidget::NextDiff() const { DiffTreeView::HighlightNextDifference(DifferencesTreeView.ToSharedRef(), Differences, Differences); } void SDiffWidget::PrevDiff() const { DiffTreeView::HighlightPrevDifference(DifferencesTreeView.ToSharedRef(), Differences, Differences); } bool SDiffWidget::HasNextDiff() const { return DiffTreeView::HasNextDifference(DifferencesTreeView.ToSharedRef(), Differences); } bool SDiffWidget::HasPrevDiff() const { return DiffTreeView::HasPrevDifference(DifferencesTreeView.ToSharedRef(), Differences); } void SDiffWidget::GenerateDifferencesList() { Differences.Empty(); GenerateDiffPanel(); DifferencesTreeView->RebuildList(); } void SDiffWidget::GenerateDiffPanel() { const UStateTree* OldStateTree = OldAssetPanel.StateTree.Get(); const UStateTree* NewStateTree = NewAssetPanel.StateTree.Get(); StateTreePanel.DiffControl = MakeShared( OldStateTree, NewStateTree, FOnDiffEntryFocused{}); StateTreePanel.DiffControl->GenerateTreeEntries(Differences); StateTreePanel.DiffControl->GetOnStateDiffEntryFocused().AddSP(this, &SDiffWidget::HandleStateDiffEntryFocused); TSharedPtr DiffSplitter = SNew(SDiffSplitter); if (OldAssetPanel.StateTree) { DiffSplitter->AddSlot( SDiffSplitter::Slot() .Value(0.5f) .StateTreeView(StateTreePanel.DiffControl->GetDetailsWidget(OldStateTree)) .StateTree(OldStateTree)); } if (NewAssetPanel.StateTree) { DiffSplitter->AddSlot( SDiffSplitter::Slot() .Value(0.5f) .StateTreeView(StateTreePanel.DiffControl->GetDetailsWidget(NewStateTree)) .StateTree(NewStateTree)); } StateTreePanel.Splitter = DiffSplitter; } void SDiffWidget::HandleStateDiffEntryFocused(const FSingleDiffEntry& StateDiff) { const FStateSoftPath LeftStatePath = StateDiff.Identifier; const FStateSoftPath RightStatePath = StateDiff.SecondaryIdentifier ? StateDiff.SecondaryIdentifier : StateDiff.Identifier; StateTreePanel.Splitter->HandleSelectionChanged(LeftStatePath, RightStatePath); const UStateTree* OldStateTree = OldAssetPanel.StateTree.Get(); const UStateTree* NewStateTree = NewAssetPanel.StateTree.Get(); const UStateTreeState* OldState = OldStateTree != nullptr ? LeftStatePath.ResolvePath(OldStateTree) : nullptr; const UStateTreeState* NewState = NewStateTree != nullptr ? RightStatePath.ResolvePath(NewStateTree) : nullptr; // If comparing states that exist in both state trees display them in the details diff view if (OldState && NewState) { SetDetailsDiff(OldState, NewState); } // If we clear selection on both state trees we can display an empty details diff view else if (!OldState && !NewState) { SetDetailsDiff(); } // If the state only exists in one of the state trees (either added or removed), details diff view will not work. else { // So the states are put into separate details views const TSharedPtr LeftWidget = SNew(SBox); const TSharedPtr RightWidget = SNew(SBox); if (OldState) { const FDetailsDiff DetailsDiff(OldState, true); LeftWidget->SetContent(DetailsDiff.DetailsWidget()); } if (NewState) { const FDetailsDiff DetailsDiff(NewState, false); RightWidget->SetContent(DetailsDiff.DetailsWidget()); } // And displayed in a way that resembles the details diff view DetailsViewContents->SetContent( SNew(SBorder) .BorderImage(FAppStyle::GetBrush("Docking.Tab", ".ContentAreaBrush")) [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 2.0f, 0.0f, 2.0f) + SVerticalBox::Slot() [ SNew(SSplitter) + SSplitter::Slot() .Value(.2f) + SSplitter::Slot() .Value(.8f) [ SNew(SSplitter) .PhysicalSplitterHandleSize(5.f) + SSplitter::Slot() .Value(.5f) [ SNew(SBox) .Padding(15.f, 0.f, 15.f, 0.f) [ LeftWidget.ToSharedRef() ] ] + SSplitter::Slot() .Value(.5f) [ SNew(SBox) .Padding(15.f, 0.f, 15.f, 0.f) [ RightWidget.ToSharedRef() ] ] ] ] ] ); } } void SDiffWidget::SetDetailsDiff(const UStateTreeState* OldState, const UStateTreeState* NewState) { const UObject* OldAsset = OldState ? OldState : OldAssetPanel.StateTree ? static_cast(OldAssetPanel.StateTree->EditorData) : nullptr; const UObject* NewAsset = NewState ? NewState : NewAssetPanel.StateTree ? static_cast(NewAssetPanel.StateTree->EditorData) : nullptr; if (const bool bIsState = OldState || NewState) { StateBindingDiffs.Reset(StateTreePanel.DiffControl->GetBindingDifferences().Num()); for (const FSingleDiffEntry& BindingDiff : StateTreePanel.DiffControl->GetBindingDifferences()) { const UStateTree* OldStateTree = OldAssetPanel.StateTree.Get(); const UStateTree* NewStateTree = NewAssetPanel.StateTree.Get(); if (OldStateTree != nullptr && NewStateTree != nullptr && BindingDiff.Identifier.ResolvePath(OldStateTree) == OldState && BindingDiff.SecondaryIdentifier.ResolvePath(NewStateTree) == NewState) { StateBindingDiffs.Push(BindingDiff); } } } else { StateBindingDiffs.Reset(); } TArray Entries; const TSharedRef DetailsDiff = SNew(SDetailsDiff) .OldAsset(OldAsset) .NewAsset(NewAsset) .OldRevision(OldAssetPanel.RevisionInfo) .NewRevision(NewAssetPanel.RevisionInfo) .ShowAssetNames(false) .OnCustomizeDetailsWidget_Static(&SDiffWidget::AddStateTreeExtensionToDetailsView) .OnGenerateCustomDiffEntries(this, &SDiffWidget::AddBindingDiffToDiffEntries) .OnOrganizeDiffEntries_Static(&SDiffWidget::OrganizeDiffEntries, OldState, NewState) .OnGenerateCustomDiffEntryWidget_Static(&SDiffWidget::GenerateCustomDiffEntryWidget, OldState, NewState) .RowHighlightColor_Static(&SDiffWidget::GetRowHighlightColor) .ShouldHighlightRow(this, &SDiffWidget::ShouldHighlightRow); DetailsViewContents->SetContent(DetailsDiff); } void SDiffWidget::AddBindingDiffToDiffEntries(TArray& OutEntries) { OutEntries.Reserve(StateBindingDiffs.Num()); for (const FSingleDiffEntry& BindingDiff : StateBindingDiffs) { EPropertyDiffType::Type DiffType = EPropertyDiffType::Invalid; switch (BindingDiff.DiffType) { case EStateDiffType::BindingAddedToA: case EStateDiffType::BindingAddedToB: case EStateDiffType::BindingChanged: DiffType = EPropertyDiffType::PropertyValueChanged; break; } if (DiffType != EPropertyDiffType::Invalid) { FSingleObjectDiffEntry Entry(BindingDiff.BindingPath, DiffType); OutEntries.Add(Entry); } } } void SDiffWidget::OrganizeDiffEntries( TArray>& OutDiffTreeEntries, const TArray& DiffEntries, TFunctionRef(const FSingleObjectDiffEntry&)> GenerateDiffTreeEntry, TFunctionRef(FText&)> GenerateCategoryEntry, const UStateTreeState* OldState, const UStateTreeState* NewState) { static FText RightRevision = LOCTEXT("NewRevisionIdentifier", "Right Revision"); static FText StateText = LOCTEXT("StateText", "State"); static FText ParameterText = LOCTEXT("ParametersText", "Parameters"); static FText ConditionText = LOCTEXT("EnterConditionsText", "Enter Conditions"); static FText TaskText = LOCTEXT("TasksText", "Tasks"); static FText TransitionText = LOCTEXT("TransitionsText", "Transitions"); static FText ConsiderationText = LOCTEXT("ConsiderationText", "Utility"); TSet ConditionIndices; TSet TaskIndices; TSet TransitionIndices; TSet ConsiderationIndices; TArray> ParametersEntries; TArray> ConditionEntries; TArray> TaskEntries; TArray> TransitionEntries; TArray> ConsiderationEntries; TArray> StateEntries; TArray SortedEntries = DiffEntries; SortedEntries.Sort([](const FSingleObjectDiffEntry& A, const FSingleObjectDiffEntry& B) { return A.Identifier.TryReadIndex(0) < B.Identifier.TryReadIndex(0); }); for (const FSingleObjectDiffEntry& Difference : SortedEntries) { constexpr int32 PropertyCountFromRoot = 2; // 2 levels down from the root; first level being the category/parent collection and the second level the property that changed FSingleObjectDiffEntry SimplifiedEntry(Difference.Identifier.GetRootProperty(PropertyCountFromRoot), Difference.DiffType); TSharedPtr Entry = GenerateDiffTreeEntry(SimplifiedEntry); if (Difference.Identifier.IsSubPropertyMatch(ConditionName)) { const int32 Index = Difference.Identifier.TryReadIndex(0); if (!ConditionIndices.Contains(Index)) { ConditionIndices.Add(Index); ConditionEntries.Add(Entry); } } else if (Difference.Identifier.IsSubPropertyMatch(TaskName)) { const int32 Index = Difference.Identifier.TryReadIndex(0); if (!TaskIndices.Contains(Index)) { TaskIndices.Add(Index); TaskEntries.Add(Entry); } } else if (Difference.Identifier.IsSubPropertyMatch(TransitionName)) { const int32 Index = Difference.Identifier.TryReadIndex(0); if (!TransitionIndices.Contains(Index)) { TransitionIndices.Add(Index); TransitionEntries.Add(Entry); } } else if (Difference.Identifier.IsSubPropertyMatch(ConsiderationName)) { const int32 Index = Difference.Identifier.TryReadIndex(0); if (!ConsiderationIndices.Contains(Index)) { ConsiderationIndices.Add(Index); ConsiderationEntries.Add(Entry); } } else if (Difference.Identifier.IsSubPropertyMatch(ParameterName)) { // @todo investigate: currently unable to resolve full property path (FInstancedPropertyBag issue?) ParametersEntries.Add(GenerateDiffTreeEntry(Difference)); } else { StateEntries.Add(Entry); } } OutDiffTreeEntries.Append(StateEntries); if (ParametersEntries.Num() > 0) { TSharedPtr ParametersEntry = GenerateCategoryEntry(ParameterText); ParametersEntry->Children = ParametersEntries; OutDiffTreeEntries.Push(ParametersEntry); } if (ConditionEntries.Num() > 0) { TSharedPtr ConditionEntry = GenerateCategoryEntry(ConditionText); ConditionEntry->Children = ConditionEntries; OutDiffTreeEntries.Push(ConditionEntry); } if (ConsiderationEntries.Num() > 0) { TSharedPtr ConsiderationEntry = GenerateCategoryEntry(ConsiderationText); ConsiderationEntry->Children = ConsiderationEntries; OutDiffTreeEntries.Push(ConsiderationEntry); } if (TaskEntries.Num() > 0) { TSharedPtr TaskEntry = GenerateCategoryEntry(TaskText); TaskEntry->Children = TaskEntries; OutDiffTreeEntries.Push(TaskEntry); } if (TransitionEntries.Num() > 0) { TSharedPtr TransitionEntry = GenerateCategoryEntry(TransitionText); TransitionEntry->Children = TransitionEntries; OutDiffTreeEntries.Push(TransitionEntry); } } TSharedRef SDiffWidget::GenerateCustomDiffEntryWidget(const FSingleObjectDiffEntry& DiffEntry, FText&, const UStateTreeState* OldState, const UStateTreeState* NewState) { const UStateTreeState* SourceState = DiffEntry.DiffType == EPropertyDiffType::PropertyAddedToB ? NewState : OldState; FText PropertyName = FText::FromString(DiffEntry.Identifier.ToDisplayName()); if (DiffEntry.Identifier.IsSubPropertyMatch(ConditionName)) { const int32 ConditionIndex = DiffEntry.Identifier.TryReadIndex(0); const FStateTreeEditorNode& ConditionEntry = SourceState->EnterConditions[ConditionIndex]; PropertyName = FText::Format(FText::FromString(TEXT("[{0}]")), FText::FromName(ConditionEntry.GetName())); } else if (DiffEntry.Identifier.IsSubPropertyMatch(TaskName)) { const int32 TaskIndex = DiffEntry.Identifier.TryReadIndex(0); const FStateTreeEditorNode& TaskEntry = SourceState->Tasks[TaskIndex]; PropertyName = FText::Format(FText::FromString(TEXT("[{0}]")), FText::FromName(TaskEntry.GetName())); } else if (DiffEntry.Identifier.IsSubPropertyMatch(ConsiderationName)) { const int32 ConsiderationIndex = DiffEntry.Identifier.TryReadIndex(0); const FStateTreeEditorNode& ConsiderationEntry = SourceState->Considerations[ConsiderationIndex]; PropertyName = FText::Format(FText::FromString(TEXT("[{0}]")), FText::FromName(ConsiderationEntry.GetName())); } else if (DiffEntry.Identifier.IsSubPropertyMatch(ParameterName)) { constexpr int32 NumberOfPathElements = 1; PropertyName = FText::Format(FText::FromString(TEXT("[{0}]")), FText::FromString(DiffEntry.Identifier.ToDisplayName(NumberOfPathElements))); } return SNew(STextBlock) .Text(GetStateDiffMessage(DiffEntry, PropertyName)) .ToolTipText(GetStateDiffMessage(DiffEntry, PropertyName)) .ColorAndOpacity(GetStateDiffMessageColor(DiffEntry)); } bool SDiffWidget::ShouldHighlightRow(const TUniquePtr& DiffNode) { if (DiffNode->DiffResult != ETreeDiffResult::Identical) { return true; } TSharedPtr DetailNode = DiffNode->ValueA.Pin(); DetailNode = DetailNode ? DetailNode : DiffNode->ValueB.Pin(); const FPropertySoftPath PropertySoftPath(DetailNode->GetPropertyPath()); if (PropertySoftPath.ToDisplayName().Len() == 0) { return false; } if (StateBindingDiffs.Num() > 0) { const FSingleDiffEntry& BindingDiff = StateBindingDiffs[0]; return BindingDiff.BindingPath.IsSubPropertyMatch(PropertySoftPath) || BindingDiff.BindingPath == PropertySoftPath; } return false; } FLinearColor SDiffWidget::GetRowHighlightColor(const TUniquePtr& DiffNode) { switch (DiffNode->DiffResult) { case ETreeDiffResult::MissingFromTree1: return FLinearColor(0.f, 1.f, 0.f, .7f); case ETreeDiffResult::MissingFromTree2: return FLinearColor(1.f, 0.f, 0.f, .7f); default: return FLinearColor(1.f, 1.f, 0.f, .7f); } } void SDiffWidget::AddStateTreeExtensionToDetailsView(const TSharedRef& DetailsView) { DetailsView->SetExtensionHandler(MakeShared().ToSharedPtr()); } } // UE::StateTree::Diff #undef LOCTEXT_NAMESPACE