EM_Task/UnrealEd/Private/AutoReimport/AutoReimportManager.cpp
Boshuang Zhao 5144a49c9b add
2026-02-13 16:18:33 +08:00

1101 lines
40 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AutoReimport/AutoReimportManager.h"
#include "HAL/PlatformFilemanager.h"
#include "HAL/FileManager.h"
#include "Misc/Paths.h"
#include "Misc/WildcardString.h"
#include "Modules/ModuleManager.h"
#include "UObject/Class.h"
#include "UObject/UObjectHash.h"
#include "UObject/UObjectIterator.h"
#include "Misc/PackageName.h"
#include "UObject/GCObject.h"
#include "Styling/SlateTypes.h"
#include "EditorReimportHandler.h"
#include "Misc/Attribute.h"
#include "TickableEditorObject.h"
#include "Settings/EditorLoadingSavingSettings.h"
#include "Factories/Factory.h"
#include "EditorFramework/AssetImportData.h"
#include "AssetData.h"
#include "Editor.h"
#include "FileHelpers.h"
#include "AutoReimport/AutoReimportUtilities.h"
#include "Logging/MessageLog.h"
#include "AutoReimport/ContentDirectoryMonitor.h"
#include "PackageTools.h"
#include "ObjectTools.h"
#include "AssetRegistryModule.h"
#include "Framework/Notifications/NotificationManager.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "AutoReimport/ReimportFeedbackContext.h"
#include "MessageLogModule.h"
#include "AutoReimport/AssetSourceFilenameCache.h"
#define LOCTEXT_NAMESPACE "AutoReimportManager"
enum class EStateMachineNode
{
CallOnce,
CallMany
};
/** Enum and value to specify the current state of our processing */
enum class ECurrentState : uint8
{
Idle,
Paused,
Aborting,
PromptUser,
Initializing,
ProcessAdditions,
ProcessModifications,
ProcessDeletions,
SavePackages
};
/** A simple state machine class that calls functions mapped on enum values. If any function returns a new enum type, it moves onto that function */
struct FStateMachine
{
typedef TFunction<TOptional<ECurrentState>(const DirectoryWatcher::FTimeLimit&)> FunctionType;
/** Constructor that specifies the initial state of the machine */
FStateMachine(ECurrentState InitialState): CurrentState(InitialState) {}
/** Add an enum->function mapping for this state machine */
void Add(ECurrentState State, EStateMachineNode NodeType, FunctionType&& Function)
{
Nodes.Add(State, FStateMachineNode(NodeType, MoveTemp(Function)));
}
/** Set a new state for this machine */
void SetState(ECurrentState NewState)
{
CurrentState = NewState;
}
/** Tick this state machine with the given time limit. Will continuously enumerate the machine until TimeLimit is reached */
void Tick(const DirectoryWatcher::FTimeLimit& TimeLimit)
{
while (!TimeLimit.Exceeded())
{
const FStateMachineNode& State = Nodes[CurrentState];
TOptional<ECurrentState> NewState = State.Endpoint(TimeLimit);
if (NewState.IsSet())
{
CurrentState = NewState.GetValue();
}
else if (State.Type == EStateMachineNode::CallOnce)
{
break;
}
}
}
private:
/** The current state of this machine */
ECurrentState CurrentState;
private:
struct FStateMachineNode
{
FStateMachineNode(EStateMachineNode InType, FunctionType&& InEndpoint): Endpoint(MoveTemp(InEndpoint)), Type(InType) {}
/** The function endpoint for this node */
FunctionType Endpoint;
/** Whether this endpoint should be called multiple times in a frame, or just once */
EStateMachineNode Type;
};
/** A map of enum value -> callback information */
TMap<ECurrentState, FStateMachineNode> Nodes;
};
/* Deals with auto reimporting of objects when the objects file on disk is modified*/
class FAutoReimportManager: public FTickableEditorObject
, public FGCObject
, public TSharedFromThis<FAutoReimportManager>
{
public:
FAutoReimportManager();
~FAutoReimportManager();
/** Get a list of currently monitored directories */
TArray<FPathAndMountPoint> GetMonitoredDirectories() const;
/** Report an external change to the manager, such that a subsequent equal change reported by the os be ignored */
void IgnoreNewFile(const FString& Filename);
void IgnoreFileModification(const FString& Filename);
void IgnoreMovedFile(const FString& SrcFilename, const FString& DstFilename);
void IgnoreDeletedFile(const FString& Filename);
/** Destroy this manager */
void Destroy();
private:
/** FTickableEditorObject interface*/
virtual void Tick(float DeltaTime) override;
virtual ETickableTickType GetTickableTickType() const override { return ETickableTickType::Conditional; }
/** Automation testing will tick its own file cache which can result in race conditions with the reimport manager. */
virtual bool IsTickable() const override { return !GIsAutomationTesting; }
virtual TStatId GetStatId() const override;
/** FGCObject interface*/
virtual void AddReferencedObjects(FReferenceCollector& Collector) override;
private:
/** Called when a new asset path has been mounted or unmounted */
void OnContentPathChanged(const FString& InAssetPath, const FString& FileSystemPath);
/** Called when an asset has been renamed */
void OnAssetRenamed(const FAssetData& AssetData, const FString& OldPath);
private:
/** Callback for when an editor user setting has changed */
void HandleLoadingSavingSettingChanged(FName PropertyName);
/** Set up monitors to all the monitored content directories */
void SetUpDirectoryMonitors();
/** Retrieve a semi-colon separated string of file extensions supported by all available editor import factories */
static FString GetAllFactoryExtensions();
private:
/** Get the number of unprocessed changes that are not part of the current processing operation */
int32 GetNumUnprocessedChanges() const;
/** Populate the message log with a list of pending changes to files */
void PopupateMessageLogWithPendingChanges(FMessageLog& MessageLog) const;
private:
/** A state machine holding information about the current state of the manager */
FStateMachine StateMachine;
private:
/** Idle processing */
TOptional<ECurrentState> Idle();
/** Prompt the user whether they would like to import the changes */
TOptional<ECurrentState> PromptUser();
/** Set up the initial work for the import operation */
TOptional<ECurrentState> InitializeOperation();
/** Process any remaining pending additions we have */
TOptional<ECurrentState> ProcessAdditions(const DirectoryWatcher::FTimeLimit& TimeLimit);
/** Save any packages that were created inside ProcessAdditions */
TOptional<ECurrentState> SavePackages();
/** Process any remaining pending modifications we have */
TOptional<ECurrentState> ProcessModifications(const DirectoryWatcher::FTimeLimit& TimeLimit);
/** Process any remaining pending deletions we have */
TOptional<ECurrentState> ProcessDeletions();
/** Wait for a user's input. Just updates the progress text for now */
TOptional<ECurrentState> Paused();
/** Abort the process */
TOptional<ECurrentState> Abort();
/** Cleanup an operation that just processed some changes */
void Cleanup();
/** Check whether we should pause the operation or not */
TOptional<ECurrentState> HandlePauseAbort(ECurrentState InCurrentState);
/** Get the text to display on the confirmation notification */
FText GetConfirmNotificationText() const;
private:
/** Feedback context that can selectively override the global context to consume progress events for saving of assets */
TSharedPtr<FReimportFeedbackContext> FeedbackContextOverride;
/** Array of objects that detect changes to directories */
TArray<TUniquePtr<FContentDirectoryMonitor>> DirectoryMonitors;
/** A list of packages to save when we've added a bunch of assets */
TArray<UPackage*> PackagesToSave;
/** Reentracy guard for when we are making changes to assets */
bool bGuardAssetChanges;
/** A timeout used to refresh directory monitors when the user has made an interactive change to the settings */
DirectoryWatcher::FTimeLimit ResetMonitorsTimeout;
/** User confirmation popup */
TSharedPtr<SNotificationItem> ConfirmNotification;
/** The paused state of the state machine */
ECurrentState PausedState;
private:
void OnPauseClicked()
{
switch (State)
{
case EProcessState::Paused: State = EProcessState::Running; break;
case EProcessState::Running: State = EProcessState::Paused; break;
default: break;
}
}
void OnAbortClicked() { State = EProcessState::Aborted; }
/** Flags for paused/aborted */
enum class EProcessState
{
Running,
Paused,
Aborted
};
EProcessState State;
};
FAutoReimportManager::FAutoReimportManager()
: StateMachine(ECurrentState::Idle), bGuardAssetChanges(false), PausedState(ECurrentState::Idle)
{
UEditorLoadingSavingSettings* Settings = GetMutableDefault<UEditorLoadingSavingSettings>();
Settings->OnSettingChanged().AddRaw(this, &FAutoReimportManager::HandleLoadingSavingSettingChanged);
FPackageName::OnContentPathMounted().AddRaw(this, &FAutoReimportManager::OnContentPathChanged);
FPackageName::OnContentPathDismounted().AddRaw(this, &FAutoReimportManager::OnContentPathChanged);
FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
if (!MessageLogModule.IsRegisteredLogListing("AssetReimport"))
{
MessageLogModule.RegisterLogListing("AssetReimport", LOCTEXT("AssetReimportLabel", "Asset Reimport"));
}
FAssetSourceFilenameCache::Get().OnAssetRenamed().AddRaw(this, &FAutoReimportManager::OnAssetRenamed);
// Only set this up for content directories if the user has this enabled
if (Settings->bMonitorContentDirectories)
{
SetUpDirectoryMonitors();
}
State = EProcessState::Running;
StateMachine.Add(ECurrentState::Idle, EStateMachineNode::CallOnce, [this](const DirectoryWatcher::FTimeLimit& T)
{
return this->Idle();
});
StateMachine.Add(ECurrentState::Paused, EStateMachineNode::CallOnce, [this](const DirectoryWatcher::FTimeLimit& T)
{
return this->Paused();
});
StateMachine.Add(ECurrentState::Aborting, EStateMachineNode::CallOnce, [this](const DirectoryWatcher::FTimeLimit& T)
{
return this->Abort();
});
StateMachine.Add(ECurrentState::PromptUser, EStateMachineNode::CallOnce, [this](const DirectoryWatcher::FTimeLimit& T)
{
return this->PromptUser();
});
StateMachine.Add(ECurrentState::Initializing, EStateMachineNode::CallOnce, [this](const DirectoryWatcher::FTimeLimit& T)
{
return this->InitializeOperation();
});
StateMachine.Add(ECurrentState::ProcessAdditions, EStateMachineNode::CallMany, [this](const DirectoryWatcher::FTimeLimit& T)
{
return this->ProcessAdditions(T);
});
StateMachine.Add(ECurrentState::ProcessModifications, EStateMachineNode::CallMany, [this](const DirectoryWatcher::FTimeLimit& T)
{
return this->ProcessModifications(T);
});
StateMachine.Add(ECurrentState::ProcessDeletions, EStateMachineNode::CallMany, [this](const DirectoryWatcher::FTimeLimit& T)
{
return this->ProcessDeletions();
});
StateMachine.Add(ECurrentState::SavePackages, EStateMachineNode::CallOnce, [this](const DirectoryWatcher::FTimeLimit& T)
{
return this->SavePackages();
});
}
FAutoReimportManager::~FAutoReimportManager()
{
if (ConfirmNotification.IsValid())
{
ConfirmNotification->SetText(FText());
}
}
TArray<FPathAndMountPoint> FAutoReimportManager::GetMonitoredDirectories() const
{
TArray<FPathAndMountPoint> Dirs;
for (const TUniquePtr<FContentDirectoryMonitor>& Monitor: DirectoryMonitors)
{
Dirs.Emplace(Monitor->GetDirectory(), Monitor->GetMountPoint());
}
return Dirs;
}
void FAutoReimportManager::IgnoreNewFile(const FString& Filename)
{
for (const TUniquePtr<FContentDirectoryMonitor>& Monitor: DirectoryMonitors)
{
if (Filename.StartsWith(Monitor->GetDirectory()))
{
Monitor->IgnoreNewFile(Filename);
}
}
}
void FAutoReimportManager::IgnoreFileModification(const FString& Filename)
{
for (const TUniquePtr<FContentDirectoryMonitor>& Monitor: DirectoryMonitors)
{
if (Filename.StartsWith(Monitor->GetDirectory()))
{
Monitor->IgnoreFileModification(Filename);
}
}
}
void FAutoReimportManager::IgnoreMovedFile(const FString& SrcFilename, const FString& DstFilename)
{
for (const TUniquePtr<FContentDirectoryMonitor>& Monitor: DirectoryMonitors)
{
const bool bSrcInFolder = SrcFilename.StartsWith(Monitor->GetDirectory());
const bool bDstInFolder = DstFilename.StartsWith(Monitor->GetDirectory());
if (bSrcInFolder && bDstInFolder)
{
Monitor->IgnoreMovedFile(SrcFilename, DstFilename);
}
else if (bSrcInFolder)
{
Monitor->IgnoreDeletedFile(SrcFilename);
}
else if (bDstInFolder)
{
Monitor->IgnoreNewFile(DstFilename);
}
}
}
void FAutoReimportManager::IgnoreDeletedFile(const FString& Filename)
{
for (const TUniquePtr<FContentDirectoryMonitor>& Monitor: DirectoryMonitors)
{
if (Filename.StartsWith(Monitor->GetDirectory()))
{
Monitor->IgnoreDeletedFile(Filename);
}
}
}
void FAutoReimportManager::Destroy()
{
FAssetRegistryModule* AssetRegistryModule = FModuleManager::GetModulePtr<FAssetRegistryModule>("AssetRegistry");
if (AssetRegistryModule)
{
FAssetSourceFilenameCache::Get().OnAssetRenamed().RemoveAll(this);
AssetRegistryModule->Get().OnInMemoryAssetDeleted().RemoveAll(this);
}
if (UEditorLoadingSavingSettings* Settings = GetMutableDefault<UEditorLoadingSavingSettings>())
{
Settings->OnSettingChanged().RemoveAll(this);
}
FPackageName::OnContentPathMounted().RemoveAll(this);
FPackageName::OnContentPathDismounted().RemoveAll(this);
// Force a save of all the caches
DirectoryMonitors.Empty();
}
void FAutoReimportManager::OnAssetRenamed(const FAssetData& AssetData, const FString& OldPath)
{
if (bGuardAssetChanges)
{
return;
}
// This code moves a source content file that reside alongside assets when the assets are renamed. We do this under the following conditions:
// 1. The sourcefile is solely referenced from the the asset that has been moved
// 2. Said asset only references a single file
//
// Additionally, we rename the source file if it matched the name of the asset before the rename/move.
// - If we rename the source file, then we also update the reimport paths for the asset
TOptional<FAssetImportInfo> ImportInfo = FAssetSourceFilenameCache::ExtractAssetImportInfo(AssetData);
if (!ImportInfo.IsSet() || ImportInfo->SourceFiles.Num() != 1)
{
return;
}
const FString& RelativeFilename = ImportInfo->SourceFiles[0].RelativeFilename;
FString OldPackagePath = FPackageName::GetLongPackagePath(OldPath) / TEXT("");
FString NewReimportPath;
// We move the file with the asset provided it is the only file referenced, and sits right beside the uasset file
if (!RelativeFilename.GetCharArray().ContainsByPredicate([](const TCHAR Char)
{
return Char == '/' || Char == '\\';
}))
{
// File resides in the same folder as the asset, so we can potentially rename the source file too
const FString AbsoluteSrcPath = FPaths::ConvertRelativePathToFull(FPackageName::LongPackageNameToFilename(OldPackagePath));
const FString AbsoluteDstPath = FPaths::ConvertRelativePathToFull(FPackageName::LongPackageNameToFilename(AssetData.PackagePath.ToString() / TEXT("")));
const FString OldAssetName = FPackageName::GetLongPackageAssetName(FPackageName::ObjectPathToPackageName(OldPath));
FString NewFileName = FPaths::GetBaseFilename(RelativeFilename);
bool bRequireReimportPathUpdate = false;
if (UPackageTools::SanitizePackageName(NewFileName) == OldAssetName)
{
NewFileName = AssetData.AssetName.ToString();
bRequireReimportPathUpdate = true;
}
const FString SrcFile = AbsoluteSrcPath / RelativeFilename;
const FString DstFile = AbsoluteDstPath / NewFileName + TEXT(".") + FPaths::GetExtension(RelativeFilename);
// We can't do this if multiple assets reference the same file. We should be checking for > 1 referencing asset, but the asset registry
// filter lookup won't return the recently renamed package because it will be Empty by now, so we check for *anything* referencing the asset (assuming that we'll never find *this* asset).
const IAssetRegistry& Registry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry").Get();
if (Utils::FindAssetsPertainingToFile(Registry, SrcFile).Num() == 0)
{
if (!FPlatformFileManager::Get().GetPlatformFile().FileExists(*DstFile) &&
IFileManager::Get().Move(*DstFile, *SrcFile, false /*bReplace */, false, true /* attributes */, true /* don't retry */))
{
IgnoreMovedFile(SrcFile, DstFile);
if (bRequireReimportPathUpdate)
{
NewReimportPath = DstFile;
}
}
}
}
if (NewReimportPath.IsEmpty() && FPackageName::GetLongPackagePath(OldPath) != AssetData.PackagePath.ToString())
{
// The asset has been moved, try and update its referenced path
FString OldSourceFilePath = FPaths::ConvertRelativePathToFull(FPackageName::LongPackageNameToFilename(OldPackagePath), RelativeFilename);
if (FPaths::FileExists(OldSourceFilePath))
{
NewReimportPath = MoveTemp(OldSourceFilePath);
}
}
if (!NewReimportPath.IsEmpty())
{
TArray<FString> Paths;
Paths.Add(NewReimportPath);
// Update the reimport file names
FReimportManager::Instance()->UpdateReimportPaths(AssetData.GetAsset(), Paths);
}
}
int32 FAutoReimportManager::GetNumUnprocessedChanges() const
{
return Utils::Reduce(DirectoryMonitors, [](const TUniquePtr<FContentDirectoryMonitor>& Monitor, int32 Total)
{
return Total + Monitor->GetNumUnprocessedChanges();
},
0);
}
void FAutoReimportManager::PopupateMessageLogWithPendingChanges(FMessageLog& MessageLog) const
{
for (const TUniquePtr<FContentDirectoryMonitor>& Monitor: DirectoryMonitors)
{
const FString& BasePath = Monitor->GetDirectory();
Monitor->IterateUnprocessedChanges([&MessageLog, &BasePath](const DirectoryWatcher::FUpdateCacheTransaction& Transaction, const FDateTime& TimeOfChange)
{
FText ThisMessage;
FString FullFilename = BasePath / Transaction.Filename.Get();
switch (Transaction.Action)
{
case DirectoryWatcher::EFileAction::Added:
ThisMessage = FText::Format(LOCTEXT("PendingChange_Add", "'{0}' has been created."), FText::FromString(FullFilename));
break;
case DirectoryWatcher::EFileAction::Removed:
ThisMessage = FText::Format(LOCTEXT("PendingChange_Delete", "'{0}' has been deleted."), FText::FromString(FullFilename));
break;
case DirectoryWatcher::EFileAction::Modified:
ThisMessage = FText::Format(LOCTEXT("PendingChange_Modified", "'{0}' has been modified."), FText::FromString(FullFilename));
break;
case DirectoryWatcher::EFileAction::Moved:
ThisMessage = FText::Format(LOCTEXT("PendingChange_Moved", "'{0}' has been moved/renamed to {1}."), FText::FromString(BasePath / Transaction.MovedFromFilename.Get()), FText::FromString(FullFilename));
break;
}
MessageLog.Message(EMessageSeverity::Info, ThisMessage);
return true;
});
}
}
FText FAutoReimportManager::GetConfirmNotificationText() const
{
const int32 TotalWork = GetNumUnprocessedChanges();
if (TotalWork == 1)
{
return LOCTEXT("UserConfirmationTextSingle", "A change to a source content file has been detected.\nWould you like to import it?");
}
else if (TotalWork > 1)
{
return FText::Format(LOCTEXT("UserConfirmationTextMultiple", "{0} changes to source content files have been detected.\nWould you like to import them?"), FText::AsNumber(TotalWork));
}
return FText();
}
TOptional<ECurrentState> FAutoReimportManager::PromptUser()
{
UEditorLoadingSavingSettings* Settings = GetMutableDefault<UEditorLoadingSavingSettings>();
// Send out a notification asking for confirmation
if (Settings->bPromptBeforeAutoImporting && !ConfirmNotification.IsValid())
{
FNotificationInfo Info(FText::GetEmpty());
Info.bFireAndForget = false;
Info.bUseLargeFont = false;
Info.ButtonDetails.Add(FNotificationButtonInfo(
LOCTEXT("ImportButtonText", "Import"),
FText(),
FSimpleDelegate::CreateLambda([this, Settings]
{
Settings->PostEditChange();
State = EProcessState::Running;
}),
SNotificationItem::CS_None));
Info.ButtonDetails.Add(FNotificationButtonInfo(
LOCTEXT("DontImportButtonText", "Don't Import"),
FText(),
FSimpleDelegate::CreateLambda([this, Settings]
{
// going back into idle
PausedState = ECurrentState::Aborting;
State = EProcessState::Running;
if (!Settings->bPromptBeforeAutoImporting)
{
// User clicked Don't import, with a don't show again. Disable auto reimport
Settings->bMonitorContentDirectories = false;
Settings->PostEditChange();
}
}),
SNotificationItem::CS_None));
Info.CheckBoxText = LOCTEXT("DontAskAgain", "Don't ask again");
Info.CheckBoxState = TAttribute<ECheckBoxState>::Create(TAttribute<ECheckBoxState>::FGetter::CreateStatic([]
{
return GetDefault<UEditorLoadingSavingSettings>()->bPromptBeforeAutoImporting ? ECheckBoxState::Unchecked : ECheckBoxState::Checked;
}));
Info.CheckBoxStateChanged = FOnCheckStateChanged::CreateStatic([](ECheckBoxState NewState)
{
GetMutableDefault<UEditorLoadingSavingSettings>()->bPromptBeforeAutoImporting = NewState == ECheckBoxState::Unchecked;
});
Info.Hyperlink = FSimpleDelegate::CreateLambda([this]
{
FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
FMessageLog MessageLog("AssetReimport");
MessageLog.NewPage(FText::Format(LOCTEXT("WhatChangedMessageLogPageLabel", "Detailed File System Changes from {0}"), FText::AsTime(FDateTime::UtcNow())));
PopupateMessageLogWithPendingChanges(MessageLog);
MessageLogModule.OpenMessageLog("AssetReimport");
});
Info.HyperlinkText = LOCTEXT("UserConfirmationHyperlink", "What Changed?");
ConfirmNotification = FSlateNotificationManager::Get().AddNotification(Info);
State = EProcessState::Paused;
}
// Keep ticking the monitors
for (const TUniquePtr<FContentDirectoryMonitor>& Monitor: DirectoryMonitors)
{
Monitor->Tick();
}
if (ConfirmNotification.IsValid())
{
ConfirmNotification->SetText(GetConfirmNotificationText());
}
// Keep ticking this if we're paused
if (State == EProcessState::Paused)
{
return TOptional<ECurrentState>();
}
if (ConfirmNotification.IsValid())
{
ConfirmNotification->SetEnabled(false);
ConfirmNotification->SetCompletionState(SNotificationItem::CS_Success);
ConfirmNotification->ExpireAndFadeout();
ConfirmNotification = nullptr;
}
if (PausedState != ECurrentState::PromptUser)
{
return PausedState;
}
PausedState = ECurrentState::Idle;
// This can get set by the user while we're prompting for action
if (!Settings->bMonitorContentDirectories)
{
return ECurrentState::Aborting;
}
else
{
return ECurrentState::Initializing;
}
}
TOptional<ECurrentState> FAutoReimportManager::InitializeOperation()
{
int32 TotalWork = 0;
for (const TUniquePtr<FContentDirectoryMonitor>& Monitor: DirectoryMonitors)
{
TotalWork += Monitor->StartProcessing();
}
if (TotalWork > 0)
{
if (!FeedbackContextOverride.IsValid())
{
// Create a new feedback context override
FeedbackContextOverride = MakeShareable(new FReimportFeedbackContext(
FSimpleDelegate::CreateSP(this, &FAutoReimportManager::OnPauseClicked),
FSimpleDelegate::CreateSP(this, &FAutoReimportManager::OnAbortClicked)));
}
FeedbackContextOverride->Show(TotalWork);
return ECurrentState::ProcessAdditions;
}
return ECurrentState::Idle;
}
TOptional<ECurrentState> FAutoReimportManager::ProcessAdditions(const DirectoryWatcher::FTimeLimit& TimeLimit)
{
TOptional<ECurrentState> NewState = HandlePauseAbort(ECurrentState::ProcessAdditions);
if (NewState.IsSet())
{
return NewState;
}
// Override the global feedback context while we do this to avoid popping up dialogs
TGuardValue<FFeedbackContext*> ScopedContextOverride(GWarn, FeedbackContextOverride.Get());
TGuardValue<bool> ScopedAssetChangesGuard(bGuardAssetChanges, true);
TMap<FString, TArray<UFactory*>> Factories;
TArray<FString> FactoryExtensions;
FactoryExtensions.Reserve(16);
// Get the list of valid factories
for (TObjectIterator<UClass> It; It; ++It)
{
UClass* CurrentClass = (*It);
if (CurrentClass->IsChildOf(UFactory::StaticClass()) && !(CurrentClass->HasAnyClassFlags(CLASS_Abstract)))
{
UFactory* Factory = Cast<UFactory>(CurrentClass->GetDefaultObject());
if (Factory->bEditorImport && Factory->ImportPriority >= 0)
{
FactoryExtensions.Reset();
Factory->GetSupportedFileExtensions(FactoryExtensions);
for (const FString& Ext: FactoryExtensions)
{
Factories.FindOrAdd(Ext).Add(Factory);
}
}
}
}
for (TTuple<FString, TArray<UFactory*>>& Pair: Factories)
{
Pair.Value.Sort([](const UFactory& A, const UFactory& B)
{
return A.ImportPriority > B.ImportPriority;
});
}
for (const TUniquePtr<FContentDirectoryMonitor>& Monitor: DirectoryMonitors)
{
Monitor->ProcessAdditions(TimeLimit, PackagesToSave, Factories, *FeedbackContextOverride);
if (TimeLimit.Exceeded())
{
return TOptional<ECurrentState>();
}
}
return ECurrentState::ProcessModifications;
}
TOptional<ECurrentState> FAutoReimportManager::ProcessModifications(const DirectoryWatcher::FTimeLimit& TimeLimit)
{
TOptional<ECurrentState> NewState = HandlePauseAbort(ECurrentState::ProcessModifications);
if (NewState.IsSet())
{
return NewState;
}
// Override the global feedback context while we do this to avoid popping up dialogs
TGuardValue<FFeedbackContext*> ScopedContextOverride(GWarn, FeedbackContextOverride.Get());
TGuardValue<bool> ScopedAssetChangesGuard(bGuardAssetChanges, true);
for (const TUniquePtr<FContentDirectoryMonitor>& Monitor: DirectoryMonitors)
{
Monitor->ProcessModifications(TimeLimit, PackagesToSave, *FeedbackContextOverride);
if (TimeLimit.Exceeded())
{
return TOptional<ECurrentState>();
}
}
return ECurrentState::ProcessDeletions;
}
TOptional<ECurrentState> FAutoReimportManager::ProcessDeletions()
{
TOptional<ECurrentState> NewState = HandlePauseAbort(ECurrentState::ProcessDeletions);
if (NewState.IsSet())
{
return NewState;
}
TGuardValue<bool> ScopedAssetChangesGuard(bGuardAssetChanges, true);
TArray<FAssetData> AssetsToDelete;
int32 TotalWork = 0;
for (const TUniquePtr<FContentDirectoryMonitor>& Monitor: DirectoryMonitors)
{
TotalWork += Monitor->GetDeletedFilesNum();
Monitor->ExtractAssetsToDelete(AssetsToDelete);
}
FeedbackContextOverride->MainTask->EnterProgressFrame(TotalWork);
if (AssetsToDelete.Num() > 0)
{
for (const FAssetData& AssetData: AssetsToDelete)
{
FeedbackContextOverride->AddMessage(EMessageSeverity::Info, FText::Format(LOCTEXT("Success_DeletedAsset", "Attempting to delete {0} (its source file has been removed)."), FText::FromName(AssetData.AssetName)));
}
ObjectTools::DeleteAssets(AssetsToDelete);
}
return ECurrentState::SavePackages;
}
TOptional<ECurrentState> FAutoReimportManager::SavePackages()
{
// We don't override the context specifically when saving packages so the user gets proper feedback
// TGuardValue<FFeedbackContext*> ScopedContextOverride(GWarn, FeedbackContextOverride.Get());
TGuardValue<bool> ScopedAssetChangesGuard(bGuardAssetChanges, true);
if (PackagesToSave.Num() > 0)
{
const bool bAlreadyCheckedOut = false;
const bool bCheckDirty = false;
const bool bPromptToSave = false;
FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave, bCheckDirty, bPromptToSave, nullptr, bAlreadyCheckedOut);
PackagesToSave.Empty();
}
Cleanup();
return ECurrentState::Idle;
}
TOptional<ECurrentState> FAutoReimportManager::HandlePauseAbort(ECurrentState InCurrentState)
{
if (State == EProcessState::Aborted)
{
return ECurrentState::Aborting;
}
else if (State == EProcessState::Paused)
{
PausedState = InCurrentState;
return ECurrentState::Paused;
}
return TOptional<ECurrentState>();
}
TOptional<ECurrentState> FAutoReimportManager::Paused()
{
TOptional<ECurrentState> NewState = HandlePauseAbort(PausedState);
if (NewState.IsSet())
{
return NewState.GetValue();
}
// No longer paused
return PausedState;
}
TOptional<ECurrentState> FAutoReimportManager::Abort()
{
for (const TUniquePtr<FContentDirectoryMonitor>& Monitor: DirectoryMonitors)
{
Monitor->Abort();
}
PackagesToSave.Empty();
Cleanup();
return ECurrentState::Idle;
}
TOptional<ECurrentState> FAutoReimportManager::Idle()
{
// Check whether we need to reset the monitors or not
if (ResetMonitorsTimeout.Exceeded())
{
const UEditorLoadingSavingSettings* Settings = GetDefault<UEditorLoadingSavingSettings>();
if (Settings->bMonitorContentDirectories)
{
DirectoryMonitors.Empty();
SetUpDirectoryMonitors();
}
else
{
// Destroy all the existing monitors, including their file caches
for (const TUniquePtr<FContentDirectoryMonitor>& Monitor: DirectoryMonitors)
{
Monitor->Destroy();
}
DirectoryMonitors.Empty();
}
ResetMonitorsTimeout = DirectoryWatcher::FTimeLimit();
return TOptional<ECurrentState>();
}
for (const TUniquePtr<FContentDirectoryMonitor>& Monitor: DirectoryMonitors)
{
Monitor->Tick();
}
const IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry").Get();
if (AssetRegistry.IsLoadingAssets())
{
return TOptional<ECurrentState>();
}
if (GetNumUnprocessedChanges() > 0)
{
PausedState = ECurrentState::PromptUser;
return ECurrentState::PromptUser;
}
return TOptional<ECurrentState>();
}
void FAutoReimportManager::Cleanup()
{
if (FeedbackContextOverride.IsValid())
{
FeedbackContextOverride->Hide();
}
}
void FAutoReimportManager::Tick(float DeltaTime)
{
// Never spend more than a 60fps frame doing this work (meaning we shouldn't drop below 30fps), we can do more if we're throttling CPU usage (ie editor running in background)
const DirectoryWatcher::FTimeLimit TimeLimit(GEditor->ShouldThrottleCPUUsage() ? 1 / 6.f : 1.f / 60.f);
StateMachine.Tick(TimeLimit);
}
TStatId FAutoReimportManager::GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FAutoReimportManager, STATGROUP_Tickables);
}
void FAutoReimportManager::AddReferencedObjects(FReferenceCollector& Collector)
{
Collector.AddReferencedObjects(PackagesToSave);
}
void FAutoReimportManager::HandleLoadingSavingSettingChanged(FName PropertyName)
{
if (PropertyName == GET_MEMBER_NAME_CHECKED(UEditorLoadingSavingSettings, bMonitorContentDirectories) ||
PropertyName == GET_MEMBER_NAME_CHECKED(UEditorLoadingSavingSettings, AutoReimportDirectorySettings))
{
ResetMonitorsTimeout = DirectoryWatcher::FTimeLimit(5.f);
}
}
void FAutoReimportManager::OnContentPathChanged(const FString& InAssetPath, const FString& FileSystemPath)
{
const UEditorLoadingSavingSettings* Settings = GetDefault<UEditorLoadingSavingSettings>();
if (Settings->bMonitorContentDirectories)
{
DirectoryMonitors.Empty();
SetUpDirectoryMonitors();
}
}
void FAutoReimportManager::SetUpDirectoryMonitors()
{
struct FParsedSettings
{
FString SourceDirectory;
FString MountPoint;
DirectoryWatcher::FMatchRules Rules;
};
TArray<FParsedSettings> FinalArray;
FString SupportedExtensions = GetAllFactoryExtensions();
for (const FAutoReimportDirectoryConfig& Setting: GetDefault<UEditorLoadingSavingSettings>()->AutoReimportDirectorySettings)
{
FParsedSettings NewMapping;
NewMapping.SourceDirectory = Setting.SourceDirectory;
NewMapping.MountPoint = Setting.MountPoint;
if (!FAutoReimportDirectoryConfig::ParseSourceDirectoryAndMountPoint(NewMapping.SourceDirectory, NewMapping.MountPoint))
{
continue;
}
// Only include extensions that match a factory
NewMapping.Rules.SetApplicableExtensions(SupportedExtensions);
for (const FAutoReimportWildcard& WildcardConfig: Setting.Wildcards)
{
NewMapping.Rules.AddWildcardRule(WildcardConfig.Wildcard, WildcardConfig.bInclude);
}
FinalArray.Add(NewMapping);
}
for (int32 Index = 0; Index < FinalArray.Num(); ++Index)
{
const FParsedSettings& Mapping = FinalArray[Index];
// We only create a directory monitor if there are no other's watching parent directories of this one
for (int32 OtherIndex = Index + 1; OtherIndex < FinalArray.Num(); ++OtherIndex)
{
if (FinalArray[Index].SourceDirectory.StartsWith(FinalArray[OtherIndex].SourceDirectory))
{
UE_LOG(LogAutoReimportManager, Warning, TEXT("Unable to watch directory %s as it will conflict with another watching %s."), *FinalArray[Index].SourceDirectory, *FinalArray[OtherIndex].SourceDirectory);
goto next;
}
}
DirectoryMonitors.Emplace(MakeUnique<FContentDirectoryMonitor>(Mapping.SourceDirectory, Mapping.Rules, Mapping.MountPoint));
next:
continue;
}
}
FString FAutoReimportManager::GetAllFactoryExtensions()
{
FString AllExtensions;
// Use a scratch buffer to avoid unnecessary re-allocation
FString Scratch;
Scratch.Reserve(32);
for (TObjectIterator<UClass> ClassIt; ClassIt; ++ClassIt)
{
UClass* Class = *ClassIt;
if (Class->IsChildOf(UFactory::StaticClass()) && !Class->HasAnyClassFlags(CLASS_Abstract))
{
UFactory* Factory = Cast<UFactory>(Class->GetDefaultObject());
if (Factory->bEditorImport)
{
for (const FString& Format: Factory->Formats)
{
int32 Index = INDEX_NONE;
if (Format.FindChar(';', Index) && Index > 0)
{
Scratch.GetCharArray().Reset();
// Include the ;
Scratch.AppendChars(*Format, Index + 1);
if (AllExtensions.Find(Scratch) == INDEX_NONE)
{
AllExtensions += Scratch;
}
}
}
}
}
}
return AllExtensions;
}
UAutoReimportManager::UAutoReimportManager(const FObjectInitializer& Init)
: Super(Init)
{
}
UAutoReimportManager::~UAutoReimportManager()
{
}
void UAutoReimportManager::Initialize()
{
Implementation = MakeShareable(new FAutoReimportManager);
}
void UAutoReimportManager::IgnoreNewFile(const FString& Filename)
{
Implementation->IgnoreNewFile(Filename);
}
void UAutoReimportManager::IgnoreFileModification(const FString& Filename)
{
Implementation->IgnoreFileModification(Filename);
}
void UAutoReimportManager::IgnoreMovedFile(const FString& SrcFilename, const FString& DstFilename)
{
Implementation->IgnoreMovedFile(SrcFilename, DstFilename);
}
void UAutoReimportManager::IgnoreDeletedFile(const FString& Filename)
{
Implementation->IgnoreDeletedFile(Filename);
}
TArray<FPathAndMountPoint> UAutoReimportManager::GetMonitoredDirectories() const
{
return Implementation->GetMonitoredDirectories();
}
void UAutoReimportManager::BeginDestroy()
{
Super::BeginDestroy();
if (Implementation.IsValid())
{
Implementation->Destroy();
Implementation = nullptr;
}
}
#undef LOCTEXT_NAMESPACE