// Copyright Epic Games, Inc. All Rights Reserved. #include "Subsystems/AssetEditorSubsystem.h" #include "AssetEditorMessages.h" #include "Containers/VersePath.h" #include "MessageEndpoint.h" #include "EngineAnalytics.h" #include "AnalyticsEventAttribute.h" #include "UObject/Package.h" #include "CoreGlobals.h" #include "AssetToolsModule.h" #include "IAssetTools.h" #include "IMessageContext.h" #include "LevelEditor.h" #include "Toolkits/AssetEditorToolkit.h" #include "Toolkits/SimpleAssetEditor.h" #include "Engine/MapBuildDataRegistry.h" #include "MRUFavoritesList.h" #include "Settings/EditorLoadingSavingSettings.h" #include "UObject/UObjectIterator.h" #include "Widgets/Notifications/SNotificationList.h" #include "Framework/Notifications/NotificationManager.h" #include "PackageTools.h" #include "UObject/PackageReload.h" #include "Interfaces/IAnalyticsProvider.h" #include "Misc/FeedbackContext.h" #include "Misc/ConfigCacheIni.h" #include "Misc/NamePermissionList.h" #include "EditorModeRegistry.h" #include "Tools/UEdMode.h" #include "AssetEditorMessages.h" #include "EditorModeManager.h" #include "Tools/LegacyEdMode.h" #include "ProfilingDebugging/StallDetector.h" #include "ToolMenus.h" #include "AssetRegistry/IAssetRegistry.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Elements/SMInstance/SMInstanceElementData.h" // For SMInstanceElementDataUtil::SMInstanceElementsEnabled #define LOCTEXT_NAMESPACE "AssetEditorSubsystem" DEFINE_LOG_CATEGORY_STATIC(LogAssetEditorSubsystem, Log, All); UAssetEditorSubsystem::UAssetEditorSubsystem() : Super() , bSavingOnShutdown(false) , bRequestRestorePreviouslyOpenAssets(false) { } void UAssetEditorSubsystem::Initialize(FSubsystemCollectionBase& Collection) { TickDelegate = FTickerDelegate::CreateUObject(this, &UAssetEditorSubsystem::HandleTicker); FTSTicker::GetCoreTicker().AddTicker(TickDelegate, 1.f); FCoreUObjectDelegates::OnPackageReloaded.AddUObject(this, &UAssetEditorSubsystem::HandlePackageReloaded); GEditor->OnEditorClose().AddUObject(this, &UAssetEditorSubsystem::OnEditorClose); FCoreDelegates::OnEnginePreExit.AddUObject(this, &UAssetEditorSubsystem::UnregisterEditorModes); FCoreDelegates::OnAllModuleLoadingPhasesComplete.AddUObject(this, &UAssetEditorSubsystem::RegisterEditorModes); if (FAssetRegistryModule* AssetRegistryModule = FModuleManager::GetModulePtr(TEXT("AssetRegistry"))) { AssetRegistryModule->Get().OnAssetRemoved().AddUObject(this, &UAssetEditorSubsystem::OnAssetRemoved); AssetRegistryModule->Get().OnAssetRenamed().AddUObject(this, &UAssetEditorSubsystem::OnAssetRenamed); } SMInstanceElementDataUtil::OnSMInstanceElementsEnabledChanged().AddUObject(this, &UAssetEditorSubsystem::OnSMInstanceElementsEnabled); RegisterLevelEditorMenuExtensions(); InitializeRecentAssets(); } void UAssetEditorSubsystem::Deinitialize() { FCoreUObjectDelegates::OnPackageReloaded.RemoveAll(this); GEditor->OnEditorClose().RemoveAll(this); FCoreDelegates::OnEnginePreExit.RemoveAll(this); FCoreDelegates::OnPostEngineInit.RemoveAll(this); SMInstanceElementDataUtil::OnSMInstanceElementsEnabledChanged().RemoveAll(this); if (FAssetRegistryModule* AssetRegistryModule = FModuleManager::GetModulePtr(TEXT("AssetRegistry"))) { AssetRegistryModule->Get().OnAssetRemoved().RemoveAll(this); AssetRegistryModule->Get().OnAssetRenamed().RemoveAll(this); } // Don't attempt to report usage stats if analytics isn't available if (FEngineAnalytics::IsAvailable()) { TArray EditorUsageAttribs; EditorUsageAttribs.Empty(3); for (auto Iter = EditorUsageAnalytics.CreateConstIterator(); Iter; ++Iter) { const FAssetEditorAnalyticInfo& Data = Iter.Value(); EditorUsageAttribs.Reset(); EditorUsageAttribs.Emplace(TEXT("TotalDurationSeconds"), Data.SumDuration.GetTotalSeconds()); EditorUsageAttribs.Emplace(TEXT("OpenedInstancesCount"), Data.NumTimesOpened); EditorUsageAttribs.Emplace(TEXT("AssetEditor"), *Iter.Key().ToString()); FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.AssetEditorClosed"), EditorUsageAttribs); } } SaveRecentAssets(); RecentAssetsList.Reset(); } void UAssetEditorSubsystem::InitializeRecentAssets() { // The current max allowed is 30 assets RecentAssetsList = MakeUnique(TEXT("AssetEditorSubsystemRecents"), TEXT("AssetEditorSubsystemFavorites"), 30); RecentAssetsList->ReadFromINI(); TArray RecentAssetEditors; GConfig->GetArray(TEXT("AssetEditorSubsystem"), TEXT("RecentAssetEditors"), RecentAssetEditors, GEditorPerProjectIni); bool bFoundRecentAssetEditorsCleanly = true; // If the number of recent assets and recent asset editors don't match, something went wrong during saving and we have potentially corrupt data if(RecentAssetsList->GetNumItems() != RecentAssetEditors.Num()) { bFoundRecentAssetEditorsCleanly = false; UE_LOG(LogAssetEditorSubsystem, Warning, TEXT("Something went wrong while loading recent assets! Num Recent Assets = %d, Num Recent Asset Editors = %d"), RecentAssetsList->GetNumItems(), RecentAssetEditors.Num()); } // If we have corrupt data, simply ignore the asset editor list instead of potentially showing an asset in the wrong asset editor's recents if(bFoundRecentAssetEditorsCleanly) { // Go in reverse since the first item should be the most recent for(int CurRecentIndex = 0; CurRecentIndex < RecentAssetsList->GetNumItems(); ++CurRecentIndex) { if(!RecentAssetEditors[CurRecentIndex].IsEmpty()) { RecentAssetToAssetEditorMap.Add(RecentAssetsList->GetMRUItem(CurRecentIndex), RecentAssetEditors[CurRecentIndex]); } } } } void UAssetEditorSubsystem::SaveRecentAssets(const bool bOnShutdown) { TArray RecentAssetEditorsToSave; // If we are closing the editor, remove all assets that weren't actually saved to disk (transient) if(bOnShutdown) { for(int CurRecentIndex = 0; CurRecentIndex < RecentAssetsList->GetNumItems(); ++CurRecentIndex) { const FString& RecentAsset = RecentAssetsList->GetMRUItem(CurRecentIndex); if(!FPackageName::DoesPackageExist(RecentAsset)) { RecentAssetsList->RemoveMRUItem(CurRecentIndex); --CurRecentIndex; } } } for(int CurRecentIndex = 0; CurRecentIndex < RecentAssetsList->GetNumItems(); ++CurRecentIndex) { FString CurRecentAsset = RecentAssetsList->GetMRUItem(CurRecentIndex); // If we have a valid asset editor for the current asset, save it if(FString* CurrentAssetEditorName = RecentAssetToAssetEditorMap.Find(CurRecentAsset)) { RecentAssetEditorsToSave.Add(*CurrentAssetEditorName); } // Otherwise add an empty entry (e.g levels) so the two arrays are always the same size else { RecentAssetEditorsToSave.Add(FString()); } } RecentAssetsList->WriteToINI(); GConfig->SetArray(TEXT("AssetEditorSubsystem"), TEXT("RecentAssetEditors"), RecentAssetEditorsToSave, GEditorPerProjectIni); } void UAssetEditorSubsystem::CullRecentAssetEditorsMap() { /* Since the Recent Asset -> Asset Editor Map is not an MRU list, it can keep infinitely growing as the user opens assets * To keep it a reasonable size while also not culling it too often, we cull it when it gets twice as big as the MRU list */ if(RecentAssetToAssetEditorMap.Num() > 2 * RecentAssetsList->GetMaxItems()) { for (TMap::TIterator It(RecentAssetToAssetEditorMap); It; ++It) { // Remove any entries that are not in the mru list or if the package isn't valid anymore if(!FPackageName::IsValidLongPackageName(It->Key) || RecentAssetsList->FindMRUItemIdx(It->Key) == INDEX_NONE) { It.RemoveCurrent(); } } } } void UAssetEditorSubsystem::OnAssetRemoved(const FAssetData& AssetData) { FString PathName = AssetData.PackageName.ToString(); // We need this early exit because FindMRUItemIdx has a check() for non valid long package names if (!FPackageName::IsValidLongPackageName(PathName)) { return; } // If the asset that was deleted was not found in the recent assets list, we have nothing to do if(RecentAssetsList->FindMRUItemIdx(PathName) == INDEX_NONE) { return; } // Remove the asset from our list and map RecentAssetsList->RemoveMRUItem(PathName); RecentAssetToAssetEditorMap.Remove(PathName); SaveRecentAssets(); } void UAssetEditorSubsystem::OnAssetRenamed(const FAssetData& AssetData, const FString& AssetOldName) { FString OldPathName = FSoftObjectPath(AssetOldName).GetLongPackageName(); FString NewPathName = AssetData.PackageName.ToString(); // We need this early exit because FindMRUItemIdx has a check() for non valid long package names if (!FPackageName::IsValidLongPackageName(OldPathName) || !FPackageName::IsValidLongPackageName(NewPathName)) { return; } // If the asset did not previously exist in the recents list, we have nothing to do if(RecentAssetsList->FindMRUItemIdx(OldPathName) == INDEX_NONE) { return; } // Otherwise remove the old name of the asset, and re-add it with the new name // NOTE: This has an unintentional side effect of bringing it to the top of the MRU list that can't be avoided RecentAssetsList->RemoveMRUItem(OldPathName); RecentAssetsList->AddMRUItem(NewPathName); if(FString* AssetEditorName = RecentAssetToAssetEditorMap.Find(OldPathName)) { RecentAssetToAssetEditorMap.Add(NewPathName, *AssetEditorName); RecentAssetToAssetEditorMap.Remove(OldPathName); } SaveRecentAssets(); } void UAssetEditorSubsystem::OnEditorClose() { SaveOpenAssetEditors(true); TGuardValue GuardOnShutdown(bSavingOnShutdown, true); CloseAllAssetEditors(); } IAssetEditorInstance* UAssetEditorSubsystem::FindEditorForAsset(UObject* Asset, bool bFocusIfOpen) { const TArray AssetEditors = FindEditorsForAsset(Asset); IAssetEditorInstance* const * PrimaryEditor = AssetEditors.FindByPredicate([](IAssetEditorInstance* Editor) { return Editor->IsPrimaryEditor(); }); const bool bEditorOpen = PrimaryEditor != NULL; if (bEditorOpen && bFocusIfOpen) { // @todo toolkit minor: We may need to handle this differently for world-centric vs standalone. (multiple level editors, etc) (*PrimaryEditor)->FocusWindow(Asset); } return bEditorOpen ? *PrimaryEditor : NULL; } TArray UAssetEditorSubsystem::FindEditorsForAsset(UObject* Asset) { TArray AssetEditors; OpenedAssets.MultiFind(Asset, AssetEditors); return AssetEditors; } TArray UAssetEditorSubsystem::FindEditorsForAssetAndSubObjects(UObject* Asset) { TArray EditorInstances; if (Asset) { for (const TPair& Pair : OpenedAssets) { if (Pair.Key.RawPtr == Asset || (Pair.Key.ObjectPtr.IsValid() && Pair.Key.ObjectPtr.Get()->IsIn(Asset))) { EditorInstances.Add(Pair.Value); } } } return EditorInstances; } int32 UAssetEditorSubsystem::CloseAllEditorsForAsset(UObject* Asset) { TArray EditorInstances = FindEditorsForAssetAndSubObjects(Asset); for (IAssetEditorInstance* EditorInstance : EditorInstances) { if (EditorInstance) { EditorInstance->CloseWindow(EAssetEditorCloseReason::CloseAllEditorsForAsset); } } AssetEditorRequestCloseEvent.Broadcast(Asset, EAssetEditorCloseReason::CloseAllEditorsForAsset); return EditorInstances.Num(); } void UAssetEditorSubsystem::RemoveAssetFromAllEditors(UObject* Asset) { TArray EditorInstances = FindEditorsForAsset(Asset); for (IAssetEditorInstance* EditorIter : EditorInstances) { if (EditorIter) { EditorIter->RemoveEditingAsset(Asset); } } AssetEditorRequestCloseEvent.Broadcast(Asset, EAssetEditorCloseReason::RemoveAssetFromAllEditors); } void UAssetEditorSubsystem::CloseOtherEditors(UObject* Asset, IAssetEditorInstance* OnlyEditor) { TArray AllAssets; for (TMultiMap::TIterator It(OpenedAssets); It; ++It) { IAssetEditorInstance* Editor = It.Value(); if (Asset == It.Key().RawPtr && Editor != OnlyEditor) { Editor->CloseWindow(EAssetEditorCloseReason::CloseOtherEditors); } } AssetEditorRequestCloseEvent.Broadcast(Asset, EAssetEditorCloseReason::CloseOtherEditors); } TArray UAssetEditorSubsystem::GetAllEditedAssets() { TArray AllAssets; for (TMultiMap::TIterator It(OpenedAssets); It; ++It) { UObject* Asset = It.Key().ObjectPtr.Get(); if (Asset != nullptr) { AllAssets.AddUnique(Asset); } } return AllAssets; } TArray UAssetEditorSubsystem::GetAllOpenEditors() const { TArray AllEditors; OpenedEditors.GenerateKeyArray(AllEditors); return AllEditors; } void UAssetEditorSubsystem::NotifyEditorOpeningPreWidgets(const TArray< UObject* >& Assets, IAssetEditorInstance* InInstance) { EditorOpeningPreWidgetsEvent.Broadcast(Assets, InInstance); } void UAssetEditorSubsystem::NotifyAssetOpened(UObject* Asset, IAssetEditorInstance* InInstance) { if (!OpenedEditors.Contains(InInstance)) { FOpenedEditorTime EditorTime; EditorTime.EditorName = InInstance->GetEditorName(); EditorTime.OpenedTime = FDateTime::UtcNow(); OpenedEditorTimes.Add(InInstance, EditorTime); } FString AssetPath = Asset->GetOuter()->GetPathName(); OpenedAssets.Add(Asset, InInstance); OpenedEditors.Add(InInstance, Asset); RecentAssetToAssetEditorMap.Add(AssetPath, InInstance->GetEditorName().ToString()); AssetOpenedInEditorEvent.Broadcast(Asset, InInstance); if(InInstance->IncludeAssetInRestoreOpenAssetsPrompt(Asset)) { SaveOpenAssetEditors(false); } } void UAssetEditorSubsystem::NotifyAssetsOpened(const TArray< UObject* >& Assets, IAssetEditorInstance* InInstance) { for (auto AssetIter = Assets.CreateConstIterator(); AssetIter; ++AssetIter) { NotifyAssetOpened(*AssetIter, InInstance); } } void UAssetEditorSubsystem::NotifyAssetClosed(UObject* Asset, IAssetEditorInstance* InInstance) { AssetClosedInEditorEvent.Broadcast(Asset, InInstance); OpenedEditors.RemoveSingle(InInstance, Asset); OpenedAssets.RemoveSingle(Asset, InInstance); SaveOpenAssetEditors(false); } void UAssetEditorSubsystem::NotifyEditorClosed(IAssetEditorInstance* InInstance) { // Remove all assets associated with the editor TArray Assets; OpenedEditors.MultiFind(InInstance, /*out*/ Assets); for (int32 AssetIndex = 0; AssetIndex < Assets.Num(); ++AssetIndex) { if(UObject* Asset = Assets[AssetIndex].ObjectPtr.Get()) { AssetClosedInEditorEvent.Broadcast(Asset, InInstance); } OpenedAssets.Remove(Assets[AssetIndex], InInstance); } // Remove the editor itself OpenedEditors.Remove(InInstance); FOpenedEditorTime EditorTime = OpenedEditorTimes.FindAndRemoveChecked(InInstance); // Record the editor open-close duration FAssetEditorAnalyticInfo& AnalyticsForThisAsset = EditorUsageAnalytics.FindOrAdd(EditorTime.EditorName); AnalyticsForThisAsset.SumDuration += FDateTime::UtcNow() - EditorTime.OpenedTime; AnalyticsForThisAsset.NumTimesOpened++; SaveOpenAssetEditors(false); } bool UAssetEditorSubsystem::CloseAllAssetEditors() { bool bAllEditorsClosed = true; for (TMultiMap::TIterator It(OpenedEditors); It; ++It) { IAssetEditorInstance* Editor = It.Key(); if (Editor != nullptr) { if (!Editor->CloseWindow(EAssetEditorCloseReason::CloseAllAssetEditors)) { bAllEditorsClosed = false; } } } AssetEditorRequestCloseEvent.Broadcast(nullptr, EAssetEditorCloseReason::CloseAllAssetEditors); return bAllEditorsClosed; } bool UAssetEditorSubsystem::IsAssetEditable(const UObject* Asset) { FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked(TEXT("AssetTools")); if (!Asset) { return false; } if (UPackage* Package = Asset->GetPackage()) { if (Package->bIsCookedForEditor) { return false; } if (!AssetToolsModule.Get().GetWritableFolderPermissionList()->PassesStartsWithFilter(Package->GetName())) { return false; } } return true; } bool UAssetEditorSubsystem::OpenEditorForAsset(UObject* Asset, const EToolkitMode::Type ToolkitMode, TSharedPtr< IToolkitHost > OpenedFromLevelEditor, const bool bShowProgressWindow, const EAssetTypeActivationOpenedMethod OpenedMethod) { FText ErrorMessage; if(!CanOpenEditorForAsset(Asset, OpenedMethod, &ErrorMessage)) { // We also log the error if the asset was null if(!Asset) { UE_LOG(LogAssetEditorSubsystem, Error, TEXT("%s"), *ErrorMessage.ToString()); } if (TSharedPtr InfoItem = FSlateNotificationManager::Get().AddNotification(FNotificationInfo(ErrorMessage))) { InfoItem->SetCompletionState(SNotificationItem::CS_Fail); } return false; } // @todo toolkit minor: When "Edit Here" happens in a different level editor from the one that an asset is already // being edited within, we should decide whether to disallow "Edit Here" in that case, or to close the old asset // editor and summon it in the new level editor, or to just foreground the old level editor (current behavior) FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked(TEXT("AssetTools")); TWeakPtr AssetTypeActions = AssetToolsModule.Get().GetAssetTypeActionsForClass(Asset->GetClass()); AssetEditorRequestOpenEvent.Broadcast(Asset); const bool bBringToFrontIfOpen = true; if ((AssetTypeActions.IsValid() && AssetTypeActions.Pin()->ShouldFindEditorForAsset()) && FindEditorForAsset(Asset, bBringToFrontIfOpen) != nullptr) { // This asset is already open in an editor! (the call to FindEditorForAsset above will bring it to the front) return true; } else { if (bShowProgressWindow) { GWarn->BeginSlowTask(LOCTEXT("OpenEditor", "Opening Editor..."), true); } } UE_LOG(LogAssetEditorSubsystem, Log, TEXT("Opening Asset editor for %s"), *Asset->GetFullName()); const FString AssetPath = Asset->GetPathName(); EAssetOpenMethod AssetOpenMethod = OpenedMethod == EAssetTypeActivationOpenedMethod::View ? EAssetOpenMethod::View : EAssetOpenMethod::Edit; // We store the open method for this asset, so that FAssetEditorToolkit can query us for this information during init AssetOpenMethodCache.Add(AssetPath, AssetOpenMethod); EToolkitMode::Type ActualToolkitMode = ToolkitMode; if (AssetTypeActions.IsValid()) { if (AssetTypeActions.Pin()->ShouldForceWorldCentric()) { // This asset type prefers a specific toolkit mode ActualToolkitMode = EToolkitMode::WorldCentric; if (!OpenedFromLevelEditor.IsValid()) { // We don't have a level editor to spawn in world-centric mode, so we'll find one now // @todo sequencer: We should eventually eliminate this code (incl include dependencies) or change it to not make assumptions about a single level editor OpenedFromLevelEditor = FModuleManager::LoadModuleChecked< FLevelEditorModule >("LevelEditor").GetFirstLevelEditor(); } } } if (ActualToolkitMode != EToolkitMode::WorldCentric && OpenedFromLevelEditor.IsValid()) { // @todo toolkit minor: Kind of lame use of a static variable here to prime the new asset editor. This was done to avoid refactoring a few dozen files for a very minor change. FAssetEditorToolkit::SetPreviousWorldCentricToolkitHostForNewAssetEditor(OpenedFromLevelEditor.ToSharedRef()); } if (AssetTypeActions.IsValid()) { TArray AssetsToEdit; AssetsToEdit.Add(Asset); // Some assets (like UWorlds) may be destroyed and recreated as part of opening. To protect against this, keep the path to the asset and try to re-find it if it disappeared. TWeakObjectPtr WeakAsset = Asset; AssetTypeActions.Pin()->OpenAssetEditor(AssetsToEdit, OpenedMethod, ActualToolkitMode == EToolkitMode::WorldCentric ? OpenedFromLevelEditor : TSharedPtr()); // If the Asset was destroyed, attempt to find it if it was recreated if (!WeakAsset.IsValid() && !AssetPath.IsEmpty()) { Asset = FindObject(nullptr, *AssetPath); } AssetEditorOpenedEvent.Broadcast(Asset); } else { // No asset type actions for this asset. Just use a properties editor. FSimpleAssetEditor::CreateEditor(ActualToolkitMode, ActualToolkitMode == EToolkitMode::WorldCentric ? OpenedFromLevelEditor : TSharedPtr(), Asset); } if (bShowProgressWindow) { GWarn->EndSlowTask(); } // Must check Asset here in addition to at the beginning of the function, because if the asset was destroyed and recreated it might not be found correctly // Do not add to recently opened asset list if this is a level-associated asset like Level Blueprint or Built Data. Their naming is not compatible if (Asset) { if (Asset->IsAsset() && !Asset->IsA(UMapBuildDataRegistry::StaticClass())) { FString AssetOuterPath = Asset->GetOuter()->GetPathName(); if (FPackageName::IsValidLongPackageName(AssetOuterPath)) { RecentAssetsList->AddMRUItem(AssetOuterPath); CullRecentAssetEditorsMap(); } } } // Since the Asset Editor has finished init once we are here, we can remove the open method from the cache AssetOpenMethodCache.Remove(AssetPath); return true; } bool UAssetEditorSubsystem::OpenEditorForAssets_Advanced(const TArray & InAssets, const EToolkitMode::Type ToolkitMode, TSharedPtr< IToolkitHost > OpenedFromLevelEditor, const EAssetTypeActivationOpenedMethod OpenedMethod) { TArray Assets; Assets.Reserve(InAssets.Num()); int32 NumNullAssets = 0; for (UObject* Asset : InAssets) { if (Asset) { Assets.AddUnique(Asset); } else { ++NumNullAssets; } } if (NumNullAssets > 1) { UE_LOG(LogAssetEditorSubsystem, Error, TEXT("Opening Asset editors failed because of null assets")); } else if (NumNullAssets > 0) { UE_LOG(LogAssetEditorSubsystem, Error, TEXT("Opening Asset editor failed because of null asset")); } if (Assets.Num() == 1) { return OpenEditorForAsset(Assets[0], ToolkitMode, OpenedFromLevelEditor, true, OpenedMethod); } else if (Assets.Num() > 0) { TArray SkipOpenAssets; for (UObject* Asset : Assets) { // If any of the assets are already open or they cannot be opened in this open method // remove them from the list of assets to open an editor for UPackage* Package = Asset->GetOutermost(); FText ErrorMessage; if (FindEditorForAsset(Asset, true) != nullptr || !CanOpenEditorForAsset(Asset, OpenedMethod, &ErrorMessage)) { SkipOpenAssets.Add(Asset); } } // Verify that all the assets are of the same class bool bAssetClassesMatch = true; UClass* AssetClass = Assets[0]->GetClass(); for (int32 i = 1; i < Assets.Num(); i++) { if (Assets[i]->GetClass() != AssetClass) { bAssetClassesMatch = false; break; } } // If the classes don't match or any of the selected assets are already open, just open each asset in its own editor. if (bAssetClassesMatch && SkipOpenAssets.Num() == 0) { FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked(TEXT("AssetTools")); TWeakPtr AssetTypeActions = AssetToolsModule.Get().GetAssetTypeActionsForClass(AssetClass); if (AssetTypeActions.IsValid() && AssetTypeActions.Pin()->SupportsOpenedMethod(OpenedMethod)) { GWarn->BeginSlowTask(LOCTEXT("OpenEditors", "Opening Editor(s)..."), true); // Determine the appropriate toolkit mode for the asset type EToolkitMode::Type ActualToolkitMode = ToolkitMode; if (AssetTypeActions.Pin()->ShouldForceWorldCentric()) { // This asset type prefers a specific toolkit mode ActualToolkitMode = EToolkitMode::WorldCentric; if (!OpenedFromLevelEditor.IsValid()) { // We don't have a level editor to spawn in world-centric mode, so we'll find one now // @todo sequencer: We should eventually eliminate this code (incl include dependencies) or change it to not make assumptions about a single level editor OpenedFromLevelEditor = FModuleManager::LoadModuleChecked< FLevelEditorModule >("LevelEditor").GetFirstLevelEditor(); } } if (ActualToolkitMode != EToolkitMode::WorldCentric && OpenedFromLevelEditor.IsValid()) { // @todo toolkit minor: Kind of lame use of a static variable here to prime the new asset editor. This was done to avoid refactoring a few dozen files for a very minor change. FAssetEditorToolkit::SetPreviousWorldCentricToolkitHostForNewAssetEditor(OpenedFromLevelEditor.ToSharedRef()); } // Some assets (like UWorlds) may be destroyed and recreated as part of opening. To protect against this, keep the path to each asset and try to re-find any if they disappear. struct FLocalAssetInfo { TWeakObjectPtr WeakAsset; FString AssetPath; FLocalAssetInfo(const TWeakObjectPtr& InWeakAsset, const FString& InAssetPath) : WeakAsset(InWeakAsset), AssetPath(InAssetPath) {} }; EAssetOpenMethod AssetOpenMethod = OpenedMethod == EAssetTypeActivationOpenedMethod::View ? EAssetOpenMethod::View : EAssetOpenMethod::Edit; TArray AssetInfoList; AssetInfoList.Reserve(Assets.Num()); for (UObject* Asset : Assets) { AssetInfoList.Add(FLocalAssetInfo(Asset, Asset->GetPathName())); // We store the open method for this asset, so that FAssetEditorToolkit can query us for this information during init AssetOpenMethodCache.Add(Asset->GetPathName(), AssetOpenMethod); } // How to handle multiple assets is left up to the type actions (i.e. open a single shared editor or an editor for each) AssetTypeActions.Pin()->OpenAssetEditor(Assets, OpenedMethod, ActualToolkitMode == EToolkitMode::WorldCentric ? OpenedFromLevelEditor : TSharedPtr()); // If any assets were destroyed, attempt to find them if they were recreated for (int32 i = 0; i < Assets.Num(); i++) { const FLocalAssetInfo& AssetInfo = AssetInfoList[i]; UObject* Asset = Assets[i]; if (!AssetInfo.WeakAsset.IsValid() && !AssetInfo.AssetPath.IsEmpty()) { Asset = FindObject(nullptr, *AssetInfo.AssetPath); } // Since the Asset Editor has finished init once we are here, we can remove the open method from the cache AssetOpenMethodCache.Remove(AssetInfo.AssetPath); } //@todo if needed, broadcast the event for every asset. It is possible, however, that a single shared editor was opened by the AssetTypeActions, not an editor for each asset. /*AssetEditorOpenedEvent.Broadcast(Asset);*/ GWarn->EndSlowTask(); } } else { // Asset types don't match or some are already open, so just open individual editors for the unopened ones for (UObject* Asset : Assets) { if (!SkipOpenAssets.Contains(Asset)) { OpenEditorForAsset(Asset, ToolkitMode, OpenedFromLevelEditor, true, OpenedMethod); } } } } return NumNullAssets == 0; } bool UAssetEditorSubsystem::OpenEditorForAssets(const TArray& Assets, const EAssetTypeActivationOpenedMethod OpenedMethod) { return OpenEditorForAssets_Advanced(Assets, EToolkitMode::Standalone, TSharedPtr(), OpenedMethod); } TOptional UAssetEditorSubsystem::GetAssetBeingOpenedMethod(TObjectPtr Asset) { if(!Asset) { return TOptional(); } EAssetOpenMethod* OpenMethod = AssetOpenMethodCache.Find(Asset->GetPathName()); return OpenMethod ? *OpenMethod : TOptional(); } TOptional UAssetEditorSubsystem::GetAssetsBeingOpenedMethod(TArray> Assets) { TOptional FoundOpenMethod; // If an asset editor supports opening multiple assets, if any of them are being opened in read only mode we ask the asset editor to open in read only mode for(const TObjectPtr& Asset : Assets) { TOptional AssetOpenMethod = GetAssetBeingOpenedMethod(Asset); if(AssetOpenMethod.IsSet()) { FoundOpenMethod = AssetOpenMethod.GetValue(); if(FoundOpenMethod == EAssetOpenMethod::View) { return FoundOpenMethod; } } } return FoundOpenMethod; } void UAssetEditorSubsystem::AddReadOnlyAssetFilter(const FName& Owner, const FReadOnlyAssetFilter& ReadOnlyAssetFilter) { ReadOnlyAssetFilters.Add(Owner, ReadOnlyAssetFilter); } void UAssetEditorSubsystem::RemoveReadOnlyAssetFilter(const FName& Owner) { ReadOnlyAssetFilters.Remove(Owner); } void UAssetEditorSubsystem::HandleRequestOpenAssetMessage(const FAssetEditorRequestOpenAsset& Message, const TSharedRef& Context) { OpenEditorForAsset(Message.AssetName); } UObject* UAssetEditorSubsystem::FindOrLoadAssetForOpening(const FSoftObjectPath& AssetPath) { UObject* Object = FindObject(AssetPath.GetAssetPath()); if (!Object) { Object = LoadObject(nullptr, *AssetPath.GetAssetPathString(), nullptr, LOAD_NoRedirects); } return Object; } void UAssetEditorSubsystem::OpenEditorForAsset(const FSoftObjectPath& AssetPath, const EAssetTypeActivationOpenedMethod OpenedMethod) { if (UObject* Object = FindOrLoadAssetForOpening(AssetPath)) { OpenEditorForAsset(Object, EToolkitMode::Standalone, TSharedPtr(), true, OpenedMethod); } } void UAssetEditorSubsystem::OpenEditorForAsset(const FString& AssetPathName, const EAssetTypeActivationOpenedMethod OpenedMethod) { OpenEditorForAsset(FSoftObjectPath(AssetPathName), OpenedMethod); } bool UAssetEditorSubsystem::HandleTicker(float DeltaTime) { QUICK_SCOPE_CYCLE_COUNTER(STAT_UAssetEditorSubsystem_HandleTicker); if (bRequestRestorePreviouslyOpenAssets) { RestorePreviouslyOpenAssets(); bRequestRestorePreviouslyOpenAssets = false; } return true; } void UAssetEditorSubsystem::RequestRestorePreviouslyOpenAssets() { // We defer the restore so that we guarantee that it happens once initialization is complete bRequestRestorePreviouslyOpenAssets = true; } UEdMode* UAssetEditorSubsystem::CreateEditorModeWithToolsOwner(FEditorModeID ModeID, FEditorModeTools& Owner) { FRegisteredModeInfo* ScriptableMode = EditorModes.Find(ModeID); if (ScriptableMode && ScriptableMode->ModeClass.IsValid()) { UEdMode* Instance = NewObject(GetTransientPackage(), ScriptableMode->ModeClass.Get()); Instance->Owner = &Owner; Instance->Initialize(); return Instance; } // If we couldn't find a valid UEdMode based class, attempt to make a UEdMode wrapped FEdMode FEditorModeInfo LegacyModeInfo; if (FindEditorModeInfo(ModeID, LegacyModeInfo)) { ULegacyEdModeWrapper* LegacyEditorMode = NewObject(GetTransientPackage()); if (LegacyEditorMode->CreateLegacyMode(ModeID, Owner)) { LegacyEditorMode->Initialize(); return LegacyEditorMode; } } return nullptr; } bool UAssetEditorSubsystem::FindEditorModeInfo(const FEditorModeID& InModeID, FEditorModeInfo& OutModeInfo) const { if (!IsEditorModeAllowed(InModeID)) { return false; } const TSharedRef* ModeFactory = FEditorModeRegistry::Get().GetFactoryMap().Find(InModeID); if (ModeFactory) { OutModeInfo = (*ModeFactory)->GetModeInfo(); return true; } if (!EditorModes.Contains(InModeID)) { return false; } OutModeInfo = EditorModes[InModeID].ModeInfo; return true; } TArray UAssetEditorSubsystem::GetEditorModeInfoOrderedByPriority() const { TArray ModeInfoArray; for (const auto& Pair : FEditorModeRegistry::Get().GetFactoryMap()) { FEditorModeInfo ModeInfo = Pair.Value->GetModeInfo(); if (IsEditorModeAllowed(ModeInfo.ID)) { ModeInfoArray.Add(MoveTemp(ModeInfo)); } } for (const auto& EditorMode : EditorModes) { const FEditorModeInfo& ModeInfo = EditorMode.Value.ModeInfo; if (IsEditorModeAllowed(ModeInfo.ID)) { ModeInfoArray.Add(ModeInfo); } } ModeInfoArray.Sort([](const FEditorModeInfo& A, const FEditorModeInfo& B) { return A.PriorityOrder < B.PriorityOrder; }); return ModeInfoArray; } void UAssetEditorSubsystem::RegisterUAssetEditor(UAssetEditor* NewAssetEditor) { OwnedAssetEditors.Add(NewAssetEditor); } void UAssetEditorSubsystem::UnregisterUAssetEditor(UAssetEditor* RemovedAssetEditor) { OwnedAssetEditors.Remove(RemovedAssetEditor); } FRegisteredModesChangedEvent& UAssetEditorSubsystem::OnEditorModesChanged() { return OnEditorModesChangedEvent; } FOnModeRegistered& UAssetEditorSubsystem::OnEditorModeRegistered() { return OnEditorModeRegisteredEvent; } FOnModeUnregistered& UAssetEditorSubsystem::OnEditorModeUnregistered() { return OnEditorModeUnregisteredEvent; } void UAssetEditorSubsystem::RestorePreviouslyOpenAssets() { TArray AllOpenAssets; TArray FilteredOpenAssets; GConfig->GetArray(TEXT("AssetEditorSubsystem"), TEXT("OpenAssetsAtExit"), AllOpenAssets, GEditorPerProjectIni); if(!RecentAssetsFilter.IsBound()) { FilteredOpenAssets = AllOpenAssets; } else { for(const FString& Asset : AllOpenAssets) { if(RecentAssetsFilter.Execute(Asset)) { FilteredOpenAssets.Add(Asset); } } } bool bCleanShutdown = true; GConfig->GetBool(TEXT("AssetEditorSubsystem"), TEXT("CleanShutdown"), bCleanShutdown, GEditorPerProjectIni); bool bDebuggerAttachedLastSession = false; GConfig->GetBool(TEXT("AssetEditorSubsystem"), TEXT("DebuggerAttached"), bDebuggerAttachedLastSession, GEditorPerProjectIni); SaveOpenAssetEditors(false); /** True if the last editor run crashed and did not have a debugger attached * A "clean" shutdown for our purposes is the logical NOT of this, which includes clean shutdowns without a debugger * along with any shutdowns when a debugger is attached */ bool bCrashedWithoutDebugger = !bDebuggerAttachedLastSession && !bCleanShutdown; if (FilteredOpenAssets.Num() > 0) { // This option overrides the saved setting if(bAutoRestoreAndDisableSaving.IsSet()) { // If bAutoRestoreAndDisableSaving is true, we automatically restore the opened assets if(bAutoRestoreAndDisableSaving.GetValue()) { OpenEditorsForAssets(FilteredOpenAssets); } return; } /* If we crashed without a debugger attached, always prompt regardless of what the user previously said * to make sure the user can never get stuck in a crash loop due to corrupted assets etc */ if(bCrashedWithoutDebugger) { SpawnRestorePreviouslyOpenAssetsNotification(!bCrashedWithoutDebugger, FilteredOpenAssets); return; } const ERestoreOpenAssetTabsMethod AutoRestoreMethod = GetDefault()->RestoreOpenAssetTabsOnRestart; switch(AutoRestoreMethod) { case ERestoreOpenAssetTabsMethod::AlwaysPrompt: { SpawnRestorePreviouslyOpenAssetsNotification(!bCrashedWithoutDebugger, FilteredOpenAssets); break; } case ERestoreOpenAssetTabsMethod::NeverRestore: // Do nothing here since the user does not want to restore anything break; case ERestoreOpenAssetTabsMethod::AlwaysRestore: { // Pretend that we showed the notification and that the user clicked "Restore Now" OpenEditorsForAssets(FilteredOpenAssets); break; } } } } void UAssetEditorSubsystem::SetAutoRestoreAndDisableSaving(const bool bInAutoRestoreAndDisableSaving) { // We preserve legacy behavior, where true is the same but false translates to not having the override set in the new logic SetAutoRestoreAndDisableSavingOverride(bInAutoRestoreAndDisableSaving ? bInAutoRestoreAndDisableSaving : TOptional()); } void UAssetEditorSubsystem::SetAutoRestoreAndDisableSavingOverride(TOptional bInAutoRestoreAndDisableSaving) { bAutoRestoreAndDisableSaving = bInAutoRestoreAndDisableSaving; // Disable any pending request to avoid trying to restore previously opened assets twice bRequestRestorePreviouslyOpenAssets = false; } TOptional UAssetEditorSubsystem::GetAutoRestoreAndDisableSavingOverride() const { return bAutoRestoreAndDisableSaving; } void UAssetEditorSubsystem::SetRecentAssetsFilter(const FMainMRUFavoritesList::FDoesMRUFavoritesItemPassFilter& InFilter) { RecentAssetsFilter = InFilter; if(RecentAssetsList) { RecentAssetsList->RegisterDoesMRUFavoritesItemPassFilterDelegate(InFilter); } } void UAssetEditorSubsystem::SpawnRestorePreviouslyOpenAssetsNotification(const bool bCleanShutdown, const TArray& AssetsToOpen) { FText NotificationMessage = bCleanShutdown ? LOCTEXT("ReopenAssetEditorsAfterClose", "{0} asset {0}|plural(one=editor was,other=editors were) open when the editor was last closed. Would you like to re-open {0}|plural(one=it,other=them)?") : LOCTEXT("ReopenAssetEditorsAfterCrash", "{0} asset {0}|plural(one=editor was,other=editors were) open when the editor quit unexpectedly. Would you like to re-open {0}|plural(one=it,other=them)?"); NotificationMessage = FText::Format(NotificationMessage, AssetsToOpen.Num()); FNotificationInfo Info = FNotificationInfo(NotificationMessage); // Add the buttons Info.ButtonDetails.Add(FNotificationButtonInfo( LOCTEXT("ReopenAssetEditors_Confirm", "Yes"), FText(), FSimpleDelegate::CreateUObject(this, &UAssetEditorSubsystem::OnConfirmRestorePreviouslyOpenAssets, AssetsToOpen), SNotificationItem::CS_None )); Info.ButtonDetails.Add(FNotificationButtonInfo( LOCTEXT("ReopenAssetEditors_Cancel", "No"), FText(), FSimpleDelegate::CreateUObject(this, &UAssetEditorSubsystem::OnCancelRestorePreviouslyOpenAssets), SNotificationItem::CS_None )); Info.bFireAndForget = false; // We want the auto-save to be subtle Info.bUseLargeFont = false; Info.bUseThrobber = false; Info.bUseSuccessFailIcons = false; // Only let the user suppress the non-crash version if (bCleanShutdown) { bRememberMyChoiceChecked = false; Info.CheckBoxState = TAttribute::CreateLambda([this]() { return bRememberMyChoiceChecked ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }); Info.CheckBoxStateChanged = FOnCheckStateChanged::CreateLambda([this](ECheckBoxState NewState) { bRememberMyChoiceChecked = (NewState == ECheckBoxState::Checked) ? true : false; }); Info.CheckBoxText = LOCTEXT("RememberCheckBoxMessage", "Remember my choice"); } // Close any existing notification TSharedPtr RestorePreviouslyOpenAssetsNotification = RestorePreviouslyOpenAssetsNotificationPtr.Pin(); if (RestorePreviouslyOpenAssetsNotification.IsValid()) { RestorePreviouslyOpenAssetsNotification->ExpireAndFadeout(); } RestorePreviouslyOpenAssetsNotificationPtr = FSlateNotificationManager::Get().AddNotification(Info); } void UAssetEditorSubsystem::OnConfirmRestorePreviouslyOpenAssets(TArray AssetsToOpen) { // Close any existing notification TSharedPtr RestorePreviouslyOpenAssetsNotification = RestorePreviouslyOpenAssetsNotificationPtr.Pin(); if (RestorePreviouslyOpenAssetsNotification.IsValid()) { RestorePreviouslyOpenAssetsNotification->SetExpireDuration(0.0f); RestorePreviouslyOpenAssetsNotification->SetFadeOutDuration(0.5f); RestorePreviouslyOpenAssetsNotification->ExpireAndFadeout(); // Change the saved setting to AlwaysRestore if the user checked "Remember my choice" if(bRememberMyChoiceChecked) { UEditorLoadingSavingSettings& Settings = *GetMutableDefault(); Settings.RestoreOpenAssetTabsOnRestart = ERestoreOpenAssetTabsMethod::AlwaysRestore; Settings.PostEditChange(); } // we do this inside the condition so that it can only be done once. OpenEditorsForAssets(AssetsToOpen); } } void UAssetEditorSubsystem::OnCancelRestorePreviouslyOpenAssets() { // Close any existing notification TSharedPtr RestorePreviouslyOpenAssetsNotification = RestorePreviouslyOpenAssetsNotificationPtr.Pin(); if (RestorePreviouslyOpenAssetsNotification.IsValid()) { // Change the saved setting to NeverRestore if the user checked "Remember my choice" if(bRememberMyChoiceChecked) { UEditorLoadingSavingSettings& Settings = *GetMutableDefault(); Settings.RestoreOpenAssetTabsOnRestart = ERestoreOpenAssetTabsMethod::NeverRestore; Settings.PostEditChange(); } RestorePreviouslyOpenAssetsNotification->SetExpireDuration(0.0f); RestorePreviouslyOpenAssetsNotification->SetFadeOutDuration(0.5f); RestorePreviouslyOpenAssetsNotification->ExpireAndFadeout(); } } bool UAssetEditorSubsystem::ShouldShowRecentAsset(const FString& AssetName, int32 RecentAssetIndex, const FName& InAssetEditorName) const { const FString* AssetEditorForCurrRecent = RecentAssetToAssetEditorMap.Find(AssetName); // If this asset wasn't opened in any valid asset editor (e.g Levels) if(!AssetEditorForCurrRecent) { return false; } // If we have a valid asset editor we are adding assets for if(!InAssetEditorName.IsNone()) { // If this asset was not opened in InAssetEditorName, ignore it if(*AssetEditorForCurrRecent != InAssetEditorName.ToString()) { return false; } } // If this asset does not pass the set filter, ignore it if (!RecentAssetsList->MRUItemPassesCurrentFilter(RecentAssetIndex)) { return false; } return true; } bool UAssetEditorSubsystem::ShouldShowRecentAssetsMenu(const FName& InAssetEditorName) const { // If we have no recent assets at all if(RecentAssetsList->GetNumItems() == 0) { return false; } for ( int32 CurRecentIndex = 0; CurRecentIndex < RecentAssetsList->GetNumItems() && CurRecentIndex < MaxRecentAssetsToShowInMenu; ++CurRecentIndex ) { const FString& CurRecent = RecentAssetsList->GetMRUItem(CurRecentIndex); // If any of the assets in the recents wil be shown, we show the menu if(ShouldShowRecentAsset(CurRecent, CurRecentIndex, InAssetEditorName)) { return true; } } return false; } void UAssetEditorSubsystem::CreateRecentAssetsMenu(UToolMenu* InMenu, const FName InAssetEditorName) { FToolMenuSection& Section = InMenu->FindOrAddSection("Recents"); const bool bShowingContentVersePath = IAssetTools::Get().ShowingContentVersePath(); // Keep adding assets until we reach the end of the MRU list, or we reach the max allowed assets for ( int32 CurRecentIndex = 0; CurRecentIndex < RecentAssetsList->GetNumItems() && CurRecentIndex < MaxRecentAssetsToShowInMenu; ++CurRecentIndex ) { const FString& CurRecent = RecentAssetsList->GetMRUItem(CurRecentIndex); if(!ShouldShowRecentAsset(CurRecent, CurRecentIndex, InAssetEditorName)) { continue; } FSoftObjectPath AssetPath(CurRecent); if (AssetPath.GetAssetFName().IsNone()) { // Mimic LoadObject in FindOrLoadAssetForOpening falling back to the asset with the same name as the package if the asset name is not provided. AssetPath = FSoftObjectPath::ConstructFromAssetPath(FTopLevelAssetPath(AssetPath.GetLongPackageFName(), FPackageName::GetShortFName(AssetPath.GetLongPackageFName()))); } FText CurPath; if (bShowingContentVersePath) { if (const IAssetRegistry* AssetRegistry = IAssetRegistry::Get()) { FAssetData AssetData; if (AssetRegistry->TryGetAssetByObjectPath(AssetPath, AssetData) == UE::AssetRegistry::EExists::Exists) { UE::Core::FVersePath VersePath = AssetData.GetVersePath(); if (VersePath.IsValid()) { CurPath = FText::FromString(MoveTemp(VersePath).ToString()); } } } } if (CurPath.IsEmpty()) { CurPath = FText::FromString(CurRecent); } const FText ToolTip = FText::Format( LOCTEXT( "RecentAssetsToolTip", "Open {0}" ), CurPath ); const FText Label = FText::FromString( AssetPath.GetAssetName() ); Section.AddMenuEntry( NAME_None, Label, ToolTip, FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([this, AssetPath]() { if (UObject* Object = FindOrLoadAssetForOpening(AssetPath)) { FText ErrorMessage; // Try to open the asset in edit mode. If that is not allowed, try to open it in read only mode if(CanOpenEditorForAsset(Object, EAssetTypeActivationOpenedMethod::Edit, &ErrorMessage)) { OpenEditorForAsset(Object, EToolkitMode::Standalone, TSharedPtr(), true, EAssetTypeActivationOpenedMethod::Edit); } else if(CanOpenEditorForAsset(Object, EAssetTypeActivationOpenedMethod::View, &ErrorMessage)) { OpenEditorForAsset(Object, EToolkitMode::Standalone, TSharedPtr(), true, EAssetTypeActivationOpenedMethod::View); } } }) ) ); } } void UAssetEditorSubsystem::RegisterLevelEditorMenuExtensions() { UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.File"); FToolMenuSection& Section = Menu->FindOrAddSection("FileAsset"); Section.AddDynamicEntry("FileRecentAssets", FNewToolMenuSectionDelegate::CreateLambda([this](FToolMenuSection& InSection) { // Since we want to show all asset types for the Level Editor, we use an empty asset editor name const FName AssetEditorName = FName(); if (!ShouldShowRecentAssetsMenu(AssetEditorName)) { return; } InSection.AddSubMenu( "RecentAssetsSubmenu", LOCTEXT("RecentAssetsSubmenu_Label", "Recent Assets"), FText::Format(LOCTEXT("RecentAssetsSubMenu_ToolTip", "Access your last {0} recently opened assets"), MaxRecentAssetsToShowInMenu), FNewToolMenuDelegate::CreateUObject(this, &UAssetEditorSubsystem::CreateRecentAssetsMenu, AssetEditorName), false, FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.RecentAssets")); })); } void UAssetEditorSubsystem::CreateRecentAssetsMenuForEditor(const IAssetEditorInstance* InAssetEditorInstance, FToolMenuSection& InSection) { if(!InAssetEditorInstance) { return; } InSection.AddDynamicEntry("FileRecentAssetEditorAssets", FNewToolMenuSectionDelegate::CreateLambda([this, InAssetEditorInstance](FToolMenuSection& InSection) { const FName EditingAssetTypeName = InAssetEditorInstance->GetEditingAssetTypeName(); // For generic asset editors (or any other special cases) that don't have one singular type of asset they are editing, show all recent assets const FName AssetEditorName = EditingAssetTypeName.IsNone() ? FName() : InAssetEditorInstance->GetEditorName(); if(!ShouldShowRecentAssetsMenu(AssetEditorName)) { return; } // Show all Recent Assets if(AssetEditorName.IsNone()) { InSection.AddSubMenu( "RecentAssetsSubmenu", LOCTEXT("RecentAssetsSubmenu_Label", "Recent Assets"), FText::Format(LOCTEXT("RecentAssetsSubMenu_ToolTip", "Access your last {0} recently opened assets"), MaxRecentAssetsToShowInMenu), FNewToolMenuDelegate::CreateUObject(this, &UAssetEditorSubsystem::CreateRecentAssetsMenu, AssetEditorName), false, FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.RecentAssets") ); } // Only show recent assets opened in this Asset Editor else { // Example submenu name: "Recent Material Assets" for the Material Editor InSection.AddSubMenu( "RecentAssetEditorAssetsSubmenu", FText::Format(LOCTEXT("RecentAssetEditorAssetsSubmenu_Label", "Recent {0} Assets"), FText::FromName(EditingAssetTypeName)), FText::Format(LOCTEXT("RecentAssetEditorAssetsSubmenu_Tooltip", "Access your recently opened {0} assets"), FText::FromName(EditingAssetTypeName)), FNewToolMenuDelegate::CreateUObject(this, &UAssetEditorSubsystem::CreateRecentAssetsMenu, AssetEditorName), false, FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.RecentAssets") ); } })); ; } void UAssetEditorSubsystem::SaveOpenAssetEditors(const bool bOnShutdown) { SaveRecentAssets(bOnShutdown); // We are already saving the open asset editors manually before bSavingOnShutdown is true. This is to avoid saving that there are no open asset editors b/c the editor is shutting down // If we are restoring the layout, bAutoRestoreAndDisable saving is true if (!bSavingOnShutdown && !bAutoRestoreAndDisableSaving.IsSet()) { TArray OpenAssets; for (const TPair& EditorPair : OpenedEditors) { IAssetEditorInstance* Editor = EditorPair.Key; if (Editor != nullptr) { UObject* EditedObject = EditorPair.Value.ObjectPtr.Get(); if (EditedObject != nullptr && Editor->IncludeAssetInRestoreOpenAssetsPrompt(EditedObject)) { // only record assets that have a valid saved package UPackage* Package = EditedObject->GetOutermost(); if (Package != nullptr && Package->GetFileSize() != 0 ) { OpenAssets.Add(EditedObject->GetPathName()); } } } } GConfig->SetArray(TEXT("AssetEditorSubsystem"), TEXT("OpenAssetsAtExit"), OpenAssets, GEditorPerProjectIni); GConfig->SetBool(TEXT("AssetEditorSubsystem"), TEXT("CleanShutdown"), bOnShutdown, GEditorPerProjectIni); GConfig->SetBool(TEXT("AssetEditorSubsystem"), TEXT("DebuggerAttached"), FPlatformMisc::IsDebuggerPresent(), GEditorPerProjectIni); GConfig->Flush(false, GEditorPerProjectIni); } } void UAssetEditorSubsystem::SaveOpenAssetEditors(const bool bOnShutdown, const bool bCancelIfDebugger) { SaveOpenAssetEditors(bOnShutdown); } void UAssetEditorSubsystem::HandlePackageReloaded(const EPackageReloadPhase InPackageReloadPhase, FPackageReloadedEvent* InPackageReloadedEvent) { static TArray> PendingAssetsToOpen; if (InPackageReloadPhase == EPackageReloadPhase::PrePackageFixup) { /** Call close for all old assets even if not open, so global callback will go off */ TArray ObjectsToClose; const TMap& RepointedMap = InPackageReloadedEvent->GetRepointedObjects(); for (const TPair& RepointPair : RepointedMap) { if (RepointPair.Key->IsAsset()) { ObjectsToClose.Add(RepointPair.Key); } } /** Look for replacement for assets that are open now so we can reopen */ for (TPair& AssetEditorPair : OpenedAssets) { UObject* NewAsset = nullptr; if (AssetEditorPair.Key.RawPtr && InPackageReloadedEvent->GetRepointedObject(AssetEditorPair.Key.RawPtr, NewAsset)) { if (NewAsset) { PendingAssetsToOpen.AddUnique(NewAsset); } // Not validating the asset here since we'd want to close editors for garbage collected assets UObject* OldAsset = AssetEditorPair.Key.RawPtr; ObjectsToClose.AddUnique(OldAsset); // Gather other assets referencing reloaded asset and mark their editors to be closed too. TArray AssetInternalReferencers, AssetExternalReferencers; AssetEditorPair.Key.RawPtr->RetrieveReferencers(&AssetInternalReferencers, &AssetExternalReferencers); for (const FReferencerInformation& Ref : AssetExternalReferencers) { ObjectsToClose.AddUnique(Ref.Referencer); if (!FindEditorsForAssetAndSubObjects(Ref.Referencer).IsEmpty()) { PendingAssetsToOpen.AddUnique(Ref.Referencer); } } } } int32 NumAssetEditorsClosed = 0; for (UObject* OldAsset : ObjectsToClose) { NumAssetEditorsClosed += CloseAllEditorsForAsset(OldAsset); } if (NumAssetEditorsClosed > 0) { // Closing asset editors might have have left objects pending GC that still reference the asset we're about to reload // Run a GC now to ensure those are cleaned up before the fix-up phase happens CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); } } if (InPackageReloadPhase == EPackageReloadPhase::PostBatchPostGC) { for (TWeakObjectPtr& NewAsset : PendingAssetsToOpen) { if (NewAsset.IsValid()) { OpenEditorForAsset(NewAsset.Get()); } } PendingAssetsToOpen.Reset(); } } void UAssetEditorSubsystem::OpenEditorsForAssets(const TArray& AssetsToOpen) { for (const FSoftObjectPath& AssetName : AssetsToOpen) { OpenEditorForAsset(AssetName); } } void UAssetEditorSubsystem::OpenEditorsForAssets(const TArray& AssetsToOpen, const EAssetTypeActivationOpenedMethod OpenedMethod) { for (const FString& AssetName : AssetsToOpen) { OpenEditorForAsset(AssetName, OpenedMethod); } } void UAssetEditorSubsystem::OpenEditorsForAssets(const TArray& AssetsToOpen, const EAssetTypeActivationOpenedMethod OpenedMethod) { for (const FName& AssetName : AssetsToOpen) { OpenEditorForAsset(AssetName.ToString(), OpenedMethod); } } bool UAssetEditorSubsystem::CanOpenEditorForAsset(UObject* Asset, const EAssetTypeActivationOpenedMethod OpenedMethod, FText* OutErrorMsg) { if (!Asset) { if(OutErrorMsg) { *OutErrorMsg = LOCTEXT("AssetNull", "Opening Asset editor failed because asset is null"); } return false; } FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked(TEXT("AssetTools")); TWeakPtr AssetTypeActions = AssetToolsModule.Get().GetAssetTypeActionsForClass(Asset->GetClass()); // First we check with the asset type action to see if it supports the open method if(AssetTypeActions.IsValid()) { if(!AssetTypeActions.Pin()->SupportsOpenedMethod(OpenedMethod)) { if(OutErrorMsg) { if(OpenedMethod == EAssetTypeActivationOpenedMethod::Edit) { *OutErrorMsg = FText::Format(LOCTEXT("AssetTypeDoesntSupportEdit", "A {0} does not support being edited!"), AssetTypeActions.Pin()->GetName()); } else { *OutErrorMsg = FText::Format(LOCTEXT("AssetTypeDoesntSupportReadOnly", "A {0} does not support being opened in read only mode!"), AssetTypeActions.Pin()->GetName()); } } return false; } } else { // Disallow opening an asset editor for classes if(Asset->IsA()) { if(OutErrorMsg) { *OutErrorMsg = LOCTEXT("UClassesCantBeOpened", "UClasses cannot be opened in Asset Editors!"); } return false; } } // If the asset needs to be edited, make sure that is possible if(OpenedMethod == EAssetTypeActivationOpenedMethod::Edit) { if(!IsAssetEditable(Asset)) { if(OutErrorMsg) { *OutErrorMsg = LOCTEXT("AssetCantBeEdited", "Unable to Edit Cooked asset"); } return false; } } if(OpenedMethod == EAssetTypeActivationOpenedMethod::View) { if (UPackage* Package = Asset->GetPackage()) { for(const TPair& Filter : ReadOnlyAssetFilters) { if(!Filter.Value.Execute(Package->GetPathName())) { if(OutErrorMsg) { *OutErrorMsg = LOCTEXT("AssetDoesntSupportOpenMethod", "This asset does not support being opened in read only mode."); } return false; } } } else { return false; // failsafe, but the package should exist at this point } } return true; } void UAssetEditorSubsystem::RegisterEditorModes() { for (FThreadSafeObjectIterator EditorModeIter(UEdMode::StaticClass()); EditorModeIter; ++EditorModeIter) { UEdMode* EditorMode = Cast(*EditorModeIter); UClass* ModeClass = EditorMode->GetClass(); if (ModeClass->HasAnyClassFlags(CLASS_Abstract | CLASS_Interface)) { continue; } FEditorModeInfo EditorModeInfo = EditorMode->GetModeInfo(); if (EditorModes.Contains(EditorModeInfo.ID)) { TWeakObjectPtr RegisteredClass = EditorModes[EditorModeInfo.ID].ModeClass; UE_LOG( LogAssetEditorSubsystem, Warning, TEXT("UAssetEditorSubsystem::RegisterEditorModes : Attempting to initialize duplicate mode with name '%s'. Conflicting classes: '%s' and '%s'."), *EditorModeInfo.ID.ToString(), *ModeClass->GetName(), *RegisteredClass.Get()->GetName() ); continue; } EditorModes.Add( EditorModeInfo.ID, FRegisteredModeInfo{ ModeClass, EditorModeInfo } ); OnEditorModeRegisteredEvent.Broadcast(EditorModeInfo.ID); } // Initialize Legacy FEditorModes FEditorModeRegistry::Get().Initialize(); OnEditorModesChangedEvent.Broadcast(); } void UAssetEditorSubsystem::UnregisterEditorModes() { FEditorModeRegistry::Get().Shutdown(); for (const auto& RegisteredMode : EditorModes) { OnEditorModeUnregisteredEvent.Broadcast(RegisteredMode.Value.ModeInfo.ID); } OnEditorModesChangedEvent.Broadcast(); EditorModes.Empty(); } void UAssetEditorSubsystem::OnSMInstanceElementsEnabled() { // Let the modes know that SM instance elements may have been enabled or disabled and update state accordingly OnEditorModesChanged().Broadcast(); } FNamePermissionList& UAssetEditorSubsystem::GetAllowedEditorModes() { return AllowedEditorModes; } bool UAssetEditorSubsystem::IsEditorModeAllowed(const FName ModeId) const { return AllowedEditorModes.PassesFilter(ModeId); } #undef LOCTEXT_NAMESPACE