// Copyright Epic Games, Inc. All Rights Reserved. #include "InputCustomizations.h" #include "ActionMappingDetails.h" #include "DetailCategoryBuilder.h" #include "DetailLayoutBuilder.h" #include "EnhancedActionKeyMapping.h" #include "IDetailChildrenBuilder.h" #include "InputMappingContext.h" #include "KeyStructCustomization.h" #include "PropertyCustomizationHelpers.h" #include "EnhancedInputDeveloperSettings.h" #include "InputEditorModule.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Blueprint/BlueprintSupport.h" #include "UObject/UObjectIterator.h" #define LOCTEXT_NAMESPACE "InputCustomization" ////////////////////////////////////////////////////////// // FInputContextDetails TSharedRef FInputContextDetails::MakeInstance() { return MakeShareable(new FInputContextDetails); } void FInputContextDetails::CustomizeDetails(class IDetailLayoutBuilder& DetailBuilder) { // Custom Action Mappings const TSharedPtr ActionMappingsPropertyHandle = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UInputMappingContext, Mappings)); ActionMappingsPropertyHandle->MarkHiddenByCustomization(); IDetailCategoryBuilder& MappingsDetailCategoryBuilder = DetailBuilder.EditCategory(ActionMappingsPropertyHandle->GetDefaultCategoryName()); const TSharedRef ActionMappingsBuilder = MakeShareable(new FActionMappingsNodeBuilderEx(&DetailBuilder, ActionMappingsPropertyHandle)); MappingsDetailCategoryBuilder.AddCustomBuilder(ActionMappingsBuilder); } ////////////////////////////////////////////////////////// // FEnhancedActionMappingCustomization void FEnhancedActionMappingCustomization::CustomizeHeader(TSharedRef PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) { MappingPropertyHandle = PropertyHandle; // Grab the FKey property TSharedPtr KeyHandle = PropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FEnhancedActionKeyMapping, Key)); TSharedRef RemoveButton = PropertyCustomizationHelpers::MakeDeleteButton(FSimpleDelegate::CreateSP(this, &FEnhancedActionMappingCustomization::RemoveMappingButton_OnClick), LOCTEXT("RemoveMappingToolTip", "Remove Mapping")); // Create a new instance of the key customization. KeyStructInstance = FKeyStructCustomization::MakeInstance(); // TODO: Use FDetailArrayBuilder? // Pass our header row into the key struct customizeheader method so it populates our row with the key struct header StaticCastSharedPtr(KeyStructInstance)->CustomizeHeaderOnlyWithButton(KeyHandle.ToSharedRef(), HeaderRow, CustomizationUtils, RemoveButton); } void FEnhancedActionMappingCustomization::CustomizeChildren(TSharedRef PropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils) { TSharedPtr TriggersHandle = PropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FEnhancedActionKeyMapping, Triggers)); TSharedPtr ModifiersHandle = PropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FEnhancedActionKeyMapping, Modifiers)); TSharedPtr IsPlayerMappableHandle = PropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FEnhancedActionKeyMapping, bIsPlayerMappable)); TSharedPtr PlayerBindingOptions = PropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FEnhancedActionKeyMapping, PlayerMappableOptions)); // TODO: ResetToDefault needs to be disabled for arrays ChildBuilder.AddProperty(TriggersHandle.ToSharedRef()); ChildBuilder.AddProperty(ModifiersHandle.ToSharedRef()); ChildBuilder.AddProperty(IsPlayerMappableHandle.ToSharedRef()); ChildBuilder.AddProperty(PlayerBindingOptions.ToSharedRef()); } void FEnhancedActionMappingCustomization::RemoveMappingButton_OnClick() const { if (MappingPropertyHandle->IsValidHandle()) { const TSharedPtr ParentHandle = MappingPropertyHandle->GetParentHandle(); const TSharedPtr ParentArrayHandle = ParentHandle->AsArray(); ParentArrayHandle->DeleteItem(MappingPropertyHandle->GetIndexInArray()); } } ////////////////////////////////////////////////////////// // FEnhancedInputDeveloperSettingsCustomization TSet FEnhancedInputDeveloperSettingsCustomization::ExcludedAssetNames; FEnhancedInputDeveloperSettingsCustomization::~FEnhancedInputDeveloperSettingsCustomization() { // Unregister settings panel listeners if (FAssetRegistryModule* AssetRegistryModule = FModuleManager::GetModulePtr("AssetRegistry")) { AssetRegistryModule->Get().OnAssetAdded().RemoveAll(this); AssetRegistryModule->Get().OnAssetRemoved().RemoveAll(this); AssetRegistryModule->Get().OnAssetRenamed().RemoveAll(this); } CachedDetailBuilder = nullptr; } void FEnhancedInputDeveloperSettingsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) { // Note: The details view for the UEnhancedInputDeveloperSettings object will be on by default // This 'EditCategory' call will ensure that the developer settings are displayed on top of the trigger/modifier default values DetailBuilder.EditCategory(UEnhancedInputDeveloperSettings::StaticClass()->GetFName(), FText::GetEmpty(), ECategoryPriority::Important); static const FName TriggerCategoryName = TEXT("Trigger Default Values"); static const FName ModifierCategoryName = TEXT("Modifier Default Values"); TArray ModifierCDOs = GatherClassDetailsCDOs(UInputModifier::StaticClass()); TArray TriggerCDOs = GatherClassDetailsCDOs(UInputTrigger::StaticClass()); ExcludedAssetNames.Reset(); // Add The modifier/trigger defaults that are generated via CDO to the details builder CustomizeCDOValues(DetailBuilder, ModifierCategoryName, ModifierCDOs); CustomizeCDOValues(DetailBuilder, TriggerCategoryName, TriggerCDOs); // Support for updating blueprint based triggers and modifiers in the settings panel FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(FName("AssetRegistry")); IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); if (!AssetRegistry.OnAssetAdded().IsBoundToObject(this)) { AssetRegistry.OnAssetAdded().AddRaw(this, &FEnhancedInputDeveloperSettingsCustomization::OnAssetAdded); AssetRegistry.OnAssetRemoved().AddRaw(this, &FEnhancedInputDeveloperSettingsCustomization::OnAssetRemoved); AssetRegistry.OnAssetRenamed().AddRaw(this, &FEnhancedInputDeveloperSettingsCustomization::OnAssetRenamed); } } void FEnhancedInputDeveloperSettingsCustomization::CustomizeCDOValues(IDetailLayoutBuilder& DetailBuilder, const FName CategoryName, const TArray& ObjectsToCustomize) { IDetailCategoryBuilder& CategoryBuilder = DetailBuilder.EditCategory(CategoryName); // All of the Objects in this array are the CDO, i.e. the class name starts with "Default__" for (UObject* CDO : ObjectsToCustomize) { if (!ensure(CDO && CDO->IsTemplate())) { continue; } // Add the CDO as an external object reference to this customization IDetailPropertyRow* Row = CategoryBuilder.AddExternalObjects({ CDO }, EPropertyLocation::Default, FAddPropertyParams().UniqueId(CDO->GetClass()->GetFName())); if (Row) { // We need to add a custom "Name" widget here, otherwise all the categories will just say "Object" Row->CustomWidget() .NameContent() [ SNew(STextBlock) .Text(CDO->GetClass()->GetDisplayNameText()) ]; } } } void FEnhancedInputDeveloperSettingsCustomization::CustomizeDetails(const TSharedPtr& DetailBuilder) { CachedDetailBuilder = DetailBuilder; CustomizeDetails(*DetailBuilder); } TArray FEnhancedInputDeveloperSettingsCustomization::GatherClassDetailsCDOs(UClass* Class) { TArray CDOs; // Search native classes for (TObjectIterator ClassIt; ClassIt; ++ClassIt) { if (!ClassIt->IsNative() || !ClassIt->IsChildOf(Class)) { continue; } // Ignore abstract, hidedropdown, and deprecated. if (ClassIt->HasAnyClassFlags(CLASS_Abstract | CLASS_HideDropDown | CLASS_Deprecated | CLASS_NewerVersionExists)) { continue; } CDOs.AddUnique(ClassIt->GetDefaultObject()); } // Search BPs via asset registry FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(FName("AssetRegistry")); IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); FARFilter Filter; Filter.ClassPaths.Add(UBlueprint::StaticClass()->GetClassPathName()); Filter.bRecursiveClasses = true; TArray BlueprintAssetData; AssetRegistry.GetAssets(Filter, BlueprintAssetData); for (FAssetData& Asset : BlueprintAssetData) { FAssetDataTagMapSharedView::FFindTagResult Result = Asset.TagsAndValues.FindTag(TEXT("NativeParentClass")); if (Result.IsSet() && !ExcludedAssetNames.Contains(Asset.AssetName)) { const FString ClassObjectPath = FPackageName::ExportTextPathToObjectPath(Result.GetValue()); if (UClass* ParentClass = FindObjectSafe(nullptr, *ClassObjectPath, true)) { if (ParentClass->IsChildOf(Class)) { // TODO: Forcibly loading these assets could cause problems on projects with a large number of them. UBlueprint* BP = CastChecked(Asset.GetAsset()); CDOs.AddUnique(BP->GeneratedClass->GetDefaultObject()); } } } } // Strip objects with no config stored properties CDOs.RemoveAll([Class](UObject* Object) { UClass* ObjectClass = Object->GetClass(); if (ObjectClass->GetMetaData(TEXT("NotInputConfigurable")).ToBool()) { return true; } while (ObjectClass) { for (FProperty* Property : TFieldRange(ObjectClass, EFieldIteratorFlags::ExcludeSuper, EFieldIteratorFlags::ExcludeDeprecated)) { if (Property->HasAnyPropertyFlags(CPF_Config)) { return false; } } // Stop searching at the base type. We don't care about configurable properties lower than that. ObjectClass = ObjectClass != Class ? ObjectClass->GetSuperClass() : nullptr; } return true; }); return CDOs; } void FEnhancedInputDeveloperSettingsCustomization::RebuildDetailsViewForAsset(const FAssetData& AssetData, const bool bIsAssetBeingRemoved) { // If the asset was a blueprint... if (AssetData.AssetClassPath == UBlueprint::StaticClass()->GetClassPathName()) { // With a native parent class... FAssetDataTagMapSharedView::FFindTagResult Result = AssetData.TagsAndValues.FindTag(FBlueprintTags::NativeParentClassPath); if (Result.IsSet()) { // And the base class is UInputModifier or UInputTrigger, then we should rebuild the details const FString ClassObjectPath = FPackageName::ExportTextPathToObjectPath(Result.GetValue()); if (UClass* ParentClass = FindObjectSafe(nullptr, *ClassObjectPath, true)) { if (ParentClass == UInputModifier::StaticClass() || ParentClass == UInputTrigger::StaticClass()) { if (bIsAssetBeingRemoved) { ExcludedAssetNames.Add(AssetData.AssetName); } if (IDetailLayoutBuilder* DetailBuilder = CachedDetailBuilder.Pin().Get()) { DetailBuilder->ForceRefreshDetails(); } } } } } } #undef LOCTEXT_NAMESPACE