// 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 #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 Results; }; struct FListBuildsState { std::atomic PendingQueries; TArray QueryState; }; } void SBuildSelection::Construct(const FArguments& InArgs) { ZenServiceInstance = InArgs._ZenServiceInstance; BuildServiceInstance = InArgs._BuildServiceInstance; OnBuildTransferStarted = InArgs._OnBuildTransferStarted; if (TSharedPtr 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 ServiceInstance = BuildServiceInstance.Get()) { TArray Namespaces; TArray Projects; TArray Streams; TArray BuildTypes; TArray Platforms; TMultiMap NamespacesAndBuckets = ServiceInstance->GetNamespacesAndBuckets(); auto StringToSegmentViews = [](const FString& Str, TArray& 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& Strings, TArray>& SharedStrings) { Strings.StableSort(); for (FString& String : Strings) { SharedStrings.Add(MakeShared(MoveTemp(String))); } }; auto ConformSelection = [](TSharedPtr& SelectedItem, const TArray>& SelectionList) { if (!SelectedItem) { SelectedItem = SelectionList.IsEmpty() ? nullptr : SelectionList[0]; return; } const TSharedPtr* FoundSelectionListItem = SelectionList.FindByPredicate([&SelectedItem](const TSharedPtr& 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> NamespacesToBucketSegmentViews; for (const TPair& NamespaceAndBucket : NamespacesAndBuckets) { BucketsToNamespaces.AddUnique(NamespaceAndBucket.Value, NamespaceAndBucket.Key); TArray& 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>& NamespaceToBucketSegmentViews : NamespacesToBucketSegmentViews) { const TArray& 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>& NamespaceToBucketSegmentViews : NamespacesToBucketSegmentViews) { const TArray& 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>& NamespaceToBucketSegmentViews : NamespacesToBucketSegmentViews) { const TArray& 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 ServiceInstance = BuildServiceInstance.Get()) { TArray NamespacePlatformBucketTuples; for (TSharedPtr Platform : PlatformList) { FString Bucket = FString::Printf(TEXT("%s.%s.%s.%s"), **SelectedProject, **SelectedBuildType, **SelectedStream, **Platform); TArray NamespacesForBucket; BucketsToNamespaces.MultiFind(Bucket, NamespacesForBucket); for (FString& Namespace : NamespacesForBucket) { NamespacePlatformBucketTuples.Emplace(MoveTemp(Namespace), *Platform, Bucket); } } TSharedPtr PendingQueryState = MakeShared(); 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&& 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 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> 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& BuildGroup = KeyedGroups.FindOrAdd(GroupKey); if (!BuildGroup) { BuildGroup = MakeShared(); } 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 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 SelectedItems = BuildListView->GetSelectedItems(); if (SelectedItems.IsEmpty()) { return; } for (const FBuildSelectionBuildGroupPtr& SelectedItem : SelectedItems) { if (!BuildGroupIsSelectableOrNavigable(SelectedItem)) { BuildListView->SetItemSelection(SelectedItem, false); } } } TSharedRef SBuildSelection::OnGenerateTextBlockFromString(TSharedPtr 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 SBuildSelection::GenerateBuildGroupRow(FBuildSelectionBuildGroupPtr InItem, const TSharedRef& 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& Platform : PlatformList) { TSharedPtr 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 SBuildSelection::GetBuildDestinationPanel() { return SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .VAlign(VAlign_Top) [ SNew(SHorizontalBox) .Visibility_Lambda([this]() { TArray 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 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 SBuildSelection::GetGridPanel() { using namespace UE::BuildSelection::Internal; TSharedRef Panel = SNew(SVerticalBox) .IsEnabled_Lambda([this] { if (TSharedPtr 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>) .OptionsSource(&StreamList) .OnSelectionChanged_Lambda([this](TSharedPtr 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>) .OptionsSource(&ProjectList) .OnSelectionChanged_Lambda([this](TSharedPtr 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>) .OptionsSource(&BuildTypeList) .OnSelectionChanged_Lambda([this](TSharedPtr 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) .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 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 ServiceInstance = BuildServiceInstance.Get()) { TArray 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& InOwnerTableView, const FBuildSelectionBuildGroupPtr InBuildGroup) { BuildGroup = InBuildGroup; SMultiColumnTableRow::Construct(FSuperRowType::FArguments(), InOwnerTableView); } TSharedRef 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::GetBorder(); } FReply SBuildGroupTableRow::OnBrowseClicked() { return FReply::Unhandled(); } #undef LOCTEXT_NAMESPACE