Files
UnrealEngine/Engine/Source/Runtime/Slate/Private/Framework/Docking/LayoutService.cpp
2025-05-18 13:04:45 +08:00

476 lines
16 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Framework/Docking/LayoutService.h"
#include "HAL/FileManager.h"
#include "Internationalization/Regex.h"
#include "Misc/App.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/FileHelper.h"
#include "Misc/StringUtility.h"
#include "Dom/JsonObject.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "String/CaseConversion.h"
DEFINE_LOG_CATEGORY_STATIC(LogLayoutService, Log, All);
const TCHAR* DefaultEditorLayoutsSectionName = TEXT("EditorLayouts");
static const TCHAR* GetEditorLayoutsSectionName()
{
static FString EditorLayoutsSectionName;
if (EditorLayoutsSectionName.IsEmpty())
{
GConfig->GetString(TEXT("Slate"), TEXT("EditorLayoutsSectionName"), EditorLayoutsSectionName, GEditorIni);
EditorLayoutsSectionName.TrimStartAndEndInline();
if (EditorLayoutsSectionName.IsEmpty())
{
EditorLayoutsSectionName = DefaultEditorLayoutsSectionName;
}
}
return *EditorLayoutsSectionName;
}
static bool GetConfigString(const TCHAR* Key, FString& Value, const FString& Filename, bool bAllowFallback = true)
{
if (!GConfig->GetString(GetEditorLayoutsSectionName(), Key, Value, Filename))
{
if (bAllowFallback && GetEditorLayoutsSectionName() != DefaultEditorLayoutsSectionName)
{
return GConfig->GetString(DefaultEditorLayoutsSectionName, Key, Value, Filename);
}
return false;
}
// Old custom layouts may have an incorrect name, so account for that here.
if (const bool bIsUserLayout = Filename.StartsWith(FPaths::EngineUserLayoutDir()))
{
// Inject corrected label into result
FRegexPattern Pattern(TEXT("\"\\w+\"")); // words between commas
FRegexMatcher Matcher(Pattern, Value);
int32 MatchStart = INDEX_NONE;
int32 MatchEnd = INDEX_NONE;
// We want the last match, so run until none found and use the last result
while (Matcher.FindNext())
{
MatchStart = Matcher.GetMatchBeginning();
MatchEnd = Matcher.GetMatchEnding();
}
if (MatchStart != INDEX_NONE && MatchEnd != INDEX_NONE)
{
Value = Value.Left(MatchStart + 1) + FString(WriteToString<16>(UE::String::PascalCase(FPaths::GetBaseFilename(Filename)))) + Value.Right(Value.Len() - MatchEnd + 1);
}
}
return true;
}
static bool GetConfigSection(TArray<FString>& Result, const FString& Filename, bool bAllowFallback = true)
{
if (!GConfig->GetSection(GetEditorLayoutsSectionName(), Result, Filename))
{
if (bAllowFallback && GetEditorLayoutsSectionName() != DefaultEditorLayoutsSectionName)
{
return GConfig->GetSection(DefaultEditorLayoutsSectionName, Result, Filename);
}
return false;
}
return true;
}
static const FConfigSection* GetConfigSectionPrivate(const bool Force, const FString& Filename, bool bAllowFallback = true)
{
const FConfigSection* FoundSection = GConfig->GetSection(GetEditorLayoutsSectionName(), /*Force*/false, Filename);
if (FoundSection)
{
if (bAllowFallback && GetEditorLayoutsSectionName() != DefaultEditorLayoutsSectionName)
{
FoundSection = GConfig->GetSection(DefaultEditorLayoutsSectionName, /*Force*/false, Filename);
}
}
return FoundSection;
}
static FString PrepareLayoutStringForIni(const FString& LayoutString)
{
// Have to store braces as parentheses due to braces causing ini issues
return LayoutString
.Replace(TEXT("{"), TEXT("("))
.Replace(TEXT("}"), TEXT(")"))
.Replace(TEXT("\r"), TEXT(""))
.Replace(TEXT("\n"), TEXT(""))
.Replace(TEXT("\t"), TEXT(""));
}
static FString GetLayoutStringFromIni(const FString& LayoutString, bool& bOutIsJson)
{
if (FTextStringHelper::IsComplexText(*LayoutString))
{
bOutIsJson = false;
return LayoutString;
}
// Revert parenthesis to braces, from ini readable to Json readable
bOutIsJson = true;
return LayoutString
.Replace(TEXT("("), TEXT("{"))
.Replace(TEXT(")"), TEXT("}"))
.Replace(TEXT("\\") LINE_TERMINATOR, LINE_TERMINATOR);
}
const FString& FLayoutSaveRestore::GetAdditionalLayoutConfigIni()
{
static const FString IniSectionAdditionalConfig = TEXT("SlateAdditionalLayoutConfig");
return IniSectionAdditionalConfig;
}
static TSharedPtr<FJsonObject> ConvertSectionToJson(const TArray<FString>& SectionStrings)
{
TSharedPtr<FJsonObject> RootObject = MakeShared<FJsonObject>();
for (const FString& SectionPair : SectionStrings)
{
FString Key, Value;
if (SectionPair.Split(TEXT("="), &Key, &Value))
{
bool bIsJson = true;
Value = GetLayoutStringFromIni(Value, bIsJson);
if (bIsJson)
{
TSharedPtr<FJsonObject> ChildObject = MakeShared<FJsonObject>();
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Value);
if (FJsonSerializer::Deserialize(Reader, ChildObject))
{
RootObject->SetObjectField(Key, ChildObject);
}
else
{
bIsJson = false;
}
}
if (!bIsJson)
{
RootObject->SetStringField(Key, Value);
}
}
}
return RootObject;
}
static FString GetLayoutJsonFileName(const FString& InConfigFileName)
{
const FString JsonFileName = FPaths::GetBaseFilename(InConfigFileName) + TEXT(".json");
#ifdef UE_SAVED_DIR_OVERRIDE
const FString UserSettingsPath = FPaths::Combine(FPlatformProcess::UserSettingsDir(), TEXT(PREPROCESSOR_TO_STRING(UE_SAVED_DIR_OVERRIDE)), TEXT("Editor"), JsonFileName);
#else
const FString UserSettingsPath = FPaths::Combine(FPlatformProcess::UserSettingsDir(), FApp::GetEpicProductIdentifier(), TEXT("Editor"), JsonFileName);
#endif
return UserSettingsPath;
}
static TSharedPtr<FJsonObject> LoadJsonFile(const FString& InFileName)
{
TSharedPtr<FJsonObject> JsonObject;
FString JsonContents;
if (FFileHelper::LoadFileToString(JsonContents, *InFileName))
{
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonContents);
FJsonSerializer::Deserialize(Reader, JsonObject);
}
return JsonObject;
}
static bool SaveJsonFile(const FString& InFileName, TSharedPtr<FJsonObject> JsonObject)
{
FString NewJsonContents;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&NewJsonContents);
if (FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer))
{
return FFileHelper::SaveStringToFile(NewJsonContents, *InFileName);
}
return false;
}
static void SaveLayoutToJson(const FString& InConfigFileName, const TSharedRef<FTabManager::FLayout>& InLayoutToSave)
{
const FString UserSettingsPath = GetLayoutJsonFileName(InConfigFileName);
TSharedPtr<FJsonObject> AllLayoutsObject = LoadJsonFile(UserSettingsPath);
if (!AllLayoutsObject.IsValid())
{
// doesn't exist
AllLayoutsObject = MakeShared<FJsonObject>();
}
AllLayoutsObject->SetObjectField(InLayoutToSave->GetLayoutName().ToString(), InLayoutToSave->ToJson());
SaveJsonFile(UserSettingsPath, AllLayoutsObject);
}
static bool LoadLayoutFromJson(const FString& InConfigFileName, const FString& InLayoutName, TSharedPtr<FTabManager::FLayout>& OutLayout)
{
const FString LayoutJsonFile = GetLayoutJsonFileName(InConfigFileName);
TSharedPtr<FJsonObject> JsonObject = LoadJsonFile(LayoutJsonFile);
if (!JsonObject.IsValid())
{
return false;
}
const TSharedPtr<FJsonObject>* LayoutJson = nullptr;
if (JsonObject->TryGetObjectField(InLayoutName, LayoutJson))
{
OutLayout = FTabManager::FLayout::NewFromJson(*LayoutJson);
return true;
}
return false;
}
void FLayoutSaveRestore::SaveToConfig( const FString& InConfigFileName, const TSharedRef<FTabManager::FLayout>& InLayoutToSave )
{
if (FGlobalTabmanager::Get()->CanSavePersistentLayouts())
{
// Only save to config if it's not the FTabManager::FLayout::NullLayout
if (InLayoutToSave->GetLayoutName() != FTabManager::FLayout::NullLayout->GetLayoutName())
{
const FString LayoutAsString = PrepareLayoutStringForIni(InLayoutToSave->ToString());
GConfig->SetString(GetEditorLayoutsSectionName(), *InLayoutToSave->GetLayoutName().ToString(), *LayoutAsString, InConfigFileName);
SaveLayoutToJson(InConfigFileName, InLayoutToSave);
}
}
}
TSharedRef<FTabManager::FLayout> FLayoutSaveRestore::LoadFromConfig(const FString& InConfigFileName, const TSharedRef<FTabManager::FLayout>& InDefaultLayout,
const EOutputCanBeNullptr InPrimaryAreaOutputCanBeNullptr)
{
TArray<FString> DummyArray;
return FLayoutSaveRestore::LoadFromConfigPrivate(InConfigFileName, InDefaultLayout, InPrimaryAreaOutputCanBeNullptr, false, DummyArray);
}
TSharedRef<FTabManager::FLayout> FLayoutSaveRestore::LoadFromConfig(const FString& InConfigFileName,
const TSharedRef<FTabManager::FLayout>& InDefaultLayout, const EOutputCanBeNullptr InPrimaryAreaOutputCanBeNullptr, TArray<FString>& OutRemovedOlderLayoutVersions)
{
return FLayoutSaveRestore::LoadFromConfigPrivate(InConfigFileName, InDefaultLayout, InPrimaryAreaOutputCanBeNullptr, true, OutRemovedOlderLayoutVersions);
}
TSharedRef<FTabManager::FLayout> FLayoutSaveRestore::LoadFromConfigPrivate(const FString& InConfigFileName, const TSharedRef<FTabManager::FLayout>& InDefaultLayout,
const EOutputCanBeNullptr InPrimaryAreaOutputCanBeNullptr, const bool bInRemoveOlderLayoutVersions, TArray<FString>& OutRemovedOlderLayoutVersions)
{
const FString LayoutNameString = InDefaultLayout->GetLayoutName().ToString();
TSharedPtr<FTabManager::FLayout> UserLayout;
// First try to load from JSON, then INI if that does not exist
if (!LoadLayoutFromJson(InConfigFileName, LayoutNameString, UserLayout))
{
FString IniLayoutString;
// If the Key (InDefaultLayout->GetLayoutName()) already exists in the section GetEditorLayoutsSectionName() of the file InConfigFileName, try to load the layout from that file
GetConfigString(*LayoutNameString, IniLayoutString, InConfigFileName);
bool bIsJson = false;
UserLayout = FTabManager::FLayout::NewFromString( GetLayoutStringFromIni( IniLayoutString, bIsJson ) );
}
if (UserLayout.IsValid())
{
// Return UserLayout in the following 2 cases:
// - By default (PrimaryAreaOutputCanBeNullptr = Never or IfNoTabValid)
// - For the case of PrimaryAreaOutputCanBeNullptr = IfNoOpenTabValid, only if the primary area has at least a valid open tab
TSharedPtr<FTabManager::FArea> PrimaryArea = UserLayout->GetPrimaryArea().Pin();
if (PrimaryArea.IsValid() && (InPrimaryAreaOutputCanBeNullptr != EOutputCanBeNullptr::IfNoOpenTabValid || FGlobalTabmanager::Get()->HasValidOpenTabs(PrimaryArea.ToSharedRef())))
{
return UserLayout.ToSharedRef();
}
}
// If the file layout could not be loaded and the caller wants to remove old fields
else if (bInRemoveOlderLayoutVersions)
{
// If File and Section exist
if (const FConfigSection* ConfigSection = GetConfigSectionPrivate(/*Force*/false, InConfigFileName))
{
// If Key does not exist (i.e., Section does but not contain that Key)
if (!ConfigSection->Find(*LayoutNameString))
{
// Create LayoutKeyToRemove
FString LayoutKeyToRemove;
for (int32 Index = LayoutNameString.Len() - 1; Index > 0; --Index)
{
if (LayoutNameString[Index] != TCHAR('.') && (LayoutNameString[Index] < TCHAR('0') || LayoutNameString[Index] > TCHAR('9')))
{
LayoutKeyToRemove = LayoutNameString.Left(Index+1);
break;
}
}
// Look for older versions of this Key
OutRemovedOlderLayoutVersions.Empty();
for (const auto& SectionPair : *ConfigSection/*->ArrayOfStructKeys*/)
{
FString CurrentKey = SectionPair.Key.ToString();
if (CurrentKey.Len() > LayoutKeyToRemove.Len() && CurrentKey.Left(LayoutKeyToRemove.Len()) == LayoutKeyToRemove)
{
OutRemovedOlderLayoutVersions.Emplace(std::move(CurrentKey));
}
}
// Remove older versions of this Key
for (const FString& KeyToRemove : OutRemovedOlderLayoutVersions)
{
if (GConfig->RemoveKey(GetEditorLayoutsSectionName(), *KeyToRemove, InConfigFileName))
{
UE_LOG(LogLayoutService, Warning, TEXT("While key \"%s\" was not found, and older version exists (key \"%s\"). This means section \"%s\" was"
" created with a previous version of UE and is no longer compatible. The old key has been removed and updated with the new one."),
*LayoutNameString, *KeyToRemove, GetEditorLayoutsSectionName());
}
else
{
UE_LOG(LogLayoutService, Warning, TEXT("Could not remove old layout key %s because it exists in the fallback section %s instead of the current section %s"),
*KeyToRemove, DefaultEditorLayoutsSectionName, GetEditorLayoutsSectionName());
}
}
}
}
}
return InDefaultLayout;
}
void FLayoutSaveRestore::SaveSectionToConfig(const FString& InConfigFileName, const FString& InSectionName, const FText& InSectionValue)
{
FString StrValue;
FTextStringHelper::WriteToBuffer(StrValue, InSectionValue);
GConfig->SetString(GetEditorLayoutsSectionName(), *InSectionName, *StrValue, InConfigFileName);
const FString JsonFileName = GetLayoutJsonFileName(InConfigFileName);
TSharedPtr<FJsonObject> JsonObject = LoadJsonFile(JsonFileName);
if (JsonObject.IsValid())
{
JsonObject->SetStringField(InSectionName, *StrValue);
SaveJsonFile(JsonFileName, JsonObject);
}
}
FText FLayoutSaveRestore::LoadSectionFromConfig(const FString& InConfigFileName, const FString& InSectionName, const bool bIsOptional)
{
FString ValueString;
const FString JsonFileName = GetLayoutJsonFileName(InConfigFileName);
TSharedPtr<FJsonObject> JsonObject = LoadJsonFile(JsonFileName);
if (JsonObject.IsValid())
{
// If optional, we check if the field exists first
if (!bIsOptional
|| (bIsOptional && JsonObject->HasTypedField(InSectionName, EJson::String)))
{
ValueString = JsonObject->GetStringField(InSectionName);
}
}
if (ValueString.IsEmpty())
{
GetConfigString(*InSectionName, ValueString, InConfigFileName);
}
FText ValueText;
FTextStringHelper::ReadFromBuffer(*ValueString, ValueText, GetEditorLayoutsSectionName());
return ValueText;
}
bool FLayoutSaveRestore::DuplicateConfig(const FString& SourceConfigFileName, const FString& TargetConfigFileName)
{
const bool bShouldReplace = true;
const bool bCopyEvenIfReadOnly = true;
const bool bCopyAttributes = false; // If true, we could copy the read-only flag of DefaultLayout.ini and cause save/load to stop working
if (IFileManager::Get().Copy(*TargetConfigFileName, *SourceConfigFileName, bShouldReplace, bCopyEvenIfReadOnly, bCopyAttributes) == COPY_Fail)
{
return false;
}
// convert this layout to a JSON file
TArray<FString> SectionPairs;
GetConfigSection(SectionPairs, TargetConfigFileName);
TSharedPtr<FJsonObject> RootObject = ConvertSectionToJson(SectionPairs);
const FString TargetJsonFilename = GetLayoutJsonFileName(TargetConfigFileName);
SaveJsonFile(TargetJsonFilename, RootObject);
return true;
}
void FLayoutSaveRestore::MigrateConfig( const FString& OldConfigFileName, const FString& NewConfigFileName )
{
TArray<FString> OldSectionStrings;
// check whether any layout configuration needs to be migrated
if (!GetConfigSection(OldSectionStrings, OldConfigFileName) || (OldSectionStrings.Num() == 0))
{
return;
}
TArray<FString> NewSectionStrings;
// migrate old configuration if a new layout configuration does not yet exist
if (!GConfig->GetSection(GetEditorLayoutsSectionName(), NewSectionStrings, NewConfigFileName) || (NewSectionStrings.Num() == 0))
{
FString Key, Value;
for (const FString& SectionString : OldSectionStrings)
{
if (SectionString.Split(TEXT("="), &Key, &Value))
{
GConfig->SetString(GetEditorLayoutsSectionName(), *Key, *Value, NewConfigFileName);
}
}
}
// remove old configuration
if (!GConfig->EmptySection(GetEditorLayoutsSectionName(), OldConfigFileName))
{
UE_LOG(LogLayoutService, Warning, TEXT("Could not remove old layout in %s because it is using the fallback section %s instead of the current section %s. New layout file will still be created."),
*OldConfigFileName, DefaultEditorLayoutsSectionName, GetEditorLayoutsSectionName());
}
GConfig->Flush(false, OldConfigFileName);
GConfig->Flush(false, NewConfigFileName);
// migrate layout to JSON as well
const FString NewLayoutJsonFileName = GetLayoutJsonFileName(NewConfigFileName);
TSharedPtr<FJsonObject> JsonObject = LoadJsonFile(NewLayoutJsonFileName);
if (!JsonObject.IsValid() || JsonObject->Values.Num() == 0)
{
TSharedPtr<FJsonObject> RootObject = ConvertSectionToJson(OldSectionStrings);
SaveJsonFile(NewLayoutJsonFileName, RootObject);
}
}
bool FLayoutSaveRestore::IsValidConfig(const FString& InConfigFileName, bool bAllowFallback)
{
if (GConfig->DoesSectionExist(GetEditorLayoutsSectionName(), *InConfigFileName))
{
return true;
}
else if (bAllowFallback && GConfig->DoesSectionExist(DefaultEditorLayoutsSectionName, *InConfigFileName))
{
return true;
}
const FString JsonFileName = GetLayoutJsonFileName(InConfigFileName);
return IFileManager::Get().FileExists(*JsonFileName);
}