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

258 lines
11 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ConfigEditorPropertyDetails.h"
#include "ConfigPropertyHelper.h"
#include "Containers/Array.h"
#include "Containers/UnrealString.h"
#include "CoreGlobals.h"
#include "Delegates/Delegate.h"
#include "DetailCategoryBuilder.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"
#include "Framework/Views/ITypedTableView.h"
#include "HAL/Platform.h"
#include "HAL/PlatformCrt.h"
#include "IConfigEditorModule.h"
#include "IDetailPropertyRow.h"
#include "IPropertyTable.h"
#include "IPropertyTableColumn.h"
#include "Internationalization/Internationalization.h"
#include "Layout/Visibility.h"
#include "Misc/AssertionMacros.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/ConfigContext.h"
#include "Modules/ModuleManager.h"
#include "PropertyEditorModule.h"
#include "PropertyHandle.h"
#include "PropertyVisualization/ConfigPropertyColumn.h"
#include "Templates/Casts.h"
#include "UObject/Class.h"
#include "UObject/Field.h"
#include "UObject/NameTypes.h"
#include "UObject/Object.h"
#include "UObject/ObjectMacros.h"
#include "UObject/Package.h"
#include "UObject/UObjectGlobals.h"
#include "UObject/UnrealType.h"
#include "UObject/WeakFieldPtr.h"
class SWidget;
#define LOCTEXT_NAMESPACE "ConfigPropertyHelperDetails"
////////////////////////////////////////////////
// FConfigPropertyHelperDetails
void FConfigPropertyHelperDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
TSharedPtr<IPropertyHandle> PropertyHandle = DetailBuilder.GetProperty("EditProperty");
DetailBuilder.HideProperty(PropertyHandle);
FProperty* PropValue;
PropertyHandle->GetValue(PropValue);
OriginalProperty = CastFieldChecked<FProperty>(PropValue);
// Create a unique runtime UClass with the provided property as the only member. We will use this in the details view for the config hierarchy.
FName TempClassName = *(FString(TEXT("TempConfigEditorUClass_")) + OriginalProperty->GetOwnerVariant().GetName() + OriginalProperty->GetName());
ConfigEditorPropertyViewClass = NewObject<UClass>(GetTransientPackage(), TempClassName, RF_Standalone);
// Keep a record of the FProperty we are looking to update
ConfigEditorCopyOfEditProperty = CastField<FProperty>(FField::Duplicate(OriginalProperty, ConfigEditorPropertyViewClass, PropValue->GetFName()));
ConfigEditorPropertyViewClass->ClassConfigName = OriginalProperty->GetOwnerClass()->ClassConfigName;
ConfigEditorPropertyViewClass->SetSuperStruct(UObject::StaticClass());
ConfigEditorPropertyViewClass->ClassFlags |= (CLASS_DefaultConfig | CLASS_Config);
ConfigEditorPropertyViewClass->AddCppProperty(ConfigEditorCopyOfEditProperty);
ConfigEditorPropertyViewClass->Bind();
ConfigEditorPropertyViewClass->StaticLink(true);
ConfigEditorPropertyViewClass->AssembleReferenceTokenStream();
ConfigEditorPropertyViewClass->AddToRoot();
// Cache the CDO for the object
ConfigEditorPropertyViewCDO = ConfigEditorPropertyViewClass->GetDefaultObject(true);
ConfigEditorPropertyViewCDO->AddToRoot();
// Get access to all of the config files where this property is configurable.
ConfigFilesHandle = DetailBuilder.GetProperty("ConfigFilePropertyObjects");
DetailBuilder.HideProperty(ConfigFilesHandle);
// Add the properties to a property table so we can edit these.
IDetailCategoryBuilder& ConfigHierarchyCategory = DetailBuilder.EditCategory("ConfigHierarchy");
ConfigHierarchyCategory.AddCustomRow(LOCTEXT("ConfigHierarchy", "ConfigHierarchy"))
[
// Create a property table with the values.
ConstructPropertyTable(DetailBuilder)
];
// Listen for changes to the properties, we handle these by updating the ini file associated.
FCoreUObjectDelegates::OnObjectPropertyChanged.AddSP(this, &FConfigPropertyHelperDetails::OnPropertyValueChanged);
}
void FConfigPropertyHelperDetails::OnPropertyValueChanged(UObject* Object, FPropertyChangedEvent& PropertyChangedEvent)
{
UClass* OwnerClass = ConfigEditorCopyOfEditProperty->GetOwnerClass();
if (Object->IsA(OwnerClass))
{
const FString* FileName = ConfigFileAndPropertySourcePairings.FindKey(Object);
if (FileName != nullptr)
{
const FString& ConfigIniName = *FileName;
// We should set this up to work with the UObject Config system, its difficult as the outer object isnt of the same type
// create a sandbox FConfigCache
FConfigCacheIni Config(EConfigCacheType::Temporary);
// add an empty file to the config so it doesn't read in the original file (see FConfigCacheIni.Find())
FConfigFile& NewFile = Config.Add(ConfigIniName, FConfigFile());
// save the object properties to this file
OriginalProperty->GetOwnerClass()->GetDefaultObject()->SaveConfig(CPF_Config, *ConfigIniName, &Config);
// Take the saved section for this object and have the config system process and write out the one property we care about.
TArray<FString> Keys;
NewFile.GetKeys(Keys);
const FString SectionName = Keys[0];
const FString PropertyName = ConfigEditorCopyOfEditProperty->GetName();
FString Value;
ConfigEditorCopyOfEditProperty->ExportText_InContainer(0, Value, Object, Object, Object, 0);
NewFile.SetString(*SectionName, *PropertyName, *Value);
GConfig->SetString(*SectionName, *PropertyName, *Value, ConfigIniName);
NewFile.UpdateSinglePropertyInSection(*ConfigIniName, *PropertyName, *SectionName);
// reload the file, so that it refresh the cache internally.
if (GConfig->FindConfigFile(ConfigIniName))
{
FConfigContext::ForceReloadIntoGConfig().Load(*OriginalProperty->GetOwnerClass()->ClassConfigName.ToString());
}
else
{
FConfigCacheIni::ClearOtherPlatformConfigs();
}
// Update the CDO, as this change might have had an impact on it's value.
OriginalProperty->GetOwnerClass()->GetDefaultObject()->ReloadConfig();
}
}
}
void FConfigPropertyHelperDetails::AddEditablePropertyForConfig(IDetailLayoutBuilder& DetailBuilder, const UPropertyConfigFileDisplayRow* ConfigFilePropertyRowObj)
{
AssociatedConfigFileAndObjectPairings.Add(ConfigFilePropertyRowObj->ConfigFileName, (UObject*)ConfigFilePropertyRowObj);
// Add the properties to a property table so we can edit these.
IDetailCategoryBuilder& TempCategory = DetailBuilder.EditCategory("TempCategory");
UObject* ConfigEntryObject = StaticDuplicateObject(ConfigEditorPropertyViewCDO, GetTransientPackage(), *(ConfigFilePropertyRowObj->ConfigFileName + TEXT("_cdoDupe_") + ConfigEditorPropertyViewCDO->GetName()));
ConfigEntryObject->AddToRoot();
FString ExistingConfigEntryValue;
FString SectionName = OriginalProperty->GetOwnerClass()->GetPathName();
FString PropertyName = ConfigEditorCopyOfEditProperty->GetName();
if (GConfig->GetString(*SectionName, *PropertyName, ExistingConfigEntryValue, ConfigFilePropertyRowObj->ConfigFileName))
{
ConfigEditorCopyOfEditProperty->ImportText_InContainer(*ExistingConfigEntryValue, ConfigEntryObject, nullptr, 0);
}
// Cache a reference for future usage.
ConfigFileAndPropertySourcePairings.Add(ConfigFilePropertyRowObj->ConfigFileName, (UObject*)ConfigEntryObject);
// We need to add a property row for each config file entry.
// This allows us to have an editable widget for each config file.
TArray<UObject*> ConfigPropertyDisplayObjects;
ConfigPropertyDisplayObjects.Add(ConfigEntryObject);
if (IDetailPropertyRow* ExternalRow = TempCategory.AddExternalObjectProperty(ConfigPropertyDisplayObjects, ConfigEditorCopyOfEditProperty->GetFName()))
{
TSharedPtr<SWidget> NameWidget;
TSharedPtr<SWidget> ValueWidget;
ExternalRow->GetDefaultWidgets(NameWidget, ValueWidget);
// Register the Value widget and config file pairing with the config editor.
// The config editor needs this to determine what a cell presenter shows.
IConfigEditorModule& ConfigEditor = FModuleManager::Get().LoadModuleChecked<IConfigEditorModule>("ConfigEditor");
ConfigEditor.AddExternalPropertyValueWidgetAndConfigPairing(ConfigFilePropertyRowObj->ConfigFileName, ValueWidget);
// now hide the property so it is not added to the property display view
ExternalRow->Visibility(EVisibility::Hidden);
}
}
TSharedRef<SWidget> FConfigPropertyHelperDetails::ConstructPropertyTable(IDetailLayoutBuilder& DetailBuilder)
{
FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyTable = PropertyEditorModule.CreatePropertyTable();
PropertyTable->SetSelectionMode(ESelectionMode::None);
PropertyTable->SetSelectionUnit(EPropertyTableSelectionUnit::None);
PropertyTable->SetIsUserAllowedToChangeRoot(false);
PropertyTable->SetShowObjectName(false);
RepopulatePropertyTable(DetailBuilder);
TArray< TSharedRef<class IPropertyTableCustomColumn>> CustomColumns;
TSharedRef<FConfigPropertyCustomColumn> EditPropertyColumn = MakeShareable(new FConfigPropertyCustomColumn());
EditPropertyColumn->EditProperty = ConfigEditorCopyOfEditProperty; // FindFieldChecked<FProperty>(FPropertyConfigFileDisplay::StaticClass(), TEXT("EditProperty"));
CustomColumns.Add(EditPropertyColumn);
//TSharedRef<FConfigPropertyConfigFileStateCustomColumn> ConfigFileStateColumn = MakeShareable(new FConfigPropertyConfigFileStateCustomColumn());
//ConfigFileStateColumn->SupportedProperty = FindFieldChecked<FProperty>(FPropertyConfigFileDisplay::StaticClass(), TEXT("FileState"));
//CustomColumns.Add(ConfigFileStateColumn);
return PropertyEditorModule.CreatePropertyTableWidget(PropertyTable.ToSharedRef(), CustomColumns);
}
void FConfigPropertyHelperDetails::RepopulatePropertyTable(IDetailLayoutBuilder& DetailBuilder)
{
// Clear out any previous entries from the table.
AssociatedConfigFileAndObjectPairings.Empty();
// Add an entry for each config so the value can be set in each of the config files independently.
uint32 ConfigCount = 0;
TSharedPtr<IPropertyHandleArray> ConfigFilesArrayHandle = ConfigFilesHandle->AsArray();
ConfigFilesArrayHandle->GetNumElements(ConfigCount);
// For each config file, add the capacity to edit this property.
for (uint32 Index = 0; Index < ConfigCount; Index++)
{
FString ConfigFile;
TSharedRef<IPropertyHandle> ConfigFileElementHandle = ConfigFilesArrayHandle->GetElement(Index);
UObject* ConfigFileSinglePropertyHelperObj = nullptr;
ensure(ConfigFileElementHandle->GetValue(ConfigFileSinglePropertyHelperObj) == FPropertyAccess::Success);
UPropertyConfigFileDisplayRow* ConfigFileSinglePropertyHelper = CastChecked<UPropertyConfigFileDisplayRow>(ConfigFileSinglePropertyHelperObj);
AddEditablePropertyForConfig(DetailBuilder, ConfigFileSinglePropertyHelper);
}
// We need a row for each config file
TArray<UObject*> ConfigPropertyDisplayObjects;
AssociatedConfigFileAndObjectPairings.GenerateValueArray(ConfigPropertyDisplayObjects);
PropertyTable->SetObjects(ConfigPropertyDisplayObjects);
// We need a column for each property in our Helper class.
for (FProperty* NextProperty = UPropertyConfigFileDisplayRow::StaticClass()->PropertyLink; NextProperty; NextProperty = NextProperty->PropertyLinkNext)
{
PropertyTable->AddColumn((TWeakFieldPtr<FProperty>)NextProperty);
}
// Ensure the columns cannot be removed.
TArray<TSharedRef<IPropertyTableColumn>> Columns = PropertyTable->GetColumns();
for (TSharedRef<IPropertyTableColumn> Column : Columns)
{
Column->SetFrozen(true);
}
// Create the 'Config File' vs 'Property' table
PropertyTable->RequestRefresh();
}
#undef LOCTEXT_NAMESPACE