// Copyright Epic Games, Inc. All Rights Reserved. #include "SConversationDiff.h" #include "DetailsViewArgs.h" #include "SlateOptMacros.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/Views/TableViewMetadata.h" #include "Widgets/Input/SButton.h" #include "IDetailsView.h" #include "Widgets/Views/SListView.h" #include "ISourceControlProvider.h" #include "ISourceControlModule.h" #include "DiffResults.h" #include "ConversationGraph.h" #include "ConversationGraphNode.h" #include "ConversationDatabase.h" #include "PropertyEditorModule.h" #include "GraphDiffControl.h" #include "EdGraphUtilities.h" #include "ConversationEditorUtils.h" // #include "Conversation/Conversation.h" #include "Framework/Commands/GenericCommands.h" #include "HAL/PlatformApplicationMisc.h" #include "ConversationCompiler.h" #define LOCTEXT_NAMESPACE "SConversationDiff" ////////////////////////////////////////////////////////////////////////// // FTreeDiffResultItem struct FTreeDiffResultItem : public TSharedFromThis { /** * Constructor * @param InResult A difference result */ FTreeDiffResultItem(const FDiffSingleResult& InResult): Result(InResult){} /** * GenerateWidget for the diff item * @return The Widget */ TSharedRef GenerateWidget() const { FText ToolTip = Result.ToolTip; FLinearColor Color = Result.GetDisplayColor(); FText Text = Result.DisplayString; if(Text.IsEmpty()) { Text = LOCTEXT("DIF_UnknownDiff", "Unknown Diff"); ToolTip = LOCTEXT("DIF_Confused", "There is an unspecified difference"); } return SNew(STextBlock) .ToolTipText(ToolTip) .ColorAndOpacity(Color) .Text(Text); } // A result of a diff const FDiffSingleResult Result; }; ////////////////////////////////////////////////////////////////////////// // FDiffListCommands class FDiffListCommands : public TCommands { public: /** Constructor */ FDiffListCommands() : TCommands("DiffList", LOCTEXT("Diff", "Behavior Tree Diff"), NAME_None, FAppStyle::GetAppStyleSetName()) { } /** Initialize commands */ virtual void RegisterCommands() override { UI_COMMAND(Previous, "Prev", "Go to previous difference", EUserInterfaceActionType::Button, FInputChord(EKeys::F7, EModifierKey::Control)); UI_COMMAND(Next, "Next", "Go to next difference", EUserInterfaceActionType::Button, FInputChord(EKeys::F7)); } /** Go to previous difference */ TSharedPtr Previous; /** Go to next difference */ TSharedPtr Next; }; ////////////////////////////////////////////////////////////////////////// // SConversationDiff BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void SConversationDiff::Construct( const FArguments& InArgs ) { LastOtherPinTarget = nullptr; FDiffListCommands::Register(); PanelOld.ConversationBank = InArgs._OldBank; PanelNew.ConversationBank = InArgs._NewBank; PanelOld.RevisionInfo = InArgs._OldRevision; PanelNew.RevisionInfo = InArgs._NewRevision; PanelOld.bShowAssetName = InArgs._ShowAssetNames; PanelNew.bShowAssetName = InArgs._ShowAssetNames; OpenInDefaults = InArgs._OpenInDefaults; TSharedRef DefaultEmptyPanel = SNew(SHorizontalBox) +SHorizontalBox::Slot() .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(LOCTEXT("ConversationDiffGraphsToolTip", "Select Graph to Diff")) ]; this->ChildSlot [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .Content() [ SNew(SSplitter) +SSplitter::Slot() .Value(0.2f) [ SNew(SBorder) [ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() [ //open in p4dif tool SNew(SButton) .OnClicked(this, &SConversationDiff::OnOpenInDefaults) .Content() [ SNew(STextBlock) .Text(LOCTEXT("DiffConversationDefaults", "Default Diff")) ] ] +SVerticalBox::Slot() .FillHeight(1.f) [ GenerateDiffListWidget() ] ] ] +SSplitter::Slot() .Value(0.8f) [ // Diff Window SNew(SSplitter) +SSplitter::Slot() .Value(0.5f) [ // Left Diff SAssignNew(PanelOld.GraphEditorBorder, SBorder) .VAlign(VAlign_Fill) [ DefaultEmptyPanel ] ] +SSplitter::Slot() .Value(0.5f) [ // Right Diff SAssignNew(PanelNew.GraphEditorBorder, SBorder) .VAlign(VAlign_Fill) [ DefaultEmptyPanel ] ] ] ] ]; //@TODO: CONVERSATION: Diff tool doesn't support more than one graph yet UEdGraph* OldGraph = PanelOld.ConversationBank? FConversationCompiler::GetGraphFromBank(PanelOld.ConversationBank, 0) : nullptr; UEdGraph* NewGraph = PanelNew.ConversationBank? FConversationCompiler::GetGraphFromBank(PanelNew.ConversationBank, 0) : nullptr; PanelOld.GeneratePanel(OldGraph, NewGraph); PanelNew.GeneratePanel(NewGraph, OldGraph); } END_SLATE_FUNCTION_BUILD_OPTIMIZATION FReply SConversationDiff::OnOpenInDefaults() { OpenInDefaults.ExecuteIfBound(PanelOld.ConversationBank, PanelNew.ConversationBank); return FReply::Handled(); } TSharedRef SConversationDiff::GenerateDiffListWidget() { BuildDiffSourceArray(); if(DiffListSource.Num() > 0) { Algo::SortBy(DiffListSource, [](const FSharedDiffOnGraph& Data) { return Data->Result.Diff; }); // Map commands through UI const FDiffListCommands& Commands = FDiffListCommands::Get(); KeyCommands = MakeShareable(new FUICommandList ); KeyCommands->MapAction(Commands.Previous, FExecuteAction::CreateSP(this, &SConversationDiff::PrevDiff)); KeyCommands->MapAction(Commands.Next, FExecuteAction::CreateSP(this, &SConversationDiff::NextDiff)); FToolBarBuilder ToolbarBuilder(KeyCommands.ToSharedRef(), FMultiBoxCustomization::None); ToolbarBuilder.AddToolBarButton(Commands.Previous, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::GetAppStyleSetName(), "BlueprintDif.PrevDiff")); ToolbarBuilder.AddToolBarButton(Commands.Next, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::GetAppStyleSetName(), "BlueprintDif.NextDiff")); TSharedRef Result = SNew(SHorizontalBox) +SHorizontalBox::Slot() .FillWidth(1.f) .MaxWidth(350.f) [ SNew(SVerticalBox) +SVerticalBox::Slot() .Padding(0.f) .AutoHeight() [ ToolbarBuilder.MakeWidget() ] +SVerticalBox::Slot() .Padding(0.f) .AutoHeight() [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("PropertyWindow.CategoryBackground")) .Padding(FMargin(2.0f)) .ForegroundColor(FAppStyle::GetColor("PropertyWindow.CategoryForeground")) .ToolTipText(LOCTEXT("BehvaiorTreeDifDifferencesToolTip", "List of differences found between revisions, click to select")) .HAlign(HAlign_Center) [ SNew(STextBlock) .Text(LOCTEXT("RevisionDifferences", "Revision Differences")) ] ] +SVerticalBox::Slot() .Padding(1.f) .FillHeight(1.f) [ SAssignNew(DiffList, SListViewType) .ListItemsSource(&DiffListSource) .OnGenerateRow(this, &SConversationDiff::OnGenerateRow) .SelectionMode(ESelectionMode::Single) .OnSelectionChanged(this, &SConversationDiff::OnSelectionChanged) ] ]; return Result; } else { return SNew(SBorder).Visibility(EVisibility::Hidden); } } void SConversationDiff::BuildDiffSourceArray() { TArray FoundDiffs; //@TODO: CONVERSATION: Support diffing multiple graphs if (PanelOld.ConversationBank && PanelNew.ConversationBank) { FGraphDiffControl::DiffGraphs(FConversationCompiler::GetGraphFromBank(PanelOld.ConversationBank, 0), FConversationCompiler::GetGraphFromBank(PanelNew.ConversationBank, 0), FoundDiffs); } DiffListSource.Empty(); for (auto DiffIt(FoundDiffs.CreateConstIterator()); DiffIt; ++DiffIt) { DiffListSource.Add(FSharedDiffOnGraph(new FTreeDiffResultItem(*DiffIt))); } } void SConversationDiff::NextDiff() { int32 Index = (GetCurrentDiffIndex() + 1) % DiffListSource.Num(); DiffList->SetSelection(DiffListSource[Index]); } void SConversationDiff::PrevDiff() { int32 Index = GetCurrentDiffIndex(); if(Index == 0) { Index = DiffListSource.Num() - 1; } else { Index = (Index - 1) % DiffListSource.Num(); } DiffList->SetSelection(DiffListSource[Index]); } int32 SConversationDiff::GetCurrentDiffIndex() { auto Selected = DiffList->GetSelectedItems(); if(Selected.Num() == 1) { int32 Index = 0; for(auto It(DiffListSource.CreateIterator());It;++It,Index++) { if(*It == Selected[0]) { return Index; } } } return 0; } TSharedRef SConversationDiff::OnGenerateRow(FSharedDiffOnGraph Item, const TSharedRef& OwnerTable) { return SNew(STableRow< FSharedDiffOnGraph >, OwnerTable) .Content() [ Item->GenerateWidget() ]; } void SConversationDiff::OnSelectionChanged(FSharedDiffOnGraph Item, ESelectInfo::Type SelectionType) { if(!Item.IsValid()) { return; } //focus the graph onto the diff that was clicked on FDiffSingleResult Result = Item->Result; if(Result.Pin1) { PanelNew.GraphEditor.Pin()->ClearSelectionSet(); PanelOld.GraphEditor.Pin()->ClearSelectionSet(); auto FocusPin = [this](UEdGraphPin* InPin) { if (InPin) { UEdGraph* NodeGraph = InPin->GetOwningNode()->GetGraph(); SGraphEditor* NodeGraphEditor = GetGraphEditorForGraph(NodeGraph); NodeGraphEditor->JumpToPin(InPin); } }; FocusPin(Result.Pin1); FocusPin(Result.Pin2); } else if(Result.Node1) { PanelNew.GraphEditor.Pin()->ClearSelectionSet(); PanelOld.GraphEditor.Pin()->ClearSelectionSet(); auto FocusNode = [this](UEdGraphNode* InNode) { if (InNode) { UEdGraph* NodeGraph = InNode->GetGraph(); SGraphEditor* NodeGraphEditor = GetGraphEditorForGraph(NodeGraph); UConversationGraphNode* BTNode = Cast(InNode); if (BTNode && BTNode->bIsSubNode) { // This is a sub-node, we need to find our parent node in the graph // todo: work out why BTNode->ParentNode is always null TObjectPtr* ParentNodePtr = NodeGraph->Nodes.FindByPredicate([BTNode](UEdGraphNode* PotentialParentNode) -> bool { UConversationGraphNode* BTPotentialParentNode = Cast(PotentialParentNode); return BTPotentialParentNode && (BTPotentialParentNode->SubNodes.Contains(BTNode)); }); // We need to call JumpToNode on the parent node, and then SetNodeSelection on the sub-node // as JumpToNode doesn't work for sub-nodes if (ParentNodePtr) { check(InNode->GetGraph() == (*ParentNodePtr)->GetGraph()); NodeGraphEditor->JumpToNode(*ParentNodePtr, false, false); } NodeGraphEditor->SetNodeSelection(InNode, true); } else { NodeGraphEditor->JumpToNode(InNode, false); } } }; FocusNode(Result.Node1); FocusNode(Result.Node2); } } SGraphEditor* SConversationDiff::GetGraphEditorForGraph(UEdGraph* Graph) const { if(PanelOld.GraphEditor.Pin()->GetCurrentGraph() == Graph) { return PanelOld.GraphEditor.Pin().Get(); } else if(PanelNew.GraphEditor.Pin()->GetCurrentGraph() == Graph) { return PanelNew.GraphEditor.Pin().Get(); } checkNoEntry(); return nullptr; } ////////////////////////////////////////////////////////////////////////// // FConversationDiffPanel ////////////////////////////////////////////////////////////////////////// SConversationDiff::FConversationDiffPanel::FConversationDiffPanel() { ConversationBank = nullptr; } void SConversationDiff::FConversationDiffPanel::GeneratePanel(UEdGraph* Graph, UEdGraph* GraphToDiff) { TSharedPtr Widget = SNew(SBorder) .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SNew(STextBlock).Text( LOCTEXT("BTDifPanelNoGraphTip", "Graph does not exist in this revision")) ]; FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked( "PropertyEditor" ); FDetailsViewArgs DetailsViewArgs; DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::ObjectsUseNameArea; DetailsViewArgs.DefaultsOnlyVisibility = EEditDefaultsOnlyNodeVisibility::Hide; DetailsView = PropertyEditorModule.CreateDetailView( DetailsViewArgs ); DetailsView->SetObject(nullptr); DetailsView->SetIsPropertyEditingEnabledDelegate(FIsPropertyEditingEnabled::CreateRaw(this, &SConversationDiff::FConversationDiffPanel::IsPropertyEditable)); if(Graph) { SGraphEditor::FGraphEditorEvents InEvents; InEvents.OnSelectionChanged = SGraphEditor::FOnSelectionChanged::CreateRaw(this, &SConversationDiff::FConversationDiffPanel::OnSelectionChanged); FGraphAppearanceInfo AppearanceInfo; AppearanceInfo.CornerText = LOCTEXT("AppearanceCornerText_BehaviorDif", "DIFF"); if (!GraphEditorCommands.IsValid()) { GraphEditorCommands = MakeShareable(new FUICommandList()); GraphEditorCommands->MapAction(FGenericCommands::Get().Copy, FExecuteAction::CreateRaw(this, &FConversationDiffPanel::CopySelectedNodes), FCanExecuteAction::CreateRaw(this, &FConversationDiffPanel::CanCopyNodes)); } auto Editor = SNew(SGraphEditor) .AdditionalCommands(GraphEditorCommands) .GraphToEdit(Graph) .GraphToDiff(GraphToDiff) .IsEditable(false) .TitleBar(SNew(SBorder).HAlign(HAlign_Center) [ SNew(STextBlock).Text(GetTitle()) ]) .Appearance(AppearanceInfo) .GraphEvents(InEvents); const FSlateBrush* ContentAreaBrush = FAppStyle::GetBrush( "Docking.Tab", ".ContentAreaBrush" ); auto NewWidget = SNew(SSplitter) .Orientation(Orient_Vertical) +SSplitter::Slot() .Value(0.8f) [ Editor ] +SSplitter::Slot() .Value(0.2f) [ SNew( SBorder ) .Visibility( EVisibility::Visible ) .BorderImage( ContentAreaBrush ) [ DetailsView.ToSharedRef() ] ]; GraphEditor = Editor; Widget = NewWidget; } GraphEditorBorder->SetContent(Widget.ToSharedRef()); } FText SConversationDiff::FConversationDiffPanel::GetTitle() const { FText Title = LOCTEXT("CurrentRevision", "Current Revision"); // if this isn't the current working version being displayed if (!RevisionInfo.Revision.IsEmpty()) { // Don't use grouping on the revision or CL numbers to match how Perforce displays them const FText DateText = FText::AsDate(RevisionInfo.Date, EDateTimeStyle::Short); const FText RevisionText = FText::FromString(RevisionInfo.Revision); const FText ChangelistText = FText::AsNumber(RevisionInfo.Changelist, &FNumberFormattingOptions::DefaultNoGrouping()); if (bShowAssetName && ConversationBank) { FString AssetName = ConversationBank->GetName(); if(ISourceControlModule::Get().GetProvider().UsesChangelists()) { FText LocalizedFormat = LOCTEXT("NamedRevisionDiffFmtUsesChangelists", "{0} - Revision {1}, CL {2}, {3}"); Title = FText::Format(LocalizedFormat, FText::FromString(AssetName), RevisionText, ChangelistText, DateText); } else { FText LocalizedFormat = LOCTEXT("NamedRevisionDiffFmt", "{0} - Revision {1}, {2}"); Title = FText::Format(LocalizedFormat, FText::FromString(AssetName), RevisionText, DateText); } } else { if(ISourceControlModule::Get().GetProvider().UsesChangelists()) { FText LocalizedFormat = LOCTEXT("PreviousRevisionDifFmtUsesChangelists", "Revision {0}, CL {1}, {2}"); Title = FText::Format(LocalizedFormat, RevisionText, ChangelistText, DateText); } else { FText LocalizedFormat = LOCTEXT("PreviousRevisionDifFmt", "Revision {0}, {2}"); Title = FText::Format(LocalizedFormat, RevisionText, DateText); } } } else if (bShowAssetName && ConversationBank) { FString AssetName = ConversationBank->GetName(); FText LocalizedFormat = LOCTEXT("NamedCurrentRevisionFmt", "{0} - Current Revision"); Title = FText::Format(LocalizedFormat, FText::FromString(AssetName)); } return Title; } FGraphPanelSelectionSet SConversationDiff::FConversationDiffPanel::GetSelectedNodes() const { FGraphPanelSelectionSet CurrentSelection; TSharedPtr FocusedGraphEd = GraphEditor.Pin(); if (FocusedGraphEd.IsValid()) { CurrentSelection = FocusedGraphEd->GetSelectedNodes(); } return CurrentSelection; } void SConversationDiff::FConversationDiffPanel::CopySelectedNodes() { // Export the selected nodes and place the text on the clipboard const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); FString ExportedText; FEdGraphUtilities::ExportNodesToText(SelectedNodes, ExportedText); FPlatformApplicationMisc::ClipboardCopy(*ExportedText); } bool SConversationDiff::FConversationDiffPanel::CanCopyNodes() const { // If any of the nodes can be duplicated then we should allow copying const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); for (FGraphPanelSelectionSet::TConstIterator SelectedIter(SelectedNodes); SelectedIter; ++SelectedIter) { UEdGraphNode* Node = Cast(*SelectedIter); if ((Node != nullptr) && Node->CanDuplicateNode()) { return true; } } return false; } void SConversationDiff::FConversationDiffPanel::OnSelectionChanged( const FGraphPanelSelectionSet& NewSelection ) { ConversationEditorUtils::FPropertySelectionInfo SelectionInfo; TArray Selection = ConversationEditorUtils::GetSelectionForPropertyEditor(NewSelection, SelectionInfo); if (Selection.Num() == 1) { if (DetailsView.IsValid()) { DetailsView->SetObjects(Selection); } } else if (DetailsView.IsValid()) { DetailsView->SetObject(nullptr); } } bool SConversationDiff::FConversationDiffPanel::IsPropertyEditable() { return false; } #undef LOCTEXT_NAMESPACE