// Copyright Epic Games, Inc. All Rights Reserved. #include "LevelFolders.h" #include "Misc/Crc.h" #include "Misc/Paths.h" #include "HAL/FileManager.h" #include "Dom/JsonObject.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" #include "ScopedTransaction.h" #include "Engine/Level.h" #include "Editor.h" #include "IWorldTreeItem.h" #define LOCTEXT_NAMESPACE "LevelFolders" /** Utility function to get a hashed filename for a level model */ FString GetLevelModelFilename(TSharedPtr LevelModel) { const FString LevelPackage = LevelModel->GetLongPackageName().ToString(); const uint32 PackageNameCrc = FCrc::MemCrc32(*LevelPackage, sizeof(TCHAR)*LevelPackage.Len()); return FPaths::Combine(*FPaths::ProjectSavedDir(), TEXT("Config"), TEXT("LevelState"), *FString::Printf(TEXT("%u.json"), PackageNameCrc)); } static FName OldPathToNewPath(const FString& InOldBranch, const FString& InNewBranch, const FString& PathToMove) { return FName(*(InNewBranch + PathToMove.RightChop(InOldBranch.Len()))); } FORCEINLINE FLevelModelKey GetLevelModelKey(TSharedRef LevelModel) { ULevel* LevelObject = LevelModel->GetLevelObject(); return LevelObject != nullptr ? *LevelObject->GetPathName() : FName(); } void UEditorLevelFolders::Serialize(FArchive& Ar) { Ar << Folders; } FOnLevelFolderCreate FLevelFolders::OnFolderCreate; FOnLevelFolderDelete FLevelFolders::OnFolderDelete; FOnLevelFolderMove FLevelFolders::OnFolderMove; FLevelFolders* FLevelFolders::Singleton = nullptr; FLevelFolders::FLevelFolders() { } FLevelFolders::~FLevelFolders() { } void FLevelFolders::AddReferencedObjects(FReferenceCollector& Collector) { Collector.AddReferencedObjects(TemporaryLevelFolders); } FLevelFolders& FLevelFolders::Get() { check(Singleton != nullptr); return *Singleton; } void FLevelFolders::Init() { Singleton = new FLevelFolders(); } void FLevelFolders::Cleanup() { delete Singleton; Singleton = nullptr; } void FLevelFolders::SaveLevel(TSharedRef LevelModel) { Housekeeping(); // Attempt to save the folder state for the levels const auto* Folders = TemporaryLevelFolders.Find(GetLevelModelKey(LevelModel)); if (Folders) { const FString Filename = GetLevelModelFilename(LevelModel); TUniquePtr Ar(IFileManager::Get().CreateFileWriter(*Filename)); if (Ar) { TSharedRef RootObject = MakeShareable(new FJsonObject); TSharedRef JsonFolders = MakeShareable(new FJsonObject); for (const auto& Pair : (*Folders)->Folders) { TSharedRef JsonFolder = MakeShareable(new FJsonObject); JsonFolder->SetBoolField(TEXT("bExpanded"), Pair.Value.bExpanded); JsonFolders->SetObjectField(Pair.Key.ToString(), JsonFolder); } RootObject->SetObjectField(TEXT("Folders"), JsonFolders); { auto Writer = TJsonWriterFactory::Create(Ar.Get()); FJsonSerializer::Serialize(RootObject, Writer); Ar->Close(); } } } } void FLevelFolders::Housekeeping() { for (auto It = TemporaryModelObjects.CreateIterator(); It; ++It) { // If the level model or its associated level object are invalid, remove them from temporary folders if (!It.Value().IsValid()) { TemporaryLevelFolders.Remove(It.Key()); It.RemoveCurrent(); } } } TMap& FLevelFolders::GetFolderProperties(TSharedRef LevelModel) { return GetOrCreateFoldersForLevel(LevelModel).Folders; } FLevelFolderProps* FLevelFolders::GetFolderProperties(TSharedRef LevelModel, FName InPath) { return GetFolderProperties(LevelModel).Find(InPath); } FName FLevelFolders::GetDefaultFolderName(TSharedRef LevelModel, FName ParentPath) { // Gets the folder properties for the world to ensure a unique name const TMap& Folders = GetFolderProperties(LevelModel); // Create a valid base name for the folder uint32 NewFolderSuffix = 1; FText NewFolderFormat = LOCTEXT("DefaultFolderNamePattern", "NewFolder{0}"); FString ParentFolderPath; if (!ParentPath.IsNone()) { ParentFolderPath = ParentPath.ToString() + TEXT('/'); } FName FolderName; do { FText LeafName = FText::Format(NewFolderFormat, FText::AsNumber(NewFolderSuffix++)); FolderName = FName(*(ParentFolderPath + LeafName.ToString())); if (NewFolderSuffix == 0) { // If this happens, something's massively broken. check(false); } } while (Folders.Contains(FolderName)); return FolderName; } UEditorLevelFolders& FLevelFolders::GetOrCreateFoldersForLevel(TSharedRef LevelModel) { if (auto* Folders = TemporaryLevelFolders.Find(GetLevelModelKey(LevelModel))) { return **Folders; } return Initialize(LevelModel); } void FLevelFolders::CreateFolder(TSharedRef LevelModel, FName InPath) { FScopedTransaction Transaction(LOCTEXT("UndoAction_CreateFolder", "Create Folder")); if (AddFolder(LevelModel, InPath)) { OnFolderCreate.Broadcast(LevelModel, InPath); } else { Transaction.Cancel(); } } void FLevelFolders::RebuildFolderList(TSharedRef LevelModel) { if (TemporaryLevelFolders.Contains(GetLevelModelKey(LevelModel))) { // Keep empty folders if we rebuild, since they have not been explicitly deleted for (TSharedPtr Child : LevelModel->GetChildren()) { AddFolder(LevelModel, Child->GetFolderPath()); } } else { Initialize(LevelModel); } } void FLevelFolders::CreateFolderContainingSelectedLevels(TSharedRef WorldModel, TSharedRef LevelModel, FName InPath) { const FScopedTransaction Transaction(LOCTEXT("UndoAction_CreateFolder", "Create Folder")); CreateFolder(LevelModel, InPath); SetSelectedLevelFolderPath(WorldModel, LevelModel, InPath); } void FLevelFolders::SetSelectedLevelFolderPath(TSharedRef WorldModel, TSharedRef LevelModel, FName InPath) const { FLevelModelList SelectedLevels = GetSelectedLevels(WorldModel, LevelModel); for (TSharedPtr Level : SelectedLevels) { Level->SetFolderPath(InPath); } } UEditorLevelFolders& FLevelFolders::Initialize(TSharedRef LevelModel) { // Clean up stale levels Housekeeping(); // Don't pass RF_Transactional to ConstructObject so that the creation of the object is not recorded by the undo buffer UEditorLevelFolders* Folders = NewObject(GetTransientPackage(), NAME_None, RF_NoFlags); Folders->SetFlags(RF_Transactional); // We still want to record changes made to the object, though FLevelModelKey LevelModelKey = GetLevelModelKey(LevelModel); // Only add the level model if it has a valid key if (!LevelModelKey.IsNone()) { TemporaryLevelFolders.Add(LevelModelKey, Folders); TemporaryModelObjects.Add(LevelModelKey, LevelModel); // Ensure the list is up to date for (TSharedPtr ChildLevel : LevelModel->GetChildren()) { AddFolder(LevelModel, ChildLevel->GetFolderPath()); } // Attempt to load the folder properties from config const FString Filename = GetLevelModelFilename(LevelModel); TUniquePtr Ar(IFileManager::Get().CreateFileReader(*Filename)); if (Ar) { TSharedPtr RootObject = MakeShareable(new FJsonObject); auto Reader = TJsonReaderFactory::Create(Ar.Get()); if (FJsonSerializer::Deserialize(Reader, RootObject)) { const TSharedPtr& JsonFolders = RootObject->GetObjectField(TEXT("Folders")); for (const auto& Pair : JsonFolders->Values) { // Only load properties for folders that still exist in the world if (FLevelFolderProps* FolderInWorld = Folders->Folders.Find(FName(*Pair.Key))) { const TSharedPtr& FolderProps = Pair.Value->AsObject(); FolderInWorld->bExpanded = FolderProps->GetBoolField(TEXT("bExpanded")); } } } } } return *Folders; } bool FLevelFolders::AddFolder(TSharedRef LevelModel, FName InPath) { bool bFolderAdded = false; if (!InPath.IsNone()) { UEditorLevelFolders& LevelFolders = GetOrCreateFoldersForLevel(LevelModel); if (!LevelFolders.Folders.Contains(InPath)) { // Also add the parent path AddFolder(LevelModel, WorldHierarchy::GetParentPath(InPath)); LevelFolders.Modify(); LevelFolders.Folders.Add(InPath); bFolderAdded = true; } } return bFolderAdded; } bool FLevelFolders::RenameFolder(TSharedRef LevelModel, FName OldPath, FName NewPath) { const FString OldPathString = OldPath.ToString(); const FString NewPathString = NewPath.ToString(); if (OldPath.IsNone() || NewPath.IsNone() || OldPath == NewPath || PathIsChildOf(NewPathString, OldPathString)) { return false; } const FScopedTransaction Transaction(LOCTEXT("UndoAction_RenameFolder", "Rename Folder")); TSet RenamedFolders; // Move any folders we currently hold UEditorLevelFolders& Folders = GetOrCreateFoldersForLevel(LevelModel); Folders.Modify(); auto FoldersCopy = Folders.Folders; for (const auto& Pair : FoldersCopy) { FName Path = Pair.Key; const FString FolderPath = Path.ToString(); if (OldPath == Path || PathIsChildOf(FolderPath, OldPathString)) { const FName NewFolder = OldPathToNewPath(OldPathString, NewPathString, FolderPath); if (!Folders.Folders.Contains(NewFolder)) { // Use the existing folder props if we have them if (FLevelFolderProps* Props = Folders.Folders.Find(Path)) { Folders.Folders.Add(NewFolder, *Props); } else { Folders.Folders.Add(NewFolder); } OnFolderMove.Broadcast(LevelModel, Path, NewFolder); OnFolderCreate.Broadcast(LevelModel, NewFolder); } RenamedFolders.Add(Path); } } // Delete old folders for (const FName& Path : RenamedFolders) { Folders.Folders.Remove(Path); OnFolderDelete.Broadcast(LevelModel, Path); } return true; } void FLevelFolders::DeleteFolder(TSharedRef LevelModel, FName FolderToDelete) { const FScopedTransaction Transaction(LOCTEXT("UndoAction_DeleteFolder", "Delete Folder")); UEditorLevelFolders& Folders = GetOrCreateFoldersForLevel(LevelModel); if (Folders.Folders.Contains(FolderToDelete)) { Folders.Modify(); Folders.Folders.Remove(FolderToDelete); OnFolderDelete.Broadcast(LevelModel, FolderToDelete); } } bool FLevelFolders::PathIsChildOf(const FString& InPotentialChild, const FString& InParent) { const int32 ParentLen = InParent.Len(); return InPotentialChild.Len() > ParentLen && (InPotentialChild[ParentLen] == '/' || InPotentialChild[ParentLen] == '\\') && InPotentialChild.Left(ParentLen) == InParent; } FLevelModelList FLevelFolders::GetSelectedLevels(TSharedRef WorldModel, TSharedRef LevelModel) const { FLevelModelList WorldModelSelectedLevels = WorldModel->GetSelectedLevels(); FLevelModelList SelectedLevels; for (TSharedPtr Model : WorldModelSelectedLevels) { if (LevelModel->HasDescendant(Model)) { SelectedLevels.Add(Model); } } return SelectedLevels; } #undef LOCTEXT_NAMESPACE