// Copyright Epic Games, Inc. All Rights Reserved. #include "SGameSyncTab.h" #include "SSyncFilterWindow.h" #include "SSettingsWindow.h" #include "Framework/Application/SlateApplication.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "SPositiveActionButton.h" #include "SSimpleComboButton.h" #include "SSimpleButton.h" #include "Widgets/SHordeBadge.h" #include "Widgets/SScheduledSyncWindow.h" #include "Widgets/SLogWidget.h" #include "Widgets/Layout/SHeader.h" #include "Widgets/Colors/SSimpleGradient.h" #include "Widgets/Images/SThrobber.h" #include "Widgets/Images/SImage.h" #include "Widgets/Layout/SSeparator.h" #include "Widgets/Views/SListView.h" #include "SlateUGSStyle.h" #include "UGSTab.h" #include "UGSTabManager.h" #define LOCTEXT_NAMESPACE "UGSWindow" namespace { const FName HordeTableColumnStatus(TEXT("Status")); const FName HordeTableColumnType(TEXT("Type")); const FName HordeTableColumnChange(TEXT("Change")); const FName HordeTableColumnTime(TEXT("Time")); const FName HordeTableColumnAuthor(TEXT("Author")); const FName HordeTableColumnDescription(TEXT("Description")); const FName HordeTableColumnEditor(TEXT("Editor")); } void SBuildDataRow::Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView, const TSharedPtr& Item) { CurrentItem = Item; SMultiColumnTableRow>::Construct(SMultiColumnTableRow::FArguments(), InOwnerTableView); } TSharedRef SBuildDataRow::GenerateWidgetForColumn(const FName& ColumnId) // Todo: maybe can refactor some of this code so there's less duplication by using the root SWidget class on different types { if (ColumnId == HordeTableColumnStatus) { TSharedRef StatusCircle = SNew(SImage).Image(FSlateUGSStyle::Get().GetBrush("Icons.FilledCircle")); // Todo: switch to using CurrentItem->ReviewStatus when that variable actually gets filled UGSCore::EReviewVerdict Verdict = UGSCore::EReviewVerdict::Unknown; if (FMath::RandRange(0, 10) > 6) { Verdict = static_cast(FMath::RandRange(1, 3)); } switch (Verdict) // switch (CurrentItem->ReviewStatus) { case UGSCore::EReviewVerdict::Good: StatusCircle->SetColorAndOpacity(FSlateUGSStyle::Get().GetColor("HordeBadge.Color.Success")); break; case UGSCore::EReviewVerdict::Bad: StatusCircle->SetColorAndOpacity(FSlateUGSStyle::Get().GetColor("HordeBadge.Color.Error")); break; case UGSCore::EReviewVerdict::Mixed: StatusCircle->SetColorAndOpacity(FSlateUGSStyle::Get().GetColor("HordeBadge.Color.Warning")); break; case UGSCore::EReviewVerdict::Unknown: default: // Fall through StatusCircle->SetColorAndOpacity(FSlateUGSStyle::Get().GetColor("HordeBadge.Color.Unknown")); break; } return SNew(SBox) .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ StatusCircle ]; } if (ColumnId == HordeTableColumnType) { bool bIsCode = CurrentItem->ChangeType.bContainsCode; bool bIsContent = CurrentItem->ChangeType.bContainsContent; TSharedRef TypeBadges = SNew(SHorizontalBox); if (bIsCode) { TypeBadges->AddSlot() .Padding(1.0f, 0.0f) .HAlign(HAlign_Center) [ SNew(SHordeBadge) .Text(FText::FromString("Code")) .BadgeState(EBadgeState::Pending) ]; } if (bIsContent) { TypeBadges->AddSlot() .Padding(1.0f, 0.0f) .HAlign(HAlign_Center) [ SNew(SHordeBadge) .Text(FText::FromString("Content")) .BadgeState(EBadgeState::Pending) ]; } return TypeBadges; } TSharedRef TextItem = SNew(STextBlock); if (ColumnId == HordeTableColumnChange) { TextItem->SetText(FText::FromString(FString::FromInt(CurrentItem->Changelist))); } if (ColumnId == HordeTableColumnTime) { TextItem->SetText(FText::FromString(CurrentItem->Time.ToString(TEXT("%h:%M %A")))); } if (ColumnId == HordeTableColumnAuthor) { TextItem->SetText(CurrentItem->Author); } if (ColumnId == HordeTableColumnDescription) { // Perforce descriptions mix line endings, so remove all possible line endings FString Description = CurrentItem->Description .Replace(TEXT("\r"), TEXT(" ")) .Replace(TEXT("\n"), TEXT(" ")); // Todo: replacing both with spaces causes lots of double spaces, maybe filter that out? TextItem->SetText(FText::FromString(Description)); } if (ColumnId == HordeTableColumnEditor) { // Todo: replace this dummy badge data with the real thing return SNew(SHorizontalBox) +SHorizontalBox::Slot() [ SNew(SHordeBadge) .Text(FText::FromString("Editor Win64")) .BadgeState(static_cast(FMath::RandRange(0, 4))) ] +SHorizontalBox::Slot() .Padding(2.0f, 0.0f) [ SNew(SHordeBadge) .Text(FText::FromString("Editor Mac")) .BadgeState(static_cast(FMath::RandRange(0, 4))) ] +SHorizontalBox::Slot() [ SNew(SHordeBadge) .Text(FText::FromString("Editor Linux")) .BadgeState(static_cast(FMath::RandRange(0, 4))) ]; } if (CurrentItem->bSyncingPrecompiled) { if (!CurrentItem->bHasZippedBinaries) { TextItem->SetColorAndOpacity(FLinearColor(0.1f, 0.1f, 0.1f)); } } if (CurrentItem->bCurrentlySynced) { // Lets make the font white and bold when we are the currently synced CL FSlateFontInfo FontInfo = FSlateUGSStyle::Get().GetFontStyle("NormalFontBold"); TextItem->SetFont(FontInfo); TextItem->SetColorAndOpacity(FLinearColor::White); } return SNew(SBox) .VAlign(VAlign_Center) .Padding(5.0f, 2.5f) [ TextItem ]; } TSharedRef SGameSyncTab::GenerateHordeBuildTableRow(TSharedPtr InItem, const TSharedRef& InOwnerTable) { if (InItem->bHeaderRow) { return SNew(STableRow>, InOwnerTable) .ShowSelection(false) [ SNew(SBox) .Padding(5.0f) [ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() [ SNew(STextBlock) .Font(FSlateUGSStyle::Get().GetFontStyle("Font.Large.Bold")) .ColorAndOpacity(FLinearColor::White) .Text(FText::FromString(InItem->Time.ToFormattedString(TEXT("%A, %B %e, %Y")))) ] +SVerticalBox::Slot() .VAlign(VAlign_Bottom) [ SNew(SSeparator) .SeparatorImage(FSlateUGSStyle::Get().GetBrush("Header.Post")) ] ] ]; } return SNew(SBuildDataRow, InOwnerTable, InItem).ToolTipText(FText::FromString(InItem->Description)); } // Button callbacks TSharedRef SGameSyncTab::MakeSyncButtonDropdown() { FMenuBuilder MenuBuilder(true, nullptr); MenuBuilder.AddMenuEntry( FText::FromString(TEXT("Sync Latest")), FText::FromString(TEXT("Sync to the latest submitted changelist")), FSlateIcon(), FUIAction(FExecuteAction::CreateRaw(Tab, &UGSTab::OnSyncLatest)), NAME_None, EUserInterfaceActionType::Button ); MenuBuilder.AddMenuEntry( FText::FromString(TEXT("Setup Schedule Sync")), FText::FromString(TEXT("Setup a schedule sync to run at specific time")), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda([this] { FSlateApplication::Get().AddModalWindow(SNew(SScheduledSyncWindow, Tab), Tab->GetTabArgs().GetOwnerWindow(), false); } )), NAME_None, EUserInterfaceActionType::Button ); return MenuBuilder.MakeWidget(); } void SGameSyncTab::Construct(const FArguments& InArgs, UGSTab* InTab) { Tab = InTab; this->ChildSlot [ SNew(SVerticalBox) // Toolbar at the top of the tab // Todo: Maybe use a FToolBarBuilder instead +SVerticalBox::Slot() .AutoHeight() .Padding(10.0f, 5.0f) [ SNew(SBorder) .BorderImage(FSlateUGSStyle::Get().GetBrush("Brushes.Panel")) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .HAlign(HAlign_Left) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ SNew(SSimpleButton) .Icon(FSlateUGSStyle::Get().GetBrush("Icons.Refresh")) .ToolTipText(LOCTEXT("RefreshBuildList", "Refreshes the Build List")) .OnClicked_Lambda([this] { Tab->RefreshBuildList(); return FReply::Handled(); }) .IsEnabled_Lambda([this] { return !Tab->IsSyncing(); }) ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SSimpleComboButton) .Text(LOCTEXT("Sync", "Sync")) .Icon(FSlateUGSStyle::Get().GetBrush("Icons.CircleArrowDown")) .HasDownArrow(true) .MenuContent() [ MakeSyncButtonDropdown() ] .IsEnabled_Lambda([this] { return !Tab->IsSyncing(); }) ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SSimpleButton) .Text(LOCTEXT("CancelSync", "Cancel")) .ToolTipText(LOCTEXT("CancelSyncTooltip", "Cancels the current sync/build operation")) .Icon(FSlateUGSStyle::Get().GetBrush("Icons.X")) .OnClicked_Lambda([this] { Tab->CancelSync(); return FReply::Handled(); }) .IsEnabled_Lambda([this] { return Tab->IsSyncing(); }) ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SPositiveActionButton) .Text(LOCTEXT("NewProjectButton", "New Project")) // Todo: replace with new tab button eventually .OnClicked_Lambda([this] { Tab->GetTabManager()->ActivateTab(); return FReply::Handled(); }) ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SSimpleButton) .Text(LOCTEXT("Build", "Build")) .IsEnabled_Lambda([this] { return !Tab->IsSyncing(); }) .OnClicked_Lambda([this] { Tab->OnBuildWorkspace(); return FReply::Handled(); }) ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SSimpleButton) .Text(LOCTEXT("RunUnrealEditor", "Run Unreal Editor")) .Icon(FSlateUGSStyle::Get().GetBrush("Icons.Launch")) .IsEnabled_Lambda([this] { return !Tab->IsSyncing(); }) .OnClicked_Lambda([this] { Tab->OnOpenEditor(); return FReply::Handled(); }) ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SSimpleButton) .Text(LOCTEXT("OpenExplorer", "Open Explorer")) .Icon(FSlateUGSStyle::Get().GetBrush("Icons.FolderOpen")) // Todo: shouldn't use this icon (repurposing, also could use other IDEs) .IsEnabled_Lambda([this] { return !Tab->IsSyncing(); }) .OnClicked_Lambda([this] { Tab->OnOpenExplorer(); return FReply::Handled(); }) ] ] +SHorizontalBox::Slot() .HAlign(HAlign_Right) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ SNew(SSimpleButton) .Text(LOCTEXT("BuildHealth", "Build Health")) // Todo: What icon? .IsEnabled(false) // Todo: enable after adding this functionality ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SSimpleButton) .Text(LOCTEXT("SDKInfo", "SDK Info")) .Icon(FSlateUGSStyle::Get().GetBrush("Icons.Settings")) // Todo: What icon? .IsEnabled(false) // Todo: enable after adding this functionality ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SSimpleButton) .Text(LOCTEXT("OpenPerforce", "Open Perforce")) .Icon(FSlateUGSStyle::Get().GetBrush("Icons.Blueprints")) // Todo: shouldn't use this icon (repurposing) .OnClicked_Lambda([this] { Tab->OnOpenPerforceClicked(); return FReply::Handled(); }) ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SSimpleButton) .Text(LOCTEXT("CleanSolution", "Clean Solution")) .Icon(FSlateUGSStyle::Get().GetBrush("Icons.Delete")) // Todo: shouldn't use this icon (repurposing) .IsEnabled_Lambda([this] { return !Tab->IsSyncing(); }) .IsEnabled(false) // Todo: enable after adding this functionality ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SSimpleButton) .Text(LOCTEXT("Filter", "Filter")) .Icon(FSlateUGSStyle::Get().GetBrush("Icons.Filter")) // Todo: this is probably the wrong "Filter" button. The functionality below should probably be in the settings dropdown .OnClicked_Lambda([this] { FSlateApplication::Get().AddModalWindow(SNew(SSyncFilterWindow, Tab), Tab->GetTabArgs().GetOwnerWindow(), false); return FReply::Handled(); }) ] +SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Right) [ SNew(SSimpleButton) .Text(LOCTEXT("Settings", "Settings")) .Icon(FSlateUGSStyle::Get().GetBrush("Icons.Settings")) .OnClicked_Lambda([this] { FSlateApplication::Get().AddModalWindow(SNew(SSettingsWindow, Tab), Tab->GetTabArgs().GetOwnerWindow(), false); return FReply::Handled(); }) ] ] ] ] // Stream banner +SVerticalBox::Slot() .Padding(0.0f, 5.0f) .AutoHeight() [ SNew(SOverlay) +SOverlay::Slot() [ SNew(SSimpleGradient) // Todo: save literals in class and use different colors depending on stream (Fortnite, UE5, etc) .StartColor(FLinearColor(161.0f / 255.0f, 57.0f / 255.0f, 191.0f / 255.0f)) .EndColor(FLinearColor(36.0f / 255.0f, 36.0f / 255.0f, 36.0f / 255.0f)) ] +SOverlay::Slot() [ SNew(SHorizontalBox) // Stream logo +SHorizontalBox::Slot() .Padding(35.0f, 0.0f) .VAlign(VAlign_Center) .HAlign(HAlign_Center) .AutoWidth() [ SNew(SImage) .Image(FSlateUGSStyle::Get().GetBrush("UnrealCircle.Thin")) .DesiredSizeOverride(CoreStyleConstants::Icon128x128) ] // Stream, CL, and uproject path +SHorizontalBox::Slot() .Padding(0.0f, 35.0f) .VAlign(VAlign_Center) [ SNew(SHorizontalBox) // Labels +SHorizontalBox::Slot() .HAlign(HAlign_Left) .AutoWidth() .Padding(10.0f, 0.0f) [ SNew(SVerticalBox) +SVerticalBox::Slot() .HAlign(HAlign_Right) [ SNew(STextBlock) .Text(LOCTEXT("StreamText", "STREAM")) .Font(FSlateUGSStyle::Get().GetFontStyle("NormalFontBold")) .ColorAndOpacity(FLinearColor(0.25f, 0.25f, 0.25f)) ] +SVerticalBox::Slot() .HAlign(HAlign_Right) .Padding(0.0f, 35.0f) [ SNew(STextBlock) .Text(LOCTEXT("ChangelistText", "CHANGELIST")) .Font(FSlateUGSStyle::Get().GetFontStyle("NormalFontBold")) .ColorAndOpacity(FLinearColor(0.25f, 0.25f, 0.25f)) ] +SVerticalBox::Slot() .HAlign(HAlign_Right) [ SNew(STextBlock) .Text(LOCTEXT("ProjectText", "PROJECT")) .Font(FSlateUGSStyle::Get().GetFontStyle("NormalFontBold")) .ColorAndOpacity(FLinearColor(0.25f, 0.25f, 0.25f)) ] ] // Data +SHorizontalBox::Slot() .HAlign(HAlign_Left) [ SNew(SVerticalBox) +SVerticalBox::Slot() .HAlign(HAlign_Left) [ SAssignNew(StreamPathText, STextBlock) .Text(LOCTEXT("StreamTextValue", "No stream path found")) .ColorAndOpacity(FLinearColor::White) ] +SVerticalBox::Slot() .HAlign(HAlign_Left) .Padding(0.0f, 35.0f) [ SAssignNew(ChangelistText, STextBlock) .Text(LOCTEXT("ChangelistTextValue", "No changelist found")) .ColorAndOpacity(FLinearColor::White) ] +SVerticalBox::Slot() .HAlign(HAlign_Left) [ SAssignNew(ProjectPathText, STextBlock) .Text(LOCTEXT("ProjectValue", "No project path found")) .ColorAndOpacity(FLinearColor::White) ] ] ] // Syncing files progress +SHorizontalBox::Slot() .VAlign(VAlign_Top) .HAlign(HAlign_Right) [ SNew(SHorizontalBox) // Todo: Only display this widget when syncing .Visibility_Lambda([this] { return Tab->IsSyncing() ? EVisibility::Visible : EVisibility::Hidden; }) +SHorizontalBox::Slot() .Padding(5.0f, 25.0f) .AutoWidth() [ SAssignNew(SyncProgressText, STextBlock) .Text(LOCTEXT("SyncProgress", "Syncing Files")) .Text_Lambda([this] { return FText::FromString(Tab->GetSyncProgress()); }) ] +SHorizontalBox::Slot() .Padding(0.0f, 12.5f, 12.5f, 5.0f) .AutoWidth() [ SNew(SThrobber) ] ] ] ] +SVerticalBox::Slot() .Padding(0.0f, 5.0f) [ SNew(SSplitter) .Orientation(EOrientation::Orient_Vertical) // Horde builds +SSplitter::Slot() .Value(0.65f) [ SAssignNew(HordeBuildsView, SListView>) .ListItemsSource(&HordeBuilds) .SelectionMode(ESelectionMode::Single) .IsEnabled_Lambda([this] { return !Tab->IsSyncing(); }) .OnGenerateRow(this, &SGameSyncTab::GenerateHordeBuildTableRow) .OnContextMenuOpening(this, &SGameSyncTab::OnRightClickedBuild) .HeaderRow( SNew(SHeaderRow) +SHeaderRow::Column(HordeTableColumnStatus) .DefaultLabel(FText()) .FixedWidth(35.0f) +SHeaderRow::Column(HordeTableColumnType) .DefaultLabel(LOCTEXT("HordeHeaderType", "Type")) .FixedWidth(150.0f) +SHeaderRow::Column(HordeTableColumnChange) .DefaultLabel(LOCTEXT("HordeHeaderChange", "Change")) .FixedWidth(75.0f) +SHeaderRow::Column(HordeTableColumnTime) .DefaultLabel(LOCTEXT("HordeHeaderTime", "Time")) .FixedWidth(70.0f) +SHeaderRow::Column(HordeTableColumnAuthor) .DefaultLabel(LOCTEXT("HordeHeaderAuthor", "Author")) .FillWidth(0.15f) +SHeaderRow::Column(HordeTableColumnDescription) .DefaultLabel(LOCTEXT("HordeHeaderDescription", "Description")) +SHeaderRow::Column(HordeTableColumnEditor) .DefaultLabel(LOCTEXT("HordeHeaderEditor", "Editor")) .FillWidth(0.5f) ) ] // Log +SSplitter::Slot() .Value(0.35f) [ SAssignNew(SyncLog, SLogWidget) ] ] ]; } TSharedPtr SGameSyncTab::GetSyncLog() const { return SyncLog; } bool SGameSyncTab::SetSyncLogLocation(const FString& LogFileName) { return SyncLog->OpenFile(*LogFileName); } void SGameSyncTab::SetStreamPathText(FText StreamPath) { StreamPathText->SetText(StreamPath); } void SGameSyncTab::SetChangelistText(int Changelist) { if (Changelist > 0) { ChangelistText->SetText(FText::FromString(FString::FromInt(Changelist))); } else { ChangelistText->SetText(FText::FromString(TEXT("Unknown"))); } } void SGameSyncTab::SetProjectPathText(FText ProjectPath) { ProjectPathText->SetText(ProjectPath); } void SGameSyncTab::AddHordeBuilds(const TArray>& Builds) { HordeBuilds = Builds; HordeBuildsView->RebuildList(); } TSharedPtr SGameSyncTab::OnRightClickedBuild() { FMenuBuilder MenuBuilder(true, nullptr); TArray> SelectedItems = HordeBuildsView->GetSelectedItems(); // Todo: since I disabled multi select, I might be able to assume the array size is always 1? if (SelectedItems.IsValidIndex(0)) { // Don't show menu items for header rows; OnContextMenuOpening handles nullptr by not showing a menu if (SelectedItems[0]->bHeaderRow) { return nullptr; } MenuBuilder.AddMenuEntry( FText::FromString(TEXT("Sync")), FText::FromString("Sync to CL " + FString::FromInt(SelectedItems[0]->Changelist)), FSlateIcon(), FUIAction(FExecuteAction::CreateRaw(Tab, &UGSTab::OnSyncChangelist, SelectedItems[0]->Changelist)), NAME_None, EUserInterfaceActionType::Button ); MenuBuilder.AddSeparator(); MenuBuilder.AddMenuEntry( FText::FromString(TEXT("View in Swarm...")), TAttribute(), FSlateIcon(), FUIAction(FExecuteAction::CreateRaw(Tab, &UGSTab::OnViewInSwarmClicked, SelectedItems[0]->Changelist)), NAME_None, EUserInterfaceActionType::Button ); MenuBuilder.AddMenuEntry( FText::FromString(TEXT("Copy Changelist")), TAttribute(), FSlateIcon(), FUIAction(FExecuteAction::CreateRaw(Tab, &UGSTab::OnCopyChangeListClicked, SelectedItems[0]->Changelist)), NAME_None, EUserInterfaceActionType::Button ); MenuBuilder.AddMenuEntry( FText::FromString(TEXT("More Info")), TAttribute(), FSlateIcon(), FUIAction(FExecuteAction::CreateRaw(Tab, &UGSTab::OnMoreInfoClicked, SelectedItems[0]->Changelist)), NAME_None, EUserInterfaceActionType::Button ); } return MenuBuilder.MakeWidget(); } #undef LOCTEXT_NAMESPACE