// Copyright Epic Games, Inc. All Rights Reserved. #include "PreflightService.h" #include "GenericPlatform/GenericPlatformProcess.h" #include "HttpModule.h" #include "Interfaces/IHttpResponse.h" #include "JsonObjectConverter.h" #include "Misc/Paths.h" #include "Modules/ModuleManager.h" #include "Serialization/JsonSerializer.h" #include "Widgets/Docking/SDockTab.h" #include "Internationalization/Regex.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Input/SButton.h" #include "Widgets/Text/STextBlock.h" #include "CommandLine/CmdLineParameters.h" #include "Configuration/Configuration.h" #include "Logic/Services/Interfaces/ISTSourceControlService.h" #include "Logic/CredentialsService.h" #include "Logic/TagService.h" #include "Logging/SubmitToolLog.h" #include "Models/ModelInterface.h" #include "Models/SubmitToolUserPrefs.h" #include "Telemetry/TelemetryService.h" #include "Parameters/SubmitToolParameters.h" FPreflightService::FPreflightService( const FHordeParameters& InSettings, FModelInterface* InModelInterface, TWeakPtr InServiceProvider) : Definition(InSettings), ServiceProvider(InServiceProvider), ModelInterface(InModelInterface), State(EPreflightServiceState::Idle), LastErrorMessage(TEXT("")) { TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateRaw(this, &FPreflightService::Tick)); DeleteShelveCallback = FSourceControlOperationComplete::CreateRaw(this, &FPreflightService::OnDeleteShelveOperationComplete); CreateShelveCallback = FSourceControlOperationComplete::CreateRaw(this, &FPreflightService::OnCreateShelveOperationComplete); PreflightTag = ServiceProvider.Pin()->GetService()->GetTagOfSubtype(TEXT("preflight")); } FPreflightService::~FPreflightService() { FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); OnPreflightDataUpdated.Clear(); } bool FPreflightService::Tick(float DeltaTime) { TSharedPtr ChangelistService = ServiceProvider.Pin()->GetService(); switch(State) { ////////////////////////////////////////////////// case EPreflightServiceState::Idle: // Do nothing, wait for someone to press the "Start" preflight button break; ////////////////////////////////////////////////// case EPreflightServiceState::RequestDeleteShelve: if(ChangelistService->GetFilesInCL().Num() == 0) { LastErrorMessage = TEXT("Missing local files! We can't update the shelved files!"); State = EPreflightServiceState::Error; } else if(ChangelistService->HasShelvedFiles()) { EDialogFactoryResult DialogResult = static_cast(ShowRecreateShelveDialog()); if(DialogResult == EDialogFactoryResult::FirstButton) { UE_LOG(LogSubmitTool, Log, TEXT("Preflight: Deleting shelved files")); State = EPreflightServiceState::WaitingForDeleteShelve; bCheckShelveInstead = false; ChangelistService->DeleteShelvedFiles(DeleteShelveCallback); } else if(DialogResult == EDialogFactoryResult::SecondButton) { UE_LOG(LogSubmitTool, Log, TEXT("Preflight: Using existing shelved files")); bCheckShelveInstead = true; State = EPreflightServiceState::StartPreflight; } else { UE_LOG(LogSubmitTool, Log, TEXT("Preflight: Request cancelled")); State = EPreflightServiceState::Idle; } } else { State = EPreflightServiceState::RequestCreateShelve; } break; ////////////////////////////////////////////////// case EPreflightServiceState::WaitingForDeleteShelve: // Do nothing, wait for the delete shelve callback break; ////////////////////////////////////////////////// case EPreflightServiceState::RequestCreateShelve: UE_LOG(LogSubmitTool, Log, TEXT("Preflight: Creating shelved files...")); State = EPreflightServiceState::WaitingForCreateShelve; ChangelistService->CreateShelvedFiles(CreateShelveCallback); break; ////////////////////////////////////////////////// case EPreflightServiceState::WaitingForCreateShelve: // Do nothing, wait for the create shelve callback break; ////////////////////////////////////////////////// case EPreflightServiceState::StartPreflight: StartPreflight(); break; ////////////////////////////////////////////////// case EPreflightServiceState::Error: UE_LOG(LogSubmitTool, Error, TEXT("Preflight: \"%s\""), *LastErrorMessage); State = EPreflightServiceState::Idle; break; } return true; } TMap FPreflightService::GetFormatParameters() const { TMap FormatMap = { { TEXT("URL"), Definition.HordeServerAddress }, { TEXT("CLID"), ServiceProvider.Pin()->GetService()->GetCLID()} }; FormatMap.Add(TEXT("Stream"), StreamName); FormatMap.Add(TEXT("Template"), FString()); FormatMap.Add(TEXT("AdditionalTasks"), FString()); FPreflightTemplateDefinition Template; if(SelectPreflightTemplate(Template)) { FormatMap[TEXT("Template")] = Template.Template; FormatMap[TEXT("AdditionalTasks")] = GetAdditionalTasksString(Template); } else { FormatMap[TEXT("Template")] = Definition.DefaultPreflightTemplate; } return FormatMap; } void FPreflightService::RequestPreflight() { if(State == EPreflightServiceState::Idle) { if(!ServiceProvider.Pin()->GetService()->GetClientStreams().IsEmpty()) { UE_LOG(LogSubmitTool, Log, TEXT("Preflight: Requesting...")); State = EPreflightServiceState::RequestDeleteShelve; } else { UE_LOG(LogSubmitTool, Error, TEXT("Couldn't retrieve stream name in this p4 client. Submit tool can't start a preflight, see previous errors.")); } } else { // Do nothing, we're already busy trying to start a preflight } } void FPreflightService::QueueFetch(bool bRequeue, float InSeconds) { FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda([this, bRequeue](float DeltaTime) { FetchPreflightInfo(bRequeue); return false; }), InSeconds); } void FPreflightService::Requeue() { float WaitTime = Definition.FetchPreflightEachSeconds; if(HordePreflights.IsValid()) { for(const FPreflightData& PFData : HordePreflights->PreflightList) { if(PFData.CachedResults.State != EPreflightState::Completed) { WaitTime = Definition.FetchPreflightEachSecondsWhenInProgress; break; } } } for(const TPair& Pair : UnlinkedHordePreflights) { if(Pair.Value.CachedResults.State != EPreflightState::Completed) { WaitTime = Definition.FetchPreflightEachSecondsWhenInProgress; break; } } QueueFetch(true, WaitTime); } void FPreflightService::FetchPreflightInfo(bool bRequeue, const FString& InOAuthToken) { TSharedPtr CredentialsService = ServiceProvider.Pin()->GetService(); if(Definition.HordeServerAddress.IsEmpty() || !CredentialsService->IsOIDCTokenEnabled() || FModelInterface::GetState() == ESubmitToolAppState::Finished) { return; } TSharedPtr ChangelistService = ServiceProvider.Pin()->GetService(); // Don't bother with the default changelist if(ChangelistService->GetCLID() == TEXT("default")) { if(bRequeue) { QueueFetch(bRequeue, Definition.FetchPreflightEachSeconds); } return; } const FString& OIDCToken = CredentialsService->IsTokenReady() ? CredentialsService->GetToken() : InOAuthToken; if(!OIDCToken.IsEmpty()) { if(!LinkedPFRequest.IsValid()) { LinkedPFRequest = FHttpModule::Get().CreateRequest(); FString FetchPreflightUrl = FString::Format(*Definition.FindPreflightURLFormat, GetFormatParameters()); LinkedPFRequest->SetURL(FetchPreflightUrl); LinkedPFRequest->SetVerb(TEXT("GET")); } else if(LinkedPFRequest->GetStatus() == EHttpRequestStatus::Processing) { // if it's still Processing, do not try to request again. return; } // ensure the token is the most up to date LinkedPFRequest->SetHeader(TEXT("Authorization"), FString::Printf(TEXT("bearer %s"), *OIDCToken)); if(!LinkedPFRequest->OnProcessRequestComplete().IsBound()) { LinkedPFRequest->OnProcessRequestComplete().Unbind(); } LinkedPFRequest->OnProcessRequestComplete().BindLambda([this, bRequeue, &OIDCToken](FHttpRequestPtr Request, FHttpResponsePtr HttpResponse, bool bConnectedSuccessfully) { if(!bConnectedSuccessfully) { if(HttpResponse.IsValid()) { UE_LOG(LogSubmitTool, Warning, TEXT("Unable to connect to horde. Connection error %d"), HttpResponse->GetResponseCode()); UE_LOG(LogSubmitToolDebug, Warning, TEXT("Unable to connect to horde. Connection error\nResponse: %s"), *HttpResponse->GetContentAsString()); } else { UE_LOG(LogSubmitTool, Warning, TEXT("Unable to connect to horde. Connection error, no response.")); } OnHordeConnectionFailed.Broadcast(); return; } if(HttpResponse.IsValid()) { //UE_LOG(LogSubmitToolDebug, Verbose, TEXT("Fetch Preflight Response: %s"), *HttpResponse->GetContentAsString()); if(EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) { TUniquePtr NewHordePreflights = MakeUnique(); FJsonObjectConverter::JsonObjectStringToUStruct(FString::Printf(TEXT("{\"PreflightList\" : %s}"), *HttpResponse->GetContentAsString()), NewHordePreflights.Get()); NewHordePreflights->Initialize(); if(PreflightTag != nullptr && !Definition.FindSinglePreflightURLFormat.IsEmpty()) { for(FString PreflightId : PreflightTag->GetValues()) { if(PreflightId.Equals(TEXT("skip")) || PreflightId.Equals(TEXT("none"))) { continue; } if(PreflightId.Contains(TEXT("/"))) { int32 SlashIdx; PreflightId.FindLastChar(TCHAR('/'), SlashIdx); PreflightId = PreflightId.RightChop(SlashIdx + 1); } PreflightId.TrimStartAndEndInline(); FRegexPattern Pattern = FRegexPattern(TEXT("(?:[0-9]|[a-f]){24}"), ERegexPatternFlags::CaseInsensitive); FRegexMatcher regex = FRegexMatcher(Pattern, PreflightId); bool match = regex.FindNext(); if(match) { const FPreflightData* FoundData = NewHordePreflights->PreflightList.FindByPredicate([&PreflightId](const FPreflightData& InData) { return InData.ID == PreflightId; }); if(FoundData == nullptr) { FetchUnlinkedPreflight(PreflightId, bRequeue, OIDCToken); } } } } if(!HordePreflights.IsValid() || *NewHordePreflights != *HordePreflights) { UE_LOG(LogSubmitToolDebug, Verbose, TEXT("Newer Preflight information received")); // Only log when there's a different number of preflights if(!HordePreflights.IsValid() || HordePreflights->PreflightList.Num() != NewHordePreflights->PreflightList.Num()) { UE_LOG(LogSubmitTool, Log, TEXT("Retrieved %d preflights for CL %s"), NewHordePreflights->PreflightList.Num(), *ServiceProvider.Pin()->GetService()->GetCLID()); } HordePreflights = MoveTemp(NewHordePreflights); if(PreflightTag != nullptr) { bool bCLDescriptionModified = false; if(HordePreflights->PreflightList.Num() != 0) { FString CurrentTagValue = PreflightTag->GetValuesText(); if(!bStopAskingTagUpdate && !CurrentTagValue.Contains(HordePreflights->PreflightList[0].ID)) { if (FSubmitToolUserPrefs::Get()->bAutoUpdatePreflight) { ModelInterface->SetTagValues(*PreflightTag, HordePreflights->PreflightList[0].ID); UE_LOG(LogSubmitTool, Log, TEXT("Tag %s has been updated with the latest associated preflight %sjob/%s"), *PreflightTag->Definition.GetTagId(), *Definition.HordeServerAddress, *HordePreflights->PreflightList[0].ID) bCLDescriptionModified = true; } else { EDialogFactoryResult Result = ShowUpdatePreflightTagDialog(); if(Result == EDialogFactoryResult::FirstButton) { // Set the latest one as the tag value ModelInterface->SetTagValues(*PreflightTag, HordePreflights->PreflightList[0].ID); UE_LOG(LogSubmitTool, Log, TEXT("Tag %s has been updated with the latest associated preflight %sjob/%s"), *PreflightTag->Definition.GetTagId(), *Definition.HordeServerAddress, *HordePreflights->PreflightList[0].ID) bCLDescriptionModified = true; } else { bStopAskingTagUpdate = true; } } } } if(bCLDescriptionModified) { ModelInterface->ValidateCLDescription(); } } } } else { UE_LOG(LogSubmitTool, Warning, TEXT("Could not retrieve preflights, Http code %d."), HttpResponse->GetResponseCode()); UE_LOG(LogSubmitToolDebug, Error, TEXT("Fetch preflight failed. Response %s"), *HttpResponse->GetContentAsString()); } } else { UE_LOG(LogSubmitTool, Warning, TEXT("Unable to fetch preflights. Failed with code %d"), HttpResponse->GetResponseCode()); UE_LOG(LogSubmitToolDebug, Warning, TEXT("Unable to fetch preflights. Failed with code %d\nResponse: %s"), HttpResponse->GetResponseCode(), *HttpResponse->GetContentAsString()); } if(ActiveUnlinkedRequests == 0) { if(OnPreflightDataUpdated.IsBound() && HordePreflights.IsValid()) { OnPreflightDataUpdated.Broadcast(HordePreflights, UnlinkedHordePreflights); } if(bRequeue) { Requeue(); } } }); FTimespan TimeSinceLast = FDateTime::UtcNow() - LastRequest; if(bRequeue || TimeSinceLast.GetTotalSeconds() > 3) { LastRequest = FDateTime::UtcNow(); UE_LOG(LogSubmitToolDebug, Log, TEXT("Fetching preflights for CL %s. URL: %s"), *ChangelistService->GetCLID(), *LinkedPFRequest->GetURL()) LinkedPFRequest->ProcessRequest(); } } else { CredentialsService->QueueWorkForToken([this, bRequeue](const FString& InToken) { if(!InToken.IsEmpty()) { FetchPreflightInfo(bRequeue, InToken); } else { UE_LOG(LogSubmitTool, Warning, TEXT("Couldn't obtain OAuth token login, communication with Horde is not possible.")); } }); } } void FPreflightService::FetchUnlinkedPreflight(const FString& InPreflightId, bool bRequeue, const FString& InOAuthToken) { if(InOAuthToken.IsEmpty()) { return; } FHttpRequestPtr& UnlinkedPFRequest = UnlinkedPFRequests.FindOrAdd(InPreflightId); if(!UnlinkedPFRequest.IsValid()) { UnlinkedPFRequest = FHttpModule::Get().CreateRequest(); FStringFormatNamedArguments ReplaceStringArgs = GetFormatParameters(); ReplaceStringArgs.Add(TEXT("PreflightId"), InPreflightId); FString FetchPreflightUrl = FString::Format(*Definition.FindSinglePreflightURLFormat, ReplaceStringArgs); UnlinkedPFRequest->SetURL(FetchPreflightUrl); UnlinkedPFRequest->SetVerb(TEXT("GET")); } else if(UnlinkedPFRequest->GetStatus() == EHttpRequestStatus::Processing) { // if it's still Processing, do not try to request again. return; } UnlinkedPFRequest->SetHeader(TEXT("Authorization"), FString::Printf(TEXT("bearer %s"), *InOAuthToken)); if(!UnlinkedPFRequest->OnProcessRequestComplete().IsBound()) { UnlinkedPFRequest->OnProcessRequestComplete().Unbind(); } ActiveUnlinkedRequests++; UnlinkedPFRequest->OnProcessRequestComplete().BindLambda([this, bRequeue, &InOAuthToken, InPreflightId](FHttpRequestPtr Request, FHttpResponsePtr HttpResponse, bool bConnectedSuccessfully) { ActiveUnlinkedRequests--; if(!bConnectedSuccessfully) { if(HttpResponse.IsValid()) { UE_LOG(LogSubmitTool, Warning, TEXT("Unable to connect to horde. Connection error %d"), HttpResponse->GetResponseCode()); UE_LOG(LogSubmitToolDebug, Warning, TEXT("Unable to connect to horde. Connection error\nResponse: %s"), *HttpResponse->GetContentAsString()); } else { UE_LOG(LogSubmitTool, Warning, TEXT("Unable to connect to horde. Connection error, no response.")); } return; } if(HttpResponse.IsValid()) { UE_LOG(LogSubmitToolDebug, Verbose, TEXT("Fetch Single Preflight Response: %s"), *HttpResponse->GetContentAsString()); if(EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) { FPreflightData ReceivedPreflightInfo; FJsonObjectConverter::JsonObjectStringToUStruct(*HttpResponse->GetContentAsString(), &ReceivedPreflightInfo); ReceivedPreflightInfo.RecalculateCachedResults(); if(!UnlinkedHordePreflights.Contains(InPreflightId) || UnlinkedHordePreflights[InPreflightId] != ReceivedPreflightInfo) { UE_LOG(LogSubmitToolDebug, Verbose, TEXT("Newer %s Preflight information received"), *InPreflightId); // Only log when the preflight is new if(!UnlinkedHordePreflights.Contains(InPreflightId)) { UE_LOG(LogSubmitTool, Log, TEXT("Retrieved information from preflight %s"), *InPreflightId); UnlinkedHordePreflights.Add(InPreflightId, ReceivedPreflightInfo); } else { UnlinkedHordePreflights[InPreflightId] = MoveTemp(ReceivedPreflightInfo); } } } else { UE_LOG(LogSubmitTool, Warning, TEXT("Could not retrieve preflights, Http code %d."), HttpResponse->GetResponseCode()); UE_LOG(LogSubmitToolDebug, Error, TEXT("Fetch preflight failed. Response %s"), *HttpResponse->GetContentAsString()); } } else { UE_LOG(LogSubmitTool, Warning, TEXT("Unable to fetch preflights. Failed with code %d"), HttpResponse->GetResponseCode()); UE_LOG(LogSubmitToolDebug, Warning, TEXT("Unable to fetch preflights. Failed with code %d\nResponse: %s"), HttpResponse->GetResponseCode(), *HttpResponse->GetContentAsString()); } if(ActiveUnlinkedRequests == 0) { if(OnPreflightDataUpdated.IsBound() && HordePreflights.IsValid()) { OnPreflightDataUpdated.Broadcast(HordePreflights, UnlinkedHordePreflights); } if(bRequeue) { Requeue(); } } }); UnlinkedPFRequest->ProcessRequest(); } void FPreflightService::StartPreflight() { TSharedPtr SCCService = ServiceProvider.Pin()->GetService(); StreamName = SCCService->GetRootStreamName(); const TArray& ShelvedPaths = ServiceProvider.Pin()->GetService()->GetShelvedFilesDepotPaths(true); if (ShelvedPaths.IsEmpty()) { LastErrorMessage = TEXT("Shelve is empty or it couldn't be retrieved from p4, can't request preflight"); State = EPreflightServiceState::Error; return; } FString CommonPath = ShelvedPaths[0]; const FString& LastPath = ShelvedPaths.Last(); for(size_t i = 0; i < FMath::Min(CommonPath.Len(), LastPath.Len()); ++i) { if(CommonPath[i] != LastPath[i]) { CommonPath = CommonPath.Left(i); break; } } if(!CommonPath.Equals(TEXT("//"))) { int32 NextSlash = CommonPath.Find(TEXT("/"), ESearchCase::IgnoreCase, ESearchDir::FromStart, 2); const FString Depot = CommonPath.Mid(2, NextSlash - 2); size_t StreamDepth = SCCService->GetDepotStreamLength(Depot); for(size_t i = 0; i < StreamDepth; ++i) { NextSlash = CommonPath.Find(TEXT("/"), ESearchCase::IgnoreCase, ESearchDir::FromStart, NextSlash+1); } if(NextSlash != -1) { const FSCCStream* FoundStream = SCCService->GetSCCStream(CommonPath.Left(NextSlash)); if(FoundStream != nullptr) { StreamName = FoundStream->Name; } } } FString StartPreflightUrl = FString::Format(*Definition.StartPreflightURLFormat, GetFormatParameters()); // If for some reason, our preflight settings are missing, this will be empty, let's not popup a browser with nothing in it if(!StartPreflightUrl.IsEmpty()) { UE_LOG(LogSubmitTool, Log, TEXT("Preflight: Starting preflight with URL: \"%s\""), *StartPreflightUrl); FTelemetryService::Get()->CustomEvent(TEXT("SubmitTool.PreflightLaunched"), MakeAnalyticsEventAttributeArray( TEXT("PreflightURL"), StartPreflightUrl, TEXT("Stream"), StreamName )); FPlatformProcess::LaunchURL(*StartPreflightUrl, nullptr, nullptr); State = EPreflightServiceState::Idle; // Do a Fetch in 10 and 30 s to try and capture the triggered preflight QueueFetch(false, 10.f); QueueFetch(false, 30.f); } else { LastErrorMessage = TEXT("Missing INI preflight settings"); State = EPreflightServiceState::Error; } } void FPreflightService::OnDeleteShelveOperationComplete(const FSourceControlOperationRef& Operation, ECommandResult::Type Result) { if(State == EPreflightServiceState::WaitingForDeleteShelve) { if(Result == ECommandResult::Type::Succeeded) { State = EPreflightServiceState::RequestCreateShelve; } else { LastErrorMessage = TEXT("Unable to delete shelve for preflight"); State = EPreflightServiceState::Error; } } else { LastErrorMessage = TEXT("Received delete shelve callback when not waiting for it"); State = EPreflightServiceState::Error; } } void FPreflightService::OnCreateShelveOperationComplete(const FSourceControlOperationRef& Operation, ECommandResult::Type Result) { if(State == EPreflightServiceState::WaitingForCreateShelve) { if(Result == ECommandResult::Type::Succeeded) { State = EPreflightServiceState::StartPreflight; } else { LastErrorMessage = TEXT("Unable to create shelve for preflight"); State = EPreflightServiceState::Error; } } else { LastErrorMessage = TEXT("Received create shelve callback when not waiting for it"); State = EPreflightServiceState::Error; } } bool FPreflightService::SelectPreflightTemplate(FPreflightTemplateDefinition& OutTemplate) const { TSharedPtr ChangelistService = ServiceProvider.Pin()->GetService(); // Get the list of files in the changelist const TArray& FilesInCl = bCheckShelveInstead ? ChangelistService->GetShelvedFilesDepotPaths() : ChangelistService->GetFilesDepotPaths(); // Loop through each definition to see if the files are in the path then check extension for(const FPreflightTemplateDefinition& Def : Definition.Definitions) { FString RegexPat = Def.RegexPath.Replace(TEXT("$(StreamRoot)"), *StreamName, ESearchCase::IgnoreCase); FRegexPattern Pattern = FRegexPattern(RegexPat, ERegexPatternFlags::CaseInsensitive); for(const FString& File : FilesInCl) { FRegexMatcher regex = FRegexMatcher(Pattern, File); if(regex.FindNext()) { OutTemplate = Def; return true; } } } return false; } FString FPreflightService::GetAdditionalTasksString(const FPreflightTemplateDefinition& InTemplate) const { TStringBuilder<256> AdditionalTaskStrBuilder; const FString BaseString = TEXT("&id-additional-tasks."); const FString EndString = TEXT("=true"); TSharedPtr ChangelistService = ServiceProvider.Pin()->GetService(); const TArray& FilesInCl = bCheckShelveInstead ? ChangelistService->GetShelvedFilesDepotPaths() : ChangelistService->GetFilesDepotPaths(); for(const FPreflightAdditionalTask& AdditionalTask : InTemplate.AdditionalTasks) { FString RegexPat = AdditionalTask.RegexPath.Replace(TEXT("$(StreamRoot)"), *StreamName, ESearchCase::IgnoreCase); FRegexPattern Pattern = FRegexPattern(RegexPat, ERegexPatternFlags::CaseInsensitive); for(const FString& File : FilesInCl) { FRegexMatcher regex = FRegexMatcher(Pattern, File); if(regex.FindNext()) { AdditionalTaskStrBuilder.Append(BaseString); AdditionalTaskStrBuilder.Append(AdditionalTask.TaskId); AdditionalTaskStrBuilder.Append(EndString); } } } return AdditionalTaskStrBuilder.ToString(); } EDialogFactoryResult FPreflightService::ShowRecreateShelveDialog() const { if(ModelInterface->GetMainTab().IsValid() && ModelInterface->GetMainTab().Pin()->GetParentWindow().IsValid()) { ModelInterface->GetMainTab().Pin()->GetParentWindow()->DrawAttention(FWindowDrawAttentionParameters()); } const FText TextTitle = FText::FromString(FString::Printf(TEXT("Preflight CL %s: Recreate shelved files?"), *ServiceProvider.Pin()->GetService()->GetCLID())); const FText TextDescription = FText::FromString(TEXT("There are already shelved files in this changelist.\n\nDo you want to delete and recreate your shelf from the latest changes in your local files for use in the preflight?")); return FDialogFactory::ShowDialog(TextTitle, TextDescription, TArray{ TEXT("Re-shelve files"), TEXT("Use existing Shelve"), TEXT("Cancel") }); } EDialogFactoryResult FPreflightService::ShowUpdatePreflightTagDialog() const { if(ModelInterface->GetMainTab().IsValid() && ModelInterface->GetMainTab().Pin()->GetParentWindow().IsValid()) { ModelInterface->GetMainTab().Pin()->GetParentWindow()->DrawAttention(FWindowDrawAttentionParameters()); } const FText TextTitle = FText::FromString(FString::Printf(TEXT("Preflight CL %s: Newer preflight available"), *ServiceProvider.Pin()->GetService()->GetCLID())); const FText TextDescription = FText::FromString(FString::Printf(TEXT("There is a newer preflight for this changelist:\n%s - %s\n\nDo you want to update the #preflight tag?"), *Definition.HordeServerAddress, *HordePreflights->PreflightList[0].ID, *HordePreflights->PreflightList[0].Name, *HordePreflights->PreflightList[0].ID)); TSharedPtr AutoUpdate = SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SCheckBox) .IsChecked_Lambda([]() { return FSubmitToolUserPrefs::Get()->bAutoUpdatePreflight ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }) .OnCheckStateChanged_Lambda([](ECheckBoxState InNewState) { FSubmitToolUserPrefs::Get()->bAutoUpdatePreflight = InNewState == ECheckBoxState::Checked; }) ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SButton) .ButtonStyle(FAppStyle::Get(), "InvisibleButton") .IsFocusable(false) .OnClicked_Lambda([this]() { FSubmitToolUserPrefs::Get()->bAutoUpdatePreflight = !FSubmitToolUserPrefs::Get()->bAutoUpdatePreflight; return FReply::Handled(); }) [ SNew(STextBlock) .Justification(ETextJustify::Left) .MinDesiredWidth(60) .Text(FText::FromString(TEXT("Always update, Don't ask again"))) ] ]; return FDialogFactory::ShowDialog(TextTitle, TextDescription, TArray{ TEXT("Update Tag Value"), TEXT("Cancel") }, AutoUpdate); }