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

1710 lines
54 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "DiffControl.h"
#include "Algo/Transform.h"
#include "DiffResults.h"
#include "GraphDiffControl.h"
#include "SBlueprintDiff.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "IDetailsView.h"
#include "ReviewComments.h"
#include "Widgets/Input/SMultiLineEditableTextBox.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SCheckBox.h"
#include "Framework/Application/SlateApplication.h"
#include "AsyncTreeDifferences.h"
#include "DetailTreeNode.h"
#include "AsyncDetailViewDiff.h"
#define LOCTEXT_NAMESPACE "SBlueprintDif"
namespace UE::DiffControl
{
KISMET_API const TArray<FReviewComment>*(*GGetReviewCommentsForFile)(const FString&) = nullptr;
KISMET_API void(*GPostReviewComment)(FReviewComment&) = nullptr;
KISMET_API void(*GEditReviewComment)(FReviewComment&) = nullptr;
KISMET_API FString (*GGetReviewerUsername)() = nullptr;
KISMET_API bool (*GIsFileInReview)(const FString& File) = nullptr;
DECLARE_MULTICAST_DELEGATE_OneParam(FOnCommentPosted, const FReviewComment&)
KISMET_API FOnCommentPosted GOnCommentPosted;
}
// well known algorithm for computing the Longest Common Subsequence table of two ordered lists
template <typename RangeType, typename ComparePredicate>
static TArray<TArray<int32>> CalculateLCSTable(const RangeType& Range1, const RangeType& Range2, ComparePredicate Comparison)
{
TArray<TArray<int32>> LCS;
LCS.SetNum(Range1.Num() + 1);
for (int32 I = 0; I <= Range1.Num(); I++)
{
LCS[I].SetNum(Range2.Num() + 1);
if (I == 0)
{
continue;
}
for (int32 J = 1; J <= Range2.Num(); J++)
{
if (Comparison(Range1[I - 1], Range2[J - 1]))
{
LCS[I][J] = LCS[I - 1][J - 1] + 1;
}
else
{
LCS[I][J] = FMath::Max(LCS[I - 1][J], LCS[I][J - 1]);
}
}
}
return LCS;
}
/////////////////////////////////////////////////////////////////////////////
/// IDiffControl
FText IDiffControl::RightRevision = LOCTEXT("OlderRevisionIdentifier", "Right Revision");
TSharedRef<SWidget> IDiffControl::GenerateSimpleDiffWidget(FText DiffText)
{
return SNew(STextBlock)
.Text(DiffText)
.ToolTipText(DiffText)
.ColorAndOpacity(DiffViewUtils::Differs());
}
TSharedRef<SWidget> IDiffControl::GenerateObjectDiffWidget(FSingleObjectDiffEntry DiffEntry, FText ObjectName)
{
return SNew(STextBlock)
.Text(DiffViewUtils::PropertyDiffMessage(DiffEntry, ObjectName))
.ToolTipText(DiffViewUtils::PropertyDiffMessage(DiffEntry, ObjectName))
.ColorAndOpacity(DiffViewUtils::Differs());
}
void IDiffControl::EnableComments(TWeakPtr<STreeView<TSharedPtr<FBlueprintDifferenceTreeEntry>>> TreeView,
const UObject* OldObject, const UObject* NewObject)
{
const UPackage* Package = OldObject ? OldObject->GetPackage() : NewObject->GetPackage();
const FPackagePath PackagePath = FPackagePath::FromPackageNameChecked(Package->GetName());
ReviewCommentsDiffControl = MakeShared<FReviewCommentsDiffControl>(PackagePath.GetLocalFullPath(), TreeView);
}
void IDiffControl::EnableComments(TWeakPtr<STreeView<TSharedPtr<FBlueprintDifferenceTreeEntry>>> TreeView, const TArray<const UObject*>& Objects)
{
for (const UObject* Object : Objects)
{
if (!ensure(Object))
{
continue;
}
if (const UPackage* Package = Object->GetPackage())
{
const FPackagePath PackagePath = FPackagePath::FromPackageNameChecked(Package->GetName());
ReviewCommentsDiffControl = MakeShared<FReviewCommentsDiffControl>(PackagePath.GetLocalFullPath(), TreeView);
return;
}
}
}
void IDiffControl::GenerateCategoryCommentTreeEntries(TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutChildrenEntries,
TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutRealDifferences, FString CategoryKey)
{
if (ReviewCommentsDiffControl)
{
ReviewCommentsDiffControl->SetCategory(CategoryKey);
ReviewCommentsDiffControl->GenerateTreeEntries(OutChildrenEntries, OutRealDifferences);
}
}
/////////////////////////////////////////////////////////////////////////////
/// FMyBlueprintDiffControl
FMyBlueprintDiffControl::FMyBlueprintDiffControl(const UBlueprint* InOldBlueprint, const UBlueprint* InNewBlueprint, FOnDiffEntryFocused InSelectionCallback)
: SelectionCallback(MoveTemp(InSelectionCallback))
, OldBlueprint(InOldBlueprint)
, NewBlueprint(InNewBlueprint)
{}
void FMyBlueprintDiffControl::GenerateTreeEntries(TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutTreeEntries, TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutRealDifferences)
{
TArray< TSharedPtr<FBlueprintDifferenceTreeEntry> > Children;
if (OldBlueprint && OldBlueprint->SkeletonGeneratedClass && NewBlueprint && NewBlueprint->SkeletonGeneratedClass)
{
for (TFieldIterator<FProperty> PropertyIt(OldBlueprint->SkeletonGeneratedClass); PropertyIt; ++PropertyIt)
{
FProperty* OldProperty = *PropertyIt;
FProperty* NewProperty = NewBlueprint->SkeletonGeneratedClass->FindPropertyByName(OldProperty->GetFName());
FText PropertyText = FText::FromString(OldProperty->GetAuthoredName());
if (NewProperty)
{
const int32 OldVarIndex = FBlueprintEditorUtils::FindNewVariableIndex(OldBlueprint, OldProperty->GetFName());
const int32 NewVarIndex = FBlueprintEditorUtils::FindNewVariableIndex(NewBlueprint, OldProperty->GetFName());
if (OldVarIndex != INDEX_NONE && NewVarIndex != INDEX_NONE)
{
TArray<FSingleObjectDiffEntry> DifferingProperties;
static const UStruct* Struct = FBPVariableDescription::StaticStruct();
const void* OldVal = &OldBlueprint->NewVariables[OldVarIndex];
const void* NewVal = &NewBlueprint->NewVariables[NewVarIndex];
const UPackage* OldPackage = OldBlueprint->GetPackage();
const UPackage* NewPackage = NewBlueprint->GetPackage();
DiffUtils::CompareUnrelatedStructs(Struct, OldVal, OldPackage, Struct, NewVal, NewPackage, DifferingProperties);
for (const FSingleObjectDiffEntry& Difference : DifferingProperties)
{
TSharedPtr<FBlueprintDifferenceTreeEntry> Entry = MakeShared<FBlueprintDifferenceTreeEntry>(
SelectionCallback,
FGenerateDiffEntryWidget::CreateStatic(&GenerateObjectDiffWidget, Difference, PropertyText));
Children.Push(Entry);
OutRealDifferences.Push(Entry);
}
}
}
else
{
FText DiffText = FText::Format(LOCTEXT("VariableRemoved", "Removed Variable {0}"), PropertyText);
TSharedPtr<FBlueprintDifferenceTreeEntry> Entry = MakeShared<FBlueprintDifferenceTreeEntry>(
SelectionCallback,
FGenerateDiffEntryWidget::CreateStatic(&GenerateSimpleDiffWidget, DiffText));
Children.Push(Entry);
OutRealDifferences.Push(Entry);
}
}
for (TFieldIterator<FProperty> PropertyIt(NewBlueprint->SkeletonGeneratedClass); PropertyIt; ++PropertyIt)
{
FProperty* NewProperty = *PropertyIt;
FProperty* OldProperty = OldBlueprint->SkeletonGeneratedClass->FindPropertyByName(NewProperty->GetFName());
if (!OldProperty)
{
FText DiffText = FText::Format(LOCTEXT("VariableAdded", "Added Variable {0}"), FText::FromString(NewProperty->GetAuthoredName()));
TSharedPtr<FBlueprintDifferenceTreeEntry> Entry = MakeShared<FBlueprintDifferenceTreeEntry>(
SelectionCallback,
FGenerateDiffEntryWidget::CreateStatic(&GenerateSimpleDiffWidget, DiffText));
Children.Push(Entry);
OutRealDifferences.Push(Entry);
}
}
}
const bool bHasDifferences = Children.Num() != 0;
if (!bHasDifferences)
{
// make one child informing the user that there are no differences:
Children.Push(FBlueprintDifferenceTreeEntry::NoDifferencesEntry());
}
static const FText MyBlueprintLabel = NSLOCTEXT("FBlueprintDifferenceTreeEntry", "MyBlueprintLabel", "My Blueprint");
static const FText MyBlueprintTooltip = NSLOCTEXT("FBlueprintDifferenceTreeEntry", "MyBlueprintTooltip", "The list of changes made to blueprint structure in the My Blueprint panel");
OutTreeEntries.Push(FBlueprintDifferenceTreeEntry::CreateCategoryEntry(
MyBlueprintLabel,
MyBlueprintTooltip,
SelectionCallback,
Children,
bHasDifferences
));
// add comments as children to this category
GenerateCategoryCommentTreeEntries(OutTreeEntries.Last()->Children, OutRealDifferences, TEXT("My Blueprint"));
}
void FMyBlueprintDiffControl::EnableComments(TWeakPtr<STreeView<TSharedPtr<FBlueprintDifferenceTreeEntry>>> TreeView)
{
IDiffControl::EnableComments(TreeView, OldBlueprint, NewBlueprint);
}
/////////////////////////////////////////////////////////////////////////////
/// FSCSDiffControl
FSCSDiffControl::FSCSDiffControl(const UBlueprint* InOldBlueprint, const UBlueprint* InNewBlueprint, FOnDiffEntryFocused InSelectionCallback)
: SelectionCallback(InSelectionCallback)
, OldSCS(InOldBlueprint)
, NewSCS(InNewBlueprint)
{
}
void FSCSDiffControl::GenerateTreeEntries(TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutTreeEntries, TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutRealDifferences)
{
TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>> Children;
if (OldSCS.GetBlueprint() && NewSCS.GetBlueprint())
{
const TArray< FSCSResolvedIdentifier > OldHierarchy = OldSCS.GetDisplayedHierarchy();
const TArray< FSCSResolvedIdentifier > NewHierarchy = NewSCS.GetDisplayedHierarchy();
DiffUtils::CompareUnrelatedSCS(OldSCS.GetBlueprint(), OldHierarchy, NewSCS.GetBlueprint(), NewHierarchy, DifferingProperties);
const auto FocusSCSDifferenceEntry = [](FSCSDiffEntry Entry, FOnDiffEntryFocused InSelectionCallback, FSCSDiffControl* Owner)
{
InSelectionCallback.ExecuteIfBound();
if (Entry.TreeIdentifier.Name != NAME_None)
{
Owner->OldSCS.HighlightProperty(Entry.TreeIdentifier.Name, FPropertyPath());
Owner->NewSCS.HighlightProperty(Entry.TreeIdentifier.Name, FPropertyPath());
}
};
const auto CreateSCSDifferenceWidget = [](FSCSDiffEntry Entry, FText ObjectName) -> TSharedRef<SWidget>
{
return SNew(STextBlock)
.Text(DiffViewUtils::SCSDiffMessage(Entry, ObjectName))
.ColorAndOpacity(Entry.DiffType == ETreeDiffType::NODE_CORRUPTED ?
DiffViewUtils::Conflicting() :
DiffViewUtils::Differs()
);
};
for (const FSCSDiffEntry& Difference : DifferingProperties.Entries)
{
TSharedPtr<FBlueprintDifferenceTreeEntry> Entry = MakeShared<FBlueprintDifferenceTreeEntry>(
FOnDiffEntryFocused::CreateStatic(FocusSCSDifferenceEntry, Difference, SelectionCallback, this),
FGenerateDiffEntryWidget::CreateStatic(CreateSCSDifferenceWidget, Difference, RightRevision));
Children.Push(Entry);
OutRealDifferences.Push(Entry);
}
}
const bool bHasDifferences = Children.Num() != 0;
if (!bHasDifferences)
{
// make one child informing the user that there are no differences:
Children.Push(FBlueprintDifferenceTreeEntry::NoDifferencesEntry());
}
static const FText SCSLabel = NSLOCTEXT("FBlueprintDifferenceTreeEntry", "SCSLabel", "Components");
static const FText SCSTooltip = NSLOCTEXT("FBlueprintDifferenceTreeEntry", "SCSTooltip", "The list of changes made in the Components panel");
OutTreeEntries.Push(FBlueprintDifferenceTreeEntry::CreateCategoryEntry(
SCSLabel,
SCSTooltip,
SelectionCallback,
Children,
bHasDifferences
));
// add comments as children to this category
GenerateCategoryCommentTreeEntries(OutTreeEntries.Last()->Children, OutRealDifferences, TEXT("Components"));
}
void FSCSDiffControl::EnableComments(TWeakPtr<STreeView<TSharedPtr<FBlueprintDifferenceTreeEntry>>> TreeView)
{
IDiffControl::EnableComments(TreeView, OldSCS.GetBlueprint(), NewSCS.GetBlueprint());
}
FDetailsDiffControl::FDetailsDiffControl(const UObject* InOldObject, const UObject* InNewObject,
FOnDiffEntryFocused InSelectionCallback, bool bPopulateOutTreeEntries)
: SelectionCallback(InSelectionCallback)
, bPopulateOutTreeEntries(bPopulateOutTreeEntries)
{
if (InOldObject)
{
InsertObject(InOldObject, true);
}
if (InNewObject)
{
InsertObject(InNewObject, false);
}
}
FDetailsDiffControl::~FDetailsDiffControl()
{
for(const TPair<const UObject*, FDetailsDiff>& Pair : DetailsDiffs)
{
Pair.Value.DetailsWidget()->SetOnDisplayedPropertiesChanged(FOnDisplayedPropertiesChanged());
}
}
void FDetailsDiffControl::Tick()
{
for (auto& [Object, PropertyTreeDiffs] : PropertyTreeDifferences)
{
if (PropertyTreeDiffs.Left)
{
constexpr float MaxTickTimeMs = 0.01f;
PropertyTreeDiffs.Left->Tick(MaxTickTimeMs);
}
}
}
void FDetailsDiffControl::GenerateTreeEntries(TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutTreeEntries, TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutRealDifferences)
{
GenerateTreeEntriesWithoutComments(OutTreeEntries, OutRealDifferences);
// add comments
GenerateCategoryCommentTreeEntries(OutTreeEntries, OutRealDifferences, TEXT("Details"));
}
void FDetailsDiffControl::GenerateTreeEntriesWithoutComments(
TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutTreeEntries,
TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutRealDifferences)
{
TArray<FSingleObjectDiffEntry> DifferingProperties;
for (int32 LeftIndex = 0; LeftIndex < ObjectDisplayOrder.Num() - 1; ++LeftIndex)
{
const UObject* LeftObject = ObjectDisplayOrder[LeftIndex];
if (!ensure(LeftObject))
{
continue;
}
const TSharedPtr<FAsyncDetailViewDiff> Diff = PropertyTreeDifferences[LeftObject].Right;
Diff->FlushQueue(); // make sure differences are fully up to date
Diff->GetPropertyDifferences(DifferingProperties);
}
for (auto&[Object, DetailsDiff] : DetailsDiffs)
{
Algo::Transform(DifferingProperties, PropertyAllowList,
[&InObject = Object](const FSingleObjectDiffEntry& DiffEntry)
{
return DiffEntry.Identifier.ResolvePath(InObject);
});
DetailsDiff.DetailsWidget()->UpdatePropertyAllowList(PropertyAllowList);
}
if (GenerateCustomEntriesCallback.IsBound())
{
TArray<FSingleObjectDiffEntry> CustomEntries;
GenerateCustomEntriesCallback.Execute(CustomEntries);
DifferingProperties.Append(CustomEntries);
}
auto GenerateDiffEntry = [&](const FSingleObjectDiffEntry& Difference)
{
return MakeShared<FBlueprintDifferenceTreeEntry>(
FOnDiffEntryFocused::CreateSP(TSharedFromThis<FDetailsDiffControl>::AsShared(), &FDetailsDiffControl::OnSelectDiffEntry, Difference.Identifier),
FGenerateDiffEntryWidget::CreateStatic(&GenerateObjectDiffWidget, Difference, RightRevision));
};
if (OrganizeEntriesCallback.IsBound())
{
auto GenerateCategoryEntry = [&](FText& CategoryName)
{
return MakeShared<FBlueprintDifferenceTreeEntry>(
/*InOnFocus*/nullptr,
FGenerateDiffEntryWidget::CreateStatic(&GenerateSimpleDiffWidget, CategoryName));
};
TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>> Entries;
OrganizeEntriesCallback.Execute(Entries, DifferingProperties, GenerateDiffEntry, GenerateCategoryEntry);
for (const TSharedPtr<FBlueprintDifferenceTreeEntry>& Entry : Entries)
{
Children.Push(Entry);
OutRealDifferences.Push(Entry);
if (bPopulateOutTreeEntries)
{
OutTreeEntries.Push(Entry);
}
}
}
else
{
for (const FSingleObjectDiffEntry& Difference : DifferingProperties)
{
TSharedPtr<FBlueprintDifferenceTreeEntry> Entry = GenerateDiffEntry(Difference);
Children.Push(Entry);
OutRealDifferences.Push(Entry);
if (bPopulateOutTreeEntries)
{
OutTreeEntries.Push(Entry);
}
}
}
}
void FDetailsDiffControl::EnableComments(TWeakPtr<STreeView<TSharedPtr<FBlueprintDifferenceTreeEntry>>> TreeView)
{
IDiffControl::EnableComments(TreeView, ObjectDisplayOrder);
}
TSharedRef<IDetailsView> FDetailsDiffControl::InsertObject(const UObject* Object, bool bScrollbarOnLeft, int32 Index)
{
FDetailsDiff DetailsDiff(Object, bScrollbarOnLeft);
const TSharedRef<IDetailsView> DetailsView = DetailsDiff.DetailsWidget();
DetailsView->UpdatePropertyAllowList(PropertyAllowList);
// hook details view so we can recalculate diffs if/when a customization
// triggers a refresh:
DetailsView->SetOnDisplayedPropertiesChanged( ::FOnDisplayedPropertiesChanged::CreateRaw(this, &FDetailsDiffControl::HandlePropertiesChanged) );
if (Index == INDEX_NONE)
{
Index = ObjectDisplayOrder.Num();
}
DetailsDiffs.Add(Object, DetailsDiff);
ObjectDisplayOrder.Insert(Object, Index);
PropertyTreeDifferences.Add(Object, {});
// set up interaction with left panel
if (ObjectDisplayOrder.IsValidIndex(Index - 1))
{
const UObject* OtherObject = ObjectDisplayOrder[Index - 1];
FDetailsDiff& OtherDetailsDiff = DetailsDiffs[OtherObject];
const TSharedRef<IDetailsView> OtherDetailsView = OtherDetailsDiff.DetailsWidget();
const auto ScrollRate = GetLinkedScrollRateAttribute(OtherDetailsView, DetailsView);
FDetailsDiff::LinkScrolling(OtherDetailsDiff, DetailsDiff, ScrollRate);
PropertyTreeDifferences[OtherObject].Right = MakeShared<FAsyncDetailViewDiff>(OtherDetailsView, DetailsView);
PropertyTreeDifferences[Object].Left = PropertyTreeDifferences[OtherObject].Right;
}
// Set up interaction with right panel
if (ObjectDisplayOrder.IsValidIndex(Index + 1))
{
const UObject* OtherObject = ObjectDisplayOrder[Index + 1];
FDetailsDiff& OtherDetailsDiff = DetailsDiffs[OtherObject];
const TSharedRef<IDetailsView> OtherDetailsView = OtherDetailsDiff.DetailsWidget();
const auto ScrollRate = GetLinkedScrollRateAttribute(DetailsView, OtherDetailsView);
FDetailsDiff::LinkScrolling(DetailsDiff, OtherDetailsDiff, ScrollRate);
PropertyTreeDifferences[OtherObject].Left = MakeShared<FAsyncDetailViewDiff>(DetailsView, OtherDetailsView);
PropertyTreeDifferences[Object].Right = PropertyTreeDifferences[OtherObject].Left;
}
return DetailsView;
}
TSharedRef<IDetailsView> FDetailsDiffControl::GetDetailsWidget(const UObject* Object) const
{
return DetailsDiffs[Object].DetailsWidget();
}
TSharedPtr<IDetailsView> FDetailsDiffControl::TryGetDetailsWidget(const UObject* Object) const
{
if (const FDetailsDiff* Found = DetailsDiffs.Find(Object))
{
return Found->DetailsWidget();
}
return nullptr;
}
TSharedPtr<FAsyncDetailViewDiff> FDetailsDiffControl::GetDifferencesWithLeft(const UObject* Object) const
{
return PropertyTreeDifferences[Object].Left;
}
TSharedPtr<FAsyncDetailViewDiff> FDetailsDiffControl::GetDifferencesWithRight(const UObject* Object) const
{
return PropertyTreeDifferences[Object].Right;
}
int32 FDetailsDiffControl::IndexOfObject(const UObject* Object) const
{
return ObjectDisplayOrder.IndexOfByKey(Object);
}
void FDetailsDiffControl::OnSelectDiffEntry(FPropertySoftPath PropertyName)
{
SelectionCallback.ExecuteIfBound();
for (auto&[Object, DetailsDiff] : DetailsDiffs)
{
DetailsDiff.HighlightProperty(PropertyName);
}
}
TAttribute<TArray<FVector2f>> FDetailsDiffControl::GetLinkedScrollRateAttribute(const TSharedRef<IDetailsView>& OldDetailsView, const TSharedRef<IDetailsView>& NewDetailsView)
{
return TAttribute<TArray<FVector2f>>::CreateRaw(this, &FDetailsDiffControl::GetLinkedScrollRate, OldDetailsView, NewDetailsView);
}
TArray<FVector2f> FDetailsDiffControl::GetLinkedScrollRate(TSharedRef<IDetailsView> LeftDetailsView, TSharedRef<IDetailsView> RightDetailsView) const
{
const UObject* LeftObject = LeftDetailsView->GetSelectedObjects()[0].Get();
return PropertyTreeDifferences[LeftObject].Right->GenerateScrollSyncRate();
}
void FDetailsDiffControl::HandlePropertiesChanged()
{
// Set up interaction with left panel
if(ObjectDisplayOrder.Num() != 2)
{
return;
}
const UObject* Object = ObjectDisplayOrder[0];
const UObject* OtherObject = ObjectDisplayOrder[1];
if(!Object || !OtherObject)
{
return; // should have any FAsyncDetailViewDiff to refresh
}
const TSharedRef<IDetailsView> DetailsView = GetDetailsWidget(Object);
const TSharedRef<IDetailsView> OtherDetailsView = GetDetailsWidget(OtherObject);
PropertyTreeDifferences[OtherObject].Left = MakeShared<FAsyncDetailViewDiff>(DetailsView, OtherDetailsView);
PropertyTreeDifferences[Object].Right = PropertyTreeDifferences[OtherObject].Left;
}
/////////////////////////////////////////////////////////////////////////////
/// FCDODiffControl
FCDODiffControl::FCDODiffControl(const UObject* InOldObject, const UObject* InNewObject, FOnDiffEntryFocused InSelectionCallback)
: FDetailsDiffControl(InOldObject, InNewObject, InSelectionCallback, false)
{
}
void FCDODiffControl::GenerateTreeEntries(TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutTreeEntries, TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutRealDifferences)
{
FDetailsDiffControl::GenerateTreeEntriesWithoutComments(OutTreeEntries, OutRealDifferences);
const bool bHasDifferences = Children.Num() != 0;
if (!bHasDifferences)
{
// make one child informing the user that there are no differences:
Children.Push(FBlueprintDifferenceTreeEntry::NoDifferencesEntry());
}
static const FText DefaultsLabel = NSLOCTEXT("FBlueprintDifferenceTreeEntry", "DefaultsLabel", "Defaults");
static const FText DefaultsTooltip = NSLOCTEXT("FBlueprintDifferenceTreeEntry", "DefaultsTooltip", "The list of changes made in the Defaults panel");
OutTreeEntries.Push(FBlueprintDifferenceTreeEntry::CreateCategoryEntry(
DefaultsLabel,
DefaultsTooltip,
SelectionCallback,
Children,
bHasDifferences
));
// add comments as children to this category
GenerateCategoryCommentTreeEntries(OutTreeEntries.Last()->Children, OutRealDifferences, TEXT("Defaults"));
Children = OutTreeEntries.Last()->Children;
}
/////////////////////////////////////////////////////////////////////////////
/// FClassSettingsDiffControl
FClassSettingsDiffControl::FClassSettingsDiffControl(const UObject* InOldObject, const UObject* InNewObject, FOnDiffEntryFocused InSelectionCallback)
: FDetailsDiffControl(InOldObject, InNewObject, InSelectionCallback, false)
{
}
void FClassSettingsDiffControl::GenerateTreeEntries(TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutTreeEntries, TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutRealDifferences)
{
FDetailsDiffControl::GenerateTreeEntriesWithoutComments(OutTreeEntries, OutRealDifferences);
const bool bHasDifferences = Children.Num() != 0;
if (!bHasDifferences)
{
// make one child informing the user that there are no differences:
Children.Push(FBlueprintDifferenceTreeEntry::NoDifferencesEntry());
}
static const FText SettingsLabel = NSLOCTEXT("FBlueprintDifferenceTreeEntry", "SettingsLabel", "Class Settings");
static const FText SettingsTooltip = NSLOCTEXT("FBlueprintDifferenceTreeEntry", "SettingsTooltip", "The list of changes made in the Class Settings panel");
OutTreeEntries.Push(FBlueprintDifferenceTreeEntry::CreateCategoryEntry(
SettingsLabel,
SettingsTooltip,
SelectionCallback,
Children,
bHasDifferences
));
// add comments as children to this category
GenerateCategoryCommentTreeEntries(OutTreeEntries.Last()->Children, OutRealDifferences, TEXT("Class Settings"));
Children = OutTreeEntries.Last()->Children;
}
/////////////////////////////////////////////////////////////////////////////
/// FBlueprintTypeDiffControl
FBlueprintTypeDiffControl::FBlueprintTypeDiffControl(const UBlueprint* InBlueprintOld, const UBlueprint* InBlueprintNew, FOnDiffEntryFocused InSelectionCallback)
: BlueprintOld(InBlueprintOld)
, BlueprintNew(InBlueprintNew)
, SelectionCallback(InSelectionCallback)
, bDiffSucceeded(false)
{
check(InBlueprintNew || InBlueprintOld);
}
void FBlueprintTypeDiffControl::GenerateTreeEntries(TArray< TSharedPtr<FBlueprintDifferenceTreeEntry> >& OutTreeEntries, TArray< TSharedPtr<FBlueprintDifferenceTreeEntry> >& OutRealDifferences)
{
BuildDiffSourceArray();
TArray< TSharedPtr<FBlueprintDifferenceTreeEntry> > Children;
bool bHasRealChange = false;
// First add manual diffs in main category
for (const TSharedPtr<FDiffResultItem>& Difference : DiffListSource)
{
TSharedPtr<FBlueprintDifferenceTreeEntry> ChildEntry = MakeShared<FBlueprintDifferenceTreeEntry>(
SelectionCallback,
FGenerateDiffEntryWidget::CreateSP(Difference.ToSharedRef(), &FDiffResultItem::GenerateWidget));
Children.Push(ChildEntry);
OutRealDifferences.Push(ChildEntry);
if (Difference->Result.IsRealDifference())
{
bHasRealChange = true;
}
}
if (Children.Num() == 0)
{
// Make one child informing the user that there are no differences, or that it is unknown
if (bDiffSucceeded)
{
Children.Push(FBlueprintDifferenceTreeEntry::NoDifferencesEntry());
}
else
{
Children.Push(FBlueprintDifferenceTreeEntry::UnknownDifferencesEntry());
}
}
TSharedPtr<FBlueprintDifferenceTreeEntry> CategoryEntry = MakeShared<FBlueprintDifferenceTreeEntry>(
SelectionCallback,
FGenerateDiffEntryWidget::CreateSP(AsShared(), &FBlueprintTypeDiffControl::GenerateCategoryWidget, bHasRealChange),
Children);
OutTreeEntries.Push(CategoryEntry);
// add comments as children to this category
const FString CommentCategoryKey = (BlueprintNew ? BlueprintNew : BlueprintOld)->GetClass()->GetName();
GenerateCategoryCommentTreeEntries(OutTreeEntries.Last()->Children, OutRealDifferences, CommentCategoryKey);
// Now add subobject diffs, one category per object
for (const TSharedPtr<FSubObjectDiff>& SubObjectDiff : SubObjectDiffs)
{
Children.Reset();
Children.Append(SubObjectDiff->Diffs);
OutRealDifferences.Append(SubObjectDiff->Diffs);
TSharedPtr<FBlueprintDifferenceTreeEntry> SubObjectEntry = FBlueprintDifferenceTreeEntry::CreateCategoryEntry(
SubObjectDiff->SourceResult.DisplayString,
SubObjectDiff->SourceResult.ToolTip,
FOnDiffEntryFocused::CreateSP(AsShared(), &FBlueprintTypeDiffControl::OnSelectSubobjectDiff, FPropertySoftPath(), SubObjectDiff),
Children,
true);
OutTreeEntries.Push(SubObjectEntry);
// add comments as children to this category
GenerateCategoryCommentTreeEntries(OutTreeEntries.Last()->Children, OutRealDifferences, SubObjectDiff->SourceResult.OwningObjectPath);
}
}
void FBlueprintTypeDiffControl::EnableComments(TWeakPtr<STreeView<TSharedPtr<FBlueprintDifferenceTreeEntry>>> TreeView)
{
IDiffControl::EnableComments(TreeView, BlueprintOld, BlueprintNew);
}
TSharedRef<SWidget> FBlueprintTypeDiffControl::GenerateCategoryWidget(bool bHasRealDiffs)
{
FLinearColor Color = FLinearColor::White;
if (bHasRealDiffs)
{
Color = DiffViewUtils::Differs();
}
const FText Label = (BlueprintNew ? BlueprintNew : BlueprintOld)->GetClass()->GetDisplayNameText();
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
[
SNew(STextBlock)
.ColorAndOpacity(Color)
.Text(Label)
];
}
void FBlueprintTypeDiffControl::BuildDiffSourceArray()
{
if (!BlueprintNew || !BlueprintOld)
{
bDiffSucceeded = true;
return;
}
TArray<FDiffSingleResult> BlueprintDiffResults;
FDiffResults BlueprintDiffs(&BlueprintDiffResults);
if (BlueprintNew->FindDiffs(BlueprintOld, BlueprintDiffs))
{
bDiffSucceeded = true;
// Add manual diffs
for (const FDiffSingleResult& CurrentDiff : BlueprintDiffResults)
{
if (CurrentDiff.Diff == EDiffType::OBJECT_REQUEST_DIFF)
{
// Turn into a subobject diff
// Invert order, we want old then new
TSharedPtr<FSubObjectDiff> SubObjectDiff = MakeShared<FSubObjectDiff>(CurrentDiff, CurrentDiff.Object2, CurrentDiff.Object1);
TArray<FSingleObjectDiffEntry> DifferingProperties;
SubObjectDiff->OldDetails.DiffAgainst(SubObjectDiff->NewDetails, DifferingProperties, true);
if (DifferingProperties.Num() > 0)
{
// Actual differences, so add to tree
SubObjectDiffs.Add(SubObjectDiff);
for (const FSingleObjectDiffEntry& Difference : DifferingProperties)
{
TSharedPtr<FBlueprintDifferenceTreeEntry> Entry = MakeShared<FBlueprintDifferenceTreeEntry>(
FOnDiffEntryFocused::CreateSP(AsShared(), &FBlueprintTypeDiffControl::OnSelectSubobjectDiff, Difference.Identifier, SubObjectDiff),
FGenerateDiffEntryWidget::CreateStatic(&GenerateObjectDiffWidget, Difference, RightRevision));
SubObjectDiff->Diffs.Push(Entry);
}
}
}
else
{
DiffListSource.Add(MakeShared<FDiffResultItem>(CurrentDiff));
}
}
Algo::SortBy(DiffListSource, [](const TSharedPtr<FDiffResultItem>& Data) { return Data->Result.Diff; });
}
}
void FBlueprintTypeDiffControl::OnSelectSubobjectDiff(FPropertySoftPath Identifier, TSharedPtr<FSubObjectDiff> SubObjectDiff)
{
// This allows the owning control to focus the correct tab (or do whatever else it likes):
SelectionCallback.ExecuteIfBound();
if (SubObjectDiff.IsValid())
{
SubObjectDiff->OldDetails.HighlightProperty(Identifier);
SubObjectDiff->NewDetails.HighlightProperty(Identifier);
OldDetailsBox->SetContent(SubObjectDiff->OldDetails.DetailsWidget());
NewDetailsBox->SetContent(SubObjectDiff->NewDetails.DetailsWidget());
}
}
/////////////////////////////////////////////////////////////////////////////
/// FGraphToDiff
FGraphToDiff::FGraphToDiff(SBlueprintDiff* InDiffWidget, UEdGraph* InGraphOld, UEdGraph* InGraphNew, const FRevisionInfo& InRevisionOld, const FRevisionInfo& InRevisionNew)
: FoundDiffs(MakeShared<TArray<FDiffSingleResult>>()), DiffWidget(InDiffWidget), GraphOld(InGraphOld), GraphNew(InGraphNew), RevisionOld(InRevisionOld), RevisionNew(InRevisionNew)
{
check(InGraphOld || InGraphNew); //one of them needs to exist
//need to know when it is modified
if (InGraphNew)
{
OnGraphChangedDelegateHandle = InGraphNew->AddOnGraphChangedHandler( FOnGraphChanged::FDelegate::CreateRaw(this, &FGraphToDiff::OnGraphChanged));
}
BuildDiffSourceArray();
}
FGraphToDiff::~FGraphToDiff()
{
if (GraphNew)
{
GraphNew->RemoveOnGraphChangedHandler( OnGraphChangedDelegateHandle);
}
}
void FGraphToDiff::GenerateTreeEntries(TArray< TSharedPtr<FBlueprintDifferenceTreeEntry> >& OutTreeEntries, TArray< TSharedPtr<FBlueprintDifferenceTreeEntry> >& OutRealDifferences)
{
if (!DiffListSource.IsEmpty())
{
RealDifferencesStartIndex = OutRealDifferences.Num();
}
TArray< TSharedPtr<FBlueprintDifferenceTreeEntry> > Children;
for (const TSharedPtr<FDiffResultItem>& Difference : DiffListSource)
{
TSharedPtr<FBlueprintDifferenceTreeEntry> ChildEntry = MakeShared<FBlueprintDifferenceTreeEntry>(
FOnDiffEntryFocused::CreateRaw(DiffWidget, &SBlueprintDiff::OnDiffListSelectionChanged, Difference),
FGenerateDiffEntryWidget::CreateSP(Difference.ToSharedRef(), &FDiffResultItem::GenerateWidget));
Children.Push(ChildEntry);
OutRealDifferences.Push(ChildEntry);
}
if (Children.Num() == 0)
{
// make one child informing the user that there are no differences:
Children.Push(FBlueprintDifferenceTreeEntry::NoDifferencesEntry());
}
TSharedPtr<FBlueprintDifferenceTreeEntry> Entry = MakeShared<FBlueprintDifferenceTreeEntry>(
FOnDiffEntryFocused::CreateRaw(DiffWidget, &SBlueprintDiff::OnGraphSelectionChanged, TSharedPtr<FGraphToDiff>(AsShared()), ESelectInfo::Direct),
FGenerateDiffEntryWidget::CreateSP(AsShared(), &FGraphToDiff::GenerateCategoryWidget),
Children);
OutTreeEntries.Push(Entry);
// add comments as children to this category
FString GraphName;
const UEdGraph* Graph = GraphOld ? GraphOld : GraphNew;
if (const UEdGraphSchema* Schema = Graph->GetSchema())
{
FGraphDisplayInfo DisplayInfo;
Schema->GetGraphDisplayInformation(*Graph, DisplayInfo);
GraphName = DisplayInfo.DisplayName.ToString();
}
else
{
GraphName = Graph->GetFName().ToString();
}
GenerateCategoryCommentTreeEntries(OutTreeEntries.Last()->Children, OutRealDifferences, GraphName);
}
UEdGraph* FGraphToDiff::GetGraphOld() const
{
return GraphOld;
}
UEdGraph* FGraphToDiff::GetGraphNew() const
{
return GraphNew;
}
void FGraphToDiff::EnableComments(TWeakPtr<STreeView<TSharedPtr<FBlueprintDifferenceTreeEntry>>> TreeView)
{
IDiffControl::EnableComments(TreeView, GetGraphOld(), GetGraphNew());
}
FText FGraphToDiff::GetToolTip()
{
if (GraphOld && GraphNew)
{
if (DiffListSource.Num() > 0)
{
return LOCTEXT("ContainsDifferences", "Revisions are different");
}
else
{
return LOCTEXT("GraphsIdentical", "Revisions appear to be identical");
}
}
else
{
UEdGraph* GoodGraph = GraphOld ? GraphOld : GraphNew;
check(GoodGraph);
const FRevisionInfo& Revision = GraphNew ? RevisionOld : RevisionNew;
FText RevisionText = LOCTEXT("CurrentRevision", "Current Revision");
if (!Revision.Revision.IsEmpty())
{
RevisionText = FText::Format(LOCTEXT("Revision Number", "Revision {0}"), FText::FromString(Revision.Revision));
}
return FText::Format(LOCTEXT("MissingGraph", "Graph '{0}' missing from {1}"), FText::FromString(GoodGraph->GetName()), RevisionText);
}
}
TSharedRef<SWidget> FGraphToDiff::GenerateCategoryWidget()
{
const UEdGraph* Graph = GraphOld ? GraphOld : GraphNew;
check(Graph);
FLinearColor Color = (GraphOld && GraphNew) ? DiffViewUtils::Identical() : FLinearColor(0.3f,0.3f,1.f);
const bool bHasDiffs = DiffListSource.Num() > 0;
if (bHasDiffs)
{
Color = DiffViewUtils::Differs();
}
FText GraphName;
if (const UEdGraphSchema* Schema = Graph->GetSchema())
{
FGraphDisplayInfo DisplayInfo;
Schema->GetGraphDisplayInformation(*Graph, DisplayInfo);
GraphName = DisplayInfo.DisplayName;
}
else
{
GraphName = FText::FromName(Graph->GetFName());
}
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
[
SNew(STextBlock)
.ColorAndOpacity(Color)
.Text(GraphName)
.ToolTipText(GetToolTip())
]
+ DiffViewUtils::Box( GraphOld != nullptr, Color )
+ DiffViewUtils::Box( GraphNew != nullptr, Color );
}
void FGraphToDiff::BuildDiffSourceArray()
{
FoundDiffs->Empty();
FGraphDiffControl::DiffGraphs(GraphOld, GraphNew, *FoundDiffs);
Algo::SortBy(*FoundDiffs, &FDiffSingleResult::Diff);
DiffListSource.Empty();
for (const FDiffSingleResult& Diff : *FoundDiffs)
{
DiffListSource.Add(MakeShared<FDiffResultItem>(Diff));
}
}
void FGraphToDiff::OnGraphChanged( const FEdGraphEditAction& Action )
{
DiffWidget->OnGraphChanged(this);
}
// returns whether a file being diffed should display comments
static bool AreCommentsEnabled(FString File)
{
if (UE::DiffControl::GIsFileInReview)
{
return UE::DiffControl::GIsFileInReview(File);
}
return false;
}
FCommentTreeEntry::FCommentTreeEntry(TWeakPtr<FReviewCommentsDiffControl> InCommentsControl, const FReviewComment& InComment, const TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& InChildren)
: FBlueprintDifferenceTreeEntry({}, FGenerateDiffEntryWidget::CreateRaw(this, &FCommentTreeEntry::CreateWidget), InChildren)
, Comment(InComment)
, CommentsControl(InCommentsControl)
{}
FCommentTreeEntry::~FCommentTreeEntry()
{
if (OnCommentPostedHandle.IsValid())
{
UE::DiffControl::GOnCommentPosted.Remove(OnCommentPostedHandle);
}
}
TSharedRef<FCommentTreeEntry> FCommentTreeEntry::Make(TWeakPtr<FReviewCommentsDiffControl> CommentsControl, const FReviewComment& Comment,
const TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& Children)
{
return MakeShared<FCommentTreeEntry>(CommentsControl, Comment, Children);
}
int32 FCommentTreeEntry::GetCommentIDChecked() const
{
return *Comment.CommentID;
}
void FCommentTreeEntry::AwaitCommentPost()
{
OnCommentPostedHandle = UE::DiffControl::GOnCommentPosted.AddSP(this, &FCommentTreeEntry::OnCommentPosted);
}
TSharedRef<SWidget> FCommentTreeEntry::CreateWidget()
{
// if a widget for this comment is being created that means it's in view. Mark the ReadBy state to reflect that the user is reading it
if (!Comment.ReadBy.IsSet())
{
Comment.ReadBy = TSet<FString>();
}
const FString Username = UE::DiffControl::GGetReviewerUsername();
if (!Comment.ReadBy->Find(Username))
{
Comment.ReadBy->Add(Username);
FReviewComment ReadByEdit;
ReadByEdit.CommentID = Comment.CommentID;
ReadByEdit.ReadBy = Comment.ReadBy;
UE::DiffControl::GEditReviewComment(ReadByEdit);
}
FDateTime DateTime = FDateTime::UtcNow();
if (Comment.EditedTime.IsSet())
{
DateTime = *Comment.EditedTime;
}
else if (Comment.CreatedTime.IsSet())
{
DateTime = *Comment.CreatedTime;
}
FText DateText;
if (DateTime.GetDay() == FDateTime::UtcNow().GetDay())
{
DateText = FText::AsTime(DateTime, EDateTimeStyle::Short);
}
else
{
DateText = FText::AsDate(DateTime, EDateTimeStyle::Short);
}
// constructs a hyperlink-like text button
auto MakeCommentOptionButton = [](const FText &Text)
{
const FLinearColor HoveredColor = FStyleColors::AccentBlue.GetSpecifiedColor();
const FLinearColor UnhoveredColor = HoveredColor.Desaturate(0.2f);
TSharedRef<SButton> Button = SNew(SButton)
.ButtonStyle(FAppStyle::Get(), "NoBorder")
.ForegroundColor(UnhoveredColor)
.Text(Text);
Button->SetOnHovered(FSimpleDelegate::CreateLambda([ButtonWeak = Button.ToWeakPtr(), HoveredColor]()
{
if (const TSharedPtr<SButton> Button = ButtonWeak.Pin())
{
Button->SetForegroundColor(HoveredColor);
}
}));
Button->SetOnUnhovered(FSimpleDelegate::CreateLambda([ButtonWeak = Button.ToWeakPtr(), UnhoveredColor]()
{
if (const TSharedPtr<SButton> Button = ButtonWeak.Pin())
{
Button->SetForegroundColor(UnhoveredColor);
}
}));
return Button;
};
const TSharedPtr<SButton> ReplyButton = MakeCommentOptionButton(LOCTEXT("ReplyToComment","Reply"));
ReplyButton->SetOnClicked(FOnClicked::CreateSP(this, &FCommentTreeEntry::OnClickReply));
const TSharedPtr<SButton> EditButton = MakeCommentOptionButton(LOCTEXT("EditComment","Edit"));
EditButton->SetOnClicked(FOnClicked::CreateSP(this, &FCommentTreeEntry::OnClickEdit));
EditButton->SetVisibility(TAttribute<EVisibility>::CreateSP(this, &FCommentTreeEntry::GetEditButtonVisibility));
FLinearColor BorderColor = FStyleColors::Recessed.GetSpecifiedColor();
BorderColor.A = 0.95f;
return SAssignNew(Content, SBorder)
.IsEnabled_Static(&AreCommentsEnabled, *Comment.Context.File)
.BorderImage(FCoreStyle::Get().GetBrush("GenericWhiteBox"))
.BorderBackgroundColor(BorderColor)
[
SNew(SVerticalBox)
+SVerticalBox::Slot()
.VAlign(VAlign_Top)
.AutoHeight()
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
[
SNew(STextBlock)
.ColorAndOpacity(this, &FCommentTreeEntry::GetUsernameColor)
.Text(FText::FromString(Comment.User.Get(TEXT("[Unknown User]"))))
.Font(FAppStyle::GetFontStyle(TEXT("BoldFont")))
]
+SHorizontalBox::Slot()
.HAlign(HAlign_Right)
.FillWidth(1)
.Padding(10.f,0.f,0.f,0.f)
[
SNew(STextBlock)
.ColorAndOpacity(FSlateColor::UseSubduedForeground())
.Text(DateText)
]
]
+SVerticalBox::Slot()
.VAlign(VAlign_Top)
.AutoHeight()
[
SAssignNew(CommentTextBox, SMultiLineEditableTextBox)
.Text(FText::FromString(GetCommentString()))
.AutoWrapText(true)
.IsReadOnly(this, &FCommentTreeEntry::IsCommentTextBoxReadonly)
]
+SVerticalBox::Slot()
.VAlign(VAlign_Top)
.AutoHeight()
[
SAssignNew(EditReplyButtonGroup, SHorizontalBox)
.Visibility(this, &FCommentTreeEntry::GetEditReplyButtonGroupVisibility)
+SHorizontalBox::Slot()
.AutoWidth()
[
ReplyButton.ToSharedRef()
]
+SHorizontalBox::Slot()
.AutoWidth()
[
EditButton.ToSharedRef()
]
+SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SButton)
.ButtonStyle(FAppStyle::Get(), "SimpleButton")
.OnClicked(this, &FCommentTreeEntry::OnLikeToggle)
.ToolTipText(this, &FCommentTreeEntry::GetLikeTooltip)
.ContentScale(0.8f)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(SImage)
.Image(this, &FCommentTreeEntry::GetLikeIcon)
.ColorAndOpacity(this, &FCommentTreeEntry::GetLikeIconColor)
]
]
]
+SVerticalBox::Slot()
.VAlign(VAlign_Top)
.AutoHeight()
[
SAssignNew(SubmitCancelButtonGroup, SHorizontalBox)
.Visibility(this, &FCommentTreeEntry::GetSubmitCancelButtonGroupVisibility)
+SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SButton)
.Text(LOCTEXT("SubmitEdit","Submit"))
.IsEnabled(this, &FCommentTreeEntry::IsSubmitButtonEnabled)
.OnClicked(this, &FCommentTreeEntry::OnEditSubmitClicked)
]
+SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SButton)
.Text(LOCTEXT("CancelEdit","Cancel"))
.OnClicked(this, &FCommentTreeEntry::OnEditCancelClicked)
]
]
];
}
bool FCommentTreeEntry::IsCommentTextBoxReadonly() const
{
return !IsEditMode();
}
FReply FCommentTreeEntry::OnClickReply()
{
if (const TSharedPtr<FReviewCommentsDiffControl> Control = CommentsControl.Pin())
{
Control->DraftReply(AsShared());
return FReply::Handled();
}
return FReply::Unhandled();
}
FReply FCommentTreeEntry::OnClickEdit()
{
SetEditMode(true);
return FReply::Handled();
}
FReply FCommentTreeEntry::OnLikeToggle()
{
if (!Comment.Likes.IsSet())
{
Comment.Likes = TSet<FString>{};
}
const FString Username = UE::DiffControl::GGetReviewerUsername();
if (Comment.Likes->Contains(Username))
{
Comment.Likes->Remove(Username);
}
else
{
Comment.Likes->Add(Username);
}
FReviewComment LikeEdit;
LikeEdit.CommentID = Comment.CommentID;
LikeEdit.Likes = Comment.Likes;
UE::DiffControl::GEditReviewComment(LikeEdit);
return FReply::Handled();
}
// convert an iterable structure to a text based list
// for example,
// {"a"} -> "{Range[0]}" -> "a"
// {"a", "b"} -> "{Range[0]} {Conjunction} {Range[1]}" -> "a and b"
// {"a", "b", "c"} -> "{Range[0]}{Delimiter} {Range[1]}{Delimiter} {Conjunction} {Range[2]}" -> "a, b, and c"
template <typename RangeType, typename PredicateType>
static FText RangeAsText(RangeType&& Range, const FText& Delimiter, const FText& Conjunction, PredicateType Transformer)
{
TArray<FText> ItemsAsText;
int32 Index = 0;
const int32 LastIndex = Range.Num() - 1;
if (Range.Num() == 1)
{
return Invoke(Transformer, *Range.begin());
}
for (auto&& Elem : Forward<RangeType>(Range))
{
FText ElementText = Invoke(Transformer, Elem);
if (Index == LastIndex && !Conjunction.IsEmpty())
{
// for the last item, prepend a conjunction
ItemsAsText.Add(FText::FormatNamed(
LOCTEXT("ListLastElement", "{Conjunction} {Element}"),
TEXT("Conjunction"), Conjunction,
TEXT("Element"), Invoke(Transformer, Elem)
));
}
else
{
ItemsAsText.Add(Invoke(Transformer, Elem));
}
++Index;
}
if (Range.Num() == 2)
{
// for two elements, don't add a comma
return FText::Join(LOCTEXT("Space"," "), ItemsAsText);
}
return FText::Join(FText::Format(LOCTEXT("DelimeterWithSpace","{0} "), Delimiter), ItemsAsText);
}
FText FCommentTreeEntry::GetLikeTooltip() const
{
if (Comment.Likes.IsSet() && !Comment.Likes->IsEmpty())
{
FText(* const StringToText)(const FString&) = &FText::FromString;
const FText LikesListText = RangeAsText(*Comment.Likes, LOCTEXT("ListDelimeter",","), LOCTEXT("ConjunctionAnd","and"), StringToText);
return FText::Format(LOCTEXT("LikedBy", "Liked By: {0}"), LikesListText);
}
return LOCTEXT("NoLikes","No Likes");
}
const FSlateBrush* FCommentTreeEntry::GetLikeIcon() const
{
if (Comment.Likes.IsSet() && !Comment.Likes->IsEmpty())
{
return FAppStyle::Get().GetBrush(TEXT("Icons.Heart"));
}
return FAppStyle::Get().GetBrush(TEXT("Icons.HollowHeart"));
}
FSlateColor FCommentTreeEntry::GetUsernameColor() const
{
const FString Username = UE::DiffControl::GGetReviewerUsername();
if (Comment.User == Username)
{
return FStyleColors::AccentBlue;
}
return FStyleColors::AccentGreen.GetSpecifiedColor().Desaturate(0.2f);
}
FSlateColor FCommentTreeEntry::GetLikeIconColor() const
{
const FString Username = UE::DiffControl::GGetReviewerUsername();
if (Comment.Likes.IsSet())
{
if (Comment.Likes->Contains(Username))
{
return FSlateColor::UseForeground();
}
}
return FSlateColor::UseSubduedForeground();
}
EVisibility FCommentTreeEntry::GetEditReplyButtonGroupVisibility() const
{
if (IsEditMode())
{
return EVisibility::Collapsed;
}
// if the comment is still being submitted, don't allow edits or replies yet
if (!Comment.CommentID.IsSet())
{
return EVisibility::Hidden;
}
return Content->IsHovered() ? EVisibility::Visible : EVisibility::Hidden;
}
EVisibility FCommentTreeEntry::GetEditButtonVisibility() const
{
const FString Reviewer = UE::DiffControl::GGetReviewerUsername();
if (Reviewer == Comment.User)
{
return EVisibility::Visible;
}
return EVisibility::Collapsed;
}
EVisibility FCommentTreeEntry::GetSubmitCancelButtonGroupVisibility() const
{
return IsEditMode() ? EVisibility::Visible : EVisibility::Collapsed;
}
bool FCommentTreeEntry::IsSubmitButtonEnabled() const
{
// don't allow submission of empty strings
return !CommentTextBox->GetText().ToString().TrimEnd().IsEmpty();
}
FReply FCommentTreeEntry::OnEditSubmitClicked()
{
if (HasCommentStringChanged())
{
SetCommentString(CommentTextBox->GetText().ToString());
FReviewComment BodyEdit;
BodyEdit.CommentID = Comment.CommentID;
BodyEdit.Body = Comment.Body;
UE::DiffControl::GEditReviewComment(BodyEdit);
}
CommentTextBox->SetText(FText::FromString(GetCommentString()));
SetEditMode(false);
return FReply::Handled();
}
FReply FCommentTreeEntry::OnEditCancelClicked()
{
CommentTextBox->SetText(FText::FromString(GetCommentString()));
SetEditMode(false);
return FReply::Handled();
}
void FCommentTreeEntry::OnCommentPosted(const FReviewComment& InComment)
{
if (InComment.Body != Comment.Body)
{
return;
}
if (InComment.User == Comment.User)
{
Comment = InComment;
UE::DiffControl::GOnCommentPosted.Remove(OnCommentPostedHandle);
}
}
FString FCommentTreeEntry::GetCommentString() const
{
return Comment.Body.Get(TEXT("[Missing Comment Body]")).TrimEnd();
}
void FCommentTreeEntry::SetCommentString(const FString& NewComment)
{
Comment.Body = NewComment.TrimEnd();
}
bool FCommentTreeEntry::HasCommentStringChanged() const
{
const FString EditedCommentString = CommentTextBox->GetText().ToString().TrimEnd();
const FString CommentString = GetCommentString();
return EditedCommentString != CommentString;
}
bool FCommentTreeEntry::IsEditMode() const
{
if (bExpectedEditMode)
{
// if user de-focused the textbox without changing anything, cancel the edit
if (!CommentTextBox->HasKeyboardFocus() && !HasCommentStringChanged())
{
CommentTextBox->SetText(FText::FromString(GetCommentString()));
return false;
}
}
return bExpectedEditMode;
}
void FCommentTreeEntry::SetEditMode(bool bIsEditMode)
{
if (bIsEditMode)
{
FSlateApplication::Get().SetKeyboardFocus(CommentTextBox);
}
bExpectedEditMode = bIsEditMode;
}
FCommentDraftTreeEntry::FCommentDraftTreeEntry(TWeakPtr<FReviewCommentsDiffControl> InCommentsControl, TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>* InSiblings, int32 InReplyID)
: FBlueprintDifferenceTreeEntry({}, FGenerateDiffEntryWidget::CreateRaw(this, &FCommentDraftTreeEntry::CreateWidget), {})
, CommentsControl(InCommentsControl)
, Siblings(InSiblings)
, ReplyID(InReplyID)
{}
TSharedRef<FCommentDraftTreeEntry> FCommentDraftTreeEntry::MakeCommentDraft(TWeakPtr<FReviewCommentsDiffControl> CommentsControl, TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>* Siblings)
{
return MakeShared<FCommentDraftTreeEntry>(CommentsControl, Siblings);
}
TSharedRef<FCommentDraftTreeEntry> FCommentDraftTreeEntry::MakeReplyDraft(TWeakPtr<FReviewCommentsDiffControl> CommentsControl, TSharedPtr<FCommentTreeEntry> InParent)
{
return MakeShared<FCommentDraftTreeEntry>(CommentsControl, &InParent->Children, InParent->GetCommentIDChecked());
}
void FCommentDraftTreeEntry::ReassignSiblings(TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>* InSiblings, int32 InReplyID)
{
const int32 MyIndex = Siblings->Find(AsShared());
if (MyIndex != INDEX_NONE)
{
Siblings->RemoveAt(MyIndex);
}
Siblings = InSiblings;
ReplyID = InReplyID;
}
void FCommentDraftTreeEntry::ReassignReplyParent(TSharedPtr<FCommentTreeEntry> InParent)
{
ReassignSiblings(&InParent->Children, InParent->GetCommentIDChecked());
}
bool FCommentDraftTreeEntry::IsReply() const
{
return ReplyID != -1;
}
TSharedRef<SWidget> FCommentDraftTreeEntry::CreateWidget()
{
FLinearColor BorderColor = FStyleColors::Recessed.GetSpecifiedColor();
BorderColor.A = 0.95f;
FString FilePath;
if (const TSharedPtr<FReviewCommentsDiffControl> Control = CommentsControl.Pin())
{
FilePath = Control->GetCommentFilePath();
}
SAssignNew(Content, SBorder)
.IsEnabled_Static(&AreCommentsEnabled, FilePath)
.BorderImage(FCoreStyle::Get().GetBrush("GenericWhiteBox"))
.BorderBackgroundColor(BorderColor)
[
SNew(SVerticalBox)
+SVerticalBox::Slot()
.VAlign(VAlign_Top)
.AutoHeight()
[
SNew(SBox)
.MinDesiredHeight(50.f)
[
SAssignNew(CommentTextBox, SMultiLineEditableTextBox)
.HintText(IsReply()? LOCTEXT("AddReply", "Reply...") : LOCTEXT("AddAComment", "Add a comment"))
.ForegroundColor(FStyleColors::White)
.AutoWrapText(true)
]
]
+SVerticalBox::Slot()
.VAlign(VAlign_Top)
.AutoHeight()
.Padding(0.f, 0.f, 0.f, 20.f)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.AutoWidth()
.Padding(0.f,0.f,10.f,0.f)
[
SNew(SButton)
.Text(LOCTEXT("PostComment","Post"))
.IsEnabled(this, &FCommentDraftTreeEntry::IsPostButtonEnabled)
.OnClicked(this, &FCommentDraftTreeEntry::OnCommentPostClicked)
]
+SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.AutoWidth()
.Padding(0.f,0.f,10.f,0.f)
[
SAssignNew(FlagAsTaskCheckBox, SCheckBox)
]
+SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.AutoWidth()
[
SNew(STextBlock)
.Text(LOCTEXT("FlagCommentAsTask","Flag as Task"))
]
]
];
return Content.ToSharedRef();
}
bool FCommentDraftTreeEntry::IsPostButtonEnabled() const
{
// only allow posting if the comment has non-whitespace characters
return !CommentTextBox->GetText().ToString().TrimEnd().IsEmpty();
}
FReply FCommentDraftTreeEntry::OnCommentPostClicked()
{
if (const TSharedPtr<FReviewCommentsDiffControl> Control = CommentsControl.Pin())
{
const FString& CommentCategory = Control->GetCommentCategory();
const FString& CommentFilePath = Control->GetCommentFilePath();
FReviewComment Comment;
Comment.Body = CommentTextBox->GetText().ToString().TrimEnd();
Comment.Context.File = CommentFilePath;
Comment.ReadBy = TSet<FString>{UE::DiffControl::GGetReviewerUsername()};
if (!CommentCategory.IsEmpty())
{
Comment.Context.Category = CommentCategory;
}
if (IsReply())
{
Comment.Context.ReplyTo = ReplyID;
}
const bool bIsTask = FlagAsTaskCheckBox->GetCheckedState() == ECheckBoxState::Checked;
Comment.TaskState = bIsTask ? EReviewCommentTaskState::Open : EReviewCommentTaskState::Comment;
const int32 InsertIndex = Siblings->Find(AsShared());
check(InsertIndex != INDEX_NONE);
Control->PostComment(Comment);
const TSharedRef<FCommentTreeEntry> CommentEntry = FCommentTreeEntry::Make(Control, Comment);
CommentEntry->AwaitCommentPost(); // tell entry to update once comment is posted
Siblings->Insert(CommentEntry, InsertIndex);
return FReply::Handled();
}
return FReply::Unhandled();
}
FReviewCommentsDiffControl::FReviewCommentsDiffControl(const FString& InCommentFilePath, TWeakPtr<STreeView<TSharedPtr<FBlueprintDifferenceTreeEntry>>> TreeView)
: CommentFilePath(InCommentFilePath)
, CommentsTreeView(TreeView)
{
}
void FReviewCommentsDiffControl::GenerateCommentThreadRecursive(const FReviewComment& Comment,
const TMap<int32, TArray<const FReviewComment*>>& CommentReplyMap,
TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutTreeEntries)
{
if (Comment.bIsClosed)
{
return;
}
TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>> Children;
if (const TArray<const FReviewComment*>* Replies = CommentReplyMap.Find(*Comment.CommentID))
{
for (const FReviewComment* Reply : *Replies)
{
GenerateCommentThreadRecursive(*Reply, CommentReplyMap, Children);
}
}
OutTreeEntries.Push(FCommentTreeEntry::Make(AsShared(), Comment, Children));
}
void FReviewCommentsDiffControl::GenerateTreeEntries(TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutTreeEntries,
TArray<TSharedPtr<FBlueprintDifferenceTreeEntry>>& OutRealDifferences)
{
if (!AreCommentsEnabled(GetCommentFilePath()))
{
return;
}
TMap<int32, TArray<const FReviewComment*>> CommentReplyMap;
TArray<const FReviewComment*> HeadComments;
if (const TArray<FReviewComment>* FileComments = UE::DiffControl::GGetReviewCommentsForFile(CommentFilePath))
{
for (const FReviewComment& Comment : *FileComments)
{
if (Comment.Context.ReplyTo.IsSet())
{
CommentReplyMap.FindOrAdd(*Comment.Context.ReplyTo, {}).Add(&Comment);
continue;
}
if (Comment.Context.Category.Get({}) != CommentCategory)
{
continue;
}
HeadComments.Add(&Comment);
}
}
// add an empty entry that visually spaces the comments from the diff entries
TSharedPtr<FBlueprintDifferenceTreeEntry> PaddingEntry = TSharedPtr<FBlueprintDifferenceTreeEntry>(new FBlueprintDifferenceTreeEntry(
{}
, FGenerateDiffEntryWidget::CreateLambda([]()
{
return
SNew(SBox)
.Padding(0.f, 20.f, 0.f, 10.f)
.HAlign(HAlign_Fill)
[
SNew(SImage)
.DesiredSizeOverride(FVector2D{100.0, 2.0})
.Image(FCoreStyle::Get().GetBrush("GenericWhiteBox"))
.ColorAndOpacity(FStyleColors::White25)
];
})
, {}
));
OutTreeEntries.Push(PaddingEntry);
for (const FReviewComment* Comment : HeadComments)
{
// add comment as a thread (including replies as it's children)
GenerateCommentThreadRecursive(*Comment, CommentReplyMap, OutTreeEntries);
}
OutTreeEntries.Push(FCommentDraftTreeEntry::MakeCommentDraft(AsShared(), &OutTreeEntries));
}
void FReviewCommentsDiffControl::SetCategory(const FString& CategoryKey)
{
CommentCategory = CategoryKey;
}
void FReviewCommentsDiffControl::PostComment(FReviewComment& Comment)
{
UE::DiffControl::GPostReviewComment(Comment);
if (const TSharedPtr<STreeView<TSharedPtr<FBlueprintDifferenceTreeEntry>>> TreeView = CommentsTreeView.Pin())
{
TreeView->RebuildList();
}
}
static void DeferredCallback(TSharedPtr<SWidget> Widget, int32 FramesToSkip, TFunction<void(void)> Callback)
{
Widget->RegisterActiveTimer(0.f, FWidgetActiveTimerDelegate::CreateLambda(
[Callback, FramesToSkip, FrameCount = 0](double, float) mutable
{
if (++FrameCount < FramesToSkip)
{
return EActiveTimerReturnType::Continue;
}
Callback();
return EActiveTimerReturnType::Stop;
}
)
);
}
TWeakPtr<FCommentDraftTreeEntry> FReviewCommentsDiffControl::ReplyDraftEntry;
void FReviewCommentsDiffControl::DraftReply(TSharedPtr<FCommentTreeEntry> ParentComment)
{
if (const TSharedPtr<STreeView<TSharedPtr<FBlueprintDifferenceTreeEntry>>> TreeView = CommentsTreeView.Pin())
{
TSharedPtr<FCommentDraftTreeEntry> DraftEntry = ReplyDraftEntry.Pin();
if (DraftEntry)
{
DraftEntry->ReassignReplyParent(ParentComment);
}
else
{
DraftEntry = FCommentDraftTreeEntry::MakeReplyDraft(AsShared(), ParentComment);
ReplyDraftEntry = DraftEntry;
}
ParentComment->Children.Push(DraftEntry);
TreeView->SetItemExpansion(ParentComment, true);
TreeView->RebuildList();
// focus comment text box once it's finished constructing
DeferredCallback(TreeView, 2, [DraftEntryWeak = TWeakPtr<FCommentDraftTreeEntry>(DraftEntry)]()
{
if (const TSharedPtr<FCommentDraftTreeEntry> DraftEntry = DraftEntryWeak.Pin())
{
FSlateApplication::Get().SetKeyboardFocus(DraftEntry->GetCommentTextBox());
}
});
}
}
void FReviewCommentsDiffControl::RebuildListView() const
{
if (const TSharedPtr<STreeView<TSharedPtr<FBlueprintDifferenceTreeEntry>>> TreeView = CommentsTreeView.Pin())
{
TreeView->RebuildList();
}
}
#undef LOCTEXT_NAMESPACE