Files
UnrealEngine/Engine/Source/Developer/SourceControlCheckInPrompt/Private/SourceControlCheckInPrompter.cpp
2025-05-18 13:04:45 +08:00

279 lines
8.3 KiB
C++

// 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<bool> 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<FGetSubmittedChangelists> Operation = ISourceControlOperation::Create<FGetSubmittedChangelists>();
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<FGetSubmittedChangelists, ESPMode::ThreadSafe> Operation = StaticCastSharedRef<FGetSubmittedChangelists>(InOperation);
const TArray<FSourceControlChangelistRef> 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