Files
UnrealEngineUWP/Engine/Source/Editor/ContentBrowser/Private/ContentBrowserUtils.cpp
2015-04-26 10:47:22 -04:00

1664 lines
53 KiB
C++

// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
#include "ContentBrowserPCH.h"
#include "Editor/UnrealEd/Public/Toolkits/AssetEditorManager.h"
#include "PackageTools.h"
#include "ObjectTools.h"
#include "ImageUtils.h"
#include "ISourceControlModule.h"
#include "MessageLog.h"
#include "EngineBuildSettings.h"
#include "SNotificationList.h"
#include "NotificationManager.h"
#include "IPluginManager.h"
#define LOCTEXT_NAMESPACE "ContentBrowser"
#define MAX_CLASS_NAME_LENGTH 32 // Enforce a reasonable class name length so the path is not too long for PLATFORM_MAX_FILEPATH_LENGTH
namespace ContentBrowserUtils
{
// Keep a map of all the paths that have custom colors, so updating the color in one location updates them all
static TMap< FString, TSharedPtr< FLinearColor > > PathColors;
}
class SContentBrowserPopup : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS( SContentBrowserPopup ){}
SLATE_ATTRIBUTE( FText, Message )
SLATE_END_ARGS()
/** Constructs this widget with InArgs */
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void Construct( const FArguments& InArgs )
{
ChildSlot
[
SNew(SBorder)
.BorderImage(FEditorStyle::GetBrush("Menu.Background"))
.Padding(10)
.OnMouseButtonDown(this, &SContentBrowserPopup::OnBorderClicked)
.BorderBackgroundColor(this, &SContentBrowserPopup::GetBorderBackgroundColor)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(0, 0, 4, 0)
[
SNew(SImage) .Image( FEditorStyle::GetBrush("ContentBrowser.PopupMessageIcon") )
]
+SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(InArgs._Message)
.WrapTextAt(450)
]
]
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
static void DisplayMessage( const FText& Message, const FSlateRect& ScreenAnchor, TSharedRef<SWidget> ParentContent )
{
TSharedRef<SContentBrowserPopup> PopupContent = SNew(SContentBrowserPopup) .Message(Message);
const FVector2D ScreenLocation = FVector2D(ScreenAnchor.Left, ScreenAnchor.Top);
const bool bFocusImmediately = true;
const bool bShouldAutoSize = true;
const FVector2D WindowSize = FVector2D::ZeroVector;
const FVector2D SummonLocationSize = ScreenAnchor.GetSize();
TSharedRef<SWindow> PopupWindow = FSlateApplication::Get().PushMenu(
ParentContent,
PopupContent,
ScreenLocation,
FPopupTransitionEffect( FPopupTransitionEffect::TopMenu ),
bFocusImmediately,
bShouldAutoSize,
WindowSize,
SummonLocationSize
);
PopupContent->SetWindow(PopupWindow);
}
private:
void SetWindow( const TSharedRef<SWindow>& InWindow )
{
Window = InWindow;
}
FReply OnBorderClicked(const FGeometry& Geometry, const FPointerEvent& MouseEvent)
{
if ( Window.IsValid() )
{
Window.Pin()->RequestDestroyWindow();
}
return FReply::Handled();
}
FSlateColor GetBorderBackgroundColor() const
{
return IsHovered() ? FLinearColor(0.5, 0.5, 0.5, 1) : FLinearColor::White;
}
private:
TWeakPtr<SWindow> Window;
};
/** A miniture confirmation popup for quick yes/no questions */
class SContentBrowserConfirmPopup : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS( SContentBrowserConfirmPopup ) {}
/** The text to display */
SLATE_ARGUMENT(FText, Prompt)
/** The Yes Button to display */
SLATE_ARGUMENT(FText, YesText)
/** The No Button to display */
SLATE_ARGUMENT(FText, NoText)
/** Invoked when yes is clicked */
SLATE_EVENT(FOnClicked, OnYesClicked)
/** Invoked when no is clicked */
SLATE_EVENT(FOnClicked, OnNoClicked)
SLATE_END_ARGS()
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void Construct( const FArguments& InArgs )
{
OnYesClicked = InArgs._OnYesClicked;
OnNoClicked = InArgs._OnNoClicked;
ChildSlot
[
SNew(SBorder)
. BorderImage(FEditorStyle::GetBrush("Menu.Background"))
. Padding(10)
[
SNew(SVerticalBox)
+SVerticalBox::Slot()
.AutoHeight()
.Padding(0, 0, 0, 5)
.HAlign(HAlign_Center)
[
SNew(STextBlock)
.Text(InArgs._Prompt)
]
+SVerticalBox::Slot()
.AutoHeight()
.HAlign(HAlign_Center)
[
SNew(SUniformGridPanel)
.SlotPadding(3)
+ SUniformGridPanel::Slot(0, 0)
.HAlign(HAlign_Fill)
[
SNew(SButton)
.HAlign(HAlign_Center)
.Text(InArgs._YesText)
.OnClicked( this, &SContentBrowserConfirmPopup::YesClicked )
]
+ SUniformGridPanel::Slot(1, 0)
.HAlign(HAlign_Fill)
[
SNew(SButton)
.HAlign(HAlign_Center)
.Text(InArgs._NoText)
.OnClicked( this, &SContentBrowserConfirmPopup::NoClicked )
]
]
]
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
/** Opens the popup using the specified component as its parent */
void OpenPopup(const TSharedRef<SWidget>& ParentContent)
{
// Show dialog to confirm the delete
PopupWindow = FSlateApplication::Get().PushMenu(
ParentContent,
SharedThis(this),
FSlateApplication::Get().GetCursorPos(),
FPopupTransitionEffect( FPopupTransitionEffect::TopMenu )
);
}
private:
/** The yes button was clicked */
FReply YesClicked()
{
if ( OnYesClicked.IsBound() )
{
OnYesClicked.Execute();
}
PopupWindow.Pin()->RequestDestroyWindow();
return FReply::Handled();
}
/** The no button was clicked */
FReply NoClicked()
{
if ( OnNoClicked.IsBound() )
{
OnNoClicked.Execute();
}
PopupWindow.Pin()->RequestDestroyWindow();
return FReply::Handled();
}
/** The window containing this popup */
TWeakPtr<SWindow> PopupWindow;
/** Delegates for button clicks */
FOnClicked OnYesClicked;
FOnClicked OnNoClicked;
};
bool ContentBrowserUtils::OpenEditorForAsset(const FString& ObjectPath)
{
// Load the asset if unloaded
TArray<UObject*> LoadedObjects;
TArray<FString> ObjectPaths;
ObjectPaths.Add(ObjectPath);
ContentBrowserUtils::LoadAssetsIfNeeded(ObjectPaths, LoadedObjects);
// Open the editor for the specified asset
UObject* FoundObject = FindObject<UObject>(NULL, *ObjectPath);
return OpenEditorForAsset(FoundObject);
}
bool ContentBrowserUtils::OpenEditorForAsset(UObject* Asset)
{
if( Asset != NULL )
{
// @todo toolkit minor: Needs world-centric support?
return FAssetEditorManager::Get().OpenEditorForAsset(Asset);
}
return false;
}
bool ContentBrowserUtils::OpenEditorForAsset(const TArray<UObject*>& Assets)
{
if ( Assets.Num() == 1 )
{
return OpenEditorForAsset(Assets[0]);
}
else if ( Assets.Num() > 1 )
{
return FAssetEditorManager::Get().OpenEditorForAssets(Assets);
}
return false;
}
bool ContentBrowserUtils::LoadAssetsIfNeeded(const TArray<FString>& ObjectPaths, TArray<UObject*>& LoadedObjects, bool bAllowedToPromptToLoadAssets, bool bLoadRedirects)
{
bool bAnyObjectsWereLoadedOrUpdated = false;
// Build a list of unloaded assets
TArray<FString> UnloadedObjectPaths;
bool bAtLeastOneUnloadedMap = false;
for (int32 PathIdx = 0; PathIdx < ObjectPaths.Num(); ++PathIdx)
{
const FString& ObjectPath = ObjectPaths[PathIdx];
UObject* FoundObject = FindObject<UObject>(NULL, *ObjectPath);
if ( FoundObject )
{
LoadedObjects.Add(FoundObject);
}
else
{
// Unloaded asset, we will load it later
UnloadedObjectPaths.Add(ObjectPath);
if ( FEditorFileUtils::IsMapPackageAsset(ObjectPath) )
{
bAtLeastOneUnloadedMap = true;
}
}
}
// if we are allowed to prompt the user to load and we have enough assets that requires prompting then we should
// prompt and load assets if the user said it was ok
bool bShouldLoadAssets = true;
if( bAllowedToPromptToLoadAssets && ShouldPromptToLoadAssets(ObjectPaths, UnloadedObjectPaths) )
{
bShouldLoadAssets = PromptToLoadAssets(ObjectPaths);
}
// Ask for confirmation if the user is attempting to load a large number of assets
if (bShouldLoadAssets == false)
{
return false;
}
// Make sure all selected objects are loaded, where possible
if ( UnloadedObjectPaths.Num() > 0 )
{
// Get the maximum objects to load before displaying the slow task
const bool bShowProgressDialog = (UnloadedObjectPaths.Num() > GetDefault<UContentBrowserSettings>()->NumObjectsToLoadBeforeWarning) || bAtLeastOneUnloadedMap;
GWarn->BeginSlowTask(LOCTEXT("LoadingObjects", "Loading Objects..."), bShowProgressDialog);
GIsEditorLoadingPackage = true;
// We usually don't want to follow redirects when loading objects for the Content Browser. It would
// allow a user to interact with a ghost/unverified asset as if it were still alive.
// This can be overridden by providing bLoadRedirects = true as a parameter.
const ELoadFlags LoadFlags = bLoadRedirects ? LOAD_None : LOAD_NoRedirects;
bool bSomeObjectsFailedToLoad = false;
for (int32 PathIdx = 0; PathIdx < UnloadedObjectPaths.Num(); ++PathIdx)
{
const FString& ObjectPath = UnloadedObjectPaths[PathIdx];
// Load up the object
UObject* LoadedObject = LoadObject<UObject>(NULL, *ObjectPath, NULL, LoadFlags, NULL);
if ( LoadedObject )
{
LoadedObjects.Add(LoadedObject);
}
else
{
bSomeObjectsFailedToLoad = true;
}
if ( bShowProgressDialog )
{
GWarn->UpdateProgress(PathIdx, UnloadedObjectPaths.Num());
}
if (GWarn->ReceivedUserCancel())
{
// If the user has canceled stop loading the remaining objects. We don't add the remaining objects to the failed string,
// this would only result in launching another dialog when by their actions the user clearly knows not all of the
// assets will have been loaded.
break;
}
}
GIsEditorLoadingPackage = false;
GWarn->EndSlowTask();
if ( bSomeObjectsFailedToLoad )
{
FNotificationInfo Info(LOCTEXT("LoadObjectFailed", "Failed to load assets"));
Info.ExpireDuration = 5.0f;
Info.Hyperlink = FSimpleDelegate::CreateStatic([](){ FMessageLog("LoadErrors").Open(EMessageSeverity::Info, true); });
Info.HyperlinkText = LOCTEXT("LoadObjectHyperlink", "Show Message Log");
FSlateNotificationManager::Get().AddNotification(Info);
return false;
}
}
return true;
}
bool ContentBrowserUtils::ShouldPromptToLoadAssets(const TArray<FString>& ObjectPaths, TArray<FString>& OutUnloadedObjects)
{
OutUnloadedObjects.Empty();
bool bShouldPrompt = false;
// Build a list of unloaded assets
for (int32 PathIdx = 0; PathIdx < ObjectPaths.Num(); ++PathIdx)
{
const FString& ObjectPath = ObjectPaths[PathIdx];
UObject* FoundObject = FindObject<UObject>(NULL, *ObjectPath);
if ( !FoundObject )
{
// Unloaded asset, we will load it later
OutUnloadedObjects.Add(ObjectPath);
}
}
// Get the maximum objects to load before displaying a warning
// Ask for confirmation if the user is attempting to load a large number of assets
if (OutUnloadedObjects.Num() > GetDefault<UContentBrowserSettings>()->NumObjectsToLoadBeforeWarning)
{
bShouldPrompt = true;
}
return bShouldPrompt;
}
bool ContentBrowserUtils::PromptToLoadAssets(const TArray<FString>& UnloadedObjects)
{
bool bShouldLoadAssets = false;
// Prompt the user to load assets
const FText Question = FText::Format( LOCTEXT("ConfirmLoadAssets", "You are about to load {0} assets. Would you like to proceed?"), FText::AsNumber( UnloadedObjects.Num() ) );
if ( EAppReturnType::Yes == FMessageDialog::Open(EAppMsgType::YesNo, Question) )
{
bShouldLoadAssets = true;
}
return bShouldLoadAssets;
}
void ContentBrowserUtils::RenameAsset(UObject* Asset, const FString& NewName, FText& ErrorMessage)
{
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
TArray<FAssetRenameData> AssetsAndNames;
const FString PackagePath = FPackageName::GetLongPackagePath(Asset->GetOutermost()->GetName());
new(AssetsAndNames) FAssetRenameData(Asset, PackagePath, NewName);
AssetToolsModule.Get().RenameAssets(AssetsAndNames);
}
void ContentBrowserUtils::CopyAssets(const TArray<UObject*>& Assets, const FString& DestPath)
{
TArray<UObject*> NewObjects;
ObjectTools::DuplicateObjects(Assets, TEXT(""), DestPath, /*bOpenDialog=*/false, &NewObjects);
// If any objects were duplicated, report the success
if ( NewObjects.Num() )
{
FFormatNamedArguments Args;
Args.Add( TEXT("Number"), NewObjects.Num() );
const FText Message = FText::Format( LOCTEXT("AssetsDroppedCopy", "{Number} asset(s) copied"), Args );
FSlateNotificationManager::Get().AddNotification(FNotificationInfo(Message));
// Now branch the files in source control if possible
check(Assets.Num() == NewObjects.Num());
for(int32 ObjectIndex = 0; ObjectIndex < Assets.Num(); ObjectIndex++)
{
UObject* SourceAsset = Assets[ObjectIndex];
UObject* DestAsset = NewObjects[ObjectIndex];
SourceControlHelpers::BranchPackage(DestAsset->GetOutermost(), SourceAsset->GetOutermost());
}
}
}
void ContentBrowserUtils::MoveAssets(const TArray<UObject*>& Assets, const FString& DestPath, const FString& SourcePath)
{
check(DestPath.Len() > 0);
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
TArray<FAssetRenameData> AssetsAndNames;
for ( auto AssetIt = Assets.CreateConstIterator(); AssetIt; ++AssetIt )
{
UObject* Asset = *AssetIt;
if ( !ensure(Asset) )
{
continue;
}
FString PackagePath;
FString ObjectName = Asset->GetName();
if ( SourcePath.Len() )
{
const FString CurrentPackageName = Asset->GetOutermost()->GetName();
// This is a relative operation
if ( !ensure(CurrentPackageName.StartsWith(SourcePath)) )
{
continue;
}
// Collect the relative path then use it to determine the new location
// For example, if SourcePath = /Game/MyPath and CurrentPackageName = /Game/MyPath/MySubPath/MyAsset
// /Game/MyPath/MySubPath/MyAsset -> /MySubPath
const int32 ShortPackageNameLen = FPackageName::GetLongPackageAssetName(CurrentPackageName).Len();
const int32 RelativePathLen = CurrentPackageName.Len() - ShortPackageNameLen - SourcePath.Len() - 1; // -1 to exclude the trailing "/"
const FString RelativeDestPath = CurrentPackageName.Mid(SourcePath.Len(), RelativePathLen);
PackagePath = DestPath + RelativeDestPath;
}
else
{
// Only a DestPath was supplied, use it
PackagePath = DestPath;
}
new(AssetsAndNames) FAssetRenameData(Asset, PackagePath, ObjectName);
}
if ( AssetsAndNames.Num() > 0 )
{
AssetToolsModule.Get().RenameAssets(AssetsAndNames);
}
}
int32 ContentBrowserUtils::DeleteAssets(const TArray<UObject*>& AssetsToDelete)
{
return ObjectTools::DeleteObjects(AssetsToDelete);
}
bool ContentBrowserUtils::DeleteFolders(const TArray<FString>& PathsToDelete)
{
// Get a list of assets in the paths to delete
TArray<FAssetData> AssetDataList;
GetAssetsInPaths(PathsToDelete, AssetDataList);
const int32 NumAssetsInPaths = AssetDataList.Num();
bool bAllowFolderDelete = false;
if ( NumAssetsInPaths == 0 )
{
// There were no assets, allow the folder delete.
bAllowFolderDelete = true;
}
else
{
// Load all the assets in the folder and attempt to delete them.
// If it was successful, allow the folder delete.
// Get a list of object paths for input into LoadAssetsIfNeeded
TArray<FString> ObjectPaths;
for ( auto AssetIt = AssetDataList.CreateConstIterator(); AssetIt; ++AssetIt )
{
ObjectPaths.Add((*AssetIt).ObjectPath.ToString());
}
// Load all the assets in the selected paths
TArray<UObject*> LoadedAssets;
if ( ContentBrowserUtils::LoadAssetsIfNeeded(ObjectPaths, LoadedAssets) )
{
// Make sure we loaded all of them
if ( LoadedAssets.Num() == NumAssetsInPaths )
{
const int32 NumAssetsDeleted = ContentBrowserUtils::DeleteAssets(LoadedAssets);
if ( NumAssetsDeleted == NumAssetsInPaths )
{
// Successfully deleted all assets in the specified path. Allow the folder to be removed.
bAllowFolderDelete = true;
}
else
{
// Not all the assets in the selected paths were deleted
}
}
else
{
// Not all the assets in the selected paths were loaded
}
}
else
{
// The user declined to load some assets or some assets failed to load
}
}
if ( bAllowFolderDelete )
{
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
for ( auto PathIt = PathsToDelete.CreateConstIterator(); PathIt; ++PathIt )
{
AssetRegistryModule.Get().RemovePath(*PathIt);
}
return true;
}
return false;
}
void ContentBrowserUtils::GetAssetsInPaths(const TArray<FString>& InPaths, TArray<FAssetData>& OutAssetDataList)
{
// Load the asset registry module
FAssetRegistryModule& AssetRegistryModule = FModuleManager::Get().LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
// Form a filter from the paths
FARFilter Filter;
Filter.bRecursivePaths = true;
for (int32 PathIdx = 0; PathIdx < InPaths.Num(); ++PathIdx)
{
new (Filter.PackagePaths) FName(*InPaths[PathIdx]);
}
// Query for a list of assets in the selected paths
AssetRegistryModule.Get().GetAssets(Filter, OutAssetDataList);
}
bool ContentBrowserUtils::SavePackages(const TArray<UPackage*>& Packages)
{
TArray< UPackage* > PackagesWithExternalRefs;
FString PackageNames;
if( PackageTools::CheckForReferencesToExternalPackages( &Packages, &PackagesWithExternalRefs ) )
{
for(int32 PkgIdx = 0; PkgIdx < PackagesWithExternalRefs.Num(); ++PkgIdx)
{
PackageNames += FString::Printf(TEXT("%s\n"), *PackagesWithExternalRefs[ PkgIdx ]->GetName());
}
bool bProceed = EAppReturnType::Yes == FMessageDialog::Open(
EAppMsgType::YesNo,
FText::Format(
NSLOCTEXT("UnrealEd", "Warning_ExternalPackageRef", "The following assets have references to external assets: \n{0}\nExternal assets won't be found when in a game and all references will be broken. Proceed?"),
FText::FromString(PackageNames) ) );
if(!bProceed)
{
return false;
}
}
const bool bCheckDirty = false;
const bool bPromptToSave = false;
const FEditorFileUtils::EPromptReturnCode Return = FEditorFileUtils::PromptForCheckoutAndSave(Packages, bCheckDirty, bPromptToSave);
return Return == FEditorFileUtils::EPromptReturnCode::PR_Success;
}
bool ContentBrowserUtils::SaveDirtyPackages()
{
const bool bPromptUserToSave = true;
const bool bSaveMapPackages = true;
const bool bSaveContentPackages = true;
const bool bFastSave = false;
const bool bNotifyNoPackagesSaved = false;
const bool bCanBeDeclined = false;
return FEditorFileUtils::SaveDirtyPackages( bPromptUserToSave, bSaveMapPackages, bSaveContentPackages, bFastSave, bNotifyNoPackagesSaved, bCanBeDeclined );
}
TArray<UPackage*> ContentBrowserUtils::LoadPackages(const TArray<FString>& PackageNames)
{
TArray<UPackage*> LoadedPackages;
GWarn->BeginSlowTask( LOCTEXT("LoadingPackages", "Loading Packages..."), true );
for (int32 PackageIdx = 0; PackageIdx < PackageNames.Num(); ++PackageIdx)
{
const FString& PackageName = PackageNames[PackageIdx];
if ( !ensure(PackageName.Len() > 0) )
{
// Empty package name. Skip it.
continue;
}
UPackage* Package = FindPackage(NULL, *PackageName);
if ( Package != NULL )
{
// The package is at least partially loaded. Fully load it.
Package->FullyLoad();
}
else
{
// The package is unloaded. Try to load the package from disk.
Package = PackageTools::LoadPackage(PackageName);
}
// If the package was loaded, add it to the loaded packages list.
if ( Package != NULL )
{
LoadedPackages.Add(Package);
}
}
GWarn->EndSlowTask();
return LoadedPackages;
}
void ContentBrowserUtils::DisplayMessage(const FText& Message, const FSlateRect& ScreenAnchor, const TSharedRef<SWidget>& ParentContent)
{
SContentBrowserPopup::DisplayMessage(Message, ScreenAnchor, ParentContent);
}
void ContentBrowserUtils::DisplayConfirmationPopup(const FText& Message, const FText& YesString, const FText& NoString, const TSharedRef<SWidget>& ParentContent, const FOnClicked& OnYesClicked, const FOnClicked& OnNoClicked)
{
TSharedRef<SContentBrowserConfirmPopup> Popup =
SNew(SContentBrowserConfirmPopup)
.Prompt(Message)
.YesText(YesString)
.NoText(NoString)
.OnYesClicked( OnYesClicked )
.OnNoClicked( OnNoClicked );
Popup->OpenPopup(ParentContent);
}
void ContentBrowserUtils::CopyFolders(const TArray<FString>& InSourcePathNames, const FString& DestPath)
{
TMap<FString, TArray<UObject*> > SourcePathToLoadedAssets;
// Make sure the destination path is not in the source path list
TArray<FString> SourcePathNames = InSourcePathNames;
SourcePathNames.Remove(DestPath);
// Load all assets in the source paths
PrepareFoldersForDragDrop(SourcePathNames, SourcePathToLoadedAssets);
// Load the Asset Registry to update paths during the copy
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
// For every path which contained valid assets...
for ( auto PathIt = SourcePathToLoadedAssets.CreateConstIterator(); PathIt; ++PathIt )
{
// Put dragged folders in a sub-folder under the destination path
FString SubFolderName = FPackageName::GetLongPackageAssetName(PathIt.Key());
FString Destination = DestPath + TEXT("/") + SubFolderName;
// Add the new path to notify sources views
AssetRegistryModule.Get().AddPath(Destination);
// If any assets were in this path...
if ( PathIt.Value().Num() > 0 )
{
// Copy assets and supply a source path to indicate it is relative
ObjectTools::DuplicateObjects( PathIt.Value(), PathIt.Key(), Destination, /*bOpenDialog=*/false );
}
}
}
void ContentBrowserUtils::MoveFolders(const TArray<FString>& InSourcePathNames, const FString& DestPath)
{
TMap<FString, TArray<UObject*> > SourcePathToLoadedAssets;
FString DestPathWithTrailingSlash = DestPath / "";
// Do not allow parent directories to be moved to themselves or children.
TArray<FString> SourcePathNames = InSourcePathNames;
TArray<FString> SourcePathNamesToRemove;
for (auto SourcePathIt = SourcePathNames.CreateConstIterator(); SourcePathIt; ++SourcePathIt)
{
if(DestPathWithTrailingSlash.StartsWith(*SourcePathIt / ""))
{
SourcePathNamesToRemove.Add(*SourcePathIt);
}
}
for (auto SourcePathToRemoveIt = SourcePathNamesToRemove.CreateConstIterator(); SourcePathToRemoveIt; ++SourcePathToRemoveIt)
{
SourcePathNames.Remove(*SourcePathToRemoveIt);
}
// Load all assets in the source paths
PrepareFoldersForDragDrop(SourcePathNames, SourcePathToLoadedAssets);
// Load the Asset Registry to update paths during the move
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
// For every path which contained valid assets...
for ( auto PathIt = SourcePathToLoadedAssets.CreateConstIterator(); PathIt; ++PathIt )
{
// Put dragged folders in a sub-folder under the destination path
const FString SourcePath = PathIt.Key();
const FString SubFolderName = FPackageName::GetLongPackageAssetName(SourcePath);
const FString Destination = DestPathWithTrailingSlash + SubFolderName;
// Add the new path to notify sources views
AssetRegistryModule.Get().AddPath(Destination);
// If any assets were in this path...
if ( PathIt.Value().Num() > 0 )
{
// Move assets and supply a source path to indicate it is relative
ContentBrowserUtils::MoveAssets( PathIt.Value(), Destination, PathIt.Key() );
}
// Attempt to remove the old paths. This operation will silently fail if any assets failed to move.
AssetRegistryModule.Get().RemovePath(SourcePath);
}
}
void ContentBrowserUtils::PrepareFoldersForDragDrop(const TArray<FString>& SourcePathNames, TMap< FString, TArray<UObject*> >& OutSourcePathToLoadedAssets)
{
TSet<UObject*> AllFoundObjects;
GWarn->BeginSlowTask( LOCTEXT("FolderDragDrop_Loading", "Loading folders"), true);
// Load the Asset Registry to update paths during the move
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
// Check up-front how many assets we might load in this operation & warn the user
TArray<FString> ObjectPathsToWarnAbout;
for ( auto PathIt = SourcePathNames.CreateConstIterator(); PathIt; ++PathIt )
{
// Get all assets in this path
TArray<FAssetData> AssetDataList;
AssetRegistryModule.Get().GetAssetsByPath(FName(**PathIt), AssetDataList, true);
for ( auto AssetIt = AssetDataList.CreateConstIterator(); AssetIt; ++AssetIt )
{
ObjectPathsToWarnAbout.Add((*AssetIt).ObjectPath.ToString());
}
}
TArray<FString> UnloadedObjects;
if(ShouldPromptToLoadAssets(ObjectPathsToWarnAbout, UnloadedObjects))
{
PromptToLoadAssets(UnloadedObjects);
}
// For every source path, load every package in the path (if necessary) and keep track of the assets that were loaded
for ( auto PathIt = SourcePathNames.CreateConstIterator(); PathIt; ++PathIt )
{
// Get all assets in this path
TArray<FAssetData> AssetDataList;
AssetRegistryModule.Get().GetAssetsByPath(FName(**PathIt), AssetDataList, true);
// Form a list of all object paths for these assets
TArray<FString> ObjectPaths;
for ( auto AssetIt = AssetDataList.CreateConstIterator(); AssetIt; ++AssetIt )
{
ObjectPaths.Add((*AssetIt).ObjectPath.ToString());
}
// Load all assets in this path if needed
TArray<UObject*> AllLoadedAssets;
LoadAssetsIfNeeded(ObjectPaths, AllLoadedAssets, false);
// Add a slash to the end of the path so StartsWith doesn't get a false positive on similarly named folders
const FString SourcePathWithSlash = *PathIt + TEXT("/");
// Find all files in this path and subpaths
TArray<FString> Filenames;
FString RootFolder = FPackageName::LongPackageNameToFilename(SourcePathWithSlash);
FPackageName::FindPackagesInDirectory(Filenames, RootFolder);
// Now find all assets in memory that were loaded from this path that are valid for drag-droppping
TArray<UObject*> ValidLoadedAssets;
for ( auto AssetIt = AllLoadedAssets.CreateConstIterator(); AssetIt; ++AssetIt )
{
UObject* Asset = *AssetIt;
if ( (Asset->GetClass() != UObjectRedirector::StaticClass() && // Skip object redirectors
!Asset->GetOutermost()->ContainsMap() && // Skip assets in maps
!AllFoundObjects.Contains(Asset) // Skip assets we have already found to avoid processing them twice
) )
{
ValidLoadedAssets.Add(Asset);
AllFoundObjects.Add(Asset);
}
}
// Add an entry of the map of source paths to assets found, whether any assets were found or not
OutSourcePathToLoadedAssets.Add(*PathIt, ValidLoadedAssets);
}
GWarn->EndSlowTask();
ensure(SourcePathNames.Num() == OutSourcePathToLoadedAssets.Num());
}
void ContentBrowserUtils::CopyAssetReferencesToClipboard(const TArray<FAssetData>& AssetsToCopy)
{
FString ClipboardText;
for ( auto AssetIt = AssetsToCopy.CreateConstIterator(); AssetIt; ++AssetIt)
{
if ( ClipboardText.Len() > 0 )
{
ClipboardText += LINE_TERMINATOR;
}
ClipboardText += (*AssetIt).GetExportTextName();
}
FPlatformMisc::ClipboardCopy( *ClipboardText );
}
void ContentBrowserUtils::CaptureThumbnailFromViewport(FViewport* InViewport, const TArray<FAssetData>& InAssetsToAssign)
{
//capture the thumbnail
uint32 SrcWidth = InViewport->GetSizeXY().X;
uint32 SrcHeight = InViewport->GetSizeXY().Y;
// Read the contents of the viewport into an array.
TArray<FColor> OrigBitmap;
if (InViewport->ReadPixels(OrigBitmap))
{
check(OrigBitmap.Num() == SrcWidth * SrcHeight);
//pin to smallest value
int32 CropSize = FMath::Min<uint32>(SrcWidth, SrcHeight);
//pin to max size
int32 ScaledSize = FMath::Min<uint32>(ThumbnailTools::DefaultThumbnailSize, CropSize);
//calculations for cropping
TArray<FColor> CroppedBitmap;
CroppedBitmap.AddUninitialized(CropSize*CropSize);
//Crop the image
int32 CroppedSrcTop = (SrcHeight - CropSize)/2;
int32 CroppedSrcLeft = (SrcWidth - CropSize)/2;
for (int32 Row = 0; Row < CropSize; ++Row)
{
//Row*Side of a row*byte per color
int32 SrcPixelIndex = (CroppedSrcTop+Row)*SrcWidth + CroppedSrcLeft;
const void* SrcPtr = &(OrigBitmap[SrcPixelIndex]);
void* DstPtr = &(CroppedBitmap[Row*CropSize]);
FMemory::Memcpy(DstPtr, SrcPtr, CropSize*4);
}
//Scale image down if needed
TArray<FColor> ScaledBitmap;
if (ScaledSize < CropSize)
{
FImageUtils::ImageResize( CropSize, CropSize, CroppedBitmap, ScaledSize, ScaledSize, ScaledBitmap, true );
}
else
{
//just copy the data over. sizes are the same
ScaledBitmap = CroppedBitmap;
}
//setup actual thumbnail
FObjectThumbnail TempThumbnail;
TempThumbnail.SetImageSize( ScaledSize, ScaledSize );
TArray<uint8>& ThumbnailByteArray = TempThumbnail.AccessImageData();
// Copy scaled image into destination thumb
int32 MemorySize = ScaledSize*ScaledSize*sizeof(FColor);
ThumbnailByteArray.AddUninitialized(MemorySize);
FMemory::Memcpy(&(ThumbnailByteArray[0]), &(ScaledBitmap[0]), MemorySize);
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
//check if each asset should receive the new thumb nail
for ( auto AssetIt = InAssetsToAssign.CreateConstIterator(); AssetIt; ++AssetIt )
{
const FAssetData& CurrentAsset = *AssetIt;
// check whether this is a type that uses one of the shared static thumbnails
if ( AssetToolsModule.Get().AssetUsesGenericThumbnail( CurrentAsset ) )
{
//assign the thumbnail and dirty
const FString ObjectFullName = CurrentAsset.GetFullName();
const FString PackageName = CurrentAsset.PackageName.ToString();
UPackage* AssetPackage = FindObject<UPackage>( NULL, *PackageName );
if ( ensure(AssetPackage) )
{
FObjectThumbnail* NewThumbnail = ThumbnailTools::CacheThumbnail(ObjectFullName, &TempThumbnail, AssetPackage);
if ( ensure(NewThumbnail) )
{
//we need to indicate that the package needs to be resaved
AssetPackage->MarkPackageDirty();
// Let the content browser know that we've changed the thumbnail
NewThumbnail->MarkAsDirty();
// Signal that the asset was changed if it is loaded so thumbnail pools will update
if ( CurrentAsset.IsAssetLoaded() )
{
CurrentAsset.GetAsset()->PostEditChange();
}
//Set that thumbnail as a valid custom thumbnail so it'll be saved out
NewThumbnail->SetCreatedAfterCustomThumbsEnabled();
}
}
}
}
}
}
void ContentBrowserUtils::ClearCustomThumbnails(const TArray<FAssetData>& InAssetsToAssign)
{
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
//check if each asset should receive the new thumb nail
for ( auto AssetIt = InAssetsToAssign.CreateConstIterator(); AssetIt; ++AssetIt )
{
const FAssetData& CurrentAsset = *AssetIt;
// check whether this is a type that uses one of the shared static thumbnails
if ( AssetToolsModule.Get().AssetUsesGenericThumbnail( CurrentAsset ) )
{
//assign the thumbnail and dirty
const FString ObjectFullName = CurrentAsset.GetFullName();
const FString PackageName = CurrentAsset.PackageName.ToString();
UPackage* AssetPackage = FindObject<UPackage>( NULL, *PackageName );
if ( ensure(AssetPackage) )
{
ThumbnailTools::CacheEmptyThumbnail( ObjectFullName, AssetPackage);
//we need to indicate that the package needs to be resaved
AssetPackage->MarkPackageDirty();
// Signal that the asset was changed if it is loaded so thumbnail pools will update
if ( CurrentAsset.IsAssetLoaded() )
{
CurrentAsset.GetAsset()->PostEditChange();
}
}
}
}
}
bool ContentBrowserUtils::AssetHasCustomThumbnail( const FAssetData& AssetData )
{
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
if ( AssetToolsModule.Get().AssetUsesGenericThumbnail(AssetData) )
{
const FObjectThumbnail* CachedThumbnail = ThumbnailTools::FindCachedThumbnail(AssetData.GetFullName());
if ( CachedThumbnail != NULL && !CachedThumbnail->IsEmpty() )
{
return true;
}
// If we don't yet have a thumbnail map, check the disk
FName ObjectFullName = FName(*AssetData.GetFullName());
TArray<FName> ObjectFullNames;
FThumbnailMap LoadedThumbnails;
ObjectFullNames.Add( ObjectFullName );
if ( ThumbnailTools::ConditionallyLoadThumbnailsForObjects( ObjectFullNames, LoadedThumbnails ) )
{
const FObjectThumbnail* Thumbnail = LoadedThumbnails.Find(ObjectFullName);
if ( Thumbnail != NULL && !Thumbnail->IsEmpty() )
{
return true;
}
}
}
return false;
}
ContentBrowserUtils::ECBFolderCategory ContentBrowserUtils::GetFolderCategory( const FString& InPath )
{
static const FString ClassesPrefix = TEXT("/Classes_");
static const FString GameClassesPrefix = TEXT("/Classes_Game");
static const FString EngineClassesPrefix = TEXT("/Classes_Engine");
const bool bIsClassDir = InPath.StartsWith(ClassesPrefix);
if(bIsClassDir)
{
const bool bIsGameClassDir = InPath.StartsWith(GameClassesPrefix);
if(bIsGameClassDir)
{
return ECBFolderCategory::GameClasses;
}
const bool bIsEngineClassDir = InPath.StartsWith(EngineClassesPrefix);
if(bIsEngineClassDir)
{
return ECBFolderCategory::EngineClasses;
}
return ECBFolderCategory::PluginClasses;
}
else
{
const bool bIsEngineContent = IsEngineFolder(InPath);
if(bIsEngineContent)
{
return ECBFolderCategory::EngineContent;
}
const bool bIsPluginContent = IsPluginFolder(InPath);
if(bIsPluginContent)
{
return ECBFolderCategory::PluginContent;
}
const bool bIsDeveloperContent = IsDevelopersFolder(InPath);
if(bIsDeveloperContent)
{
return ECBFolderCategory::DeveloperContent;
}
return ECBFolderCategory::GameContent;
}
}
bool ContentBrowserUtils::IsEngineFolder( const FString& InPath )
{
return InPath.StartsWith(TEXT("/Engine")) || InPath == TEXT("Engine");
}
bool ContentBrowserUtils::IsDevelopersFolder( const FString& InPath )
{
const FString DeveloperPathWithSlash = FPackageName::FilenameToLongPackageName(FPaths::GameDevelopersDir());
const FString DeveloperPathWithoutSlash = DeveloperPathWithSlash.LeftChop(1);
return InPath.StartsWith(DeveloperPathWithSlash) || InPath == DeveloperPathWithoutSlash;
}
bool ContentBrowserUtils::IsPluginFolder( const FString& InPath )
{
FString PathWithSlash = InPath / TEXT("");
for(const TSharedRef<IPlugin>& Plugin: IPluginManager::Get().GetEnabledPlugins())
{
if(Plugin->CanContainContent())
{
if(PathWithSlash.StartsWith(Plugin->GetMountedAssetPath()) || InPath == Plugin->GetName())
{
return true;
}
}
}
return false;
}
void ContentBrowserUtils::GetObjectsInAssetData(const TArray<FAssetData>& AssetList, TArray<UObject*>& OutDroppedObjects)
{
for (int32 AssetIdx = 0; AssetIdx < AssetList.Num(); ++AssetIdx)
{
const FAssetData& AssetData = AssetList[AssetIdx];
UObject* Obj = AssetData.GetAsset();
if (Obj)
{
OutDroppedObjects.Add(Obj);
}
}
}
bool ContentBrowserUtils::IsValidFolderName(const FString& FolderName, FText& Reason)
{
// Check length of the folder name
if ( FolderName.Len() == 0 )
{
Reason = LOCTEXT( "InvalidFolderName_IsTooShort", "Please provide a name for this folder." );
return false;
}
if ( FolderName.Len() > MAX_UNREAL_FILENAME_LENGTH )
{
Reason = FText::Format( LOCTEXT("InvalidFolderName_TooLongForCooking", "Filename '{0}' is too long; this may interfere with cooking for consoles. Unreal filenames should be no longer than {1} characters." ),
FText::FromString(FolderName), FText::AsNumber(MAX_UNREAL_FILENAME_LENGTH) );
return false;
}
const FString InvalidChars = INVALID_LONGPACKAGE_CHARACTERS TEXT("/"); // Slash is an invalid character for a folder name
// See if the name contains invalid characters.
FString Char;
for( int32 CharIdx = 0; CharIdx < FolderName.Len(); ++CharIdx )
{
Char = FolderName.Mid(CharIdx, 1);
if ( InvalidChars.Contains(*Char) )
{
FString ReadableInvalidChars = InvalidChars;
ReadableInvalidChars.ReplaceInline(TEXT("\r"), TEXT(""));
ReadableInvalidChars.ReplaceInline(TEXT("\n"), TEXT(""));
ReadableInvalidChars.ReplaceInline(TEXT("\t"), TEXT(""));
Reason = FText::Format(LOCTEXT("InvalidFolderName_InvalidCharacters", "A folder name may not contain any of the following characters: {0}"), FText::FromString(ReadableInvalidChars));
return false;
}
}
return FEditorFileUtils::IsFilenameValidForSaving( FolderName, Reason );
}
bool ContentBrowserUtils::DoesFolderExist(const FString& FolderPath)
{
// todo: jdale - CLASS - Will need updating to handle class folders
TArray<FString> SubPaths;
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
AssetRegistryModule.Get().GetSubPaths(FPaths::GetPath(FolderPath), SubPaths, false);
for(auto SubPathIt(SubPaths.CreateConstIterator()); SubPathIt; SubPathIt++)
{
if ( *SubPathIt == FolderPath )
{
return true;
}
}
return false;
}
bool ContentBrowserUtils::IsRootDir(const FString& FolderPath)
{
return IsAssetRootDir(FolderPath) || IsClassRootDir(FolderPath);
}
bool ContentBrowserUtils::IsAssetRootDir(const FString& FolderPath)
{
// All root asset folders start with "/" (not "/Classes_") and contain only a single / (at the beginning)
int32 LastSlashIndex = INDEX_NONE;
return FolderPath.Len() > 1 && !IsClassPath(FolderPath) && FolderPath.FindLastChar(TEXT('/'), LastSlashIndex) && LastSlashIndex == 0;
}
bool ContentBrowserUtils::IsClassRootDir(const FString& FolderPath)
{
// All root class folders start with "/Classes_" and contain only a single / (at the beginning)
int32 LastSlashIndex = INDEX_NONE;
return IsClassPath(FolderPath) && FolderPath.FindLastChar(TEXT('/'), LastSlashIndex) && LastSlashIndex == 0;
}
FText ContentBrowserUtils::GetRootDirDisplayName(const FString& FolderPath)
{
// Strip off any leading or trailing forward slashes
// We just want the name without any path separators
FString CleanFolderPath = FolderPath;
while(CleanFolderPath.StartsWith(TEXT("/")))
{
CleanFolderPath = CleanFolderPath.Mid(1);
}
while(CleanFolderPath.EndsWith(TEXT("/")))
{
CleanFolderPath = CleanFolderPath.Mid(0, CleanFolderPath.Len() - 1);
}
static const FString ClassesPrefix = TEXT("Classes_");
const bool bIsClassDir = CleanFolderPath.StartsWith(ClassesPrefix);
// Strip off the "Classes_" prefix
if(bIsClassDir)
{
CleanFolderPath = CleanFolderPath.Mid(ClassesPrefix.Len());
}
// Also localize well known folder names, like "Engine" and "Game"
static const FString EngineFolderName = TEXT("Engine");
static const FString GameFolderName = TEXT("Game");
FText LocalizedFolderName;
if(CleanFolderPath == EngineFolderName)
{
LocalizedFolderName = LOCTEXT("EngineFolderName", "Engine");
}
else if(CleanFolderPath == GameFolderName)
{
//LocalizedFolderName = LOCTEXT("GameFolderName", "Game");
}
else
{
LocalizedFolderName = FText::FromString(CleanFolderPath);
}
if(LocalizedFolderName.IsEmpty())
{
return (bIsClassDir) ? LOCTEXT("ClassesFolder", "C++ Classes") : LOCTEXT("ContentFolder", "Content");
}
return FText::Format((bIsClassDir) ? LOCTEXT("ClassesFolderFmt", "{0} C++ Classes") : LOCTEXT("ContentFolderFmt", "{0} Content"), LocalizedFolderName);
}
bool ContentBrowserUtils::IsClassPath(const FString& InPath)
{
static const FString ClassesRootPrefix = TEXT("/Classes_");
return InPath.StartsWith(ClassesRootPrefix);
}
void ContentBrowserUtils::CountPathTypes(const TArray<FString>& InPaths, int32& OutNumAssetPaths, int32& OutNumClassPaths)
{
OutNumAssetPaths = 0;
OutNumClassPaths = 0;
for(const FString& Path : InPaths)
{
if(IsClassPath(Path))
{
++OutNumClassPaths;
}
else
{
++OutNumAssetPaths;
}
}
}
void ContentBrowserUtils::CountPathTypes(const TArray<FName>& InPaths, int32& OutNumAssetPaths, int32& OutNumClassPaths)
{
OutNumAssetPaths = 0;
OutNumClassPaths = 0;
for(const FName& Path : InPaths)
{
if(IsClassPath(Path.ToString()))
{
++OutNumClassPaths;
}
else
{
++OutNumAssetPaths;
}
}
}
void ContentBrowserUtils::CountItemTypes(const TArray<FAssetData>& InItems, int32& OutNumAssetItems, int32& OutNumClassItems)
{
OutNumAssetItems = 0;
OutNumClassItems = 0;
for(const FAssetData& Item : InItems)
{
if(Item.AssetClass == NAME_Class)
{
++OutNumClassItems;
}
else
{
++OutNumAssetItems;
}
}
}
bool ContentBrowserUtils::IsValidPathToCreateNewClass(const FString& InPath)
{
// Classes can currently only be added to game modules - if this is restricted, we can use IsClassPath here instead
// Classes can only be created in modules, so that will be at least two folders deep (two /)
static const FString GameClassesRootPrefix = TEXT("/Classes_Game");
int32 LastSlashIndex = INDEX_NONE;
return InPath.StartsWith(GameClassesRootPrefix) && InPath.FindLastChar(TEXT('/'), LastSlashIndex) && LastSlashIndex != 0;
}
bool ContentBrowserUtils::IsValidPathToCreateNewFolder(const FString& InPath)
{
// We can't currently make folders in class paths
// If we do later allow folders in class paths, they must only be created within modules (see IsValidPathToCreateNewClass above)
return !IsClassPath(InPath);
}
const TSharedPtr<FLinearColor> ContentBrowserUtils::LoadColor(const FString& FolderPath)
{
auto LoadColorInternal = [](const FString& InPath) -> TSharedPtr<FLinearColor>
{
// See if we have a value cached first
TSharedPtr<FLinearColor> CachedColor = PathColors.FindRef(InPath);
if(CachedColor.IsValid())
{
return CachedColor;
}
// Loads the color of folder at the given path from the config
if(FPaths::FileExists(GEditorPerProjectIni))
{
// Create a new entry from the config, skip if it's default
FString ColorStr;
if(GConfig->GetString(TEXT("PathColor"), *InPath, ColorStr, GEditorPerProjectIni))
{
FLinearColor Color;
if(Color.InitFromString(ColorStr) && !Color.Equals(ContentBrowserUtils::GetDefaultColor()))
{
return PathColors.Add(InPath, MakeShareable(new FLinearColor(Color)));
}
}
else
{
return PathColors.Add(InPath, MakeShareable(new FLinearColor(ContentBrowserUtils::GetDefaultColor())));
}
}
return nullptr;
};
// First try and find the color using the given path, as this works correctly for both assets and classes
TSharedPtr<FLinearColor> FoundColor = LoadColorInternal(FolderPath);
if(FoundColor.IsValid())
{
return FoundColor;
}
// If that failed, try and use the filename (assets used to use this as their color key, but it doesn't work with classes)
if(!IsClassPath(FolderPath))
{
const FString RelativePath = FPackageName::LongPackageNameToFilename(FolderPath + TEXT("/"));
return LoadColorInternal(RelativePath);
}
return nullptr;
}
void ContentBrowserUtils::SaveColor(const FString& FolderPath, const TSharedPtr<FLinearColor>& FolderColor, bool bForceAdd)
{
auto SaveColorInternal = [](const FString& InPath, const TSharedPtr<FLinearColor>& InFolderColor)
{
// Saves the color of the folder to the config
if(FPaths::FileExists(GEditorPerProjectIni))
{
GConfig->SetString(TEXT("PathColor"), *InPath, *InFolderColor->ToString(), GEditorPerProjectIni);
}
// Update the map too
PathColors.Add(InPath, InFolderColor);
};
auto RemoveColorInternal = [](const FString& InPath)
{
// Remove the color of the folder from the config
if(FPaths::FileExists(GEditorPerProjectIni))
{
GConfig->RemoveKey(TEXT("PathColor"), *InPath, GEditorPerProjectIni);
}
// Update the map too
PathColors.Remove(InPath);
};
// Remove the color if it's invalid or default
const bool bRemove = !FolderColor.IsValid() || (!bForceAdd && FolderColor->Equals(ContentBrowserUtils::GetDefaultColor()));
if(bRemove)
{
RemoveColorInternal(FolderPath);
}
else
{
SaveColorInternal(FolderPath, FolderColor);
}
// Make sure and remove any colors using the legacy path format
if(!IsClassPath(FolderPath))
{
const FString RelativePath = FPackageName::LongPackageNameToFilename(FolderPath + TEXT("/"));
return RemoveColorInternal(RelativePath);
}
}
bool ContentBrowserUtils::HasCustomColors( TArray< FLinearColor >* OutColors )
{
// Check to see how many paths are currently using this color
// Note: we have to use the config, as paths which haven't been rendered yet aren't registered in the map
bool bHasCustom = false;
if (FPaths::FileExists(GEditorPerProjectIni))
{
// Read individual entries from a config file.
TArray< FString > Section;
GConfig->GetSection( TEXT("PathColor"), Section, GEditorPerProjectIni );
for( int32 SectionIndex = 0; SectionIndex < Section.Num(); SectionIndex++ )
{
FString EntryStr = Section[ SectionIndex ];
EntryStr.Trim();
FString PathStr;
FString ColorStr;
if ( EntryStr.Split( TEXT( "=" ), &PathStr, &ColorStr ) )
{
// Ignore any that have invalid or default colors
FLinearColor CurrentColor;
if( CurrentColor.InitFromString( ColorStr ) && !CurrentColor.Equals( ContentBrowserUtils::GetDefaultColor() ) )
{
bHasCustom = true;
if ( OutColors )
{
// Only add if not already present (ignores near matches too)
bool bAdded = false;
for( int32 ColorIndex = 0; ColorIndex < OutColors->Num(); ColorIndex++ )
{
const FLinearColor& Color = (*OutColors)[ ColorIndex ];
if( CurrentColor.Equals( Color ) )
{
bAdded = true;
break;
}
}
if ( !bAdded )
{
OutColors->Add( CurrentColor );
}
}
else
{
break;
}
}
}
}
}
return bHasCustom;
}
FLinearColor ContentBrowserUtils::GetDefaultColor()
{
// The default tint the folder should appear as
return FLinearColor::Gray;
}
FText ContentBrowserUtils::GetExploreFolderText()
{
FFormatNamedArguments Args;
Args.Add(TEXT("FileManagerName"), FPlatformMisc::GetFileManagerName());
return FText::Format(NSLOCTEXT("GenericPlatform", "ShowInFileManager", "Show In {FileManagerName}"), Args);
}
bool ContentBrowserUtils::IsValidObjectPathForCreate(const FString& ObjectPath, FText& OutErrorMessage, bool bAllowExistingAsset)
{
const FString ObjectName = FPackageName::ObjectPathToObjectName(ObjectPath);
// Make sure the name is not already a class or otherwise invalid for saving
if ( !FEditorFileUtils::IsFilenameValidForSaving(ObjectName, OutErrorMessage) )
{
// Return false to indicate that the user should enter a new name
return false;
}
// Make sure the new name only contains valid characters
if ( !FName(*ObjectName).IsValidXName( INVALID_OBJECTNAME_CHARACTERS INVALID_LONGPACKAGE_CHARACTERS, &OutErrorMessage ) )
{
// Return false to indicate that the user should enter a new name
return false;
}
// Make sure we are not creating an FName that is too large
if ( ObjectPath.Len() > NAME_SIZE )
{
// This asset already exists at this location, inform the user and continue
OutErrorMessage = LOCTEXT("AssetNameTooLong", "This asset name is too long. Please choose a shorter name.");
// Return false to indicate that the user should enter a new name
return false;
}
const FString PackageName = FPackageName::ObjectPathToPackageName(ObjectPath);
if (!IsValidPackageForCooking(PackageName, OutErrorMessage))
{
return false;
}
// Make sure we are not creating an path that is too long for the OS
const FString RelativePathFilename = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension()); // full relative path with name + extension
const FString FullPath = FPaths::ConvertRelativePathToFull(RelativePathFilename); // path to file on disk
if ( ObjectPath.Len() > (PLATFORM_MAX_FILEPATH_LENGTH - MAX_CLASS_NAME_LENGTH) || FullPath.Len() > PLATFORM_MAX_FILEPATH_LENGTH )
{
// The full path for the asset is too long
OutErrorMessage = FText::Format( LOCTEXT("AssetPathTooLong",
"The full path for the asset is too deep, the maximum is '{0}'. \nPlease choose a shorter name for the asset or create it in a shallower folder structure."),
FText::AsNumber(PLATFORM_MAX_FILEPATH_LENGTH) );
// Return false to indicate that the user should enter a new name
return false;
}
// Check for an existing asset, unless it we were asked not to.
if ( !bAllowExistingAsset )
{
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
FAssetData ExistingAsset = AssetRegistryModule.Get().GetAssetByObjectPath(FName(*ObjectPath));
if (ExistingAsset.IsValid())
{
// This asset already exists at this location, inform the user and continue
OutErrorMessage = FText::Format( LOCTEXT("RenameAssetAlreadyExists", "An asset already exists at this location with the name '{0}'."), FText::FromString( ObjectName ) );
// Return false to indicate that the user should enter a new name
return false;
}
}
return true;
}
bool ContentBrowserUtils::IsValidFolderPathForCreate(const FString& InFolderPath, const FString& NewFolderName, FText& OutErrorMessage)
{
if (!ContentBrowserUtils::IsValidFolderName(NewFolderName, OutErrorMessage))
{
return false;
}
const FString NewFolderPath = InFolderPath / NewFolderName;
if (ContentBrowserUtils::DoesFolderExist(NewFolderPath))
{
OutErrorMessage = LOCTEXT("RenameFolderAlreadyExists", "A folder already exists at this location with this name.");
return false;
}
// Make sure we are not creating a folder path that is too long
if (NewFolderPath.Len() > PLATFORM_MAX_FILEPATH_LENGTH - MAX_CLASS_NAME_LENGTH)
{
// The full path for the folder is too long
OutErrorMessage = FText::Format(LOCTEXT("RenameFolderPathTooLong",
"The full path for the folder is too deep, the maximum is '{0}'. Please choose a shorter name for the folder or create it in a shallower folder structure."),
FText::AsNumber(PLATFORM_MAX_FILEPATH_LENGTH));
// Return false to indicate that the user should enter a new name for the folder
return false;
}
return true;
}
bool ContentBrowserUtils::IsValidPackageForCooking(const FString& PackageName, FText& OutErrorMessage)
{
// We assume the game name is 20 characters (the maximum allowed) to make sure that content can be ported between projects
// 260 characters is the limit on Windows, which is the shortest max path of any platforms that support cooking
static const int32 MaxGameNameLen = 20;
static const int32 MaxPathLen = 260;
// Pad out the game name to the maximum allowed
const FString GameName = FApp::GetGameName();
FString GameNamePadded = GameName;
while (GameNamePadded.Len() < MaxGameNameLen)
{
GameNamePadded += TEXT(" ");
}
// We use "WindowsNoEditor" below as it's the longest platform name, so will also prove that any shorter platform names will validate correctly
const FString AbsoluteRootPath = FPaths::ConvertRelativePathToFull(FPaths::RootDir());
const FString AbsoluteGamePath = FPaths::ConvertRelativePathToFull(FPaths::GameDir());
const FString AbsoluteCookPath = AbsoluteGamePath / TEXT("Saved") / TEXT("Cooked") / TEXT("WindowsNoEditor") / GameName;
const FString RelativePathToAsset = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
const FString AbsolutePathToAsset = FPaths::ConvertRelativePathToFull(RelativePathToAsset);
FString AssetPathWithinCookDir = AbsolutePathToAsset;
FPaths::RemoveDuplicateSlashes(AssetPathWithinCookDir);
AssetPathWithinCookDir.RemoveFromStart(AbsoluteGamePath, ESearchCase::CaseSensitive);
// Test that the package can be cooked based on the current project path
{
FString AbsoluteCookPathToAsset = AbsoluteCookPath / AssetPathWithinCookDir;
AbsoluteCookPathToAsset.ReplaceInline(*GameName, *GameNamePadded, ESearchCase::CaseSensitive);
if (AbsoluteCookPathToAsset.Len() > MaxPathLen)
{
// The projected length of the path for cooking is too long
OutErrorMessage = LOCTEXT("AssetCookingPathTooLong", "The path to the asset is too long for cooking\nPlease choose a shorter name for the asset or create it in a shallower folder structure with shorter folder names.");
// Return false to indicate that the user should enter a new name
return false;
}
}
// See TTP# 332328:
// The following checks are done mostly to prevent / alleviate the problems that "long" paths are causing with the BuildFarm and cooked builds.
// The BuildFarm uses a verbose path to encode extra information to provide more information when things fail, however this makes the path limitation a problem.
// - We assume a base path of D:/BuildFarm/buildmachine_++depot+UE4-Releases+4.10/
// - We assume the game name is 20 characters (the maximum allowed) to make sure that content can be ported between projects
// - We calculate the cooked game path relative to the game root (eg, Showcases/Infiltrator/Saved/Cooked/WindowsNoEditor/Infiltrator)
// - We calculate the asset path relative to (and including) the Content directory (eg, Content/Environment/Infil1/Infil1_Underground/Infrastructure/Model/SM_Infil1_Tunnel_Ceiling_Pipes_1xEntryCurveOuter_Double.uasset)
if (FEngineBuildSettings::IsInternalBuild())
{
// We assume a constant size for the build machine base path, so strip either the root or game path from the start
// (depending on whether the project is part of the main UE4 source tree or located elsewhere)
FString CookDirWithoutBasePath = AbsoluteCookPath;
if (CookDirWithoutBasePath.StartsWith(AbsoluteRootPath, ESearchCase::CaseSensitive))
{
CookDirWithoutBasePath.RemoveFromStart(AbsoluteRootPath, ESearchCase::CaseSensitive);
}
else
{
CookDirWithoutBasePath.RemoveFromStart(AbsoluteGamePath, ESearchCase::CaseSensitive);
}
FString AbsoluteBuildMachineCookPathToAsset = FString(TEXT("D:/BuildFarm/buildmachine_++depot+UE4-Releases+4.10")) / CookDirWithoutBasePath / AssetPathWithinCookDir;
AbsoluteBuildMachineCookPathToAsset.ReplaceInline(*GameName, *GameNamePadded, ESearchCase::CaseSensitive);
if (AbsoluteBuildMachineCookPathToAsset.Len() > MaxPathLen)
{
// The projected length of the path for cooking is too long
OutErrorMessage = LOCTEXT("AssetCookingPathTooLongForBuildMachine", "The path to the asset is too long for cooking by the build machines\nPlease choose a shorter name for the asset or create it in a shallower folder structure with shorter folder names.");
// Return false to indicate that the user should enter a new name
return false;
}
}
return true;
}
#undef LOCTEXT_NAMESPACE