// Copyright Epic Games, Inc. All Rights Reserved. #include "SourceControlCheckInPrompter.h" #include "SourceControlCheckInPromptModule.h" #include "SourceControlHelpers.h" #include "SourceControlOperations.h" #include "ISourceControlModule.h" #include "Containers/Ticker.h" #include "UObject/ObjectSaveContext.h" #include "UObject/Package.h" #include "HAL/IConsoleManager.h" #include "Editor.h" #define LOCTEXT_NAMESPACE "SourceControlCheckInPrompter" static const FTimespan IntervalNoCheckins = FTimespan::FromDays(1); static const FTimespan IntervalBetweenPrompts = FTimespan::FromDays(1); static const FTimespan IntervalBetweenGetSubmittedChangelists = FTimespan::FromMinutes(10); static const FTimespan IntervalSessionLength = FTimespan::FromMinutes(30); extern TAutoConsoleVariable CVarSourceControlEnablePeriodicCheckInPrompt; static FString GetEditorMapName() { if (UWorld* EditorWorld = GEditor->GetEditorWorldContext().World()) { if (UPackage* EditorWorldPackage = EditorWorld->GetPackage()) { const FString EditorWorldPackageName = EditorWorldPackage->GetName(); if (!EditorWorldPackageName.StartsWith(TEXT("/Temp/"))) { return EditorWorldPackageName; } } } return FString(); } FSourceControlCheckInPrompter::FSourceControlCheckInPrompter() : PromptFlowMapName() , TimeCheckInPromptShown() , TimeGetSubmittedChangelistsExecuted() , bPromptDelayed(false) { } FSourceControlCheckInPrompter::~FSourceControlCheckInPrompter() { UPackage::PackageSavedWithContextEvent.RemoveAll(this); } void FSourceControlCheckInPrompter::Init() { UPackage::PackageSavedWithContextEvent.AddRaw(this, &FSourceControlCheckInPrompter::OnPackageSaved); // Set a ticker to periodically check if there have been any project changes. FTSTicker::GetCoreTicker().AddTicker( FTickerDelegate::CreateSPLambda(this, [this] (float DeltaTime) { FString SourceControlProjectDir = ISourceControlModule::Get().GetSourceControlProjectDir(); if (ProjectDirectory != SourceControlProjectDir) { ProjectDirectory = SourceControlProjectDir; ProjectActivationTime = FDateTime::UtcNow(); bPromptDelayed = false; } if (bPromptDelayed) { CheckPrompt(); } return true; } ), 60.f); } void FSourceControlCheckInPrompter::OnPackageSaved(const FString& Filename, UPackage* Pkg, FObjectPostSaveContext ObjectSaveContext) { CheckPrompt(); } // Step 1: Initiate the periodic prompt flow. // Trigger a SourceControl operation to determine how much changes the user submitted in the past day. void FSourceControlCheckInPrompter::OnStartPrompt() { // Execute it with a filter that looks for submitted changelist for the current user in the last day. TSharedRef Operation = ISourceControlOperation::Create(); Operation->SetDateToFilter(FDateTime::UtcNow()); Operation->SetDateFromFilter(FDateTime::UtcNow() - IntervalNoCheckins); Operation->SetOwnedFilter(true); ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); if (SourceControlProvider.IsAvailable() && SourceControlProvider.CanExecuteOperation(Operation)) { // Start flow. PromptFlowMapName = GetEditorMapName(); // Update the time the get submitted changelists operation was last executed. TimeGetSubmittedChangelistsExecuted.Add(PromptFlowMapName, FDateTime::Now()); // Execute it asynchronously. SourceControlProvider.Execute(Operation, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &FSourceControlCheckInPrompter::OnSourceControlOperationComplete) ); } } // Step 2: When the SourceControl operation completes, check if any check-ins were found. // If yes, end flow. // If no, continue flow by showing the prompt. void FSourceControlCheckInPrompter::OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult) { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); if (InOperation->GetName() == TEXT("GetSubmittedChangelists")) { TSharedRef Operation = StaticCastSharedRef(InOperation); const TArray SubmittedChangelists = Operation->GetSubmittedChangelists(); if (SubmittedChangelists.Num() == 0) { // Continue flow. // Prompt user as he's got no recent check-ins. FTSTicker::GetCoreTicker().AddTicker( FTickerDelegate::CreateSP(this, &FSourceControlCheckInPrompter::OnAttemptPrompt), 1.0f ); } else { PromptFlowMapName.Empty(); } } } // Step 3: When it's determined that a prompt should be shown, wait for a suitable moment in the editor. bool FSourceControlCheckInPrompter::OnAttemptPrompt(float) { // Abort if map changed, project closed, etc... FString EditorMapName = GetEditorMapName(); if (PromptFlowMapName != EditorMapName) { PromptFlowMapName.Empty(); return false; } // Update the time the prompt was last shown. TimeCheckInPromptShown.Add(EditorMapName, FDateTime::Now()); // Show the prompt. FSourceControlCheckInPromptModule::Get().ShowToast( LOCTEXT("MessageToast", "It's been more than 24 hours since you checked in. Check in to revision control to back up your project?") ); PromptFlowMapName.Empty(); return false; } void FSourceControlCheckInPrompter::CheckPrompt() { FString EditorMapName = GetEditorMapName(); if (PromptFlowMapName.IsEmpty() && !EditorMapName.IsEmpty()) { bool bPromptEnabled = CVarSourceControlEnablePeriodicCheckInPrompt.GetValueOnGameThread(); bool bPromptShow = false; if (bPromptEnabled) { if (bPromptDelayed) { bPromptShow = true; } else { bool bIsPromptAllowed = IsPromptAllowed(); bool bIsGetSubmittedChangelistsAllowed = IsGetSubmittedChangelistsAllowed(); bPromptShow = bIsPromptAllowed && bIsGetSubmittedChangelistsAllowed; } } if (bPromptShow) { if (FDateTime::UtcNow() - IntervalSessionLength < ProjectActivationTime) { // As the user has not been active in this world for a sufficient amount of time, // the prompt is delayed until the required interval has passed. bPromptShow = false; bPromptDelayed = true; } else { bPromptDelayed = false; } } if (bPromptEnabled && bPromptShow) { OnStartPrompt(); } } } bool FSourceControlCheckInPrompter::IsPromptAllowed() const { // Ensure the SourceControl system is available. ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); if (!SourceControlProvider.IsAvailable()) { return false; } // Ensure it has something to check-in. if (SourceControlProvider.GetNumLocalChanges().IsSet()) { int NumLocalChanges = SourceControlProvider.GetNumLocalChanges().GetValue(); if (NumLocalChanges == 0) { return false; } } // Ensure there's a world in the editor. UWorld* EditorWorld = GEditor->GetEditorWorldContext().World(); if (EditorWorld == nullptr) { return false; } // Ensure the project activation time applies to the current project. FString SourceControlProjectDir = ISourceControlModule::Get().GetSourceControlProjectDir(); if (ProjectDirectory != SourceControlProjectDir) { return false; } // Ensure the prompt hasn't been shown too recently for that world. FString PackageName = EditorWorld->GetPackage()->GetName(); if (const FDateTime* LastPrompt = TimeCheckInPromptShown.Find(PackageName)) { return FDateTime::Now() >= *LastPrompt + IntervalBetweenPrompts; } else { return true; // A prompt wasn't shown before. } } bool FSourceControlCheckInPrompter::IsGetSubmittedChangelistsAllowed() const { // Ensure the SourceControl system is available. ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); if (!SourceControlProvider.IsAvailable()) { return false; } // Ensure there's a world in the editor. UWorld* EditorWorld = GEditor->GetEditorWorldContext().World(); if (EditorWorld == nullptr) { return false; } // Ensure the operation isn't executed too often for that world. FString PackageName = EditorWorld->GetPackage()->GetName(); if (const FDateTime* LastOperation = TimeGetSubmittedChangelistsExecuted.Find(PackageName)) { return FDateTime::Now() >= *LastOperation + IntervalBetweenGetSubmittedChangelists; } else { return true; // An operation wasn't executed yet. } } #undef LOCTEXT_NAMESPACE