1497 lines
57 KiB
C++
1497 lines
57 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "Editor/UnrealEdEngine.h"
|
|
#include "HAL/PlatformFilemanager.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/ConfigCacheIni.h"
|
|
#include "Misc/CoreDelegates.h"
|
|
#include "Misc/App.h"
|
|
#include "Modules/ModuleManager.h"
|
|
#include "UObject/UObjectIterator.h"
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "Components/PrimitiveComponent.h"
|
|
#include "CookOnTheSide/CookOnTheFlyServer.h"
|
|
#include "Materials/Material.h"
|
|
#include "Editor/EditorPerProjectUserSettings.h"
|
|
#include "ISourceControlModule.h"
|
|
#include "SourceControlHelpers.h"
|
|
#include "SourceControlOperations.h"
|
|
#include "Settings/EditorExperimentalSettings.h"
|
|
#include "Settings/EditorLoadingSavingSettings.h"
|
|
#include "ThumbnailRendering/ThumbnailManager.h"
|
|
#include "Preferences/UnrealEdKeyBindings.h"
|
|
#include "Preferences/UnrealEdOptions.h"
|
|
#include "GameFramework/Volume.h"
|
|
#include "Components/ArrowComponent.h"
|
|
#include "Components/BillboardComponent.h"
|
|
#include "Components/BrushComponent.h"
|
|
#include "Engine/Selection.h"
|
|
#include "Editor.h"
|
|
#include "LevelEditorViewport.h"
|
|
#include "EditorModeRegistry.h"
|
|
#include "EditorModeManager.h"
|
|
#include "EditorModes.h"
|
|
#include "UnrealEdMisc.h"
|
|
#include "UnrealEdGlobals.h"
|
|
|
|
#include "Matinee/InterpData.h"
|
|
#include "Matinee/MatineeActor.h"
|
|
#include "Animation/AnimCompress.h"
|
|
|
|
#include "EditorSupportDelegates.h"
|
|
#include "EditorLevelUtils.h"
|
|
#include "EdMode.h"
|
|
#include "PropertyEditorModule.h"
|
|
#include "LevelEditor.h"
|
|
#include "Interfaces/IMainFrameModule.h"
|
|
#include "Settings/EditorLoadingSavingSettingsCustomization.h"
|
|
#include "Settings/GameMapsSettingsCustomization.h"
|
|
#include "Settings/LevelEditorPlaySettingsCustomization.h"
|
|
#include "Settings/ProjectPackagingSettingsCustomization.h"
|
|
#include "Settings/LevelEditorPlayNetworkEmulationSettings.h"
|
|
#include "StatsViewerModule.h"
|
|
#include "SnappingUtils.h"
|
|
#include "PackageAutoSaver.h"
|
|
#include "DDCNotifications.h"
|
|
#include "PerformanceMonitor.h"
|
|
#include "BSPOps.h"
|
|
#include "SourceCodeNavigation.h"
|
|
#include "AutoReimport/AutoReimportManager.h"
|
|
#include "Framework/Notifications/NotificationManager.h"
|
|
#include "Widgets/Notifications/SNotificationList.h"
|
|
#include "AutoReimport/AssetSourceFilenameCache.h"
|
|
#include "UObject/UObjectThreadContext.h"
|
|
#include "EngineUtils.h"
|
|
#include "EngineAnalytics.h"
|
|
#include "CookerSettings.h"
|
|
#include "Logging/MessageLog.h"
|
|
#include "Misc/MessageDialog.h"
|
|
#include "Logging/MessageLog.h"
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogUnrealEdEngine, Log, All);
|
|
|
|
void UUnrealEdEngine::Init(IEngineLoop* InEngineLoop)
|
|
{
|
|
Super::Init(InEngineLoop);
|
|
|
|
// Display warnings to the user about disk space issues
|
|
ValidateFreeDiskSpace();
|
|
|
|
// Build databases used by source code navigation
|
|
FSourceCodeNavigation::Initialize();
|
|
|
|
PackageAutoSaver.Reset(new FPackageAutoSaver);
|
|
PackageAutoSaver->LoadRestoreFile();
|
|
|
|
#if !UE_BUILD_DEBUG
|
|
if (!GEditorSettingsIni.IsEmpty())
|
|
{
|
|
// We need the game agnostic ini for this code
|
|
PerformanceMonitor = new FPerformanceMonitor;
|
|
}
|
|
#endif
|
|
|
|
// Register for the package dirty state updated callback to catch packages that have been modified and need to be checked out.
|
|
UPackage::PackageDirtyStateChangedEvent.AddUObject(this, &UUnrealEdEngine::OnPackageDirtyStateUpdated);
|
|
|
|
// Set-up the initial set of content mount paths to check for write permision
|
|
TArray<FString> RootPaths;
|
|
FPackageName::QueryRootContentPaths(RootPaths);
|
|
for (const FString& RootPath: RootPaths)
|
|
{
|
|
VerifyMountPointWritePermission(*RootPath);
|
|
}
|
|
// Watch for new content mount paths
|
|
FPackageName::OnContentPathMounted().AddUObject(this, &UUnrealEdEngine::OnContentPathMounted);
|
|
FPackageName::OnContentPathDismounted().AddUObject(this, &UUnrealEdEngine::OnContentPathDismounted);
|
|
|
|
// Register to the PostGarbageCollect delegate, as we want to use this to trigger the RefreshAllBroweser delegate from
|
|
// here rather then from Core
|
|
FCoreUObjectDelegates::GetPostGarbageCollect().AddUObject(this, &UUnrealEdEngine::OnPostGarbageCollect);
|
|
|
|
// register to color picker changed event and trigger RedrawAllViewports when that happens */
|
|
FCoreDelegates::ColorPickerChanged.AddUObject(this, &UUnrealEdEngine::OnColorPickerChanged);
|
|
|
|
// register windows message pre and post handler
|
|
FEditorSupportDelegates::PreWindowsMessage.AddUObject(this, &UUnrealEdEngine::OnPreWindowsMessage);
|
|
FEditorSupportDelegates::PostWindowsMessage.AddUObject(this, &UUnrealEdEngine::OnPostWindowsMessage);
|
|
|
|
USelection::SelectionChangedEvent.AddUObject(this, &UUnrealEdEngine::OnEditorSelectionChanged);
|
|
|
|
// Initialize the snap manager
|
|
FSnappingUtils::InitEditorSnappingTools();
|
|
|
|
// Register for notification of volume changes
|
|
AVolume::GetOnVolumeShapeChangedDelegate().AddStatic(&FBSPOps::HandleVolumeShapeChanged);
|
|
|
|
// Iterate over all always fully loaded packages and load them.
|
|
if (!IsRunningCommandlet())
|
|
{
|
|
DDCNotifications.Reset(new FDDCNotifications);
|
|
|
|
for (int32 PackageNameIndex = 0; PackageNameIndex < PackagesToBeFullyLoadedAtStartup.Num(); PackageNameIndex++)
|
|
{
|
|
const FString& PackageName = PackagesToBeFullyLoadedAtStartup[PackageNameIndex];
|
|
// Load package if it's found in the package file cache.
|
|
if (FPackageName::DoesPackageExist(PackageName))
|
|
{
|
|
LoadPackage(NULL, *PackageName, LOAD_None);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Populate the data structures related to the sprite category visibility feature for use elsewhere in the editor later
|
|
TArray<FSpriteCategoryInfo> SortedSpriteInfo;
|
|
UUnrealEdEngine::MakeSortedSpriteInfo(SortedSpriteInfo);
|
|
|
|
// Iterate over the sorted list, constructing a mapping of unlocalized categories to the index the localized category
|
|
// resides in. This is an optimization to prevent having to localize values repeatedly.
|
|
for (int32 InfoIndex = 0; InfoIndex < SortedSpriteInfo.Num(); ++InfoIndex)
|
|
{
|
|
const FSpriteCategoryInfo& SpriteInfo = SortedSpriteInfo[InfoIndex];
|
|
SpriteIDToIndexMap.Add(SpriteInfo.Category, InfoIndex);
|
|
}
|
|
|
|
if (FPaths::IsProjectFilePathSet() && GIsEditor && !FApp::IsUnattended())
|
|
{
|
|
AutoReimportManager = NewObject<UAutoReimportManager>();
|
|
AutoReimportManager->Initialize();
|
|
}
|
|
|
|
// register details panel customizations
|
|
if (!HasAnyFlags(RF_ClassDefaultObject))
|
|
{
|
|
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
|
|
|
|
PropertyModule.RegisterCustomClassLayout("EditorLoadingSavingSettings", FOnGetDetailCustomizationInstance::CreateStatic(&FEditorLoadingSavingSettingsCustomization::MakeInstance));
|
|
PropertyModule.RegisterCustomClassLayout("GameMapsSettings", FOnGetDetailCustomizationInstance::CreateStatic(&FGameMapsSettingsCustomization::MakeInstance));
|
|
PropertyModule.RegisterCustomClassLayout("LevelEditorPlaySettings", FOnGetDetailCustomizationInstance::CreateStatic(&FLevelEditorPlaySettingsCustomization::MakeInstance));
|
|
PropertyModule.RegisterCustomClassLayout("ProjectPackagingSettings", FOnGetDetailCustomizationInstance::CreateStatic(&FProjectPackagingSettingsCustomization::MakeInstance));
|
|
|
|
PropertyModule.RegisterCustomPropertyTypeLayout("LevelEditorPlayNetworkEmulationSettings", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FLevelEditorPlayNetworkEmulationSettingsDetail::MakeInstance));
|
|
}
|
|
|
|
if (!IsRunningCommandlet())
|
|
{
|
|
UEditorExperimentalSettings const* ExperimentalSettings = GetDefault<UEditorExperimentalSettings>();
|
|
UCookerSettings const* CookerSettings = GetDefault<UCookerSettings>();
|
|
ECookInitializationFlags BaseCookingFlags = ECookInitializationFlags::AutoTick | ECookInitializationFlags::AsyncSave;
|
|
BaseCookingFlags |= CookerSettings->bEnableBuildDDCInBackground ? ECookInitializationFlags::BuildDDCInBackground : ECookInitializationFlags::None;
|
|
|
|
if (CookerSettings->bIterativeCookingForLaunchOn)
|
|
{
|
|
BaseCookingFlags |= ECookInitializationFlags::Iterative;
|
|
BaseCookingFlags |= CookerSettings->bIgnoreIniSettingsOutOfDateForIteration ? ECookInitializationFlags::IgnoreIniSettingsOutOfDate : ECookInitializationFlags::None;
|
|
BaseCookingFlags |= CookerSettings->bIgnoreScriptPackagesOutOfDateForIteration ? ECookInitializationFlags::IgnoreScriptPackagesOutOfDate : ECookInitializationFlags::None;
|
|
}
|
|
|
|
if (CookerSettings->bEnableCookOnTheSide)
|
|
{
|
|
if (ExperimentalSettings->bSharedCookedBuilds)
|
|
{
|
|
BaseCookingFlags |= ECookInitializationFlags::IterateSharedBuild | ECookInitializationFlags::IgnoreIniSettingsOutOfDate;
|
|
}
|
|
|
|
CookServer = NewObject<UCookOnTheFlyServer>();
|
|
CookServer->Initialize(ECookMode::CookOnTheFlyFromTheEditor, BaseCookingFlags);
|
|
CookServer->StartNetworkFileServer(false);
|
|
}
|
|
else if (!ExperimentalSettings->bDisableCookInEditor)
|
|
{
|
|
CookServer = NewObject<UCookOnTheFlyServer>();
|
|
CookServer->Initialize(ECookMode::CookByTheBookFromTheEditor, BaseCookingFlags);
|
|
}
|
|
}
|
|
|
|
if (FParse::Param(FCommandLine::Get(), TEXT("nomcp")))
|
|
{
|
|
// If our editor has nomcp, pass it through to any subprocesses.
|
|
FCommandLine::AddToSubprocessCommandline(TEXT(" -nomcp"));
|
|
}
|
|
|
|
bPivotMovedIndependently = false;
|
|
}
|
|
|
|
bool CanCookForPlatformInThisProcess(const FString& PlatformName)
|
|
{
|
|
////////////////////////////////////////
|
|
// hack remove this hack when we properly support changing the mobileHDR setting
|
|
// check if our mobile hdr setting in memory is different from the one which is saved in the config file
|
|
|
|
FConfigFile PlatformEngineIni;
|
|
GConfig->LoadLocalIniFile(PlatformEngineIni, TEXT("Engine"), true, *PlatformName);
|
|
|
|
FString IniValueString;
|
|
bool ConfigSetting = false;
|
|
if (PlatformEngineIni.GetString(TEXT("/Script/Engine.RendererSettings"), TEXT("r.MobileHDR"), IniValueString) == false)
|
|
{
|
|
// must always match the RSetting setting because we don't have a config setting
|
|
return true;
|
|
}
|
|
ConfigSetting = IniValueString.ToBool();
|
|
|
|
// this was stolen from void IsMobileHDR()
|
|
static TConsoleVariableData<int32>* MobileHDRCvar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.MobileHDR"));
|
|
const bool CurrentRSetting = MobileHDRCvar->GetValueOnAnyThread() == 1;
|
|
|
|
if (CurrentRSetting != ConfigSetting)
|
|
{
|
|
UE_LOG(LogUnrealEdEngine, Warning, TEXT("Unable to use cook in editor because r.MobileHDR from Engine ini doesn't match console value r.MobileHDR"));
|
|
return false;
|
|
}
|
|
////////////////////////////////////////
|
|
return true;
|
|
}
|
|
|
|
bool UUnrealEdEngine::CanCookByTheBookInEditor(const FString& PlatformName) const
|
|
{
|
|
if (!CookServer)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!CanCookForPlatformInThisProcess(PlatformName))
|
|
{
|
|
CookServer->ClearAllCookedData();
|
|
return false;
|
|
}
|
|
|
|
return CookServer->GetCookMode() == ECookMode::CookByTheBookFromTheEditor;
|
|
}
|
|
|
|
bool UUnrealEdEngine::CanCookOnTheFlyInEditor(const FString& PlatformName) const
|
|
{
|
|
if (!CookServer)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!CanCookForPlatformInThisProcess(PlatformName))
|
|
{
|
|
CookServer->ClearAllCookedData();
|
|
return false;
|
|
}
|
|
|
|
return CookServer->GetCookMode() == ECookMode::CookOnTheFlyFromTheEditor;
|
|
}
|
|
|
|
void UUnrealEdEngine::StartCookByTheBookInEditor(const TArray<ITargetPlatform*>& TargetPlatforms, const TArray<FString>& CookMaps, const TArray<FString>& CookDirectories, const TArray<FString>& CookCultures, const TArray<FString>& IniMapSections)
|
|
{
|
|
UCookOnTheFlyServer::FCookByTheBookStartupOptions StartupOptions;
|
|
StartupOptions.CookMaps = CookMaps;
|
|
StartupOptions.TargetPlatforms = TargetPlatforms;
|
|
StartupOptions.CookDirectories = CookDirectories;
|
|
StartupOptions.CookCultures = CookCultures;
|
|
StartupOptions.IniMapSections = IniMapSections;
|
|
|
|
CookServer->StartCookByTheBook(StartupOptions);
|
|
}
|
|
|
|
bool UUnrealEdEngine::IsCookByTheBookInEditorFinished() const
|
|
{
|
|
return CookServer ? !CookServer->IsCookByTheBookRunning() : true;
|
|
}
|
|
|
|
void UUnrealEdEngine::CancelCookByTheBookInEditor()
|
|
{
|
|
CookServer->QueueCancelCookByTheBook();
|
|
}
|
|
|
|
void UUnrealEdEngine::MakeSortedSpriteInfo(TArray<FSpriteCategoryInfo>& OutSortedSpriteInfo)
|
|
{
|
|
struct Local
|
|
{
|
|
static void AddSortedSpriteInfo(TArray<FSpriteCategoryInfo>& InOutSortedSpriteInfo, const FSpriteCategoryInfo& InSpriteInfo)
|
|
{
|
|
const FSpriteCategoryInfo* ExistingSpriteInfo = InOutSortedSpriteInfo.FindByPredicate([&InSpriteInfo](const FSpriteCategoryInfo& SpriteInfo)
|
|
{
|
|
return InSpriteInfo.Category == SpriteInfo.Category;
|
|
});
|
|
if (ExistingSpriteInfo != NULL)
|
|
{
|
|
// Already present
|
|
checkSlow(ExistingSpriteInfo->DisplayName.EqualTo(InSpriteInfo.DisplayName)); // Catch mismatches between DisplayNames
|
|
}
|
|
else
|
|
{
|
|
// Add the category to the correct position in the array to keep it sorted
|
|
const int32 CatIndex = InOutSortedSpriteInfo.IndexOfByPredicate([&InSpriteInfo](const FSpriteCategoryInfo& SpriteInfo)
|
|
{
|
|
return InSpriteInfo.DisplayName.CompareTo(SpriteInfo.DisplayName) < 0;
|
|
});
|
|
if (CatIndex != INDEX_NONE)
|
|
{
|
|
InOutSortedSpriteInfo.Insert(InSpriteInfo, CatIndex);
|
|
}
|
|
else
|
|
{
|
|
InOutSortedSpriteInfo.Add(InSpriteInfo);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Iterate over all classes searching for those which derive from AActor and are neither deprecated nor abstract.
|
|
// It would be nice to only check placeable classes here, but we cannot do that as some non-placeable classes
|
|
// still end up in the editor (with sprites) procedurally, such as prefab instances and landscape actors.
|
|
for (UClass* Class: TObjectRange<UClass>())
|
|
{
|
|
if (Class->IsChildOf(AActor::StaticClass()) && !(Class->HasAnyClassFlags(CLASS_Abstract | CLASS_Deprecated)))
|
|
{
|
|
// Check if the class default actor has billboard components or arrow components that should be treated as
|
|
// sprites, and if so, add their categories to the array
|
|
const AActor* CurDefaultClassActor = Class->GetDefaultObject<AActor>();
|
|
if (CurDefaultClassActor)
|
|
{
|
|
for (UActorComponent* Comp: CurDefaultClassActor->GetComponents())
|
|
{
|
|
const UBillboardComponent* CurSpriteComponent = Cast<UBillboardComponent>(Comp);
|
|
const UArrowComponent* CurArrowComponent = (CurSpriteComponent ? nullptr : Cast<UArrowComponent>(Comp));
|
|
if (CurSpriteComponent)
|
|
{
|
|
Local::AddSortedSpriteInfo(OutSortedSpriteInfo, CurSpriteComponent->SpriteInfo);
|
|
}
|
|
else if (CurArrowComponent && CurArrowComponent->bTreatAsASprite)
|
|
{
|
|
Local::AddSortedSpriteInfo(OutSortedSpriteInfo, CurArrowComponent->SpriteInfo);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// It wont find sounds, but we want it to be there
|
|
{
|
|
FSpriteCategoryInfo SpriteInfo;
|
|
SpriteInfo.Category = TEXT("Sounds");
|
|
SpriteInfo.DisplayName = NSLOCTEXT("SpriteCategory", "Sounds", "Sounds");
|
|
Local::AddSortedSpriteInfo(OutSortedSpriteInfo, SpriteInfo);
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::PreExit()
|
|
{
|
|
FAssetSourceFilenameCache::Get().Shutdown();
|
|
|
|
Super::PreExit();
|
|
}
|
|
|
|
UUnrealEdEngine::~UUnrealEdEngine()
|
|
{
|
|
if (this == GUnrealEd)
|
|
{
|
|
GUnrealEd = NULL;
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::FinishDestroy()
|
|
{
|
|
if (CookServer)
|
|
{
|
|
FCoreUObjectDelegates::OnObjectPropertyChanged.RemoveAll(CookServer);
|
|
FCoreUObjectDelegates::OnObjectModified.RemoveAll(CookServer);
|
|
}
|
|
|
|
if (PackageAutoSaver.Get())
|
|
{
|
|
// We've finished shutting down, so disable the auto-save restore
|
|
PackageAutoSaver->UpdateRestoreFile(false);
|
|
PackageAutoSaver.Reset();
|
|
}
|
|
|
|
DDCNotifications.Reset();
|
|
|
|
if (PerformanceMonitor)
|
|
{
|
|
delete PerformanceMonitor;
|
|
}
|
|
|
|
FPackageName::OnContentPathMounted().RemoveAll(this);
|
|
FPackageName::OnContentPathDismounted().RemoveAll(this);
|
|
UPackage::PackageDirtyStateChangedEvent.RemoveAll(this);
|
|
FCoreUObjectDelegates::GetPostGarbageCollect().RemoveAll(this);
|
|
FCoreDelegates::ColorPickerChanged.RemoveAll(this);
|
|
Super::FinishDestroy();
|
|
}
|
|
|
|
void UUnrealEdEngine::Tick(float DeltaSeconds, bool bIdleMode)
|
|
{
|
|
Super::Tick(DeltaSeconds, bIdleMode);
|
|
|
|
// Increment the "seconds since last autosave" counter, then try to autosave.
|
|
if (!GSlowTaskOccurred)
|
|
{
|
|
// Don't increment autosave count while in game/pie/automation testing or while in Matinee
|
|
const bool PauseAutosave = (PlayWorld != nullptr) || GIsAutomationTesting;
|
|
if (!PauseAutosave && PackageAutoSaver.Get())
|
|
{
|
|
PackageAutoSaver->UpdateAutoSaveCount(DeltaSeconds);
|
|
}
|
|
}
|
|
if (!GIsSlowTask)
|
|
{
|
|
GSlowTaskOccurred = false;
|
|
}
|
|
|
|
// Display any load errors that happened while starting up the editor.
|
|
static bool bFirstTick = true;
|
|
if (bFirstTick)
|
|
{
|
|
FEditorDelegates::DisplayLoadErrors.Broadcast();
|
|
}
|
|
bFirstTick = false;
|
|
|
|
if (PackageAutoSaver.Get())
|
|
{
|
|
PackageAutoSaver->AttemptAutoSave();
|
|
}
|
|
|
|
// Try and notify the user about modified packages needing checkout and write permission warnings
|
|
AttemptModifiedPackageNotification();
|
|
|
|
// Update lightmass
|
|
UpdateBuildLighting();
|
|
}
|
|
|
|
void UUnrealEdEngine::OnPackageDirtyStateUpdated(UPackage* Pkg)
|
|
{
|
|
// The passed in object should never be NULL
|
|
check(Pkg);
|
|
|
|
UPackage* Package = Pkg->GetOutermost();
|
|
const FString PackageName = Package->GetName();
|
|
|
|
if (Package->IsDirty())
|
|
{
|
|
// Find out if we have already asked the user to modify this package
|
|
const uint8* PromptState = PackageToNotifyState.Find(Package);
|
|
const bool bAlreadyAsked = PromptState != NULL;
|
|
|
|
// During an autosave, packages are saved in the autosave directory which switches off their dirty flags.
|
|
// To preserve the pre-autosave state, any saved package is then remarked as dirty because it wasn't saved in the normal location where it would be picked up by source control.
|
|
// Any callback that happens during an autosave is bogus since a package wasn't marked dirty due to a user modification.
|
|
const bool bIsAutoSaving = PackageAutoSaver.Get() && PackageAutoSaver->IsAutoSaving();
|
|
|
|
if (!bIsAutoSaving &&
|
|
!GIsEditorLoadingPackage && // Don't ask if the package was modified as a result of a load
|
|
!GIsCookerLoadingPackage) // don't ask if the package was modified as a result of a cooker load
|
|
{
|
|
PackagesDirtiedThisTick.Add(Package);
|
|
|
|
const UEditorLoadingSavingSettings* Settings = GetDefault<UEditorLoadingSavingSettings>();
|
|
if (!bAlreadyAsked && // Don't ask if we already asked once!
|
|
(Settings->bPromptForCheckoutOnAssetModification || Settings->bAutomaticallyCheckoutOnAssetModification))
|
|
{
|
|
PackageToNotifyState.Add(Package, NS_Updating);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// This package was saved, the user should be prompted again if they checked in the package
|
|
PackagesDirtiedThisTick.Remove(Package);
|
|
PackageToNotifyState.Remove(Package);
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::AttemptModifiedPackageNotification()
|
|
{
|
|
bool bIsCooking = CookServer && CookServer->IsCookingInEditor() && CookServer->IsCookByTheBookRunning();
|
|
|
|
if (bShowPackageNotification && !bIsCooking)
|
|
{
|
|
ShowPackageNotification();
|
|
}
|
|
|
|
bool bShowWritePermissionWarning = false;
|
|
FMessageLog EditorLog("EditorErrors");
|
|
if (PackagesDirtiedThisTick.Num() > 0 && !bIsCooking)
|
|
{
|
|
// Force source control state to be updated
|
|
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
|
|
|
|
FString PackageName;
|
|
TArray<FString> Files;
|
|
TArray<TWeakObjectPtr<UPackage>> Packages;
|
|
for (const TWeakObjectPtr<UPackage>& Package: PackagesDirtiedThisTick)
|
|
{
|
|
if (Package.IsValid())
|
|
{
|
|
Packages.Add(Package);
|
|
Files.Add(SourceControlHelpers::PackageFilename(Package.Get()));
|
|
|
|
// if we do not have write permission under the mount point for this package log an error in the message log to link to.
|
|
PackageName = Package->GetName();
|
|
if (!HasMountWritePersmissionForPackage(PackageName))
|
|
{
|
|
bShowWritePermissionWarning = true;
|
|
EditorLog.Warning(FText::Format(NSLOCTEXT("UnrealEd", "WritePermissionFailureLog", "Insufficient writing permission to save {0}"), FText::FromString(PackageName)));
|
|
}
|
|
}
|
|
}
|
|
SourceControlProvider.Execute(ISourceControlOperation::Create<FUpdateStatus>(), SourceControlHelpers::AbsoluteFilenames(Files), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateUObject(this, &UUnrealEdEngine::OnSourceControlStateUpdated, Packages));
|
|
}
|
|
|
|
if (bShowWritePermissionWarning)
|
|
{
|
|
// Display a toast notification when a writing permission failure occurs.
|
|
FText WarningText = NSLOCTEXT("UnrealEd", "WritePermissionFailureNotification", "Insufficient permission to save content.");
|
|
if (!WritePermissionWarningNotificationWeakPtr.IsValid())
|
|
{
|
|
FNotificationInfo WarningNotification(WarningText);
|
|
WarningNotification.bFireAndForget = true;
|
|
WarningNotification.Hyperlink = FSimpleDelegate::CreateLambda([]()
|
|
{
|
|
FMessageLog("EditorErrors").Open();
|
|
});
|
|
WarningNotification.HyperlinkText = NSLOCTEXT("UnrealEd", "WritePermissionFailureHyperlink", "Open Message Log");
|
|
WarningNotification.ExpireDuration = 6.0f;
|
|
WarningNotification.bUseThrobber = false;
|
|
|
|
// For adding notifications.
|
|
WritePermissionWarningNotificationWeakPtr = FSlateNotificationManager::Get().AddNotification(WarningNotification);
|
|
}
|
|
else
|
|
{
|
|
WritePermissionWarningNotificationWeakPtr.Pin()->SetText(WarningText);
|
|
WritePermissionWarningNotificationWeakPtr.Pin()->ExpireAndFadeout();
|
|
}
|
|
}
|
|
|
|
PackagesDirtiedThisTick.Empty();
|
|
}
|
|
|
|
void UUnrealEdEngine::OnSourceControlStateUpdated(const FSourceControlOperationRef& SourceControlOp, ECommandResult::Type ResultType, TArray<TWeakObjectPtr<UPackage>> Packages)
|
|
{
|
|
if (ResultType == ECommandResult::Succeeded)
|
|
{
|
|
// Get the source control state of the package
|
|
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
|
|
|
|
TArray<TWeakObjectPtr<UPackage>> PackagesToAutomaticallyCheckOut;
|
|
TArray<FString> FilesToAutomaticallyCheckOut;
|
|
|
|
const UEditorLoadingSavingSettings* Settings = GetDefault<UEditorLoadingSavingSettings>();
|
|
for (const TWeakObjectPtr<UPackage>& PackagePtr: Packages)
|
|
{
|
|
if (PackagePtr.IsValid())
|
|
{
|
|
UPackage* Package = PackagePtr.Get();
|
|
|
|
FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(Package, EStateCacheUsage::Use);
|
|
if (SourceControlState.IsValid())
|
|
{
|
|
if (SourceControlState->CanCheckout())
|
|
{
|
|
// If this package is checked out or modified in another branch
|
|
if (SourceControlState->IsCheckedOutOrModifiedInOtherBranch())
|
|
{
|
|
PackageToNotifyState.Add(PackagePtr, NS_PendingWarning);
|
|
bShowPackageNotification = true;
|
|
}
|
|
else
|
|
{
|
|
if (Settings->bAutomaticallyCheckoutOnAssetModification)
|
|
{
|
|
PackagesToAutomaticallyCheckOut.Add(PackagePtr);
|
|
FilesToAutomaticallyCheckOut.Add(SourceControlHelpers::PackageFilename(Package));
|
|
}
|
|
else
|
|
{
|
|
PackageToNotifyState.Add(PackagePtr, NS_PendingPrompt);
|
|
bShowPackageNotification = true;
|
|
}
|
|
}
|
|
}
|
|
else if (!SourceControlState->IsCurrent() || SourceControlState->IsCheckedOutOther())
|
|
{
|
|
PackageToNotifyState.Add(PackagePtr, NS_PendingWarning);
|
|
bShowPackageNotification = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (FilesToAutomaticallyCheckOut.Num() > 0)
|
|
{
|
|
SourceControlProvider.Execute(ISourceControlOperation::Create<FCheckOut>(), SourceControlHelpers::AbsoluteFilenames(FilesToAutomaticallyCheckOut), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateUObject(this, &UUnrealEdEngine::OnPackagesCheckedOut, PackagesToAutomaticallyCheckOut));
|
|
}
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::OnPackagesCheckedOut(const FSourceControlOperationRef& SourceControlOp, ECommandResult::Type ResultType, TArray<TWeakObjectPtr<UPackage>> Packages)
|
|
{
|
|
if (ResultType == ECommandResult::Succeeded)
|
|
{
|
|
FNotificationInfo Notification(NSLOCTEXT("SourceControl", "AutoCheckOutNotification", "Packages automatically checked out."));
|
|
Notification.bFireAndForget = true;
|
|
Notification.ExpireDuration = 4.0f;
|
|
Notification.bUseThrobber = true;
|
|
|
|
FSlateNotificationManager::Get().AddNotification(Notification);
|
|
|
|
for (const TWeakObjectPtr<UPackage>& Package: Packages)
|
|
{
|
|
PackageToNotifyState.Add(Package, NS_DialogPrompted);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FNotificationInfo ErrorNotification(NSLOCTEXT("SourceControl", "AutoCheckOutFailedNotification", "Unable to automatically check out packages."));
|
|
ErrorNotification.bFireAndForget = true;
|
|
ErrorNotification.ExpireDuration = 4.0f;
|
|
ErrorNotification.bUseThrobber = true;
|
|
|
|
FSlateNotificationManager::Get().AddNotification(ErrorNotification);
|
|
|
|
for (const TWeakObjectPtr<UPackage>& Package: Packages)
|
|
{
|
|
PackageToNotifyState.Add(Package, NS_PendingPrompt);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::OnPostGarbageCollect()
|
|
{
|
|
// Refresh Editor browsers after GC in case objects where removed. Note that if the user is currently
|
|
// playing in a PIE level, we don't want to interrupt performance by refreshing the Generic Browser window.
|
|
if (GIsEditor && !GIsPlayInEditorWorld)
|
|
{
|
|
FEditorDelegates::RefreshAllBrowsers.Broadcast();
|
|
}
|
|
|
|
// Clean up any GCed packages in the PackageToNotifyState
|
|
for (TMap<TWeakObjectPtr<UPackage>, uint8>::TIterator It(PackageToNotifyState); It; ++It)
|
|
{
|
|
if (It.Key().IsValid() == false)
|
|
{
|
|
It.RemoveCurrent();
|
|
}
|
|
}
|
|
|
|
RedrawAllViewports();
|
|
}
|
|
|
|
void UUnrealEdEngine::OnColorPickerChanged()
|
|
{
|
|
FEditorSupportDelegates::RedrawAllViewports.Broadcast();
|
|
|
|
FEditorSupportDelegates::PreWindowsMessage.RemoveAll(this);
|
|
FEditorSupportDelegates::PostWindowsMessage.RemoveAll(this);
|
|
}
|
|
|
|
UWorld* SavedGWorld = NULL;
|
|
|
|
void UUnrealEdEngine::OnPreWindowsMessage(FViewport* Viewport, uint32 Message)
|
|
{
|
|
// Make sure the proper GWorld is set before handling the windows message
|
|
if (GameViewport && !bIsSimulatingInEditor && GameViewport->Viewport == Viewport && !GIsPlayInEditorWorld)
|
|
{
|
|
// remember the current GWorld that will be restored in the PostWindowsMessage callback
|
|
SavedGWorld = GWorld;
|
|
SetPlayInEditorWorld(PlayWorld);
|
|
}
|
|
else
|
|
{
|
|
SavedGWorld = NULL;
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::OnPostWindowsMessage(FViewport* Viewport, uint32 Message)
|
|
{
|
|
if (SavedGWorld)
|
|
{
|
|
RestoreEditorWorld(SavedGWorld);
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::OnOpenMatinee()
|
|
{
|
|
// Register a delegate to pickup when Matinee is closed.
|
|
UpdateEdModeOnMatineeCloseDelegateHandle = GLevelEditorModeTools().OnEditorModeIDChanged().AddUObject(this, &UUnrealEdEngine::UpdateEdModeOnMatineeClose);
|
|
}
|
|
|
|
bool UUnrealEdEngine::IsAutosaving() const
|
|
{
|
|
if (PackageAutoSaver)
|
|
{
|
|
return PackageAutoSaver->IsAutoSaving();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void UUnrealEdEngine::ConvertMatinees()
|
|
{
|
|
FVector StartLocation = FVector::ZeroVector;
|
|
UWorld* World = GWorld;
|
|
if (World)
|
|
{
|
|
ULevel* Level = World->GetCurrentLevel();
|
|
if (!Level)
|
|
{
|
|
Level = World->PersistentLevel;
|
|
}
|
|
check(Level);
|
|
for (TObjectIterator<UInterpData> It; It; ++It)
|
|
{
|
|
UInterpData* InterpData = *It;
|
|
if (InterpData->IsIn(Level))
|
|
{
|
|
// We dont care about renaming references or adding redirectors. References to this will be old seqact_interps
|
|
RenameObject(InterpData, Level->GetOutermost(), *InterpData->GetName());
|
|
|
|
AMatineeActor* MatineeActor = Level->OwningWorld->SpawnActor<AMatineeActor>(StartLocation, FRotator::ZeroRotator);
|
|
StartLocation.Y += 50;
|
|
|
|
MatineeActor->MatineeData = InterpData;
|
|
FProperty* MatineeDataProp = NULL;
|
|
for (FProperty* Property = MatineeActor->GetClass()->PropertyLink; Property != NULL; Property = Property->PropertyLinkNext)
|
|
{
|
|
if (Property->GetName() == TEXT("MatineeData"))
|
|
{
|
|
MatineeDataProp = Property;
|
|
break;
|
|
}
|
|
}
|
|
|
|
FPropertyChangedEvent PropertyChangedEvent(MatineeDataProp);
|
|
MatineeActor->PostEditChangeProperty(PropertyChangedEvent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::ShowActorProperties()
|
|
{
|
|
// See if we have any unlocked property windows available. If not, create a new one.
|
|
if (FSlateApplication::IsInitialized())
|
|
{
|
|
IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame"));
|
|
|
|
bool bHasUnlockedViews = false;
|
|
|
|
FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>(TEXT("PropertyEditor"));
|
|
bHasUnlockedViews = PropertyEditorModule.HasUnlockedDetailViews();
|
|
|
|
// If the slate main frame is shown, summon a new property viewer in the Level editor module
|
|
if (MainFrameModule.IsWindowInitialized())
|
|
{
|
|
FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked<FLevelEditorModule>(TEXT("LevelEditor"));
|
|
LevelEditorModule.SummonSelectionDetails();
|
|
}
|
|
|
|
if (!bHasUnlockedViews)
|
|
{
|
|
UpdateFloatingPropertyWindows();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool UUnrealEdEngine::GetMapBuildCancelled() const
|
|
{
|
|
return FUnrealEdMisc::Get().GetMapBuildCancelled();
|
|
}
|
|
|
|
void UUnrealEdEngine::SetMapBuildCancelled(bool InCancelled)
|
|
{
|
|
FUnrealEdMisc::Get().SetMapBuildCancelled(InCancelled);
|
|
}
|
|
|
|
// namespace to match the original in the old loc system
|
|
#define LOCTEXT_NAMESPACE "UnrealEd"
|
|
|
|
FText FClassPickerDefaults::GetName() const
|
|
{
|
|
FText Result = LOCTEXT("NullClass", "(null class)");
|
|
|
|
if (UClass* ItemClass = LoadClass<UObject>(NULL, *ClassName, NULL, LOAD_None, NULL))
|
|
{
|
|
Result = ItemClass->GetDisplayNameText();
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
FText FClassPickerDefaults::GetDescription() const
|
|
{
|
|
FText Result = LOCTEXT("NullClass", "(null class)");
|
|
|
|
if (UClass* ItemClass = LoadClass<UObject>(NULL, *ClassName, NULL, LOAD_None, NULL))
|
|
{
|
|
Result = ItemClass->GetToolTipText(/*bShortTooltip=*/true);
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|
|
|
|
UUnrealEdKeyBindings::UUnrealEdKeyBindings(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
}
|
|
|
|
UUnrealEdOptions::UUnrealEdOptions(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
}
|
|
|
|
void UUnrealEdOptions::PostInitProperties()
|
|
{
|
|
Super::PostInitProperties();
|
|
if (!HasAnyFlags(RF_ClassDefaultObject | RF_NeedLoad))
|
|
{
|
|
EditorKeyBindings = NewObject<UUnrealEdKeyBindings>(this, FName("EditorKeyBindingsInst"));
|
|
}
|
|
}
|
|
|
|
UUnrealEdOptions* UUnrealEdEngine::GetUnrealEdOptions()
|
|
{
|
|
if (EditorOptionsInst == NULL)
|
|
{
|
|
EditorOptionsInst = NewObject<UUnrealEdOptions>();
|
|
}
|
|
|
|
return EditorOptionsInst;
|
|
}
|
|
|
|
void UUnrealEdEngine::CloseEditor()
|
|
{
|
|
check(GEngine);
|
|
|
|
// if PIE is still happening, stop it before doing anything
|
|
if (PlayWorld)
|
|
{
|
|
EndPlayMap();
|
|
}
|
|
|
|
// End any play on console/pc games still happening
|
|
EndPlayOnLocalPc();
|
|
|
|
// Can't use FPlatformMisc::RequestExit as it uses PostQuitMessage which is not what we want here.
|
|
RequestEngineExit(TEXT("UUnrealEdEngine::CloseEditor()"));
|
|
}
|
|
|
|
bool UUnrealEdEngine::AllowSelectTranslucent() const
|
|
{
|
|
return GetDefault<UEditorPerProjectUserSettings>()->bAllowSelectTranslucent;
|
|
}
|
|
|
|
bool UUnrealEdEngine::OnlyLoadEditorVisibleLevelsInPIE() const
|
|
{
|
|
return GetDefault<ULevelEditorPlaySettings>()->bOnlyLoadVisibleLevelsInPIE;
|
|
}
|
|
|
|
bool UUnrealEdEngine::PreferToStreamLevelsInPIE() const
|
|
{
|
|
return GetDefault<ULevelEditorPlaySettings>()->bPreferToStreamLevelsInPIE;
|
|
}
|
|
|
|
void UUnrealEdEngine::RedrawLevelEditingViewports(bool bInvalidateHitProxies)
|
|
{
|
|
// Redraw Slate based viewports
|
|
if (FModuleManager::Get().IsModuleLoaded("LevelEditor"))
|
|
{
|
|
FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked<FLevelEditorModule>("LevelEditor");
|
|
LevelEditor.BroadcastRedrawViewports(bInvalidateHitProxies);
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::TakeHighResScreenShots()
|
|
{
|
|
// Tell all viewports to take a screenshot
|
|
if (FModuleManager::Get().IsModuleLoaded("LevelEditor"))
|
|
{
|
|
FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked<FLevelEditorModule>("LevelEditor");
|
|
LevelEditor.BroadcastTakeHighResScreenShots();
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::SetCurrentClass(UClass* InClass)
|
|
{
|
|
USelection* SelectionSet = GetSelectedObjects();
|
|
SelectionSet->DeselectAll(UClass::StaticClass());
|
|
|
|
if (InClass != NULL)
|
|
{
|
|
SelectionSet->Select(InClass);
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::GetPackageList(TArray<UPackage*>* InPackages, UClass* InClass)
|
|
{
|
|
InPackages->Empty();
|
|
|
|
for (FThreadSafeObjectIterator It; It; ++It)
|
|
{
|
|
if (It->GetOuter() && It->GetOuter() != GetTransientPackage())
|
|
{
|
|
UObject* TopParent = NULL;
|
|
|
|
if (InClass == NULL || It->IsA(InClass))
|
|
TopParent = It->GetOutermost();
|
|
|
|
if (Cast<UPackage>(TopParent))
|
|
InPackages->AddUnique((UPackage*)TopParent);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool UUnrealEdEngine::CanSavePackage(UPackage* PackageToSave)
|
|
{
|
|
return HasMountWritePersmissionForPackage(PackageToSave->GetName());
|
|
}
|
|
|
|
UThumbnailManager* UUnrealEdEngine::GetThumbnailManager()
|
|
{
|
|
return &(UThumbnailManager::Get());
|
|
}
|
|
|
|
void UUnrealEdEngine::Serialize(FArchive& Ar)
|
|
{
|
|
Super::Serialize(Ar);
|
|
Ar << MaterialCopyPasteBuffer;
|
|
Ar << AnimationCompressionAlgorithms;
|
|
Ar << MatineeCopyPasteBuffer;
|
|
}
|
|
|
|
void UUnrealEdEngine::MakeSelectedActorsLevelCurrent()
|
|
{
|
|
ULevel* LevelToMakeCurrent = NULL;
|
|
|
|
// Look to the selected actors for the level to make current.
|
|
// If actors from multiple levels are selected, do nothing.
|
|
for (FSelectionIterator It(GetSelectedActorIterator()); It; ++It)
|
|
{
|
|
AActor* Actor = static_cast<AActor*>(*It);
|
|
checkSlow(Actor->IsA(AActor::StaticClass()));
|
|
|
|
ULevel* ActorLevel = Actor->GetLevel();
|
|
|
|
if (!LevelToMakeCurrent)
|
|
{
|
|
// First assignment.
|
|
LevelToMakeCurrent = ActorLevel;
|
|
}
|
|
else if (LevelToMakeCurrent != ActorLevel)
|
|
{
|
|
// Actors from multiple levels are selected -- abort.
|
|
LevelToMakeCurrent = NULL;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (LevelToMakeCurrent)
|
|
{
|
|
if (!LevelToMakeCurrent->IsCurrentLevel())
|
|
{
|
|
// Change the current level to something different
|
|
EditorLevelUtils::MakeLevelCurrent(LevelToMakeCurrent);
|
|
}
|
|
// Set the Level tab's selected level.
|
|
TArray<class ULevel*> SelectedLevelsList(&LevelToMakeCurrent, 1);
|
|
LevelToMakeCurrent->GetWorld()->SetSelectedLevels(SelectedLevelsList);
|
|
}
|
|
}
|
|
|
|
int32 UUnrealEdEngine::GetSpriteCategoryIndex(const FName& InSpriteCategory)
|
|
{
|
|
// Find the sprite category in the unlocalized to index map, if possible
|
|
const int32* CategoryIndexPtr = SpriteIDToIndexMap.Find(InSpriteCategory);
|
|
|
|
const int32 CategoryIndex = CategoryIndexPtr ? *CategoryIndexPtr : INDEX_NONE;
|
|
|
|
return CategoryIndex;
|
|
}
|
|
|
|
void UUnrealEdEngine::ShowLightingStaticMeshInfoWindow()
|
|
{
|
|
// first invoke the stats viewer tab
|
|
FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked<FLevelEditorModule>(TEXT("LevelEditor"));
|
|
TSharedPtr<FTabManager> LevelEditorTabManager = LevelEditorModule.GetLevelEditorTabManager();
|
|
LevelEditorTabManager->TryInvokeTab(FName("LevelEditorStatsViewer"));
|
|
|
|
// then switch pages
|
|
FStatsViewerModule& StatsViewerModule = FModuleManager::Get().LoadModuleChecked<FStatsViewerModule>(TEXT("StatsViewer"));
|
|
StatsViewerModule.GetPage(EStatsPage::StaticMeshLightingInfo)->Show();
|
|
}
|
|
|
|
void UUnrealEdEngine::OpenSceneStatsWindow()
|
|
{
|
|
// first invoke the stats viewer tab
|
|
FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked<FLevelEditorModule>(TEXT("LevelEditor"));
|
|
TSharedPtr<FTabManager> LevelEditorTabManager = LevelEditorModule.GetLevelEditorTabManager();
|
|
LevelEditorTabManager->TryInvokeTab(FName("LevelEditorStatsViewer"));
|
|
|
|
// then switch pages
|
|
FStatsViewerModule& StatsViewerModule = FModuleManager::Get().LoadModuleChecked<FStatsViewerModule>(TEXT("StatsViewer"));
|
|
StatsViewerModule.GetPage(EStatsPage::PrimitiveStats)->Show();
|
|
}
|
|
|
|
void UUnrealEdEngine::OpenTextureStatsWindow()
|
|
{
|
|
// first invoke the stats viewer tab
|
|
FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked<FLevelEditorModule>(TEXT("LevelEditor"));
|
|
TSharedPtr<FTabManager> LevelEditorTabManager = LevelEditorModule.GetLevelEditorTabManager();
|
|
LevelEditorTabManager->TryInvokeTab(FName("LevelEditorStatsViewer"));
|
|
|
|
// then switch pages
|
|
FStatsViewerModule& StatsViewerModule = FModuleManager::Get().LoadModuleChecked<FStatsViewerModule>(TEXT("StatsViewer"));
|
|
StatsViewerModule.GetPage(EStatsPage::TextureStats)->Show();
|
|
}
|
|
|
|
void UUnrealEdEngine::GetSortedVolumeClasses(TArray<UClass*>* VolumeClasses)
|
|
{
|
|
// Add all of the volume classes to the passed in array and then sort it
|
|
for (UClass* Class: TObjectRange<UClass>())
|
|
{
|
|
if (Class->IsChildOf(AVolume::StaticClass()) && !Class->HasAnyClassFlags(CLASS_Deprecated | CLASS_Abstract | CLASS_NotPlaceable) && Class->ClassGeneratedBy == nullptr)
|
|
{
|
|
VolumeClasses->AddUnique(Class);
|
|
}
|
|
}
|
|
|
|
VolumeClasses->Sort();
|
|
}
|
|
|
|
void UUnrealEdOptions::GenerateCommandMap()
|
|
{
|
|
CommandMap.Empty();
|
|
for (int32 CmdIdx = 0; CmdIdx < EditorCommands.Num(); CmdIdx++)
|
|
{
|
|
FEditorCommand& Cmd = EditorCommands[CmdIdx];
|
|
|
|
CommandMap.Add(Cmd.CommandName, CmdIdx);
|
|
}
|
|
}
|
|
|
|
FString UUnrealEdOptions::GetExecCommand(FKey Key, bool bAltDown, bool bCtrlDown, bool bShiftDown, FName EditorSet)
|
|
{
|
|
TArray<FEditorKeyBinding>& KeyBindings = EditorKeyBindings->KeyBindings;
|
|
FString Result;
|
|
|
|
for (int32 BindingIdx = 0; BindingIdx < KeyBindings.Num(); BindingIdx++)
|
|
{
|
|
FEditorKeyBinding& Binding = KeyBindings[BindingIdx];
|
|
int32* CommandIdx = CommandMap.Find(Binding.CommandName);
|
|
|
|
if (CommandIdx && EditorCommands.IsValidIndex(*CommandIdx))
|
|
{
|
|
FEditorCommand& Cmd = EditorCommands[*CommandIdx];
|
|
|
|
if (Cmd.Parent == EditorSet)
|
|
{
|
|
// See if this key binding matches the key combination passed in.
|
|
if (bAltDown == Binding.bAltDown && bCtrlDown == Binding.bCtrlDown && bShiftDown == Binding.bShiftDown && Key == Binding.Key)
|
|
{
|
|
int32* EditorCommandIdx = CommandMap.Find(Binding.CommandName);
|
|
|
|
if (EditorCommandIdx && EditorCommands.IsValidIndex(*EditorCommandIdx))
|
|
{
|
|
FEditorCommand& EditorCommand = EditorCommands[*EditorCommandIdx];
|
|
Result = EditorCommand.ExecCommand;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
/**
|
|
* Does the update for volume actor visibility
|
|
*
|
|
* @param ActorsToUpdate The list of actors to update
|
|
* @param ViewClient The viewport client to apply visibility changes to
|
|
*/
|
|
static void InternalUpdateVolumeActorVisibility(TArray<AActor*>& ActorsToUpdate, const FLevelEditorViewportClient& ViewClient, TArray<AActor*>& OutActorsThatChanged)
|
|
{
|
|
for (int32 ActorIdx = 0; ActorIdx < ActorsToUpdate.Num(); ++ActorIdx)
|
|
{
|
|
AVolume* VolumeToUpdate = Cast<AVolume>(ActorsToUpdate[ActorIdx]);
|
|
|
|
if (VolumeToUpdate)
|
|
{
|
|
const bool bIsVisible = ViewClient.IsVolumeVisibleInViewport(*VolumeToUpdate);
|
|
|
|
uint64 OriginalViews = VolumeToUpdate->HiddenEditorViews;
|
|
if (bIsVisible)
|
|
{
|
|
// If the actor should be visible, unset the bit for the actor in this viewport
|
|
VolumeToUpdate->HiddenEditorViews &= ~((uint64)1 << ViewClient.ViewIndex);
|
|
}
|
|
else
|
|
{
|
|
if (VolumeToUpdate->IsSelected())
|
|
{
|
|
// We are hiding the actor, make sure its not selected anymore
|
|
GEditor->SelectActor(VolumeToUpdate, false, true);
|
|
}
|
|
|
|
// If the actor should be hidden, set the bit for the actor in this viewport
|
|
VolumeToUpdate->HiddenEditorViews |= ((uint64)1 << ViewClient.ViewIndex);
|
|
}
|
|
|
|
if (OriginalViews != VolumeToUpdate->HiddenEditorViews)
|
|
{
|
|
// At least one actor has visibility changes
|
|
OutActorsThatChanged.AddUnique(VolumeToUpdate);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::UpdateVolumeActorVisibility(UClass* InVolumeActorClass, FLevelEditorViewportClient* InViewport)
|
|
{
|
|
TSubclassOf<AActor> VolumeClassToCheck = InVolumeActorClass ? InVolumeActorClass : AVolume::StaticClass();
|
|
|
|
// Build a list of actors that need to be updated. Only take actors of the passed in volume class.
|
|
UWorld* World = InViewport ? InViewport->GetWorld() : GWorld;
|
|
TArray<AActor*> ActorsToUpdate;
|
|
for (TActorIterator<AActor> It(World, VolumeClassToCheck); It; ++It)
|
|
{
|
|
ActorsToUpdate.Add(*It);
|
|
}
|
|
|
|
if (ActorsToUpdate.Num() > 0)
|
|
{
|
|
TArray<AActor*> ActorsThatChanged;
|
|
if (!InViewport)
|
|
{
|
|
// Update the visibility state of each actor for each viewport
|
|
for (FLevelEditorViewportClient* ViewClient: GetLevelViewportClients())
|
|
{
|
|
// Only update the editor frame clients as those are the only viewports right now that show volumes.
|
|
InternalUpdateVolumeActorVisibility(ActorsToUpdate, *ViewClient, ActorsThatChanged);
|
|
if (ActorsThatChanged.Num())
|
|
{
|
|
// If actor visibility changed in the viewport, it needs to be redrawn
|
|
ViewClient->Invalidate();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Only update the editor frame clients as those are the only viewports right now that show volumes.
|
|
InternalUpdateVolumeActorVisibility(ActorsToUpdate, *InViewport, ActorsThatChanged);
|
|
if (ActorsThatChanged.Num())
|
|
{
|
|
// If actor visibility changed in the viewport, it needs to be redrawn
|
|
InViewport->Invalidate();
|
|
}
|
|
}
|
|
|
|
// Push all changes in the actors to the scene proxy so the render thread correctly updates visibility
|
|
for (int32 ActorIdx = 0; ActorIdx < ActorsThatChanged.Num(); ++ActorIdx)
|
|
{
|
|
AActor* ActorToUpdate = ActorsThatChanged[ActorIdx];
|
|
|
|
// Find all registered primitive components and update the scene proxy with the actors updated visibility map
|
|
TInlineComponentArray<UPrimitiveComponent*> PrimitiveComponents;
|
|
ActorToUpdate->GetComponents(PrimitiveComponents);
|
|
|
|
for (int32 ComponentIdx = 0; ComponentIdx < PrimitiveComponents.Num(); ++ComponentIdx)
|
|
{
|
|
UPrimitiveComponent* PrimitiveComponent = PrimitiveComponents[ComponentIdx];
|
|
if (PrimitiveComponent->IsRegistered())
|
|
{
|
|
// Push visibility to the render thread
|
|
PrimitiveComponent->PushEditorVisibilityToProxy(ActorToUpdate->HiddenEditorViews);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::FixAnyInvertedBrushes(UWorld* World)
|
|
{
|
|
// Get list of brushes with inverted polys
|
|
TArray<ABrush*> Brushes;
|
|
for (TActorIterator<ABrush> It(World); It; ++It)
|
|
{
|
|
ABrush* Brush = *It;
|
|
if (Brush->GetBrushComponent() && Brush->GetBrushComponent()->HasInvertedPolys())
|
|
{
|
|
Brushes.Add(Brush);
|
|
}
|
|
}
|
|
|
|
if (Brushes.Num() > 0)
|
|
{
|
|
for (ABrush* Brush: Brushes)
|
|
{
|
|
UE_LOG(LogUnrealEdEngine, Warning, TEXT("Brush '%s' appears to be inside out - fixing."), *Brush->GetName());
|
|
|
|
// Invert the polys of the brush
|
|
for (FPoly& Poly: Brush->GetBrushComponent()->Brush->Polys->Element)
|
|
{
|
|
Poly.Reverse();
|
|
Poly.CalcNormal();
|
|
}
|
|
|
|
if (Brush->IsStaticBrush())
|
|
{
|
|
// Static brushes require a full BSP rebuild
|
|
ABrush::SetNeedRebuild(Brush->GetLevel());
|
|
}
|
|
else
|
|
{
|
|
// Dynamic brushes can be fixed up here
|
|
FBSPOps::csgPrepMovingBrush(Brush);
|
|
Brush->GetBrushComponent()->BuildSimpleBrushCollision();
|
|
}
|
|
|
|
Brush->MarkPackageDirty();
|
|
}
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::RegisterComponentVisualizer(FName ComponentClassName, TSharedPtr<FComponentVisualizer> Visualizer)
|
|
{
|
|
if (!ComponentClassName.IsNone())
|
|
{
|
|
ComponentVisualizerMap.Add(ComponentClassName, Visualizer);
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::UnregisterComponentVisualizer(FName ComponentClassName)
|
|
{
|
|
TSharedPtr<FComponentVisualizer> Visualizer = FindComponentVisualizer(ComponentClassName);
|
|
VisualizersForSelection.RemoveAll([&Visualizer](const auto& CachedComponentVisualizer)
|
|
{
|
|
return CachedComponentVisualizer.Visualizer == Visualizer;
|
|
});
|
|
|
|
ComponentVisualizerMap.Remove(ComponentClassName);
|
|
}
|
|
|
|
TSharedPtr<FComponentVisualizer> UUnrealEdEngine::FindComponentVisualizer(FName ComponentClassName) const
|
|
{
|
|
TSharedPtr<FComponentVisualizer> Visualizer = NULL;
|
|
|
|
const TSharedPtr<FComponentVisualizer>* VisualizerPtr = ComponentVisualizerMap.Find(ComponentClassName);
|
|
if (VisualizerPtr != NULL)
|
|
{
|
|
Visualizer = *VisualizerPtr;
|
|
}
|
|
|
|
return Visualizer;
|
|
}
|
|
|
|
/** Find a component visualizer for the given component class (checking parent classes too) */
|
|
TSharedPtr<class FComponentVisualizer> UUnrealEdEngine::FindComponentVisualizer(UClass* ComponentClass) const
|
|
{
|
|
TSharedPtr<FComponentVisualizer> Visualizer;
|
|
while (!Visualizer.IsValid() && (ComponentClass != nullptr) && (ComponentClass != UActorComponent::StaticClass()))
|
|
{
|
|
Visualizer = FindComponentVisualizer(ComponentClass->GetFName());
|
|
ComponentClass = ComponentClass->GetSuperClass();
|
|
}
|
|
|
|
return Visualizer;
|
|
}
|
|
|
|
void UUnrealEdEngine::DrawComponentVisualizers(const FSceneView* View, FPrimitiveDrawInterface* PDI)
|
|
{
|
|
for (FCachedComponentVisualizer& CachedVisualizer: VisualizersForSelection)
|
|
{
|
|
CachedVisualizer.Visualizer->DrawVisualization(CachedVisualizer.ComponentPropertyPath.GetComponent(), View, PDI);
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::DrawComponentVisualizersHUD(const FViewport* Viewport, const FSceneView* View, FCanvas* Canvas)
|
|
{
|
|
for (FCachedComponentVisualizer& CachedVisualizer: VisualizersForSelection)
|
|
{
|
|
CachedVisualizer.Visualizer->DrawVisualizationHUD(CachedVisualizer.ComponentPropertyPath.GetComponent(), Viewport, View, Canvas);
|
|
}
|
|
}
|
|
|
|
void UUnrealEdEngine::OnEditorSelectionChanged(UObject* SelectionThatChanged)
|
|
{
|
|
auto GetVisualizersForSelection = [&](AActor* Actor)
|
|
{
|
|
// Iterate over components of that actor (and recurse through child components)
|
|
TInlineComponentArray<UActorComponent*> Components;
|
|
Actor->GetComponents(Components, true);
|
|
|
|
for (int32 CompIdx = 0; CompIdx < Components.Num(); CompIdx++)
|
|
{
|
|
UActorComponent* Comp = Components[CompIdx];
|
|
if (Comp->IsRegistered())
|
|
{
|
|
// Try and find a visualizer
|
|
TSharedPtr<FComponentVisualizer> Visualizer = FindComponentVisualizer(Comp->GetClass());
|
|
if (Visualizer.IsValid())
|
|
{
|
|
VisualizersForSelection.Add(FCachedComponentVisualizer(Comp, Visualizer));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
if (SelectionThatChanged == GetSelectedActors())
|
|
{
|
|
// actor selection changed. Update the list of component visualizers
|
|
// This is expensive so we do not search for visualizers each time they want to draw
|
|
VisualizersForSelection.Empty();
|
|
|
|
// Iterate over all selected actors
|
|
for (FSelectionIterator It(GetSelectedActorIterator()); It; ++It)
|
|
{
|
|
AActor* Actor = Cast<AActor>(*It);
|
|
if (Actor != nullptr)
|
|
{
|
|
GetVisualizersForSelection(Actor);
|
|
}
|
|
}
|
|
}
|
|
else if (SelectionThatChanged == GetSelectedComponents())
|
|
{
|
|
if (USelection* Selection = Cast<USelection>(SelectionThatChanged))
|
|
{
|
|
// Do not proceed if the selection contains no components. This occurs when a component is
|
|
// deselected while selecting its owner actor. But a corresponding actor selection is not invoked
|
|
// so if the visualizers are cleared here, they will not be properly reset for the selected actor.
|
|
if (Selection->Num() > 0)
|
|
{
|
|
VisualizersForSelection.Empty();
|
|
|
|
TArray<AActor*> ActorsProcessed;
|
|
|
|
// Iterate over all selected components
|
|
for (FSelectionIterator It(GetSelectedComponentIterator()); It; ++It)
|
|
{
|
|
if (UActorComponent* Comp = Cast<UActorComponent>(*It))
|
|
{
|
|
if (AActor* Actor = Comp->GetOwner())
|
|
{
|
|
if (!ActorsProcessed.Contains(Actor))
|
|
{
|
|
GetVisualizersForSelection(Actor);
|
|
ActorsProcessed.Emplace(Actor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If there is an undo/redo operation in progress, restore the active component visualizer.
|
|
if (GIsTransacting)
|
|
{
|
|
for (FCachedComponentVisualizer& CachedComponentVisualizer: VisualizersForSelection)
|
|
{
|
|
if (CachedComponentVisualizer.Visualizer->GetEditedComponent() != nullptr)
|
|
{
|
|
ComponentVisManager.SetActiveComponentVis(GCurrentLevelEditingViewportClient, CachedComponentVisualizer.Visualizer);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool UUnrealEdEngine::HasMountWritePersmissionForPackage(const FString& PackageName)
|
|
{
|
|
FName MountPoint = FPackageName::GetPackageMountPoint(PackageName, false);
|
|
if (const bool* bWritePermission = MountPointCheckedForWritePermission.Find(MountPoint))
|
|
{
|
|
return *bWritePermission;
|
|
}
|
|
return VerifyMountPointWritePermission(MountPoint);
|
|
}
|
|
|
|
bool UUnrealEdEngine::VerifyMountPointWritePermission(FName MountPoint)
|
|
{
|
|
// If mount is unknown assume write permissions are fine
|
|
if (MountPoint.IsNone())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Get a unique temp filename under the mount point
|
|
FString WriteCheckPath = FPackageName::LongPackageNameToFilename(MountPoint.ToString()) + TEXT("WritePermissions.") + FGuid::NewGuid().ToString() + TEXT(".temp");
|
|
|
|
// Check if we could successfully write to a file
|
|
bool bHasWritePermission = FFileHelper::SaveStringToFile(TEXT("Write"), *WriteCheckPath);
|
|
if (bHasWritePermission)
|
|
{
|
|
// We can successfully write to the folder containing the package.
|
|
// Delete the temp file.
|
|
IFileManager::Get().Delete(*WriteCheckPath);
|
|
}
|
|
|
|
// Add the result to the mount point checks
|
|
MountPointCheckedForWritePermission.Add(MountPoint, bHasWritePermission);
|
|
return bHasWritePermission;
|
|
}
|
|
|
|
void UUnrealEdEngine::OnContentPathMounted(const FString& AssetPath, const FString& /*FileSystemPath*/)
|
|
{
|
|
VerifyMountPointWritePermission(*AssetPath);
|
|
}
|
|
|
|
void UUnrealEdEngine::OnContentPathDismounted(const FString& AssetPath, const FString& /*FileSystemPath*/)
|
|
{
|
|
MountPointCheckedForWritePermission.Remove(*AssetPath);
|
|
}
|
|
void UUnrealEdEngine::UpdateEdModeOnMatineeClose(const FEditorModeID& EditorModeID, bool IsEntering)
|
|
{
|
|
// if we are closing the Matinee editor
|
|
if (!IsEntering && EditorModeID == FBuiltinEditorModes::EM_InterpEdit)
|
|
{
|
|
// set the autosave timer to save soon
|
|
if (PackageAutoSaver)
|
|
{
|
|
PackageAutoSaver->ForceMinimumTimeTillAutoSave();
|
|
}
|
|
|
|
// Remove this delegate.
|
|
GLevelEditorModeTools().OnEditorModeIDChanged().Remove(UpdateEdModeOnMatineeCloseDelegateHandle);
|
|
}
|
|
}
|
|
|
|
bool IsBelowFreeDiskSpaceLimit(const TCHAR* TestDir, FText& OutAppendMessage, const FText& LocationDescriptor, const uint64 MinMB = 5120)
|
|
{
|
|
uint64 TotalDiskSpace = 0;
|
|
uint64 FreeDiskSpace = 0;
|
|
|
|
if (FPlatformMisc::GetDiskTotalAndFreeSpace(TestDir, TotalDiskSpace, FreeDiskSpace))
|
|
{
|
|
const uint64 HardDriveFreeMB = FreeDiskSpace / (1024 * 1024);
|
|
if (HardDriveFreeMB < MinMB)
|
|
{
|
|
static const FText AppendWarning = NSLOCTEXT("DriveSpaceDialog", "LowHardDriveSpaceFormatMsg", "{0}\n {1} MB Free \t\t {2}\n \t\t\t\t {3} \n");
|
|
|
|
OutAppendMessage = FText::Format(AppendWarning, OutAppendMessage, HardDriveFreeMB, FText::FromString(FPaths::ConvertRelativePathToFull(TestDir)), LocationDescriptor);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void UUnrealEdEngine::ValidateFreeDiskSpace() const
|
|
{
|
|
FText Message = NSLOCTEXT("DriveSpaceDialog", "LowHardDriveSpaceMsgHeader", "The following drive locations have limited free space.\nIt is recommended that you free some space to avoid issues such as crashed and data loss due to files not being able to be written and saved.\n");
|
|
|
|
bool bShowWarning = false;
|
|
bShowWarning |= IsBelowFreeDiskSpaceLimit(FPlatformProcess::BaseDir(), Message, NSLOCTEXT("DriveSpaceDialog", "BaseDirDescriptor", "The base engine directory."));
|
|
bShowWarning |= IsBelowFreeDiskSpaceLimit(FPlatformMisc::ProjectDir(), Message, NSLOCTEXT("DriveSpaceDialog", "ProjectDirDescriptor", "The project directory."));
|
|
bShowWarning |= IsBelowFreeDiskSpaceLimit(FPlatformProcess::UserDir(), Message, NSLOCTEXT("DriveSpaceDialog", "UserDirDescriptor", "User directory where user specific settings are stored."), 1024);
|
|
|
|
if (bShowWarning)
|
|
{
|
|
FEngineAnalytics::LowDriveSpaceDetected();
|
|
|
|
const FText Title = NSLOCTEXT("DriveSpaceDialog", "LowHardDriveSpaceMsgTitle", "Low drive space warning");
|
|
|
|
FMessageDialog::Open(EAppMsgType::Ok, Message, &Title);
|
|
}
|
|
}
|