Implemented the 'Unsaved' Editor status bar button that displays the number of unsaved assets (dirty packages) the user and potential warnings with respect to the source control those asset may have.

- Implemented the UnsavedAssetTrackers plugin and enabled it by default.
  - Added the 'Unsaved' button to the Editor taskbars to display the number of unsaved packages along with a warning icon if warning are associated with one of the unsaved files.
  - Updated the 'Save Content' package to display warnings if one of the packages has warnings.
  - Added a class to monitor and periodically update the source controlled status of a collection of files/packages.
  - Changed the default value of PromptForCheckoutOnAssetModification settings to False because the Unsaved button flow is complementary (and the popup was noisy for OFPA users).
  - Updated the changelist window to use the soure control file status monitor.

#jira UE-163734 - Implement the 'Unsaved' Editor Taskbar Button
#rb Patrick.Enfedaque
#preflight 633ae915b12b8af5fde80f7c

[CL 22322224 by patrick laflamme in ue5-main branch]
This commit is contained in:
patrick laflamme
2022-10-03 20:38:09 -04:00
parent e5ea6d9d04
commit 068b6058ab
23 changed files with 1346 additions and 23 deletions

View File

@@ -0,0 +1,297 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SourceControlFileStatusMonitor.h"
#include "Framework/Application/SlateApplication.h"
#include "HAL/PlatformTime.h"
#include "ISourceControlModule.h"
#include "ISourceControlProvider.h"
#include "Math/NumericLimits.h"
#include "SourceControlOperations.h"
FSourceControlFileStatusMonitor::FSourceControlFileStatusMonitor()
: ProbationPeriodPolicy(FTimespan::FromSeconds(1))
, RefreshPeriodPolicy(FTimespan::FromMinutes(5))
{
TickerHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateRaw(this, &FSourceControlFileStatusMonitor::Tick));
SetSuspendMonitoringPolicy([]()
{
// By default, suspend monitoring if the user didn't interact for the last 5 minutes.
return FPlatformTime::Seconds() - FSlateApplication::Get().GetLastUserInteractionTime() > FTimespan::FromMinutes(5).GetTotalSeconds();
});
}
FSourceControlFileStatusMonitor::~FSourceControlFileStatusMonitor()
{
FTSTicker::GetCoreTicker().RemoveTicker(TickerHandle);
}
TSharedPtr<FSourceControlFileStatusMonitor::FSourceControlFileStatus> FSourceControlFileStatusMonitor::FindFileStatus(const FString& Pathname) const
{
if (const TSharedPtr<FSourceControlFileStatus>* FileStatus = MonitoredFiles.Find(Pathname))
{
return *FileStatus;
}
return nullptr;
}
void FSourceControlFileStatusMonitor::OnSourceControlProviderChanged(ISourceControlProvider& OldProvider, ISourceControlProvider& NewProvider)
{
// Start new with the new provider.
RequestedStatusFiles.Reset();
for (const TPair<FString, TSharedPtr<FSourceControlFileStatus>>& Pair : MonitoredFiles)
{
if (Pair.Value->FileState)
{
for (TPair<uintptr_t, FOnSourceControlFileStatus>& OwnerDelegatePair : Pair.Value->OwnerDelegateMap)
{
OwnerDelegatePair.Value.ExecuteIfBound(Pair.Key, nullptr); // Passing 'nullptr' state means that the state is now unknown.
}
Pair.Value->LastStatusCheckTimestampSecs = 0.0;
Pair.Value->FileState.Reset();
}
}
NewAddedFileCount = MonitoredFiles.Num();
LastAddedFileTimeSecs = FPlatformTime::Seconds();
OldestFileStatusTimeSecs = 0.0;
}
void FSourceControlFileStatusMonitor::StartMonitoringFile(uintptr_t OwnerId, const FString& Pathname, FOnSourceControlFileStatus OnSourceControlFileStatus)
{
// If the file is already monitored.
if (TSharedPtr<FSourceControlFileStatus> FileStatus = FindFileStatus(Pathname))
{
// Another 'client' is looking at this file (if the client already exit, override the callback with the new one).
FOnSourceControlFileStatus& OnSourceControlFileStatusDelegate = FileStatus->OwnerDelegateMap.FindOrAdd(OwnerId);
OnSourceControlFileStatusDelegate = OnSourceControlFileStatus;
// The monitor already knows the status.
if (FileStatus->FileState)
{
OnSourceControlFileStatusDelegate.ExecuteIfBound(Pathname, FileStatus->FileState.Get());
}
}
else
{
MonitoredFiles.Emplace(Pathname, MakeShared<FSourceControlFileStatus>(OwnerId, MoveTemp(OnSourceControlFileStatus)));
LastAddedFileTimeSecs = FPlatformTime::Seconds();
++NewAddedFileCount;
}
if (!SourceControlProviderChangedDelegateHandle.IsValid())
{
SourceControlProviderChangedDelegateHandle = ISourceControlModule::Get().RegisterProviderChanged(
FSourceControlProviderChanged::FDelegate::CreateSP(this, &FSourceControlFileStatusMonitor::OnSourceControlProviderChanged));
}
}
void FSourceControlFileStatusMonitor::StartMonitoringFiles(uintptr_t OwnerId, const TArray<FString>& Pathnames, FOnSourceControlFileStatus OnSourceControlledFileStatus)
{
for (const FString& Pathname : Pathnames)
{
StartMonitoringFile(OwnerId, Pathname, OnSourceControlledFileStatus);
}
}
void FSourceControlFileStatusMonitor::StartMonitoringFiles(uintptr_t OwnerId, const TSet<FString>& Pathnames, FOnSourceControlFileStatus OnSourceControlledFileStatus)
{
for (const FString& Pathname : Pathnames)
{
StartMonitoringFile(OwnerId, Pathname, OnSourceControlledFileStatus);
}
}
void FSourceControlFileStatusMonitor::StopMonitoringFile(uintptr_t OwnerId, const FString& Pathname)
{
if (TSharedPtr<FSourceControlFileStatus> FileStatus = FindFileStatus(Pathname))
{
if (FileStatus->OwnerDelegateMap.Remove(OwnerId) > 0 && FileStatus->OwnerDelegateMap.IsEmpty())
{
MonitoredFiles.Remove(Pathname);
}
}
}
void FSourceControlFileStatusMonitor::StopMonitoringFiles(uintptr_t OwnerId, const TArray<FString>& Pathnames)
{
for (const FString& Pathname : Pathnames)
{
StopMonitoringFile(OwnerId, Pathname);
}
}
void FSourceControlFileStatusMonitor::StopMonitoringFiles(uintptr_t OwnerId, const TSet<FString>& Pathnames)
{
for (const FString& Pathname : Pathnames)
{
StopMonitoringFile(OwnerId, Pathname);
}
}
void FSourceControlFileStatusMonitor::StopMonitoringFiles(uintptr_t OwnerId)
{
for (auto It = MonitoredFiles.CreateIterator(); ;++It)
{
if (It->Value->OwnerDelegateMap.Remove(OwnerId) > 0 && It->Value->OwnerDelegateMap.IsEmpty())
{
It.RemoveCurrent();
}
}
}
TSet<FString> FSourceControlFileStatusMonitor::GetMonitoredFiles(uintptr_t OwnerId)
{
TSet<FString> Pathnames;
for (const TPair<FString, TSharedPtr<FSourceControlFileStatus>>& Pair : MonitoredFiles)
{
if (!Pair.Value->OwnerDelegateMap.Contains(OwnerId))
{
Pathnames.Add(Pair.Key);
}
}
return Pathnames;
}
TOptional<FTimespan> FSourceControlFileStatusMonitor::GetStatusAge(const FString& Pathname) const
{
TOptional<FTimespan> Age;
if (TSharedPtr<FSourceControlFileStatus> FileStatus = FindFileStatus(Pathname))
{
Age.Emplace(FTimespan::FromSeconds(FileStatus->FileState ? FPlatformTime::Seconds() - FileStatus->LastStatusCheckTimestampSecs : 0.0));
}
return Age;
}
bool FSourceControlFileStatusMonitor::Tick(float DeltaTime)
{
// Nothing to check or a request is already in-flight.
if (!ISourceControlModule::Get().IsEnabled() || MonitoredFiles.IsEmpty() || HasOngoingRequest())
{
return true;
}
// Check if the monitor is suspended, not allowed to send requests.
if (SuspendMonitoringPolicy && SuspendMonitoringPolicy())
{
return true;
}
double NowSecs = FPlatformTime::Seconds();
// Throttle the source control status check when no new files need to be checked. Don't overload the source control server with too many requests.
if (NewAddedFileCount == 0 && NowSecs - OldestFileStatusTimeSecs < RefreshPeriodPolicy.GetTotalSeconds())
{
return true; // Nothing to do this time around.
}
// Throttle the checks when new files are added. Batch new files edited close in time to be more efficient.
if (NowSecs - LastAddedFileTimeSecs < ProbationPeriodPolicy.GetTotalSeconds() && NewAddedFileCount < MaxFileNumPerRequestPolicy)
{
return true; // Delay the request, give chance to capture more files.
}
TArray<const TPair<FString, TSharedPtr<FSourceControlFileStatus>>*> NewFiles;
NewFiles.Reserve(NewAddedFileCount);
TArray<const TPair<FString, TSharedPtr<FSourceControlFileStatus>>*> RefreshedFiles;
RefreshedFiles.Reserve(MonitoredFiles.Num());
// List all the files that are new and those that weren't updated recently.
for (const TPair<FString, TSharedPtr<FSourceControlFileStatus>>& Pair : MonitoredFiles)
{
if (!Pair.Value->FileState.IsValid())
{
NewFiles.Add(&Pair);
}
else if (NowSecs - Pair.Value->LastStatusCheckTimestampSecs >= RefreshPeriodPolicy.GetTotalSeconds())
{
RefreshedFiles.Add(&Pair);
}
}
// Too many status to query/refresh?
if (NewFiles.Num() < MaxFileNumPerRequestPolicy && NewFiles.Num() + RefreshedFiles.Num() > MaxFileNumPerRequestPolicy)
{
// Get the status of all newly added files and refresh the ones that were less recently updated.
RefreshedFiles.Sort([](const TPair<FString, TSharedPtr<FSourceControlFileStatus>>& Lhs, const TPair<FString, TSharedPtr<FSourceControlFileStatus>>& Rhs)
{
// Sort ascending as we are going to use Last()/Pop() later, so the oldest must be at the end.
return Lhs.Value->LastStatusCheckTimestampSecs > Rhs.Value->LastStatusCheckTimestampSecs;
});
}
RequestedStatusFiles.Reserve(MaxFileNumPerRequestPolicy);
while (RequestedStatusFiles.Num() < MaxFileNumPerRequestPolicy)
{
if (NewFiles.Num())
{
RequestedStatusFiles.Emplace(NewFiles.Last()->Key);
NewFiles.Pop(/*bAllowShrinking*/false);
NewAddedFileCount--;
}
else if (RefreshedFiles.Num())
{
RequestedStatusFiles.Emplace(RefreshedFiles.Last()->Key);
RefreshedFiles.Pop(/*bAllowShrinking*/false);
}
else
{
break; // All files to query/refresh were added.
}
}
if (RequestedStatusFiles.Num())
{
LastSourceControlCheckSecs = NowSecs;
TSharedRef<FUpdateStatus> UpdateStatusRequest = ISourceControlOperation::Create<FUpdateStatus>();
UpdateStatusRequest->SetForceUpdate(true);
ISourceControlModule::Get().GetProvider().Execute(UpdateStatusRequest, RequestedStatusFiles, EConcurrency::Asynchronous,
FSourceControlOperationComplete::CreateSP(this, &FSourceControlFileStatusMonitor::OnSourceControlStatusUpdate));
}
return true;
}
void FSourceControlFileStatusMonitor::OnSourceControlStatusUpdate(const TSharedRef<ISourceControlOperation>& InOperation, ECommandResult::Type InResult)
{
check(IsInGameThread()); // Concurrency issues if the callback in invoked from a background thread.
double NowSecs = FPlatformTime::Seconds();
ON_SCOPE_EXIT
{
RequestedStatusFiles.Reset();
OldestFileStatusTimeSecs = NowSecs;
for (const TPair<FString, TSharedPtr<FSourceControlFileStatus>>& Pair : MonitoredFiles)
{
OldestFileStatusTimeSecs = FMath::Min(OldestFileStatusTimeSecs, Pair.Value->LastStatusCheckTimestampSecs);
}
};
if (InResult != ECommandResult::Succeeded)
{
return;
}
for (const FString& Pathname : RequestedStatusFiles)
{
// NOTE: The file and its status can be removed while exeucting OnFileStatusUpdateDelegate. Keep the shared pointer to avoid early destruction.
if (TSharedPtr<FSourceControlFileStatus> FileStatus = FindFileStatus(Pathname))
{
if (TSharedPtr<ISourceControlState> FileState = ISourceControlModule::Get().GetProvider().GetState(Pathname, EStateCacheUsage::Use))
{
FileStatus->FileState = MoveTemp(FileState);
FileStatus->LastStatusCheckTimestampSecs = NowSecs;
for (TPair<uintptr_t, FOnSourceControlFileStatus>& OwnerDelegatePair : FileStatus->OwnerDelegateMap)
{
OwnerDelegatePair.Value.ExecuteIfBound(Pathname, FileStatus->FileState.Get());
}
}
}
}
}