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

1329 lines
38 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SBuildSelection.h"
#include "DesktopPlatformModule.h"
#include "Internationalization/FastDecimalFormat.h"
#include "Math/BasicMathExpressionEvaluator.h"
#include "Math/UnitConversion.h"
#include "Misc/App.h"
#include "Misc/ExpressionParser.h"
#include "Misc/Paths.h"
#include "Misc/UProjectInfo.h"
#include "SMultiSelectComboBox.h"
#include "Styling/StyleColors.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SCheckBox.h"
#include "Widgets/Input/SEditableTextBox.h"
#include "Widgets/Input/SHyperlink.h"
#include "Widgets/Layout/SGridPanel.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Views/SHeaderRow.h"
#include "Widgets/Views/SListView.h"
#include "ZenServiceInstanceManager.h"
#include <atomic>
#define LOCTEXT_NAMESPACE "StorageServerBuild"
namespace UE::BuildSelection::Internal
{
namespace FBuildGroupIds
{
const FName ColName = TEXT("Name");
const FName ColCommit = TEXT("Commit");
const FName ColSuffix = TEXT("Suffix");
const FName ColCategory = TEXT("Category");
const FName ColCreated = TEXT("Created");
}
struct FNamespacePlatformBucketTuple
{
FString Namespace;
FString Platform;
FString Bucket;
};
struct FBuildState
{
FString Namespace;
FString Platform;
TArray<UE::Zen::Build::FBuildServiceInstance::FBuildRecord> Results;
};
struct FListBuildsState
{
std::atomic<uint32> PendingQueries;
TArray<FBuildState> QueryState;
};
}
void SBuildSelection::Construct(const FArguments& InArgs)
{
ZenServiceInstance = InArgs._ZenServiceInstance;
BuildServiceInstance = InArgs._BuildServiceInstance;
OnBuildTransferStarted = InArgs._OnBuildTransferStarted;
if (TSharedPtr<UE::Zen::Build::FBuildServiceInstance> ServiceInstance = BuildServiceInstance.Get())
{
ServiceInstance->OnRefreshNamespacesAndBucketsComplete().AddSP(this, &SBuildSelection::RebuildLists);
}
this->ChildSlot
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.Padding(0, 0, 0, 0)
.Expose(GridSlot)
[
GetGridPanel()
]
];
}
SBuildSelection::EBuildType SBuildSelection::GetSelectedBuildType() const
{
if (!SelectedBuildType)
{
return EBuildType::Unknown;
}
else if (SelectedBuildType->Contains(TEXT("oplog")))
{
return EBuildType::Oplog;
}
else if (SelectedBuildType->Contains(TEXT("stagedbuild")) || SelectedBuildType->Contains(TEXT("staged-build")))
{
return EBuildType::StagedBuild;
}
else if (SelectedBuildType->Contains(TEXT("packagedbuild")) || SelectedBuildType->Contains(TEXT("packaged-build")))
{
return EBuildType::PackagedBuild;
}
else if (SelectedBuildType->Contains(TEXT("ugs-pcb")))
{
return EBuildType::EditorPreCompiledBinary;
}
else if (SelectedBuildType->Contains(TEXT("installedbuild")) || SelectedBuildType->Contains(TEXT("installed-build")))
{
return EBuildType::EditorInstalledBuild;
}
return EBuildType::Unknown;
}
void SBuildSelection::RebuildLists()
{
using namespace UE::Zen::Build;
using namespace UE::BuildSelection::Internal;
BuildListRefreshesInProgress.fetch_add(1);
BucketsToNamespaces.Empty();
StreamList.Empty();
ProjectList.Empty();
BuildTypeList.Empty();
PlatformList.Empty();
if (TSharedPtr<FBuildServiceInstance> ServiceInstance = BuildServiceInstance.Get())
{
TArray<FString> Namespaces;
TArray<FString> Projects;
TArray<FString> Streams;
TArray<FString> BuildTypes;
TArray<FString> Platforms;
TMultiMap<FString, FString> NamespacesAndBuckets = ServiceInstance->GetNamespacesAndBuckets();
auto StringToSegmentViews = [](const FString& Str, TArray<FStringView>& OutViews)
{
FStringView WorkingStringView(Str);
int32 CurrentIndex = 0;
while (WorkingStringView.FindChar(TCHAR('.'), CurrentIndex))
{
if (CurrentIndex != 0)
{
OutViews.Add(WorkingStringView.Left(CurrentIndex));
}
WorkingStringView.RightChopInline(CurrentIndex+1);
}
if (!WorkingStringView.IsEmpty())
{
OutViews.Add(WorkingStringView);
}
};
NamespacesAndBuckets.GetKeys(Namespaces);
auto ConvertToSharedPtrs = [] (TArray<FString>& Strings, TArray<TSharedPtr<FString>>& SharedStrings)
{
Strings.StableSort();
for (FString& String : Strings)
{
SharedStrings.Add(MakeShared<FString>(MoveTemp(String)));
}
};
auto ConformSelection = [](TSharedPtr<FString>& SelectedItem, const TArray<TSharedPtr<FString>>& SelectionList)
{
if (!SelectedItem)
{
SelectedItem = SelectionList.IsEmpty() ? nullptr : SelectionList[0];
return;
}
const TSharedPtr<FString>* FoundSelectionListItem = SelectionList.FindByPredicate([&SelectedItem](const TSharedPtr<FString>& Item)
{
return *Item == *SelectedItem;
});
SelectedItem = FoundSelectionListItem ? *FoundSelectionListItem : SelectionList.IsEmpty() ? nullptr : SelectionList[0];
};
const uint32 SegmentIndexProject = 0;
const uint32 SegmentIndexBuildType = 1;
const uint32 SegmentIndexStream = 2;
const uint32 SegmentIndexPlatform = 3;
const uint32 SegmentIndexNum = 4;
// Stream list generation and selection conforming
TMultiMap<FStringView, TArray<FStringView>> NamespacesToBucketSegmentViews;
for (const TPair<FString, FString>& NamespaceAndBucket : NamespacesAndBuckets)
{
BucketsToNamespaces.AddUnique(NamespaceAndBucket.Value, NamespaceAndBucket.Key);
TArray<FStringView>& BucketSegmentViews = NamespacesToBucketSegmentViews.Add(NamespaceAndBucket.Key);
StringToSegmentViews(NamespaceAndBucket.Value, BucketSegmentViews);
if (BucketSegmentViews.Num() == SegmentIndexNum)
{
Streams.AddUnique(FString(BucketSegmentViews[SegmentIndexStream]));
}
}
ConvertToSharedPtrs(Streams, StreamList);
ConformSelection(SelectedStream, StreamList);
// Project list generation and selection conforming
for (const TPair<FStringView, TArray<FStringView>>& NamespaceToBucketSegmentViews : NamespacesToBucketSegmentViews)
{
const TArray<FStringView>& BucketSegmentViews = NamespaceToBucketSegmentViews.Value;
if (BucketSegmentViews.Num() == SegmentIndexNum)
{
if (BucketSegmentViews[SegmentIndexStream] != (SelectedStream ? *SelectedStream : Streams[0]))
{
continue;
}
Projects.AddUnique(FString(BucketSegmentViews[SegmentIndexProject]));
}
}
ConvertToSharedPtrs(Projects, ProjectList);
ConformSelection(SelectedProject, ProjectList);
// BuildType list generation and selection conforming
for (const TPair<FStringView, TArray<FStringView>>& NamespaceToBucketSegmentViews : NamespacesToBucketSegmentViews)
{
const TArray<FStringView>& BucketSegmentViews = NamespaceToBucketSegmentViews.Value;
if (BucketSegmentViews.Num() == SegmentIndexNum)
{
if (BucketSegmentViews[SegmentIndexStream] != (SelectedStream ? *SelectedStream : Streams[0]))
{
continue;
}
if (BucketSegmentViews[SegmentIndexProject] != (SelectedProject ? *SelectedProject : Projects[0]))
{
continue;
}
BuildTypes.AddUnique(FString(BucketSegmentViews[SegmentIndexBuildType]));
}
}
ConvertToSharedPtrs(BuildTypes, BuildTypeList);
ConformSelection(SelectedBuildType, BuildTypeList);
// Platform list generation
for (const TPair<FStringView, TArray<FStringView>>& NamespaceToBucketSegmentViews : NamespacesToBucketSegmentViews)
{
const TArray<FStringView>& BucketSegmentViews = NamespaceToBucketSegmentViews.Value;
if (BucketSegmentViews.Num() == SegmentIndexNum)
{
if (BucketSegmentViews[SegmentIndexStream] != (SelectedStream ? *SelectedStream : Streams[0]))
{
continue;
}
if (BucketSegmentViews[SegmentIndexProject] != (SelectedProject ? *SelectedProject : Projects[0]))
{
continue;
}
if (BucketSegmentViews[SegmentIndexBuildType] != (SelectedBuildType ? *SelectedBuildType : BuildTypes[0]))
{
continue;
}
Platforms.AddUnique(FString(BucketSegmentViews[SegmentIndexPlatform]));
}
}
ConvertToSharedPtrs(Platforms, PlatformList);
}
ExecuteOnGameThread(UE_SOURCE_LOCATION,
[this]
{
StreamWidget->RefreshOptions();
StreamWidget->SetSelectedItem(SelectedStream);
ProjectWidget->RefreshOptions();
ProjectWidget->SetSelectedItem(SelectedProject);
BuildTypeWidget->RefreshOptions();
BuildTypeWidget->SetSelectedItem(SelectedBuildType);
RegenerateActivePlatformFilters();
});
if (TSharedPtr<FBuildServiceInstance> ServiceInstance = BuildServiceInstance.Get())
{
TArray<FNamespacePlatformBucketTuple> NamespacePlatformBucketTuples;
for (TSharedPtr<FString> Platform : PlatformList)
{
FString Bucket = FString::Printf(TEXT("%s.%s.%s.%s"), **SelectedProject, **SelectedBuildType, **SelectedStream, **Platform);
TArray<FString> NamespacesForBucket;
BucketsToNamespaces.MultiFind(Bucket, NamespacesForBucket);
for (FString& Namespace : NamespacesForBucket)
{
NamespacePlatformBucketTuples.Emplace(MoveTemp(Namespace), *Platform, Bucket);
}
}
TSharedPtr<FListBuildsState> PendingQueryState = MakeShared<FListBuildsState>();
PendingQueryState->PendingQueries = NamespacePlatformBucketTuples.Num();
PendingQueryState->QueryState.SetNum(NamespacePlatformBucketTuples.Num());
uint32 QueryIndex = 0;
++BuildRefreshGeneration;
BuildGroups.Empty();
for (const FNamespacePlatformBucketTuple& NamespacePlatformBucket : NamespacePlatformBucketTuples)
{
ServiceInstance->ListBuilds(NamespacePlatformBucket.Namespace, NamespacePlatformBucket.Bucket,
[this, QueryIndex, Namespace = NamespacePlatformBucket.Namespace, Platform = NamespacePlatformBucket.Platform,
ExpectedBuildRefreshGeneration = BuildRefreshGeneration.load(), PendingQueryState]
(TArray<FBuildServiceInstance::FBuildRecord>&& Results) mutable
{
FBuildState& NewBuildState = PendingQueryState->QueryState[QueryIndex];
NewBuildState.Namespace = MoveTemp(Namespace);
NewBuildState.Platform = MoveTemp(Platform);
NewBuildState.Results = MoveTemp(Results);
if (--PendingQueryState->PendingQueries == 0)
{
// All queries complete
if (ExpectedBuildRefreshGeneration == BuildRefreshGeneration)
{
// Expected generation is the current generation
RegenerateBuildGroups(*PendingQueryState);
ExecuteOnGameThread(UE_SOURCE_LOCATION,
[this]
{
BuildListView->RequestListRefresh();
BuildListRefreshesInProgress.fetch_sub(1);
});
}
else
{
// Expected generation is not the current generation
ExecuteOnGameThread(UE_SOURCE_LOCATION,
[this]
{
BuildListRefreshesInProgress.fetch_sub(1);
});
}
}
});
++QueryIndex;
}
if (NamespacePlatformBucketTuples.IsEmpty())
{
BuildListRefreshesInProgress.fetch_sub(1);
}
}
else
{
BuildListRefreshesInProgress.fetch_sub(1);
}
}
void SBuildSelection::ReselectDestination(TSharedPtr<FBuildGroup> Item)
{
// TODO: Select the destination info (path & zen project id based on the selected item)
if (!SelectedBuildType)
{
return;
}
EBuildType BuildType = GetSelectedBuildType();
FString ProjectFilename = FUProjectDictionary::GetDefault().GetProjectPathForGame(**SelectedProject);
if (ProjectFilename.IsEmpty())
{
if (BuildType != EBuildType::Oplog)
{
DestinationFolderPath = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("DownloadedBuilds"));
}
return;
}
switch (BuildType)
{
case EBuildType::Oplog:
DestinationZenProjectId = FApp::GetZenStoreProjectIdForProject(ProjectFilename);
break;
case EBuildType::StagedBuild:
DestinationFolderPath = FPaths::Combine(FPaths::GetPath(ProjectFilename), TEXT("Saved"), TEXT("StagedBuilds"));
break;
case EBuildType::PackagedBuild:
DestinationFolderPath = FPaths::Combine(FPaths::GetPath(ProjectFilename), TEXT("Saved"), TEXT("Packages"));
break;
default:
DestinationFolderPath = FPaths::Combine(FPaths::GetPath(ProjectFilename), TEXT("Saved"), TEXT("DownloadedBuilds"));
break;
}
}
void SBuildSelection::RegenerateBuildGroups(UE::BuildSelection::Internal::FListBuildsState& ListBuildsState)
{
using namespace UE::Zen::Build;
using namespace UE::BuildSelection::Internal;
BuildGroups.Empty();
auto MakeGroupKey = [](const FString& Namespace, const FString& CommitIdentifier, const FBuildServiceInstance::FBuildRecord& BuildRecord)
{
if (!CommitIdentifier.IsEmpty())
{
return FString(WriteToString<64>(Namespace, ".", CommitIdentifier));
}
if (FCbFieldView NameField = BuildRecord.Metadata["name"]; NameField.HasValue() && !NameField.HasError())
{
return FString(WriteToString<64>(Namespace, ".", NameField.AsString()));
}
return FString(WriteToString<64>(BuildRecord.BuildId));
};
TMap<FString, TSharedPtr<FBuildGroup>> KeyedGroups;
for (FBuildState& BuildState : ListBuildsState.QueryState)
{
for (FBuildServiceInstance::FBuildRecord& BuildRecord : BuildState.Results)
{
FString CommitIdentifier;
if (FCbFieldView ChangelistField = BuildRecord.Metadata["changelist"]; ChangelistField.HasValue() && !ChangelistField.HasError())
{
if (ChangelistField.IsString())
{
CommitIdentifier = FUTF8ToTCHAR(ChangelistField.AsString());
}
else if (ChangelistField.IsInteger())
{
CommitIdentifier = *WriteToString<64>(ChangelistField.AsUInt64());
}
else if (ChangelistField.IsFloat())
{
CommitIdentifier = *WriteToString<64>((uint64)ChangelistField.AsDouble());
}
}
else if (FCbFieldView CommitField = BuildRecord.Metadata["commit"]; CommitField.HasValue() && !CommitField.HasError())
{
if (CommitField.IsString())
{
CommitIdentifier = FUTF8ToTCHAR(CommitField.AsString());
}
else if (CommitField.IsInteger())
{
CommitIdentifier = *WriteToString<64>(CommitField.AsUInt64());
}
else if (CommitField.IsFloat())
{
CommitIdentifier = *WriteToString<64>((uint64)CommitField.AsDouble());
}
}
FString GroupKey = MakeGroupKey(BuildState.Namespace, CommitIdentifier, BuildRecord);
TSharedPtr<FBuildGroup>& BuildGroup = KeyedGroups.FindOrAdd(GroupKey);
if (!BuildGroup)
{
BuildGroup = MakeShared<FBuildGroup>();
}
if (BuildGroup->DisplayName.IsEmpty())
{
FString Category;
if (FCbFieldView TemplateIdField = BuildRecord.Metadata["hordeTemplateId"]; TemplateIdField.HasValue() && !TemplateIdField.HasError())
{
Category = FUTF8ToTCHAR(TemplateIdField.AsString());
}
FDateTime CreatedAt;
if (FCbFieldView CreatedAtField = BuildRecord.Metadata["createdAt"]; CreatedAtField.HasValue() && !CreatedAtField.HasError())
{
if (CreatedAtField.IsString())
{
FDateTime::ParseIso8601(FUTF8ToTCHAR(CreatedAtField.AsString()).Get(), CreatedAt);
}
else if (CreatedAtField.IsDateTime())
{
CreatedAt = CreatedAtField.AsDateTime();
}
}
FString ItemName;
if (FCbFieldView NameView = BuildRecord.Metadata["name"]; NameView.HasValue() && !NameView.HasError())
{
// TODO: This name manipulation needs to be removed when the metadata is more consistent.
ItemName = FUTF8ToTCHAR(NameView.AsString());
int32 CLStartIndex = ItemName.Find(TEXT("-CL"));
if (CLStartIndex != INDEX_NONE)
{
int32 TruncationIndex = ItemName.Find(TEXT("-"), ESearchCase::IgnoreCase, ESearchDir::FromStart, CLStartIndex + 4);
if (TruncationIndex != INDEX_NONE)
{
ItemName.LeftInline(TruncationIndex);
}
TruncationIndex = ItemName.Find(TEXT("."), ESearchCase::IgnoreCase, ESearchDir::FromStart, CLStartIndex + 4);
if (TruncationIndex != INDEX_NONE)
{
ItemName.LeftInline(TruncationIndex);
}
}
if (!Category.IsEmpty() && ItemName.StartsWith(Category))
{
ItemName.RightChopInline(Category.Len());
}
bool bCharRemoved = false;
do
{
ItemName.TrimCharInline(TCHAR('.'), &bCharRemoved);
}
while(bCharRemoved);
do
{
ItemName.TrimCharInline(TCHAR('+'), &bCharRemoved);
}
while(bCharRemoved);
ItemName.ReplaceCharInline(TCHAR('+'), TCHAR('-'));
}
else
{
ItemName = *WriteToString<64>(BuildRecord.BuildId);
}
BuildGroup->Namespace = BuildState.Namespace;
BuildGroup->DisplayName = ItemName;
BuildGroup->CommitIdentifier = CommitIdentifier;
BuildGroup->Category = Category;
BuildGroup->CreatedAt = CreatedAt;
}
BuildGroup->PerPlatformBuilds.FindOrAdd(BuildState.Platform, MoveTemp(BuildRecord));
}
}
KeyedGroups.GenerateValueArray(BuildGroups);
}
void SBuildSelection::RegenerateActivePlatformFilters()
{
ActivePlatformFilters.Empty();
for (TSharedPtr<FString> Platform : PlatformList)
{
if (Platform && RequiredPlatformsWidget)
{
if (RequiredPlatformsWidget->IsChecked(*Platform))
{
ActivePlatformFilters.Add(*Platform);
SelectedGroupSelectedPlatforms.AddUnique(*Platform);
}
else
{
SelectedGroupSelectedPlatforms.Remove(*Platform);
}
}
}
}
void SBuildSelection::ValidateBuildGroupSelection()
{
BuildListView->UpdateSelectionSet();
TArray<FBuildSelectionBuildGroupPtr> SelectedItems = BuildListView->GetSelectedItems();
if (SelectedItems.IsEmpty())
{
return;
}
for (const FBuildSelectionBuildGroupPtr& SelectedItem : SelectedItems)
{
if (!BuildGroupIsSelectableOrNavigable(SelectedItem))
{
BuildListView->SetItemSelection(SelectedItem, false);
}
}
}
TSharedRef<SWidget> SBuildSelection::OnGenerateTextBlockFromString(TSharedPtr<FString> Item)
{
return SNew(STextBlock)
.Text(FText::FromString(*Item));
}
bool SBuildSelection::BuildGroupIsSelectableOrNavigable(FBuildSelectionBuildGroupPtr InItem) const
{
if (!InItem)
{
return false;
}
for (const FString& ActivePlatformFilter : ActivePlatformFilters)
{
if (!InItem->PerPlatformBuilds.Contains(ActivePlatformFilter))
{
return false;
}
}
return true;
}
TSharedRef<ITableRow> SBuildSelection::GenerateBuildGroupRow(FBuildSelectionBuildGroupPtr InItem, const TSharedRef<STableViewBase>& InOwningTable)
{
return SNew(SBuildGroupTableRow, InOwningTable, InItem)
.Visibility_Lambda([this, InItem]()
{
for (const FString& ActivePlatformFilter : ActivePlatformFilters)
{
if (!InItem->PerPlatformBuilds.Contains(ActivePlatformFilter))
{
return EVisibility::Collapsed;
}
}
return EVisibility::Visible;
});
}
void SBuildSelection::BuildGroupSelectionChanged(FBuildSelectionBuildGroupPtr Item, ESelectInfo::Type SelectInfo)
{
using namespace UE::Zen::Build;
if (!SelectedGroupPlatformGrid)
{
return;
}
SelectedGroupPlatformGrid->ClearChildren();
if (!Item)
{
return;
}
ReselectDestination(Item);
int32 Row = 0;
int32 Column = 0;
for (const TSharedPtr<FString>& Platform : PlatformList)
{
TSharedPtr<SCheckBox> AssociatedCheckbox;
SelectedGroupPlatformGrid->AddSlot(Column, Row)
.Padding(0,0,0,2)
[
SNew(SHorizontalBox)
.IsEnabled_Lambda([this, Item, Platform]
{
return Item->PerPlatformBuilds.Contains(*Platform);
})
+SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SAssignNew(AssociatedCheckbox, SCheckBox)
.IsChecked_Lambda([this, Item, Platform]
{
bool bIsChecked = Item->PerPlatformBuilds.Contains(*Platform) && SelectedGroupSelectedPlatforms.Contains(*Platform);
return bIsChecked ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
})
.OnCheckStateChanged_Lambda([this, Platform](ECheckBoxState InNewState)
{
if (InNewState == ECheckBoxState::Checked)
{
SelectedGroupSelectedPlatforms.AddUnique(*Platform);
}
else if (InNewState == ECheckBoxState::Unchecked)
{
SelectedGroupSelectedPlatforms.Remove(*Platform);
}
})
]
+SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(SButton)
.ButtonStyle(FAppStyle::Get(), "InvisibleButton")
.IsFocusable(false)
.OnClicked_Lambda([this, AssociatedCheckbox, Platform]()
{
ECheckBoxState NewState = ECheckBoxState::Checked;
if (AssociatedCheckbox->IsChecked())
{
NewState = ECheckBoxState::Unchecked;
}
AssociatedCheckbox->SetIsChecked(NewState);
if (NewState == ECheckBoxState::Checked)
{
SelectedGroupSelectedPlatforms.AddUnique(*Platform);
}
else if (NewState == ECheckBoxState::Unchecked)
{
SelectedGroupSelectedPlatforms.Remove(*Platform);
}
return FReply::Handled();
})
[
SNew(STextBlock)
.Justification(ETextJustify::Left)
.Text(FText::FromString(*Platform))
]
]
];
if (++Row > 2)
{
Column++;
Row = 0;
}
}
}
void SBuildSelection::OnOpenDestinationDirectoryClicked()
{
bool bDirectorySelected = false;
if (IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get())
{
const FString Title = LOCTEXT("BuildSelection_DestinationDirectoryBrowserTitle", "Choose destination directory").ToString();
bDirectorySelected = DesktopPlatform->OpenDirectoryDialog(
FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr),
Title,
DestinationFolderPath,
DestinationFolderPath);
}
}
TSharedRef<SWidget> SBuildSelection::GetBuildDestinationPanel()
{
return
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.VAlign(VAlign_Top)
[
SNew(SHorizontalBox)
.Visibility_Lambda([this]()
{
TArray<FBuildSelectionBuildGroupPtr> SelectedItems = BuildListView->GetSelectedItems();
return (SelectedItems.Num() == 0) || GetSelectedBuildType() == EBuildType::Oplog ? EVisibility::Collapsed : EVisibility::Visible;
})
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.VAlign(VAlign_Center)
[
SNew(SEditableTextBox)
.OverflowPolicy(ETextOverflowPolicy::MiddleEllipsis)
.MinDesiredWidth(200.0f)
.Text_Lambda([this]()
{
return FText::FromString(DestinationFolderPath);
})
.OnTextChanged_Lambda([this](const FText& Text)
{
DestinationFolderPath = Text.ToString();
})
.OnTextCommitted_Lambda([this](const FText& Text, const ETextCommit::Type CommitType)
{
DestinationFolderPath = Text.ToString();
})
]
+ SHorizontalBox::Slot()
.AutoWidth()
.HAlign(HAlign_Right)
[
SNew(SButton)
.OnClicked_Lambda([this]()
{
OnOpenDestinationDirectoryClicked();
return FReply::Handled();
})
.ButtonStyle(FAppStyle::Get(), "SimpleButton")
[
SNew(SImage)
.Image(FAppStyle::Get().GetBrush("Zen.BrowseContent"))
.ColorAndOpacity(FSlateColor::UseForeground())
]
]
]
+ SVerticalBox::Slot()
.AutoHeight()
.VAlign(VAlign_Top)
[
SNew(SHorizontalBox)
.Visibility_Lambda([this]()
{
TArray<FBuildSelectionBuildGroupPtr> SelectedItems = BuildListView->GetSelectedItems();
return (SelectedItems.Num() == 0) || GetSelectedBuildType() != EBuildType::Oplog ? EVisibility::Collapsed : EVisibility::Visible;
})
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.VAlign(VAlign_Center)
[
SNew(SEditableTextBox)
.OverflowPolicy(ETextOverflowPolicy::MiddleEllipsis)
.MinDesiredWidth(200.0f)
.Text_Lambda([this]()
{
return FText::FromString(DestinationZenProjectId);
})
.OnTextChanged_Lambda([this](const FText& Text)
{
DestinationZenProjectId = Text.ToString();
})
.OnTextCommitted_Lambda([this](const FText& Text, const ETextCommit::Type CommitType)
{
DestinationZenProjectId = Text.ToString();
})
]
];
}
TSharedRef<SWidget> SBuildSelection::GetGridPanel()
{
using namespace UE::BuildSelection::Internal;
TSharedRef<SVerticalBox> Panel =
SNew(SVerticalBox)
.IsEnabled_Lambda([this]
{
if (TSharedPtr<UE::Zen::Build::FBuildServiceInstance> ServiceInstance = BuildServiceInstance.Get())
{
return ServiceInstance->GetConnectionState() == UE::Zen::Build::FBuildServiceInstance::EConnectionState::ConnectionSucceeded && !ServiceInstance->GetNamespacesAndBuckets().IsEmpty();
}
return false;
});
const float MinDesiredWidth = 50.0f;
const float RowMargin = 2.0f;
const float ColumnMargin = 10.0f;
const FSlateColor TitleColor = FStyleColors::AccentWhite;
const FSlateFontInfo TitleFont = FCoreStyle::GetDefaultFontStyle("Bold", 10);
Panel->AddSlot()
.AutoHeight()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Top)
[
SNew(SHorizontalBox)
// Stream
+SHorizontalBox::Slot()
.AutoWidth()
.FillWidth(1.0f)
.HAlign(HAlign_Fill)
.VAlign(VAlign_Top)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(FMargin(ColumnMargin, RowMargin))
.VAlign(VAlign_Top)
[
SNew(STextBlock)
.ColorAndOpacity(TitleColor)
.Font(TitleFont)
.Text(LOCTEXT("BuildSelection_Stream", "Stream"))
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(FMargin(ColumnMargin, RowMargin))
.HAlign(HAlign_Fill)
.VAlign(VAlign_Top)
[
SAssignNew(StreamWidget, SComboBox<TSharedPtr<FString>>)
.OptionsSource(&StreamList)
.OnSelectionChanged_Lambda([this](TSharedPtr<FString> Item, ESelectInfo::Type SelectInfo)
{
if (Item.IsValid() && *Item != *SelectedStream)
{
SelectedStream = Item;
RebuildLists();
}
})
.OnGenerateWidget(this, &SBuildSelection::OnGenerateTextBlockFromString)
[
SNew(STextBlock)
.MinDesiredWidth(MinDesiredWidth)
.Text_Lambda([this]()
{
return FText::FromString(SelectedStream ? **SelectedStream : TEXT(""));
})
]
]
]
// Project
+SHorizontalBox::Slot()
.AutoWidth()
.FillWidth(1.0f)
.HAlign(HAlign_Fill)
.VAlign(VAlign_Top)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(FMargin(ColumnMargin, RowMargin))
.VAlign(VAlign_Top)
[
SNew(STextBlock)
.ColorAndOpacity(TitleColor)
.Font(TitleFont)
.Text(LOCTEXT("BuildSelection_Project", "Project"))
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(FMargin(ColumnMargin, RowMargin))
.HAlign(HAlign_Fill)
.VAlign(VAlign_Top)
[
SAssignNew(ProjectWidget, SComboBox<TSharedPtr<FString>>)
.OptionsSource(&ProjectList)
.OnSelectionChanged_Lambda([this](TSharedPtr<FString> Item, ESelectInfo::Type SelectInfo)
{
if (Item.IsValid() && *Item != *SelectedProject)
{
SelectedProject = Item;
RebuildLists();
}
})
.OnGenerateWidget(this, &SBuildSelection::OnGenerateTextBlockFromString)
[
SNew(STextBlock)
.MinDesiredWidth(MinDesiredWidth)
.Text_Lambda([this]()
{
return FText::FromString(SelectedProject ? **SelectedProject : TEXT(""));
})
]
]
]
// Build Type
+SHorizontalBox::Slot()
.AutoWidth()
.FillWidth(1.0f)
.HAlign(HAlign_Fill)
.VAlign(VAlign_Top)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(FMargin(ColumnMargin, RowMargin))
.VAlign(VAlign_Top)
[
SNew(STextBlock)
.ColorAndOpacity(TitleColor)
.Font(TitleFont)
.Text(LOCTEXT("BuildSelection_BuildType", "Build Type"))
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(FMargin(ColumnMargin, RowMargin))
.HAlign(HAlign_Fill)
.VAlign(VAlign_Top)
[
SAssignNew(BuildTypeWidget, SComboBox<TSharedPtr<FString>>)
.OptionsSource(&BuildTypeList)
.OnSelectionChanged_Lambda([this](TSharedPtr<FString> Item, ESelectInfo::Type SelectInfo)
{
if (Item.IsValid() && *Item != *SelectedBuildType)
{
SelectedBuildType = Item;
RebuildLists();
}
})
.OnGenerateWidget(this, &SBuildSelection::OnGenerateTextBlockFromString)
[
SNew(STextBlock)
.MinDesiredWidth(MinDesiredWidth)
.Text_Lambda([this]()
{
return FText::FromString(SelectedBuildType ? **SelectedBuildType : TEXT(""));
})
]
]
]
// Required Platforms
+SHorizontalBox::Slot()
.AutoWidth()
.FillWidth(1.0f)
.HAlign(HAlign_Fill)
.VAlign(VAlign_Top)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(FMargin(ColumnMargin, RowMargin))
.VAlign(VAlign_Top)
[
SNew(STextBlock)
.ColorAndOpacity(TitleColor)
.Font(TitleFont)
.Text(LOCTEXT("BuildSelection_RequiredPlatforms", "Required Platforms"))
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(FMargin(ColumnMargin, RowMargin))
.HAlign(HAlign_Fill)
.VAlign(VAlign_Top)
[
SAssignNew(RequiredPlatformsWidget, SMultiSelectComboBox)
.SelectValues(&PlatformList)
.OnCheckedValuesChanged_Lambda([this]()
{
RegenerateActivePlatformFilters();
ValidateBuildGroupSelection();
})
]
]
];
Panel->AddSlot()
.Padding(FMargin(ColumnMargin, 10, ColumnMargin, 0))
.AutoHeight()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Top)
[
SNew(STextBlock)
.ColorAndOpacity(TitleColor)
.Font(TitleFont)
.Text(LOCTEXT("BuildSelection_BuildsLabel", "Builds"))
];
Panel->AddSlot()
.Padding(FMargin(ColumnMargin, RowMargin))
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
SNew(SVerticalBox)
+SVerticalBox::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
SAssignNew(BuildListView, SListView<FBuildSelectionBuildGroupPtr>)
.ListItemsSource(&BuildGroups)
.OnGenerateRow(this, &SBuildSelection::GenerateBuildGroupRow)
.OnSelectionChanged(this, &SBuildSelection::BuildGroupSelectionChanged)
.SelectionMode(ESelectionMode::Single)
.OnIsSelectableOrNavigable(this, &SBuildSelection::BuildGroupIsSelectableOrNavigable)
.IsEnabled_Lambda([this]
{
return !BuildListRefreshesInProgress;
})
.HeaderRow
(
SNew(SHeaderRow)
+ SHeaderRow::Column(FBuildGroupIds::ColName).DefaultLabel(LOCTEXT("BuildSelection_BuildGroupColName", "Name"))
.FillWidth(0.4f)
+ SHeaderRow::Column(FBuildGroupIds::ColCommit).DefaultLabel(LOCTEXT("BuildSelection_BuildGroupColCommit", "Commit"))
.DefaultTooltip(LOCTEXT("BuildSelection_BuildGroupColCommitTooltip", "Commit/Changelist for the build"))
.FillWidth(0.10f).HAlignCell(HAlign_Center).HAlignHeader(HAlign_Center).VAlignCell(VAlign_Center)
+ SHeaderRow::Column(FBuildGroupIds::ColSuffix).DefaultLabel(LOCTEXT("BuildSelection_BuildGroupColSuffix", "Suffix"))
.DefaultTooltip(LOCTEXT("BuildSelection_BuildGroupColSuffixTooltip", "Modifier on top of the commit/changelist for the build"))
.FillWidth(0.10f).HAlignCell(HAlign_Center).HAlignHeader(HAlign_Center).VAlignCell(VAlign_Center)
+ SHeaderRow::Column(FBuildGroupIds::ColCategory).DefaultLabel(LOCTEXT("BuildSelection_BuildGroupColCategory", "Category"))
.DefaultTooltip(LOCTEXT("BuildSelection_BuildGroupColCategoryTooltip", "Category for the build"))
.FillWidth(0.25f).HAlignCell(HAlign_Left).HAlignHeader(HAlign_Center).VAlignCell(VAlign_Center)
+ SHeaderRow::Column(FBuildGroupIds::ColCreated).DefaultLabel(LOCTEXT("BuildSelection_BuildGroupColCreated", "Created"))
.DefaultTooltip(LOCTEXT("BuildSelection_BuildGroupColCreatedTooltip", "When the build was created"))
.FillWidth(0.15f).HAlignCell(HAlign_Left).HAlignHeader(HAlign_Center).VAlignCell(VAlign_Center)
)
]
];
Panel->AddSlot()
.Padding(FMargin(ColumnMargin, 3, ColumnMargin, 0))
.AutoHeight()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Bottom)
[
SNew(STextBlock)
.Font(FAppStyle::Get().GetFontStyle("SmallFont"))
.Text_Lambda([this]()
{
if (BuildListRefreshesInProgress)
{
return LOCTEXT("BuildSelection_ResultLoading", "Loading...");
}
int32 VisibleItemCount = 0;
if (ActivePlatformFilters.IsEmpty())
{
VisibleItemCount = BuildGroups.Num();
}
else
{
for (FBuildSelectionBuildGroupPtr BuildGroup : BuildGroups)
{
bool bHasAllRequiredPlatforms = true;
for (const FString& ActivePlatformFilter : ActivePlatformFilters)
{
if (!BuildGroup->PerPlatformBuilds.Contains(ActivePlatformFilter))
{
bHasAllRequiredPlatforms = false;
break;
}
}
if (bHasAllRequiredPlatforms)
{
VisibleItemCount++;
}
}
}
if (VisibleItemCount == 1)
{
return LOCTEXT("BuildSelection_ResultDescriptionMultiple", "1 item");
}
else
{
return FText::Format(LOCTEXT("BuildSelection_ResultDescriptionMultiple", "{0} items"), FText::AsNumber(VisibleItemCount));
}
})
];
Panel->AddSlot()
.Padding(FMargin(ColumnMargin, RowMargin))
.AutoHeight()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Bottom)
[
SNew(SHorizontalBox)
.Visibility_Lambda([this]()
{
return !!BuildListRefreshesInProgress || BuildListView->GetNumItemsSelected() == 0 ? EVisibility::Collapsed : EVisibility::Visible;
})
+SHorizontalBox::Slot()
.HAlign(HAlign_Left)
.FillWidth(0.5f)
[
SNew(SVerticalBox)
+SVerticalBox::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Top)
.AutoHeight()
[
SNew(STextBlock)
.ColorAndOpacity(TitleColor)
.Font(TitleFont)
.Text(LOCTEXT("BuildSelection_AvailablePlatforms", "Available Platforms"))
]
+SVerticalBox::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Top)
.AutoHeight()
[
SAssignNew(SelectedGroupPlatformGrid, SGridPanel)
]
]
+SHorizontalBox::Slot()
.HAlign(HAlign_Right)
.FillWidth(0.5f)
[
SNew(SVerticalBox)
+SVerticalBox::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Top)
.AutoHeight()
[
SNew(STextBlock)
.ColorAndOpacity(TitleColor)
.Font(TitleFont)
.Text(LOCTEXT("BuildSelection_Destination", "Destination"))
]
+SVerticalBox::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Top)
[
GetBuildDestinationPanel()
]
+SVerticalBox::Slot()
.HAlign(HAlign_Right)
.VAlign(VAlign_Bottom)
.Padding(FMargin(0, RowMargin))
[
SNew(SButton)
.Text(LOCTEXT("BuildSelection_Download", "Download"))
.ToolTipText(LOCTEXT("BuildSelection_DownloadTooltip", "Start a download of the selected build for the selected platforms"))
.ButtonStyle(FAppStyle::Get(), "Button")
.IsEnabled_Lambda([this]()
{
const bool bDestinationValid = GetSelectedBuildType() == EBuildType::Oplog ? !DestinationZenProjectId.IsEmpty() : !DestinationFolderPath.IsEmpty();
if (!bDestinationValid)
{
return false;
}
TArray<FBuildSelectionBuildGroupPtr> SelectedItems = BuildListView->GetSelectedItems();
if (SelectedItems.Num() == 0)
{
return false;
}
for (const FString& PlatformForSelectedGroup : SelectedGroupSelectedPlatforms)
{
if (SelectedItems[0]->PerPlatformBuilds.Contains(PlatformForSelectedGroup))
{
return true;
}
}
return false;
})
.OnClicked_Lambda([this]()
{
using namespace UE::Zen::Build;
if (TSharedPtr<FBuildServiceInstance> ServiceInstance = BuildServiceInstance.Get())
{
TArray<FBuildSelectionBuildGroupPtr> SelectedItems = BuildListView->GetSelectedItems();
if (SelectedItems.Num() == 0)
{
return FReply::Handled();
}
for (const FString& PlatformForSelectedGroup : SelectedGroupSelectedPlatforms)
{
if (FBuildServiceInstance::FBuildRecord* BuildRecord = SelectedItems[0]->PerPlatformBuilds.Find(PlatformForSelectedGroup))
{
FString Bucket = FString::Printf(TEXT("%s.%s.%s.%s"), **SelectedProject, **SelectedBuildType, **SelectedStream, *PlatformForSelectedGroup);
if (GetSelectedBuildType() == EBuildType::Oplog)
{
if (FCbFieldView CookPlatformField = BuildRecord->Metadata["cookPlatform"]; CookPlatformField.HasValue() && !CookPlatformField.HasError())
{
FString ProjectFilePath = FUProjectDictionary::GetDefault().GetProjectPathForGame(**SelectedProject);
FString DestinationOplogId = *WriteToString<64>(CookPlatformField.AsString());
FBuildServiceInstance::FBuildTransfer BuildTransfer =
ServiceInstance->StartOplogBuildTransfer(BuildRecord->BuildId, DestinationZenProjectId, DestinationOplogId, ProjectFilePath, SelectedItems[0]->Namespace, Bucket);
OnBuildTransferStarted.ExecuteIfBound(BuildTransfer, SelectedItems[0]->DisplayName, PlatformForSelectedGroup);
}
}
else
{
FString DestinationFolder = FPaths::Combine(DestinationFolderPath, PlatformForSelectedGroup);
FBuildServiceInstance::FBuildTransfer BuildTransfer =
ServiceInstance->StartBuildTransfer(BuildRecord->BuildId, DestinationFolder, SelectedItems[0]->Namespace, Bucket);
OnBuildTransferStarted.ExecuteIfBound(BuildTransfer, SelectedItems[0]->DisplayName, PlatformForSelectedGroup);
}
}
}
}
return FReply::Handled();
})
]
]
];
return Panel;
}
void
SBuildGroupTableRow::Construct(const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView, const FBuildSelectionBuildGroupPtr InBuildGroup)
{
BuildGroup = InBuildGroup;
SMultiColumnTableRow<FBuildSelectionBuildGroupPtr>::Construct(FSuperRowType::FArguments(), InOwnerTableView);
}
TSharedRef<SWidget>
SBuildGroupTableRow::GenerateWidgetForColumn(const FName& ColumnName)
{
using namespace UE::BuildSelection::Internal;
if (ColumnName == FBuildGroupIds::ColName)
{
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
[
SNew(STextBlock).Text(FText::FromString(BuildGroup->DisplayName))
];
}
else if (ColumnName == FBuildGroupIds::ColCommit)
{
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
[
SNew(STextBlock).Text(FText::FromString(BuildGroup->CommitIdentifier))
];
}
else if (ColumnName == FBuildGroupIds::ColSuffix)
{
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
[
SNew(STextBlock).Text(FText::FromString(BuildGroup->Suffix))
];
}
else if (ColumnName == FBuildGroupIds::ColCategory)
{
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
[
SNew(STextBlock).Text(FText::FromString(BuildGroup->Category))
];
}
else if (ColumnName == FBuildGroupIds::ColCreated)
{
if (BuildGroup->CreatedAt.GetTicks() != 0)
{
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
[
SNew(STextBlock).Text(FText::AsDateTime(BuildGroup->CreatedAt, EDateTimeStyle::Short))
];
}
}
return SNullWidget::NullWidget;
}
const FSlateBrush*
SBuildGroupTableRow::GetBorder() const
{
return STableRow<FBuildSelectionBuildGroupPtr>::GetBorder();
}
FReply
SBuildGroupTableRow::OnBrowseClicked()
{
return FReply::Unhandled();
}
#undef LOCTEXT_NAMESPACE