Files
UnrealEngine/Engine/Source/Editor/MainFrame/Private/Menus/LayoutsMenu.cpp
2025-05-18 13:04:45 +08:00

1168 lines
53 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#if WITH_EDITOR
#include "Menus/LayoutsMenu.h"
// Runtime/Core
#include "Containers/Array.h"
#include "GenericPlatform/GenericPlatformFile.h"
#include "GenericPlatform/GenericPlatformMath.h"
#include "HAL/FileManagerGeneric.h"
#include "Logging/MessageLog.h"
#include "Templates/SharedPointer.h"
// Runtime
#include "CoreGlobals.h"
#include "Framework/Application/SlateApplication.h"
#include "Framework/Commands/UICommandInfo.h"
#include "Framework/Docking/LayoutService.h"
#include "Framework/Notifications/NotificationManager.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/FileHelper.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "Widgets/SBoxPanel.h"
// Developer
#include "IDesktopPlatform.h"
#include "DesktopPlatformModule.h"
#include "ToolMenus.h"
// Editor
#include "Settings/EditorStyleSettings.h"
#include "Dialog/SCustomDialog.h"
#include "Editor/EditorPerProjectUserSettings.h"
#include "Frame/MainFrameActions.h"
#include "LevelViewportActions.h"
#include "Menus/SaveLayoutDialog.h"
#include "Misc/MessageDialog.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "UnrealEdGlobals.h"
#include "UnrealEdMisc.h"
#define LOCTEXT_NAMESPACE "MainFrameActions"
DEFINE_LOG_CATEGORY_STATIC(LogLayoutsMenu, Fatal, All);
/* FPrivateLayoutsMenu definition
*****************************************************************************/
class FPrivateLayoutsMenu
{
public:
enum class ELayoutsMenu
{
Load,
Save,
Remove
};
/** Get LayoutsDirectory path */
static FString CreateAndGetDefaultLayoutDirInternal(const FLayoutsMenu::ELayoutsType InLayoutsType);
static TArray<FString> GetIniFilesInFolderInternal(const FString& InStringDirectory);
static bool TrySaveLayoutOrWarnInternal(const FString& InSourceFilePath, const FString& InTargetFilePath, const FText& InWhatIsThis,
const bool bCleanLayoutNameAndDescriptionFieldsIfNoSameValues, const bool bShouldAskBeforeCleaningLayoutNameAndDescriptionFields = false, const bool bShowSaveToast = false);
/** Name into display text */
static FText GetDisplayTextInternal(const FString& InString);
static FText GetTooltipTextInternal(const FText& InDisplayName, const FString& InLayoutFilePath, const FText& InLayoutName);
static void DisplayLayoutsInternal(FToolMenuSection& InSection, const TArray<FString>& InLayoutIniFileNames, const FString& InLayoutsDirectory, const ELayoutsMenu InLayoutsMenu,
const FLayoutsMenu::ELayoutsType InLayoutsType);
/**
* GetOriginalEditorLayoutIniFilePathInternal() and GetDuplicatedEditorLayoutIniFilePathInternal() are used because sometimes the layout saved is not the same than the one loaded, even
* though the visual display and screenshot are 100% the same. In those cases, we still want to show the check mark in the load/save/remove menu indicating that the layout is the same
* than the one in the loaded ini file, even though the actual files might not be exactly the same. In addition, this also helps when temporarily closing unrecognized tabs (i.e., the
* ones that FTabManager::SpawnTab cannot recognized and keeps closed but still saves them in the layout).
*/
static const FString& GetOriginalEditorLayoutIniFilePathInternal();
static const FString& GetDuplicatedEditorLayoutIniFilePathInternal();
static bool AreFilesIdenticalInternal(const FString& InFirstFileFullPath, const FString& InSecondFileFullPath);
static void RemoveTempEditorLayoutIniFilesInternal();
static bool IsLayoutCheckedInternal(const FString& InLayoutFullPath, const bool bCheckTempFileToo);
static FText GetProjectLayoutSectionName();
static void MakeXLayoutsMenuInternal(UToolMenu* InToolMenu, const ELayoutsMenu InLayoutsMenu);
/** Checks and returns whether the selected layout can be read (e.g., when loading layouts). */
static bool CanChooseLayoutWhenReadInternal();
/** Checks and returns whether the selected layout can be modified (e.g., when overriding or removing layouts). */
static bool CanChooseLayoutWhenWriteInternal(const FLayoutsMenu::ELayoutsType InLayoutsType);
static void SaveLayoutWithoutRemovingTempLayoutFiles();
/**
* It simply checks whether PIE, SIE, or any Asset Editor is opened, and asks the user whether they want to continue closing them or cancel the Editor layout load
* @return Whether we should continue loading the layout
*/
static bool CheckAskUserToClosePIESIE(const FText& InitialMessage);
static FText GenerateLocalizedTextForFile(const FText& InText);
static void SaveExportLayoutCommon(const FString& InDefaultDirectory, const bool bMustBeSavedInDefaultDirectory, const FText& InWhatIsThis,
const bool bShouldAskBeforeCleaningLayoutNameAndDescriptionFields);
static int32 GetNumberLayoutFiles(const FString& InLayoutsDirectory);
};
/* FPrivateLayoutsMenu public functions
*****************************************************************************/
FString FPrivateLayoutsMenu::CreateAndGetDefaultLayoutDirInternal(const FLayoutsMenu::ELayoutsType InLayoutsType)
{
// Get LayoutsDirectory path
const FString LayoutsDirectory = (
InLayoutsType == FLayoutsMenu::ELayoutsType::Engine ? FPaths::EngineDefaultLayoutDir() :
InLayoutsType == FLayoutsMenu::ELayoutsType::Project ? FPaths::EngineProjectLayoutDir() :
InLayoutsType == FLayoutsMenu::ELayoutsType::User ? FPaths::EngineUserLayoutDir() :
TEXT(""));
checkf(LayoutsDirectory.Len() > 0, TEXT("LayoutsDirectory.Len() was 0 with LayoutType = %d"), int32(InLayoutsType));
// If the directory does not exist, create it (but it will not have saved Layouts inside)
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
if (!PlatformFile.DirectoryExists(*LayoutsDirectory))
{
PlatformFile.CreateDirectory(*LayoutsDirectory);
}
// Return result
return LayoutsDirectory;
}
TArray<FString> FPrivateLayoutsMenu::GetIniFilesInFolderInternal(const FString& InStringDirectory)
{
// Find all ini files in folder
TArray<FString> LayoutIniFileNames;
const FString LayoutIniFilePaths = FPaths::Combine(*InStringDirectory, TEXT("*.ini"));
FFileManagerGeneric::Get().FindFiles(LayoutIniFileNames, *LayoutIniFilePaths, true, false);
return LayoutIniFileNames;
}
bool FPrivateLayoutsMenu::TrySaveLayoutOrWarnInternal(const FString& InSourceFilePath, const FString& InTargetFilePath, const FText& InWhatIsThis,
const bool bCleanLayoutNameAndDescriptionFieldsIfNoSameValues, const bool bShouldAskBeforeCleaningLayoutNameAndDescriptionFields, const bool bShowSaveToast)
{
// We must re-read configs to avoid the Editor using a previously cached version
GConfig->UnloadFile(InSourceFilePath);
GConfig->UnloadFile(InTargetFilePath);
bool bCleanLayoutNameAndDescriptionFields = false;
// If we are checking whether to clean the fields, we only want to maintain them if we are saving the file into an existing file that already has the same field values
if (bCleanLayoutNameAndDescriptionFieldsIfNoSameValues)
{
// read the source name and description
const FText LayoutNameSource = FLayoutSaveRestore::LoadSectionFromConfig(InSourceFilePath, "LayoutName");
const FText LayoutDescriptionSource = FLayoutSaveRestore::LoadSectionFromConfig(InSourceFilePath, "LayoutDescription");
// read the target name and description
const FText LayoutNameTarget = FLayoutSaveRestore::LoadSectionFromConfig(InTargetFilePath, "LayoutName");
const FText LayoutDescriptionTarget = FLayoutSaveRestore::LoadSectionFromConfig(InTargetFilePath, "LayoutDescription");
// The output target exists (overriding)
// These fields are not empty in source
if (!LayoutNameSource.IsEmpty() || !LayoutDescriptionSource.IsEmpty())
{
// These fields are different than the ones in target
if ((LayoutNameSource.ToString() != LayoutNameTarget.ToString()) || (LayoutDescriptionSource.ToString() != LayoutDescriptionTarget.ToString()))
{
bCleanLayoutNameAndDescriptionFields = true;
// We should clean the layout name and description fields, but ask user first
if (bShouldAskBeforeCleaningLayoutNameAndDescriptionFields)
{
// Open Dialog
const FText TextTitle = LOCTEXT("OverrideLayoutNameAndDescriptionFieldBodyTitle", "Preserve UI Layout Name and Description Fields?");
const FText TextBody = FText::Format(
LOCTEXT("OverrideLayoutNameAndDescriptionFieldBody", "You are saving a layout that contains a custom layout name and/or description. Do you also want to copy these 2 properties?\n - Current layout name: {0}\n - Current layout description: {1}\n\nIf you select \"Preserve Values\", the displayed name and description of the original layout customization will also be copied into the new configuration file.\n\nIf you select \"Clear Values\", these fields will be emptied.\n\nIf you are not sure, select \"Preserve Values\" if you are exporting the layout configuration without making any changes, or \"Clear Values\" if you"" have made or plan to make changes to the layout.\n\n"),
LayoutNameSource, LayoutDescriptionSource);
// Dialog SWidget
TSharedRef<SVerticalBox> DialogContents = SNew(SVerticalBox);
DialogContents->AddSlot()
.Padding(0, 16, 0, 0)
[
SNew(STextBlock)
.Text(TextBody)
];
const FText PreserveValuesText = LOCTEXT("PreserveValuesText", "Preserve Values");
const FText ClearValuesText = LOCTEXT("ClearValuesText", "Clear Values");
const FText CancelText = NSLOCTEXT("Dialogs", "EAppReturnTypeCancel", "Cancel");
TSharedRef<SCustomDialog> CustomDialog = SNew(SCustomDialog)
.Title(TextTitle)
.Content()
[
DialogContents
]
.Buttons({ SCustomDialog::FButton(PreserveValuesText), SCustomDialog::FButton(ClearValuesText), SCustomDialog::FButton(CancelText) });
// Returns 0 when "Preserve Values" is pressed, 1 when "Clear Values" is pressed, or 2 when Cancel/Esc is pressed
const int32 ButtonPressed = CustomDialog->ShowModal();
// Preserve Values
if (ButtonPressed == 0)
{
bCleanLayoutNameAndDescriptionFields = false;
}
// Clear Values
else if (ButtonPressed == 1)
{
bCleanLayoutNameAndDescriptionFields = true;
}
// Cancel or Esc or window closed
else if (ButtonPressed == 2 || ButtonPressed == -1)
{
return false;
}
// This should never occur
else
{
ensureMsgf(false, TEXT("This option should never occur, something went wrong!"));
return false;
}
}
}
}
}
const FString TargetAbsoluteFilePath = FPaths::ConvertRelativePathToFull(InTargetFilePath);
if (!FLayoutSaveRestore::DuplicateConfig(InSourceFilePath, InTargetFilePath))
{
const FText SourceAbsoluteFilePathText = FText::FromString(FPaths::ConvertRelativePathToFull(InSourceFilePath));
const FText TargetAbsoluteFilePathText = FText::FromString(TargetAbsoluteFilePath);
FMessageLog EditorErrors("EditorErrors");
FText TextBody;
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("WhatIs"), InWhatIsThis);
// Source does not exist
if (!FPaths::FileExists(InSourceFilePath))
{
Arguments.Add(TEXT("FileName"), SourceAbsoluteFilePathText);
TextBody = FText::Format(LOCTEXT("UnsuccessfulSave_NoExist_Notification", "The requested operation ({WhatIs}) was unsuccessful, the desired file does not exist. File path:\n{FileName}"), Arguments);
EditorErrors.Warning(TextBody);
}
// Target is read-only
else if (IFileManager::Get().IsReadOnly(*InTargetFilePath))
{
Arguments.Add(TEXT("FileName"), TargetAbsoluteFilePathText);
TextBody = FText::Format(LOCTEXT("UnsuccessfulSave_ReadOnly_Notification", "The requested operation ({WhatIs}) was unsuccessful, the target file path is read-only. File path:\n{FileName}"), Arguments);
EditorErrors.Warning(TextBody);
}
// Target and source are the same
else if (TargetAbsoluteFilePathText.EqualTo(SourceAbsoluteFilePathText))
{
Arguments.Add(TEXT("SourceFileName"), SourceAbsoluteFilePathText);
Arguments.Add(TEXT("FinalFileName"), TargetAbsoluteFilePathText);
TextBody = FText::Format(LOCTEXT("UnsuccessfulSave_Identical_Notification", "The requested operation ({WhatIs}) was unsuccessful, target and source layout file paths are the same ({SourceFileName})!\nAre you trying to import or replace a file that is already in the layouts folder? If so, remove the current file first."), Arguments);
EditorErrors.Warning(TextBody);
}
// We don't specifically know why it failed, this is a fallback
else
{
Arguments.Add(TEXT("SourceFileName"), SourceAbsoluteFilePathText);
Arguments.Add(TEXT("FinalFileName"), TargetAbsoluteFilePathText);
TextBody = FText::Format(LOCTEXT("UnsuccessfulSave_Fallback_Notification", "The requested operation ({WhatIs}) was unsuccessful while copying the layout file from\n{SourceFileName}\ninto\n{FinalFileName}\n\nUsually, this occurs when the introduced file name contains unsupported characters or the total path length exceeds the OS limit."), Arguments);
EditorErrors.Warning(TextBody);
}
EditorErrors.Notify(LOCTEXT("LoadUnsuccessful_Title", "Load Unsuccessful!"));
// Show reason
FMessageDialog::Open(EAppMsgType::Ok, TextBody, LOCTEXT("UnsuccessfulCopyHeader", "Unsuccessful copy!"));
// Return
return false;
}
// Copy successful
else
{
// Clean Layout Name and Description fields
// We copy twice to make sure we can copy.
// Problem if we only copied once: If the copy fails, the current EditorLayout.ini would be modified and no longer matches the previous one.
// The ini file should only be modified if it has been successfully copied to the new (and modified) INI file.
if (bCleanLayoutNameAndDescriptionFields)
{
// Update fields
FLayoutSaveRestore::SaveSectionToConfig(GEditorLayoutIni, "LayoutName", FText::FromString(""));
FLayoutSaveRestore::SaveSectionToConfig(GEditorLayoutIni, "LayoutDescription", FText::FromString(""));
// Flush file
const bool bRead = true;
GConfig->Flush(bRead, GEditorLayoutIni);
// Re-copy file
if (TargetAbsoluteFilePath != FPaths::ConvertRelativePathToFull(GEditorLayoutIni))
{
const bool bShouldReplace = true;
const bool bCopyEvenIfReadOnly = true;
const bool bCopyAttributes = false;
IFileManager::Get().Copy(*TargetAbsoluteFilePath, *GEditorLayoutIni, bShouldReplace, bCopyEvenIfReadOnly, bCopyAttributes);
}
}
// Display Editor toast to inform the user of the result of the operation
if (bShowSaveToast)
{
// Code copied from EditorViewportClient.cpp --> FEditorViewportClient::TakeScreenshot(...) to maintain same format than when saving a screenshot
FNotificationInfo Info(FText::GetEmpty());
Info.ExpireDuration = 5.0f;
Info.bUseSuccessFailIcons = false;
Info.bUseLargeFont = false;
TSharedPtr<SNotificationItem> SaveMessagePtr = FSlateNotificationManager::Get().AddNotification(Info);
if (SaveMessagePtr.IsValid())
{
const FString HyperLinkString = TargetAbsoluteFilePath;
auto OpenScreenshotFolder = [HyperLinkString]
{
FPlatformProcess::ExploreFolder(*HyperLinkString);
};
SaveMessagePtr->SetText(LOCTEXT("SuccessfulSave_Toast", "Editor layout file saved as"));
SaveMessagePtr->SetHyperlink(FSimpleDelegate::CreateLambda(OpenScreenshotFolder), FText::FromString(HyperLinkString));
SaveMessagePtr->SetCompletionState(SNotificationItem::CS_Success);
}
}
}
return true;
}
FText FPrivateLayoutsMenu::GetDisplayTextInternal(const FString& InString)
{
const FText DisplayNameText = FText::FromString(FPaths::GetBaseFilename(*InString));
const bool bIsBool = false;
const FText DisplayName = FText::FromString(FName::NameToDisplayString(DisplayNameText.ToString(), bIsBool));
return DisplayName;
}
FText FPrivateLayoutsMenu::GetTooltipTextInternal(const FText& InDisplayName, const FString& InLayoutFilePath, const FText& InLayoutName)
{
FText Tooltip;
if (InLayoutName.IsEmpty())
{
Tooltip = FText::Format(LOCTEXT("DisplayNameFmt", "Layout name:\n{0}\n\nFull file path:\n{1}"), InDisplayName, FText::FromString(InLayoutFilePath));
}
else
{
Tooltip = FText::Format(LOCTEXT("LayoutNameFmt", "Description:\n{0}.\n\nFull file path:\n{1}"), InLayoutName, FText::FromString(InLayoutFilePath));
}
return Tooltip;
}
void FPrivateLayoutsMenu::DisplayLayoutsInternal(FToolMenuSection& InSection, const TArray<FString>& InLayoutIniFileNames, const FString& InLayoutsDirectory,
const ELayoutsMenu InLayoutsMenu, const FLayoutsMenu::ELayoutsType InLayoutsType)
{
// If there are Layout ini files, read them
for (int32 LayoutIndex = 0; LayoutIndex < InLayoutIniFileNames.Num(); ++LayoutIndex)
{
FString LayoutFilePath = FConfigCacheIni::NormalizeConfigIniPath(*FPaths::Combine(InLayoutsDirectory, InLayoutIniFileNames[LayoutIndex]));
// Make sure it is a layout file
GConfig->UnloadFile(LayoutFilePath); // We must re-read it to avoid the Editor to use a previously cached name and description
if (FLayoutSaveRestore::IsValidConfig(LayoutFilePath))
{
// Read and display localization name from INI file
constexpr bool bAreSectionsOptional = true;
const FText LayoutName = FLayoutSaveRestore::LoadSectionFromConfig(LayoutFilePath, "LayoutName", bAreSectionsOptional);
const FText LayoutDescription = FLayoutSaveRestore::LoadSectionFromConfig(LayoutFilePath, "LayoutDescription", bAreSectionsOptional);
// If no localization name, then display the file name
const FText DisplayName = (!LayoutName.IsEmpty() ? LayoutName : GetDisplayTextInternal(InLayoutIniFileNames[LayoutIndex]));
const FText Tooltip = GetTooltipTextInternal(DisplayName, LayoutFilePath, LayoutDescription);
// Create UI action here that calls the necessary code in FLayoutsMenuLoad, Save, or Remove
FUIAction UIAction;
if (InLayoutsMenu == ELayoutsMenu::Load)
{
UIAction = FUIAction(FExecuteAction::CreateStatic(&FLayoutsMenuLoad::LoadLayout, LayoutIndex, InLayoutsType));
}
else if (InLayoutsMenu == ELayoutsMenu::Save)
{
if (FPrivateLayoutsMenu::CanChooseLayoutWhenWriteInternal(InLayoutsType))
{
UIAction = FUIAction(FExecuteAction::CreateStatic(&FLayoutsMenuSave::OverrideLayout, LayoutIndex, InLayoutsType));
}
else
{
// Cannot save engine or project layouts
continue;
}
}
else if (InLayoutsMenu == ELayoutsMenu::Remove)
{
if (FPrivateLayoutsMenu::CanChooseLayoutWhenWriteInternal(InLayoutsType))
{
UIAction = FUIAction(FExecuteAction::CreateStatic(&FLayoutsMenuRemove::RemoveLayout, LayoutIndex, InLayoutsType));
}
else
{
// Cannot delete engine or project layouts
continue;
}
}
else
{
ensureMsgf(false, TEXT("InLayoutsMenu = %d."), int32(InLayoutsMenu));
}
// Run desired action
InSection.AddMenuEntry(NAME_None, DisplayName, Tooltip, FSlateIcon(), FToolUIActionChoice(UIAction));
}
}
}
const FString& FPrivateLayoutsMenu::GetOriginalEditorLayoutIniFilePathInternal()
{
static const FString OriginalEditorLayoutIniFilePath = GEditorLayoutIni + TEXT("_orig.ini");
return OriginalEditorLayoutIniFilePath;
}
const FString& FPrivateLayoutsMenu::GetDuplicatedEditorLayoutIniFilePathInternal()
{
static const FString DuplicatedEditorLayoutIniFilePath = GEditorLayoutIni + TEXT("_temp.ini");
return DuplicatedEditorLayoutIniFilePath;
}
bool FPrivateLayoutsMenu::AreFilesIdenticalInternal(const FString& InFirstFileFullPath, const FString& InSecondFileFullPath)
{
// Checked if same file. I.e.,
// 1. Same size
// 2. And same internal text
const bool bHaveSameSize = (IFileManager::Get().FileSize(*InFirstFileFullPath) == IFileManager::Get().FileSize(*InSecondFileFullPath));
// Same size --> Same layout file?
if (bHaveSameSize)
{
// Read files and check whether they have the exact same text
FString StringFirstFileFullPath;
FFileHelper::LoadFileToString(StringFirstFileFullPath, *InFirstFileFullPath);
FString StringSecondFileFullPath;
FFileHelper::LoadFileToString(StringSecondFileFullPath, *InSecondFileFullPath);
// (No) same text = (No) same layout file
return (StringFirstFileFullPath == StringSecondFileFullPath);
}
// No same size = No same layout file
else
{
return false;
}
}
void FPrivateLayoutsMenu::RemoveTempEditorLayoutIniFilesInternal()
{
const bool bRequireExists = false;
const bool bEvenIfReadOnly = true;
const bool bIsQuiet = false;
// DuplicatedEditorLayoutIniFilePath
const FString& DuplicatedEditorLayoutIniFilePath = GetDuplicatedEditorLayoutIniFilePathInternal();
IFileManager::Get().Delete(*DuplicatedEditorLayoutIniFilePath, bRequireExists, bEvenIfReadOnly, bIsQuiet);
GConfig->UnloadFile(DuplicatedEditorLayoutIniFilePath);
// OriginalEditorLayoutIniFilePath
const FString& OriginalEditorLayoutIniFilePath = GetOriginalEditorLayoutIniFilePathInternal();
IFileManager::Get().Delete(*OriginalEditorLayoutIniFilePath, bRequireExists, bEvenIfReadOnly, bIsQuiet);
GConfig->UnloadFile(OriginalEditorLayoutIniFilePath);
}
bool FPrivateLayoutsMenu::IsLayoutCheckedInternal(const FString& InLayoutFullPath, const bool bCheckTempFileToo)
{
// If same file, return true
if (AreFilesIdenticalInternal(InLayoutFullPath, GEditorLayoutIni))
{
return true;
}
// No same size, check if same than temporary one
else if (bCheckTempFileToo)
{
const FString& OriginalEditorLayoutIniFilePath = GetOriginalEditorLayoutIniFilePathInternal();
const FString& DuplicatedEditorLayoutIniFilePath = GetDuplicatedEditorLayoutIniFilePathInternal();
if (AreFilesIdenticalInternal(GEditorLayoutIni, DuplicatedEditorLayoutIniFilePath))
{
return AreFilesIdenticalInternal(InLayoutFullPath, OriginalEditorLayoutIniFilePath);
}
// If GEditorLayoutIni != DuplicatedEditorLayoutIniFilePath, remove DuplicatedEditorLayoutIniFilePath & OriginalEditorLayoutIniFilePath
else
{
RemoveTempEditorLayoutIniFilesInternal();
return false;
}
}
// No same size, and we should not check if same than temporary ones, so then it is false
else
{
return false;
}
}
FText FPrivateLayoutsMenu::GetProjectLayoutSectionName()
{
const FString ProjectDir = FPaths::ProjectDir(); // "../../../QAGame/"
const FString ProjectDirName = FPaths::GetBaseFilename(FPaths::GetPath(ProjectDir)); // "QAGame"
return FText::Format(LOCTEXT("ProjectLayoutsHeader", "{0} Layouts"), FText::FromString(ProjectDirName));
}
void FPrivateLayoutsMenu::MakeXLayoutsMenuInternal(UToolMenu* InToolMenu, const ELayoutsMenu InLayoutsMenu)
{
// Display (if load-only) engine/project layouts and (always) user layouts
const TArray<FLayoutsMenu::ELayoutsType> LayoutSectionTypes = InLayoutsMenu == ELayoutsMenu::Load
? TArray<FLayoutsMenu::ELayoutsType>({FLayoutsMenu::ELayoutsType::Engine, FLayoutsMenu::ELayoutsType::Project, FLayoutsMenu::ELayoutsType::User})
: /*InLayoutsMenu == ELayoutsMenu::Save|Remove ? */ TArray<FLayoutsMenu::ELayoutsType>({FLayoutsMenu::ELayoutsType::User});
for (const FLayoutsMenu::ELayoutsType LayoutSectionType : LayoutSectionTypes)
{
FToolMenuSection& Section = (
LayoutSectionType == FLayoutsMenu::ELayoutsType::Engine ? InToolMenu->AddSection("DefaultLayouts", LOCTEXT("DefaultLayoutsHeading", "Default Layouts")) :
LayoutSectionType == FLayoutsMenu::ELayoutsType::Project ? InToolMenu->AddSection("ProjectDefaultLayouts", GetProjectLayoutSectionName()) :
/*LayoutSectionType == FLayoutsMenu::ELayoutsType::User ?*/ InToolMenu->AddSection("UserDefaultLayouts", LOCTEXT("UserDefaultLayoutsHeading", "User Layouts")));
// (Create if it does not exist and) Get LayoutsDirectory path
const FString LayoutsDirectory = CreateAndGetDefaultLayoutDirInternal(LayoutSectionType);
// Get Layout init files
const TArray<FString> LayoutIniFileNames = GetIniFilesInFolderInternal(LayoutsDirectory);
// If there are user Layout ini files, read and display them
DisplayLayoutsInternal(Section, LayoutIniFileNames, LayoutsDirectory, InLayoutsMenu, LayoutSectionType);
}
}
bool FPrivateLayoutsMenu::CanChooseLayoutWhenReadInternal()
{
// All can be read
return true;
}
bool FPrivateLayoutsMenu::CanChooseLayoutWhenWriteInternal(const FLayoutsMenu::ELayoutsType InLayoutsType)
{
// Only the layouts created by the user can be modified
return (InLayoutsType == FLayoutsMenu::ELayoutsType::User);
}
void FPrivateLayoutsMenu::SaveLayoutWithoutRemovingTempLayoutFiles()
{
// Save the layout into the Editor
FGlobalTabmanager::Get()->SaveAllVisualState();
// Write the saved layout to disk (if it has changed since the last time it was read/written)
// We must set bRead = true. Otherwise, FLayoutsMenuLoad::ReloadCurrentLayout() would reload the old config file (because it would be cached on memory)
const bool bRead = true;
GConfig->Flush(bRead, GEditorLayoutIni);
}
bool FPrivateLayoutsMenu::CheckAskUserToClosePIESIE(const FText& InitialMessage)
{
if (!GEditor)
{
ensureMsgf(false, TEXT("GEditor should not be false when CheckAskUserToClosePIESIE() is called."));
return true;
}
// If PIE/SIE are not opened, return true
const bool bIsPIEOrSIERunning = ((GEditor && GEditor->PlayWorld) || GIsPlayInEditorWorld);
if (!bIsPIEOrSIERunning)
{
return true;
}
// If PIE/SIE are opened
// FMessageDialog - Ask the user whether they wants to automatically close them and continue loading the layout
const FText TextTitle = LOCTEXT("CheckAskUserToClosePIESIEIfYesHeaderPIE", "Close PIE/SIE?");
const FText IfYesText = LOCTEXT("CheckAskUserToClosePIESIEIfYesBodyPIE", "If \"Yes\", your current game instances (PIE or SIE) will be closed. Any unsaved changes in those will also be lost.");
const FText IfNoText = LOCTEXT("CheckAskUserToClosePIESIEIfNoBody", "If \"No\", you can manually reload the layout from the \"User Layouts\" section later.");
const FText TextBody = FText::Format(LOCTEXT("ClosePIESIEAssetEditorsBody", "{0}\n\n{1}\n\n{2}"), InitialMessage, IfYesText, IfNoText);
// Return if the user did not want to close them
if (EAppReturnType::Yes != FMessageDialog::Open(EAppMsgType::YesNo, TextBody, TextTitle))
{
return false;
}
// Close PIE/SIE
if (GEditor && GEditor->PlayWorld)
{
GEditor->EndPlayMap();
}
else
{
ensureMsgf(false,
TEXT("This has not been tested because the code does not reach this by default. The layout is loaded through the Editor UI, and GIsPlayInEditorWorld should"
" not have any kind of Editor UI, so it should not be possible to load a layout in that status."));
}
return true;
}
FText FPrivateLayoutsMenu::GenerateLocalizedTextForFile(const FText& InText)
{
// Proper FText to FString
FString StringSimulatingText;
FTextStringHelper::WriteToBuffer(StringSimulatingText, InText);
// Sanitize text (truncate if too big)
FString SanitazedTruncatedText = (StringSimulatingText.Len() < 100 ? StringSimulatingText : StringSimulatingText.Left(100));
FSaveLayoutDialogUtils::SanitizeText(SanitazedTruncatedText);
// Create full file path
if (StringSimulatingText.Len() < 10 || StringSimulatingText.Left(9) != TEXT("NSLOCTEXT"))
{
FString StringSimulatingTextRecreated =
// Namespace
TEXT("NSLOCTEXT(\"LayoutNamespace\", "
// Key
"\"") + SanitazedTruncatedText + TEXT("\", "
// Source string
"\"") + StringSimulatingText + TEXT("\")");
return FText::FromString(StringSimulatingTextRecreated);
}
else
{
return FText::FromString(StringSimulatingText);
}
}
void FPrivateLayoutsMenu::SaveExportLayoutCommon(const FString& InDefaultDirectory, const bool bMustBeSavedInDefaultDirectory, const FText& InWhatIsThis,
const bool bShouldAskBeforeCleaningLayoutNameAndDescriptionFields)
{
// Export/SaveAs the user-selected layout configuration files and load one of them
IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get();
if (DesktopPlatform)
{
bool bWereFilesSelected = false;
TArray<FString> LayoutFilePaths;
bool bWasDialogOpened = bMustBeSavedInDefaultDirectory;
// "Save Layout As..."
if (bMustBeSavedInDefaultDirectory)
{
TArray<FText> LayoutNames;
LayoutNames.Emplace(FLayoutSaveRestore::LoadSectionFromConfig(GEditorLayoutIni, "LayoutName"));
TArray<FText> LayoutDescriptions;
LayoutDescriptions.Emplace(FLayoutSaveRestore::LoadSectionFromConfig(GEditorLayoutIni, "LayoutDescription"));
// We want to avoid the duplication of the file/name/description fields, so we add "Copy of " at the beginning of the name and description fields
if (LayoutNames[0].ToString().Len() > 0)
{
LayoutNames[0] = FText::Format(LOCTEXT("CopyOfLayoutName", "Copy of {0}"), FText::FromString(LayoutNames[0].ToString()));
}
if (LayoutDescriptions[0].ToString().Len() > 0)
{
LayoutDescriptions[0] = FText::Format(LOCTEXT("CopyOfLayoutName", "Copy of {0}"), FText::FromString(LayoutDescriptions[0].ToString()));
}
// Create SWidget for saving the layout in its own SWindow and block the thread until it is finished
const TSharedRef<FSaveLayoutDialogParams> SaveLayoutDialogParams = MakeShared<FSaveLayoutDialogParams>(InDefaultDirectory, TEXT(".ini"), LayoutNames, LayoutDescriptions);
bWasDialogOpened = FSaveLayoutDialogUtils::CreateSaveLayoutAsDialogInStandaloneWindow(SaveLayoutDialogParams);
bWereFilesSelected = SaveLayoutDialogParams->bWereFilesSelected;
LayoutFilePaths = SaveLayoutDialogParams->LayoutFilePaths;
LayoutNames = SaveLayoutDialogParams->LayoutNames;
LayoutDescriptions = SaveLayoutDialogParams->LayoutDescriptions;
// Update GEditorLayoutIni file if LayoutNames or LayoutDescriptions were modified by the user
if (bWereFilesSelected && LayoutNames.Num() > 0 && LayoutDescriptions.Num() > 0 && (LayoutNames[0].ToString().Len() > 0 || LayoutDescriptions[0].ToString().Len() > 0))
{
checkf(LayoutNames.Num() == LayoutDescriptions.Num(), TEXT("There should be the same number of LayoutNames and LayoutDescriptions."));
for (int32 Index = 0; Index < LayoutNames.Num(); ++Index)
{
const FText& LayoutNameAsTextText = GenerateLocalizedTextForFile(LayoutNames[Index]);
const FText& LayoutDescriptionAsTextText = GenerateLocalizedTextForFile(LayoutDescriptions[Index]);
// Update fields
FLayoutSaveRestore::SaveSectionToConfig(GEditorLayoutIni, "LayoutName", LayoutNameAsTextText);
FLayoutSaveRestore::SaveSectionToConfig(GEditorLayoutIni, "LayoutDescription", LayoutDescriptionAsTextText);
// Flush file
const bool bRead = true;
GConfig->Flush(bRead, GEditorLayoutIni);
}
}
}
// "Export Layout..." (or "Save Layout As..." dialog could not be opened)
if (!bWasDialogOpened)
{
// Open the "save file" dialog so the user can save their layout configuration file
const FString DefaultFile = "";
bWereFilesSelected = DesktopPlatform->SaveFileDialog(
FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr),
TEXT("Export a Layout Configuration File"),
InDefaultDirectory,
DefaultFile,
TEXT("Layout configuration files|*.ini|"),
EFileDialogFlags::None, //EFileDialogFlags::Multiple, // Allow/Avoid multiple file selection
LayoutFilePaths
);
}
// If file(s) selected, copy them into the user layouts directory and load one of them
if (bWereFilesSelected && LayoutFilePaths.Num() > 0)
{
// Iterate over selected layout ini files
FString FirstGoodLayoutFile = "";
for (const FString& LayoutFilePath : LayoutFilePaths)
{
// If writing in the right folder
const FString LayoutFilePathAbsolute = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*FPaths::GetPath(LayoutFilePath));
const FString DefaultDirectoryAbsolute = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*FPaths::GetPath(InDefaultDirectory));
if (!bMustBeSavedInDefaultDirectory || (LayoutFilePathAbsolute == DefaultDirectoryAbsolute))
{
// Save in the user layout folder
const FString& SourceFilePath = GEditorLayoutIni;
const FString& TargetFilePath = LayoutFilePath;
const bool bCleanLayoutNameAndDescriptionFieldsIfNoSameValues = !bMustBeSavedInDefaultDirectory;
const bool bShowSaveToast = true;
TrySaveLayoutOrWarnInternal(SourceFilePath, TargetFilePath, InWhatIsThis, bCleanLayoutNameAndDescriptionFieldsIfNoSameValues,
bShouldAskBeforeCleaningLayoutNameAndDescriptionFields, bShowSaveToast);
}
// If trying to write in a different folder (which is not allowed)
else
{
// Warn the user that the file will not be copied in there
FMessageDialog::Open(
EAppMsgType::Ok,
FText::Format(
LOCTEXT("SaveAsFailedMsg", "In order to save the layout and allow Unreal to use it, you must save it in the predefined folder:\n{0}\n\nNevertheless, you tried to save it in:\n{1}\n\nIf you simply wish to export a copy of the current configuration in {1} (e.g., to later copy it into a different machine), you could use the \"Export Layout...\" functionality. However, Unreal would not be able to load it until you import it with \"Import Layout...\"."),
FText::FromString(DefaultDirectoryAbsolute), FText::FromString(LayoutFilePathAbsolute)),
LOCTEXT("SaveAsFailedMsg_Title", "Save As Failed"));
}
}
}
}
}
int32 FPrivateLayoutsMenu::GetNumberLayoutFiles(const FString& InLayoutsDirectory)
{
// Get Layout init files in desired directory
const TArray<FString> LayoutIniFileNames = GetIniFilesInFolderInternal(InLayoutsDirectory);
// Count how many layout files exist
int32 NumberLayoutFiles = 0;
for (const FString& LayoutIniFileName : LayoutIniFileNames)
{
const FString LayoutFilePath = FPaths::Combine(InLayoutsDirectory, LayoutIniFileName);
GConfig->UnloadFile(LayoutFilePath); // We must re-read it to avoid the Editor to use a previously cached name and description
if (FLayoutSaveRestore::IsValidConfig(LayoutFilePath))
{
++NumberLayoutFiles;
}
}
// Return result
return NumberLayoutFiles;
}
/* FLayoutsMenuLoad public functions
*****************************************************************************/
void FLayoutsMenuLoad::MakeLoadLayoutsMenu(UToolMenu* InToolMenu)
{
// MakeLoadLayoutsMenu
FPrivateLayoutsMenu::MakeXLayoutsMenuInternal(InToolMenu, FPrivateLayoutsMenu::ELayoutsMenu::Load);
// Additional sections
if (GetDefault<UEditorStyleSettings>()->bEnableUserEditorLayoutManagement)
{
FToolMenuSection& Section = InToolMenu->FindOrAddSection("UserDefaultLayouts");
// Separator
if (FLayoutsMenu::IsThereUserLayouts())
{
Section.AddSeparator("AdditionalSectionsSeparator");
}
// Import...
{
Section.AddMenuEntry(FMainFrameCommands::Get().ImportLayout);
}
}
}
bool FLayoutsMenuLoad::CanLoadChooseLayout(const int32 InLayoutIndex, const FLayoutsMenu::ELayoutsType InLayoutsType)
{
return !FLayoutsMenu::IsLayoutChecked(InLayoutIndex, InLayoutsType) && FPrivateLayoutsMenu::CanChooseLayoutWhenReadInternal();
}
void FLayoutsMenuLoad::ReloadCurrentLayout()
{
// If PIE, SIE, or any Asset Editors are opened, ask the user whether they want to automatically close them and continue loading the layout
if (!FPrivateLayoutsMenu::CheckAskUserToClosePIESIE(LOCTEXT("AreYouSureToLoadHeader", "Are you sure you want to continue loading the selected layout profile?")))
{
return;
}
// Create duplicated ini file (OriginalEditorLayoutIniFilePath)
// Explanation:
// Assume a layout is saved with (at least) a window that is dependent on a plugin. If that plugin is disabled and the editor restarted, that window will be saved on the layout but will
// not visually appear. We still wanna save the layout with it, so if its plugin is re-enabled, the window appear again. However, while the plugin is disabled, the layout ini file changes
// to reflect that the plugin is not opened.
// Technical details:
// Rather than changing the string generated in the ini file (which could affect other parts of the code), we will duplicate the ini file when loaded. If the ini file is different than
// its duplicated copy, then some widget is missing (most probably due to disabled plugins). If that is the case, we will re-save the ini file without telling UE that it changed. This way,
// the ini file would match its original one, and it would only be re-modified if the user modifies the layout (but in that case it should no longer match the original one).
const bool bShouldReplace = true;
const bool bEvenIfReadOnly = true;
const bool bCopyAttributes = false; // If true, it could e.g., copy the read-only flag of DefaultLayout.ini and make all the save/load stuff stop working
const FString& OriginalEditorLayoutIniFilePath = FPrivateLayoutsMenu::GetOriginalEditorLayoutIniFilePathInternal();
IFileManager::Get().Copy(*OriginalEditorLayoutIniFilePath, *GEditorLayoutIni, bShouldReplace, bEvenIfReadOnly, bCopyAttributes);
GConfig->UnloadFile(OriginalEditorLayoutIniFilePath);
// Disable config saving
UAssetEditorSubsystem* AssetEditorSubsystem = (GEditor ? GEditor->GetEditorSubsystem<UAssetEditorSubsystem>() : nullptr);
const bool bAreAssetEditorOpened = (AssetEditorSubsystem ? AssetEditorSubsystem->GetAllEditedAssets().Num() > 0 : false); // Are there asset editors opened?
TOptional<bool> bAutoRestoreAndDisableSavingOverride;
if (bAreAssetEditorOpened)
{
// Save open asset editors + disable manual saving (no need to close them with AssetEditorSubsystem->CloseAllAssetEditors)
AssetEditorSubsystem->SaveOpenAssetEditors(true);
bAutoRestoreAndDisableSavingOverride = AssetEditorSubsystem->GetAutoRestoreAndDisableSavingOverride();
AssetEditorSubsystem->SetAutoRestoreAndDisableSavingOverride(true);
}
FGlobalTabmanager::Get()->SetCanSavePersistentLayouts(false);
// Editor is reset on-the-fly
EditorReinit();
// Reenable config saving
FGlobalTabmanager::Get()->SetCanSavePersistentLayouts(true);
if (bAreAssetEditorOpened)
{
check(AssetEditorSubsystem);
// Restore asset editors + disable manual saving and avoid trying to re-open the asset editors twice
AssetEditorSubsystem->RestorePreviouslyOpenAssets();
AssetEditorSubsystem->SetAutoRestoreAndDisableSavingOverride(bAutoRestoreAndDisableSavingOverride);
}
// Save layout and create duplicated ini file (DuplicatedEditorLayoutIniFilePath)
FPrivateLayoutsMenu::SaveLayoutWithoutRemovingTempLayoutFiles();
// If same file, remove temp files
const bool bCheckTempFileToo = false;
if (FPrivateLayoutsMenu::IsLayoutCheckedInternal(OriginalEditorLayoutIniFilePath, bCheckTempFileToo))
{
FPrivateLayoutsMenu::RemoveTempEditorLayoutIniFilesInternal();
}
// Else, create DuplicatedEditorLayoutIniFilePath
else
{
const FString& DuplicatedEditorLayoutIniFilePath = FPrivateLayoutsMenu::GetDuplicatedEditorLayoutIniFilePathInternal();
IFileManager::Get().Copy(*DuplicatedEditorLayoutIniFilePath, *GEditorLayoutIni, bShouldReplace, bEvenIfReadOnly, bCopyAttributes);
GConfig->UnloadFile(DuplicatedEditorLayoutIniFilePath);
}
}
void FLayoutsMenuLoad::LoadLayout(const FString& InLayoutPath)
{
// Replace main layout with desired one
const FString& SourceFilePath = InLayoutPath;
const FString& TargetFilePath = GEditorLayoutIni;
const bool bCleanLayoutNameAndDescriptionFieldsIfNoSameValues = false;
const bool bShouldAskBeforeCleaningLayoutNameAndDescriptionFields = false;
const bool SucessfullySaved = FPrivateLayoutsMenu::TrySaveLayoutOrWarnInternal(SourceFilePath, TargetFilePath, LOCTEXT("LoadLayoutText", "loading the layout"),
bCleanLayoutNameAndDescriptionFieldsIfNoSameValues, bShouldAskBeforeCleaningLayoutNameAndDescriptionFields);
// Reload current layout
if (SucessfullySaved)
{
FLayoutsMenuLoad::ReloadCurrentLayout();
}
}
void FLayoutsMenuLoad::LoadLayout(const int32 InLayoutIndex, const FLayoutsMenu::ELayoutsType InLayoutsType)
{
// Replace main layout with desired one, reset layout & restart Editor
LoadLayout(FLayoutsMenu::GetLayout(InLayoutIndex, InLayoutsType));
}
void FLayoutsMenuLoad::ImportLayout()
{
// Import the user-selected layout configuration files and load one of them
IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get();
if (DesktopPlatform)
{
// Open File Dialog so the user can select their desired layout configuration files
TArray<FString> LayoutFilePaths;
const FString LastDirectory = FPaths::ProjectContentDir();
const FString DefaultDirectory = LastDirectory;
const FString DefaultFile = "";
const bool bWereFilesSelected = DesktopPlatform->OpenFileDialog(
FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr),
TEXT("Import a Layout Configuration File"),
DefaultDirectory,
DefaultFile,
TEXT("Layout configuration files|*.ini|"),
EFileDialogFlags::Multiple, //EFileDialogFlags::None, // Allow/Avoid multiple file selection
LayoutFilePaths
);
// If file(s) selected, copy them into the user layouts directory and load one of them
if (bWereFilesSelected && LayoutFilePaths.Num() > 0)
{
// (Create if it does not exist and) Get UserLayoutsDirectory path
const FString UserLayoutsDirectory = FPrivateLayoutsMenu::CreateAndGetDefaultLayoutDirInternal(FLayoutsMenu::ELayoutsType::User);
// Iterate over selected layout ini files
FString FirstGoodLayoutFile = "";
const FText TrySaveLayoutOrWarnInternalText = LOCTEXT("ImportLayoutText", "importing the layout");
for (const FString& LayoutFilePath : LayoutFilePaths)
{
// If file is a layout file, import it
GConfig->UnloadFile(LayoutFilePath); // We must re-read it to avoid the Editor to use a previously cached name and description
if (FLayoutSaveRestore::IsValidConfig(LayoutFilePath))
{
if (FirstGoodLayoutFile == "")
{
FirstGoodLayoutFile = LayoutFilePath;
}
// Save in the user layout folder
const FString& SourceFilePath = LayoutFilePath;
const FString& TargetFilePath = FPaths::Combine(FPaths::GetPath(UserLayoutsDirectory), FPaths::GetCleanFilename(LayoutFilePath));
const bool bCleanLayoutNameAndDescriptionFieldsIfNoSameValues = false;
const bool bShouldAskBeforeCleaningLayoutNameAndDescriptionFields = false;
FPrivateLayoutsMenu::TrySaveLayoutOrWarnInternal(SourceFilePath, TargetFilePath, TrySaveLayoutOrWarnInternalText,
bCleanLayoutNameAndDescriptionFieldsIfNoSameValues, bShouldAskBeforeCleaningLayoutNameAndDescriptionFields);
}
// File is not a layout file, warn the user
else
{
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("FileName"), FText::FromString(FPaths::ConvertRelativePathToFull(LayoutFilePath)));
const FText TextBody = FText::Format(LOCTEXT("UnsuccessfulImportBody", "Unsuccessful import, {FileName} is not a layout configuration file!"), Arguments);
const FText TextTitle = LOCTEXT("UnsuccessfulImportHeader", "Unsuccessful Import!");
FMessageDialog::Open(EAppMsgType::Ok, TextBody, TextTitle);
}
}
// If PIE, SIE, or any Asset Editors are opened, ask the user whether they wants to automatically close them and continue loading the layout
if (!FPrivateLayoutsMenu::CheckAskUserToClosePIESIE(LOCTEXT("LayoutImportClosePIEAndEditorAssetsHeader", "The layout(s) were successfully imported into the \"User Layouts\" section. Do you want to continue loading the selected layout profile?")))
{
return;
}
// Replace current layout with first one
if (FirstGoodLayoutFile != "")
{
const FString& SourceFilePath = FirstGoodLayoutFile;
const FString& TargetFilePath = GEditorLayoutIni;
const bool bCleanLayoutNameAndDescriptionFieldsIfNoSameValues = false;
const bool bShouldAskBeforeCleaningLayoutNameAndDescriptionFields = false;
const bool SucessfullySaved = FPrivateLayoutsMenu::TrySaveLayoutOrWarnInternal(SourceFilePath, TargetFilePath, TrySaveLayoutOrWarnInternalText,
bCleanLayoutNameAndDescriptionFieldsIfNoSameValues, bShouldAskBeforeCleaningLayoutNameAndDescriptionFields);
// Reload current layout
if (SucessfullySaved)
{
ReloadCurrentLayout();
}
}
}
}
}
/* FLayoutsMenuSave public functions
*****************************************************************************/
void FLayoutsMenuSave::MakeSaveLayoutsMenu(UToolMenu* InToolMenu)
{
if (GetDefault<UEditorStyleSettings>()->bEnableUserEditorLayoutManagement)
{
// MakeOverrideLayoutsMenu
FPrivateLayoutsMenu::MakeXLayoutsMenuInternal(InToolMenu, FPrivateLayoutsMenu::ELayoutsMenu::Save);
// Additional sections
{
FToolMenuSection& Section = InToolMenu->FindOrAddSection("UserDefaultLayouts");
// Separator
if (FLayoutsMenu::IsThereUserLayouts())
{
Section.AddSeparator("AdditionalSectionsSeparator");
}
// Save as...
{
Section.AddMenuEntry(FMainFrameCommands::Get().SaveLayoutAs);
}
// Export...
{
Section.AddMenuEntry(FMainFrameCommands::Get().ExportLayout);
}
}
}
}
bool FLayoutsMenuSave::CanSaveChooseLayout(const int32 InLayoutIndex, const FLayoutsMenu::ELayoutsType InLayoutsType)
{
return !FLayoutsMenu::IsLayoutChecked(InLayoutIndex, InLayoutsType) && FPrivateLayoutsMenu::CanChooseLayoutWhenWriteInternal(InLayoutsType);
}
void FLayoutsMenuSave::OverrideLayout(const int32 InLayoutIndex, const FLayoutsMenu::ELayoutsType InLayoutsType)
{
// Default layouts should never be modified, so this function should never be called
checkf(InLayoutsType == FLayoutsMenu::ELayoutsType::User, TEXT("Default layouts should never be modified, so this function should never be called."));
// (Create if it does not exist and) Get LayoutsDirectory path
const FString LayoutsDirectory = FPrivateLayoutsMenu::CreateAndGetDefaultLayoutDirInternal(InLayoutsType);
// Get Layout init files
const TArray<FString> LayoutIniFileNames = FPrivateLayoutsMenu::GetIniFilesInFolderInternal(LayoutsDirectory);
const FString DesiredLayoutFullPath = FPaths::Combine(FPaths::GetPath(LayoutsDirectory), LayoutIniFileNames[InLayoutIndex]);
// Are you sure you want to do this?
if (!FSaveLayoutDialogUtils::OverrideLayoutDialog(LayoutIniFileNames[InLayoutIndex]))
{
return;
}
// Target and source files
const FString& SourceFilePath = GEditorLayoutIni;
const FString& TargetFilePath = DesiredLayoutFullPath;
// Update GEditorLayoutIni file
SaveLayout();
// Replace desired layout with current one
const bool bCleanLayoutNameAndDescriptionFieldsIfNoSameValues = true;
const bool bShouldAskBeforeCleaningLayoutNameAndDescriptionFields = false;
const bool bShowSaveToast = true;
FPrivateLayoutsMenu::TrySaveLayoutOrWarnInternal(SourceFilePath, TargetFilePath, LOCTEXT("OverrideLayoutText", "overriding the layout"),
bCleanLayoutNameAndDescriptionFieldsIfNoSameValues, bShouldAskBeforeCleaningLayoutNameAndDescriptionFields, bShowSaveToast);
}
void FLayoutsMenuSave::SaveLayout()
{
// Save layout
FPrivateLayoutsMenu::SaveLayoutWithoutRemovingTempLayoutFiles();
// Remove temporary Editor Layout ini files if the layout (thus also GEditorLayoutIni) changed
const bool bCheckTempFileToo = false;
const FString& DuplicatedEditorLayoutIniFilePath = FPrivateLayoutsMenu::GetDuplicatedEditorLayoutIniFilePathInternal();
if (!FPrivateLayoutsMenu::IsLayoutCheckedInternal(DuplicatedEditorLayoutIniFilePath, bCheckTempFileToo))
{
FPrivateLayoutsMenu::RemoveTempEditorLayoutIniFilesInternal();
}
}
void FLayoutsMenuSave::SaveLayoutAs()
{
// Update GEditorLayoutIni file
SaveLayout();
// Copy GEditorLayoutIni into desired file
const FString DefaultDirectory = FPrivateLayoutsMenu::CreateAndGetDefaultLayoutDirInternal(FLayoutsMenu::ELayoutsType::User);
const bool bMustBeSavedInDefaultDirectory = true;
const bool bShouldAskBeforeCleaningLayoutNameAndDescriptionFields = false;
FPrivateLayoutsMenu::SaveExportLayoutCommon(DefaultDirectory, bMustBeSavedInDefaultDirectory, LOCTEXT("SaveLayoutText", "saving the layout"),
bShouldAskBeforeCleaningLayoutNameAndDescriptionFields);
}
void FLayoutsMenuSave::ExportLayout()
{
// Update GEditorLayoutIni file
SaveLayout();
// Copy GEditorLayoutIni into desired file
const FString DefaultDirectory = FPaths::ProjectContentDir();
const bool bMustBeSavedInDefaultDirectory = false;
const bool bShouldAskBeforeCleaningLayoutNameAndDescriptionFields = true;
FPrivateLayoutsMenu::SaveExportLayoutCommon(DefaultDirectory, bMustBeSavedInDefaultDirectory, LOCTEXT("ExportLayoutText", "exporting the layout"),
bShouldAskBeforeCleaningLayoutNameAndDescriptionFields);
}
/* FLayoutsMenuRemove public functions
*****************************************************************************/
void FLayoutsMenuRemove::MakeRemoveLayoutsMenu(UToolMenu* InToolMenu)
{
if (GetDefault<UEditorStyleSettings>()->bEnableUserEditorLayoutManagement)
{
// MakeRemoveLayoutsMenu
FPrivateLayoutsMenu::MakeXLayoutsMenuInternal(InToolMenu, FPrivateLayoutsMenu::ELayoutsMenu::Remove);
// Additional sections
{
FToolMenuSection& Section = InToolMenu->FindOrAddSection("UserDefaultLayouts");
// Separator
if (FLayoutsMenu::IsThereUserLayouts())
{
Section.AddSeparator("AdditionalSectionsSeparator");
}
// Remove all
Section.AddMenuEntry(FMainFrameCommands::Get().RemoveUserLayouts);
}
}
}
bool FLayoutsMenuRemove::CanRemoveChooseLayout(const FLayoutsMenu::ELayoutsType InLayoutsType)
{
return FPrivateLayoutsMenu::CanChooseLayoutWhenWriteInternal(InLayoutsType);
}
void FLayoutsMenuRemove::RemoveLayout(const int32 InLayoutIndex, const FLayoutsMenu::ELayoutsType InLayoutsType)
{
// Default layouts should never be modified, so this function should never be called
checkf(InLayoutsType == FLayoutsMenu::ELayoutsType::User, TEXT("Default layouts should never be modified, so this function should never be called."));
// (Create if it does not exist and) Get LayoutsDirectory path
const FString LayoutsDirectory = FPrivateLayoutsMenu::CreateAndGetDefaultLayoutDirInternal(InLayoutsType);
// Get Layout init files
const TArray<FString> LayoutIniFileNames = FPrivateLayoutsMenu::GetIniFilesInFolderInternal(LayoutsDirectory);
const FString DesiredLayoutFullPath = FPaths::Combine(FPaths::GetPath(LayoutsDirectory), LayoutIniFileNames[InLayoutIndex]);
// Are you sure you want to do this?
const FText TextFileNameToRemove = FText::FromString(FPaths::GetBaseFilename(LayoutIniFileNames[InLayoutIndex]));
const FText TextBody = FText::Format(LOCTEXT("ActionRemoveMsg", "Are you sure you want to permanently delete the layout profile \"{0}\"? This action cannot be undone."), TextFileNameToRemove);
const FText TextTitle = FText::Format(LOCTEXT("RemoveUILayout_Title", "Remove UI Layout \"{0}\"?"), TextFileNameToRemove);
if (EAppReturnType::Ok != FMessageDialog::Open(EAppMsgType::OkCancel, TextBody, TextTitle))
{
return;
}
// Remove layout
FPlatformFileManager::Get().GetPlatformFile().DeleteFile(*DesiredLayoutFullPath);
}
void FLayoutsMenuRemove::RemoveUserLayouts()
{
// (Create if it does not exist and) Get UserLayoutsDirectory path
const FString UserLayoutsDirectory = FPrivateLayoutsMenu::CreateAndGetDefaultLayoutDirInternal(FLayoutsMenu::ELayoutsType::User);
// Count how many layout files exist
const int32 NumberUserLayoutFiles = FPrivateLayoutsMenu::GetNumberLayoutFiles(UserLayoutsDirectory);
// If files to remove, warn user and remove them all
if (NumberUserLayoutFiles > 0)
{
// Are you sure you want to do this?
const FText TextBody = FText::Format(LOCTEXT("ActionRemoveAllUserLayoutMsg", "Are you sure you want to permanently remove {0} layout {0}|plural(one=profile,other=profiles)? This action cannot be undone."), NumberUserLayoutFiles);
const FText TextTitle = LOCTEXT("RemoveAllUserLayouts_Title", "Remove All User-Created Layouts?");
if (EAppReturnType::Ok != FMessageDialog::Open(EAppMsgType::OkCancel, TextBody, TextTitle))
{
return;
}
// Get User Layout init files
const TArray<FString> UserLayoutIniFileNames = FPrivateLayoutsMenu::GetIniFilesInFolderInternal(UserLayoutsDirectory);
// If there are user Layout ini files, read them
for (const FString& UserLayoutIniFileName : UserLayoutIniFileNames)
{
// Remove file if it is a layout
const FString LayoutFilePath = FPaths::Combine(UserLayoutsDirectory, UserLayoutIniFileName);
GConfig->UnloadFile(LayoutFilePath); // We must re-read it to avoid the Editor to use a previously cached name and description
if (FLayoutSaveRestore::IsValidConfig(LayoutFilePath))
{
FPlatformFileManager::Get().GetPlatformFile().DeleteFile(*LayoutFilePath);
}
}
}
// If no files to remove, warn user
else
{
// Show reason
const FText TextBody = LOCTEXT("UnsuccessfulRemoveLayoutBody", "There are no layout profile files created by the user, so none could be removed.");
const FText TextTitle = LOCTEXT("UnsuccessfulRemoveLayoutHeader", "Unsuccessful Remove All User Layouts!");
FMessageDialog::Open(EAppMsgType::Ok, TextBody, TextTitle);
}
}
/* FLayoutsMenu public functions
*****************************************************************************/
FString FLayoutsMenu::GetLayout(const int32 InLayoutIndex, const ELayoutsType InLayoutsType)
{
// (Create if it does not exist and) Get LayoutsDirectory path, layout init files, and desired layout path
const FString LayoutsDirectory = FPrivateLayoutsMenu::CreateAndGetDefaultLayoutDirInternal(InLayoutsType);
const TArray<FString> LayoutIniFileNames = FPrivateLayoutsMenu::GetIniFilesInFolderInternal(LayoutsDirectory);
const FString DesiredLayoutFullPath = FPaths::Combine(FPaths::GetPath(LayoutsDirectory), LayoutIniFileNames[InLayoutIndex]);
// Return full path
return DesiredLayoutFullPath;
}
bool FLayoutsMenu::IsThereUserLayouts()
{
// (Create if it does not exist and) Get UserLayoutsDirectory path
const FString UserLayoutsDirectory = FPrivateLayoutsMenu::CreateAndGetDefaultLayoutDirInternal(ELayoutsType::User);
// At least 1 user layout file?
return FPrivateLayoutsMenu::GetNumberLayoutFiles(UserLayoutsDirectory) > 0;
}
bool FLayoutsMenu::IsLayoutChecked(const int32 InLayoutIndex, const ELayoutsType InLayoutsType)
{
#if PLATFORM_MAC // On Mac, each time a key is pressed, all menus are re-generated, stalling the Editor given that SaveLayout is slow on Mac because it does not caches as in Windows.
return false;
#else
// Check if the desired layout file matches the one currently loaded
const bool bCheckTempFileToo = true;
return FPrivateLayoutsMenu::IsLayoutCheckedInternal(GetLayout(InLayoutIndex, InLayoutsType), bCheckTempFileToo);
#endif
}
#undef LOCTEXT_NAMESPACE
#endif