Slate ActorComponent Picker ComboBox (for Components on Current Actor)

I am attempting to create an editor widget for ActorComponents on the current Actor, and am getting mostly there.

in the EditorModule.Build.cs: {“Core”, “CoreUObject”, “Engine”, “Slate”, “SlateCore”, “PropertyEditor”}
the header for my ComponentPicker

#pragma once


 “CoreMinimal.h”
 “IPropertyTypeCustomization.h” // implementing the interface so should have this
 “DetailWidgetRow.h” // it is a struct in function definition (declared outside the interface)

/**
*
*/
class RUINSGAMEEDITOR_API FActorComponentPicker : public IPropertyTypeCustomization
{

public:
static TSharedRef MakeInstance();

virtual void CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow,
	IPropertyTypeCustomizationUtils& CustomizationUtils) override;
virtual void CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle, IDetailChildrenBuilder& ChildBuilder,
	IPropertyTypeCustomizationUtils& CustomizationUtils) override {}

private:
TArray<TWeakObjectPtr> Components;
TWeakObjectPtr CurrentlyEditedObject;
TSharedPtr cPH; // currentPropertyHandle
bool bUsingDefaultEditor = false;

void CollectComponents(UObject* EditedObject, UClass* RequiredClass, TArray<TWeakObjectPtr<UActorComponent>>& OutComponents);

// helper functions to avoid raw this capture in Lambda
TSharedRef<SWidget> OnGenerateWidget(TWeakObjectPtr<UActorComponent> Item) const;
void OnSelectionChanged(TWeakObjectPtr<UActorComponent> NewValue, ESelectInfo::Type SelectInfo);
FText GetCurrentSelectionText() const;
FReply OnDefaultClicked();

};

then the Cpp




 “ActorComponentPicker.h”
// supposedly I need all of these

 “Widgets/Text/STextBlock.h”
 “Widgets/Input/SComboBox.h”
 “Widgets/SWidget.h”
 “Widgets/SBoxPanel.h”

void FActorComponentPicker::CustomizeHeader(TSharedRef InPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
cPH = InPropertyHandle;

// Detect type: UClass of Property
FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(cPH->GetProperty());
// not an Object Property
if ( ObjProp == nullptr )
{
	HeaderRow.NameContent()[cPH->CreatePropertyNameWidget()];
	HeaderRow.ValueContent()[cPH->CreatePropertyValueWidget()];
	return;
}

UClass* RequiredClass = ObjProp->PropertyClass;

// GetEditing objects
TArray<UObject*> OuterObjects;
cPH->GetOuterObjects(OuterObjects);
// there is no EditingObjects
if ( OuterObjects.IsEmpty() )
{
	HeaderRow.NameContent()[cPH->CreatePropertyNameWidget()];
	HeaderRow.ValueContent()[cPH->CreatePropertyValueWidget()];
	return;
}

// should be the Object itself
CurrentlyEditedObject = OuterObjects[0];

CollectComponents(CurrentlyEditedObject.Get(), RequiredClass, Components);

if ( bUsingDefaultEditor == true || Components.Num() == 0 )
{
	HeaderRow.NameContent()[cPH->CreatePropertyNameWidget()];
	HeaderRow.ValueContent()[cPH->CreatePropertyValueWidget()];
	return;
}
HeaderRow.NameContent()[cPH->CreatePropertyNameWidget()].ValueContent()[
	SNew(SHorizontalBox)

//*		// attempting to trouble shoot and isolate something about the Lambda
+ SHorizontalBox::Slot().FillWidth(1)
[
SNew(SComboBox<TWeakObjectPtr>)
.OptionsSource(&Components)
// my guess is here
.OnGenerateWidget_Lambda(
(TWeakObjectPtr Item)
{
return SNew(STextBlock).Text(FText::FromString(Item.IsValid() ? Item->GetName() : TEXT(“None”)));
})
// or here
.OnSelectionChanged_Lambda(
(TWeakObjectPtr NewValue, ESelectInfo::Type){})
]
//*/
/*		// attempting to avoid passing this as raw Lambda capture, by using function as arguments
+ SHorizontalBox::Slot().FillWidth(1)
[
SNew(SComboBox<TWeakObjectPtr>)
.OptionsSource(&Components)
.OnGenerateWidget(this, &FActorComponentPicker::OnGenerateWidget)
.OnSelectionChanged(this, &FActorComponentPicker::OnSelectionChanged)
[
SNew(STextBlock).Text(this,&FActorComponentPicker::GetCurrentSelectionText)
]
]
*/
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
[
SNew(SButton)
.ToolTipText(FText::FromString(“Use Default picker”))
.Text(FText::FromString(“Default”))
.OnClicked(this, &FActorComponentPicker::OnDefaultClicked)
]
];
}

void FActorComponentPicker::CollectComponents(UObject* EditedObject, UClass* RequiredClass, TArray<TWeakObjectPtr>& OutComponents)
{
AActor* OwnerActor = nullptr;
if ( AActor* AsActor = Cast(CurrentlyEditedObject) )
{
OwnerActor = AsActor;
}
else
{
OwnerActor = EditedObject ? EditedObject->GetTypedOuter() : nullptr;
}

if ( OwnerActor == nullptr ) { return; }

for ( UActorComponent* Comp : OwnerActor->GetComponents() )
{
	if ( Comp != nullptr && Comp->IsA(RequiredClass) )
	{
		OutComponents.Add(Comp);
	}
}

}

TSharedRef FActorComponentPicker::OnGenerateWidget(TWeakObjectPtr Item) const
{
return SNew(STextBlock).Text(FText::FromString(Item.IsValid() ? Item->GetName() : TEXT(“None”)));
}
void FActorComponentPicker::OnSelectionChanged(TWeakObjectPtr NewValue, ESelectInfo::Type SelectInfo)
{
UObject* Obj = (NewValue.IsValid() ? NewValue.Get() : nullptr);
if ( cPH.IsValid() ) { cPH->SetValue(Obj); }
}
FText FActorComponentPicker::GetCurrentSelectionText() const
{
UObject* Value = nullptr;
if ( cPH.IsValid() ) { cPH->GetValue(Value); }
return FText::FromString(Value ? Value->GetName() : TEXT(“None”));
}
FReply FActorComponentPicker::OnDefaultClicked()
{
bUsingDefaultEditor = !bUsingDefaultEditor;
return FReply::Handled();
}

The helper LLM got bent out of shape because the .FillWidth()wasn’t on a new line, showing it doesn’t understand C++ human readable formatting vs compliable code.

I am guessing I am still missing include an because with both parts of the combobox commented out I get a full build, with either section active I get 10 instances of unresolved Externals.

wishful questions:

  • add the checkbox to the default implementation so that I can toggle back to this changed version without unselecting the field, and selecting it again.
  • select a different actor with the ActorPicker, and then get the list of their components.

ok so I got most of the Linker issues, but am having a logic bug, I think.

for completeness the issue was that InputCore is needed to be added to the .Build.cs because is a dependency of Slate, and Slate does not pull it in by default, and I was able to clean up cpp the includes, by pulling the modules.

and the new Header is:

#pragma once

#include "CoreMinimal.h"
#include "IPropertyTypeCustomization.h"
#include "DetailWidgetRow.h"

/**
 * 
 */
class RUINSGAMEEDITOR_API FActorComponentPicker : public IPropertyTypeCustomization
{
	
public:
	static TSharedRef<IPropertyTypeCustomization> MakeInstance() { return MakeShared<FActorComponentPicker>(); }

	virtual void CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow,
		IPropertyTypeCustomizationUtils& CustomizationUtils) override;
	virtual void CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle, IDetailChildrenBuilder& ChildBuilder,
		IPropertyTypeCustomizationUtils& CustomizationUtils) override {}

private:
	TArray<TWeakObjectPtr<UActorComponent>> Components;
	TWeakObjectPtr<UObject> CurrentlyEditedObject;
	TSharedPtr<IPropertyHandle> cPH; // currentPropertyHandle
	bool bUsingActorComponentPickerDefaultEditor = false;
    
	FORCEINLINE void DefaultImpul(FDetailWidgetRow& HeaderRow);

	void CollectComponents(UObject* EditedObject, UClass* RequiredClass, TArray<TWeakObjectPtr<UActorComponent>>& OutComponents);

	TSharedRef<SWidget> OnGenerateWidget(TWeakObjectPtr<UActorComponent> Item);
	void OnSelectionChanged(TWeakObjectPtr<UActorComponent> NewValue, ESelectInfo::Type SelectInfo);
	FText GetCurrentSelectionText() const;
	FReply OnDefaultClicked();
};

and the implementation:

#include "ActorComponentPicker.h"
#include "IPropertyUtilities.h"

void FActorComponentPicker::CustomizeHeader(TSharedRef<IPropertyHandle> InPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
	TSharedPtr<IPropertyUtilities> PropertyUtilities = CustomizationUtils.GetPropertyUtilities();
	cPH = InPropertyHandle;

	// Detect type: UClass of Property
	FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(cPH->GetProperty());
	// not an Object Property
	if ( ObjProp == nullptr ) { DefaultImpul(HeaderRow); return; }

	UClass* RequiredClass = ObjProp->PropertyClass;

	// GetEditing objects
	TArray<UObject*> OuterObjects;
	cPH->GetOuterObjects(OuterObjects);
	// there is no EditingObjects
	if ( OuterObjects.IsEmpty() ) { DefaultImpul(HeaderRow); return; }

	// should be the Object itself
	CurrentlyEditedObject = OuterObjects[0];
	Components.Empty();

	CollectComponents(CurrentlyEditedObject.Get(), RequiredClass, Components);
	if ( bUsingActorComponentPickerDefaultEditor == true )
	{
		UE_LOG(LogTemp, Display, TEXT("bUsingDefaultEditor = true Drawing default"));
		DefaultImpul(HeaderRow); return;
	}
	else if ( Components.Num() <= 1 ) { DefaultImpul(HeaderRow); return; }
	HeaderRow.NameContent()[cPH->CreatePropertyNameWidget()].ValueContent()[
		SNew(SHorizontalBox)
		// attempting to avoid passing this as raw Lambda capture, by using function as arguments
		+ SHorizontalBox::Slot().FillWidth(1)
		[
			SNew(SComboBox<TWeakObjectPtr<UActorComponent>>)
				.OptionsSource(&Components)
				.OnGenerateWidget(this, &FActorComponentPicker::OnGenerateWidget)
				.OnSelectionChanged(this, &FActorComponentPicker::OnSelectionChanged)
			[
				SNew(STextBlock).Text(this,&FActorComponentPicker::GetCurrentSelectionText)
			]
		]
		+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
		[
			SNew(SButton)
			.ToolTipText(FText::FromString("Use Default picker"))
			.Text(FText::FromString("Default"))
//			.OnClicked(this, &FActorComponentPicker::OnDefaultClicked)
			.OnClicked_Lambda([this, PropertyUtilities]()
			{
				UE_LOG(LogTemp, Display, TEXT("ActorComponentPicker::OnClicked, B:%s"), (bUsingActorComponentPickerDefaultEditor ? TEXT("T") : TEXT("F")));
				bUsingActorComponentPickerDefaultEditor = !bUsingActorComponentPickerDefaultEditor;
				if ( PropertyUtilities.IsValid() )
				{
					UE_LOG(LogTemp, Display, TEXT("Property exists"));
					PropertyUtilities->RequestRefresh();
//					PropertyUtilities->ForceRefresh();
//					PropertyUtilities->RequestForceRefresh();
				}
				UE_LOG(LogTemp, Display, TEXT("ActorComponentPicker::OnClicked, B:%s"), (bUsingActorComponentPickerDefaultEditor ? TEXT("T") : TEXT("F")));
				return FReply::Handled();
			})
		]
	];
//*/
}

void FActorComponentPicker::DefaultImpul(FDetailWidgetRow& HeaderRow)
{
	HeaderRow.NameContent()[cPH->CreatePropertyNameWidget()];
	HeaderRow.ValueContent()[cPH->CreatePropertyValueWidget()];
}

void FActorComponentPicker::CollectComponents(UObject* EditedObject, UClass* RequiredClass, TArray<TWeakObjectPtr<UActorComponent>>& OutComponents)
{
	AActor* OwnerActor = nullptr;
	if ( AActor* AsActor = Cast<AActor>(CurrentlyEditedObject) )
	{
		OwnerActor = AsActor;
	}
	else
	{
		OwnerActor = EditedObject ? EditedObject->GetTypedOuter<AActor>() : nullptr;
	}

	if ( OwnerActor == nullptr ) { return; }
    // added way to unset value, because NoValue should be valid.
	OutComponents.Add(nullptr);

	for ( UActorComponent* Comp : OwnerActor->GetComponents() )
	{
		if ( Comp != nullptr && Comp->IsA(RequiredClass) )
		{
			OutComponents.Add(Comp);
		}
	}
}

TSharedRef<SWidget> FActorComponentPicker::OnGenerateWidget(TWeakObjectPtr<UActorComponent> Item)
{
	return SNew(STextBlock).Text(FText::FromString(Item.IsValid() ? Item->GetName() : TEXT("None")));
}
void FActorComponentPicker::OnSelectionChanged(TWeakObjectPtr<UActorComponent> NewValue, ESelectInfo::Type SelectInfo)
{
	UObject* Obj = (NewValue.IsValid() ? NewValue.Get() : nullptr);
	if ( cPH.IsValid() ) { cPH->SetValue(Obj); }
}
FText FActorComponentPicker::GetCurrentSelectionText() const
{
	UObject* Value = nullptr;
	if ( cPH.IsValid() ) { cPH->GetValue(Value); }
	return FText::FromString(Value ? Value->GetName() : TEXT("None"));
}
FReply FActorComponentPicker::OnDefaultClicked()
{
	UE_LOG(LogTemp, Display, TEXT("ActorComponentPicker::OnClicked, B:%s"), (bUsingActorComponentPickerDefaultEditor ? TEXT("T") : TEXT("F")));
	bUsingActorComponentPickerDefaultEditor = !bUsingActorComponentPickerDefaultEditor;
	cPH->NotifyPostChange(EPropertyChangeType::ValueSet);
	UE_LOG(LogTemp, Display, TEXT("ActorComponentPicker::OnClicked, F:%s"), (bUsingActorComponentPickerDefaultEditor ? TEXT("T") : TEXT("F")));
	return FReply::Handled();
}

for those interested in following along the StartupModule() for registering it is:

	FPropertyEditorModule& PropertyEditor = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
	PropertyEditor.RegisterCustomPropertyTypeLayout("ActorComponent", FOnGetPropertyTypeCustomizationInstance::CreateStatic(
		&FActorComponentPicker::MakeInstance
	));

the issue I am having has to do with getting the button to redraw the property Header with the default:

RequestRefresh() looks to have no response, and ForceRefresh() and RequestForceRefresh() have the correct effect for a few frames then it resets the instanced bool.

by swapping the bool value in the header I do get the original behavior, so toggling the bool should work, but it doesn’t redraw automatically.


I am looking for a way to get some kind of stateful situation at least for the current actor, but the current modification instance is thrown away when I can get it to redraw. even something like having the bool be static, and reset when the actor loses focus, which might go back to the wishful help of injecting the button into the default implementation.

my biggest use case for ActorComponent* with EditAnywhere is to get a specific ActorComponent (or derivative of T) from the current actor, or leave it null and then get it from another actor.

I am 90% there, still lacking persistent state for the toggle to work, and not sure the best way to add it (probably a different question if this lingers)

for completeness what I was able to get working:

swapped button for checkbox/toggle, added nullptr option in ComboBox, injected checkbox/toggle into default implementation. works for raw T*, Blueprint T&, TObjectPtr<T>, and TWeakObjectPtr<T>

ActorComponentPick.h (name might generate collisions ???)

#pragma once

#include "CoreMinimal.h"
#include "IPropertyTypeCustomization.h"
#include "IPropertyUtilities.h"

// it is really "special" that I have to do this to get a nullptr into the ComboBox.OptionsSource()
// with just Components being TWeakObjectPtr<UActorComponent>, nullptr won't show up in the list
// , so "None" won't be an option
struct FComponentPickerItem
{
	TWeakObjectPtr<UActorComponent> Component;
	// this could be the FString for the Display Name,
	// the lookup cost is worth the smaller data for potentially longer/verbose names
	// proceeded by "b" to prevent collision with TSharedRef.IsValid() and TSharedPtr.IsValid()
	bool bIsValid = true;

	FComponentPickerItem(UActorComponent* inComp)
		: Component(inComp)
		, bIsValid(inComp != nullptr)
	{}
};

/**
 * Allows for selecting from the list of ActorComponents of type T on the current Actor.
 * supports filtering for derivatives of UActorComponent through defining T
 * 
 * Leaving crumbs for removing the above struct, but will lose None in the lists,
 * so user would need to depend on "reset to default value" for getting nullptr/None
 */
class RUINSGAMEEDITOR_API FActorComponentPicker : public IPropertyTypeCustomization
{
	
public:
	static TSharedRef<IPropertyTypeCustomization> MakeInstance() { return MakeShared<FActorComponentPicker>(); }

	virtual void CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow,
		IPropertyTypeCustomizationUtils& CustomizationUtils) override;
	virtual void CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle, IDetailChildrenBuilder& ChildBuilder,
		IPropertyTypeCustomizationUtils& CustomizationUtils) override {}

private:
//	TArray<TWeakObjectPtr<UActorComponent>> Components;
	TArray<TSharedRef<FComponentPickerItem>> Components;
	TWeakObjectPtr<UObject> CurrentlyEditedObject;
	TSharedPtr<IPropertyHandle> cPH; // currentPropertyHandle
	TSharedPtr<IPropertyUtilities> propUtils;
	// this should probably be false to start, but persistent state is still an issue,
	// can change line in cpp to get engine default behavior
	bool bThisActorOnly = true;

	FORCEINLINE void DefaultPickerAppend(FDetailWidgetRow& HeaderRow);

//	void CollectComponents(UObject* EditedObject, UClass* RequiredClass, TArray<TWeakObjectPtr<UActorComponent>>& OutComponents);
	void CollectComponents(UObject* EditedObject, UClass* RequiredClass, TArray<TSharedRef<FComponentPickerItem>>& OutComponents);

//	TSharedRef<SWidget> OnGenerateWidget(TWeakObjectPtr<UActorComponent> Item);
	TSharedRef<SWidget> OnGenerateWidget(TSharedRef<FComponentPickerItem> Item);
//	void OnSelectionChanged(TWeakObjectPtr<UActorComponent> NewValue, ESelectInfo::Type SelectInfo);
	void OnSelectionChanged(TSharedPtr<FComponentPickerItem> NewValue, ESelectInfo::Type SelectInfo);
	FText GetCurrentSelectionText() const;
	ECheckBoxState GetToggleState() const { return bThisActorOnly ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }
	void OnToggleChanged(ECheckBoxState newState);
};

ActorComponentPicker.cpp

#include "ActorComponentPicker.h"
#include "DetailWidgetRow.h"

// leaving crumbs for replacing FComponentPickerItem with TWeakObjectPtr<UActorComponent>

void FActorComponentPicker::CustomizeHeader(TSharedRef<IPropertyHandle> InPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
	// early exit in case we are handed a false positive from registering "WeakObjectProperty"
	if ( FWeakObjectProperty* WeakProp = CastField<FWeakObjectProperty>(InPropertyHandle->GetProperty()) )
	{
		UClass* PropClass = WeakProp->PropertyClass;
		if ( PropClass == nullptr || !PropClass->IsChildOf(UActorComponent::StaticClass()) )
		{
			HeaderRow.NameContent()[InPropertyHandle->CreatePropertyNameWidget()];
			HeaderRow.ValueContent()[InPropertyHandle->CreatePropertyValueWidget()];
			return;
		}
	}

	propUtils = CustomizationUtils.GetPropertyUtilities();
	cPH = InPropertyHandle;
    /** HERE **/
	// until supporting persist state this line would need to be changed for default functionality
	if ( bThisActorOnly == false ) { DefaultPickerAppend(HeaderRow); return; }

	// Detect type: UClass of Property
	UClass* RequiredClass = nullptr;
	if ( FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(cPH->GetProperty()) )
	{
		RequiredClass = ObjProp->PropertyClass;
	}
	// not an Object Property
	else { DefaultPickerAppend(HeaderRow); return; }

	// GetEditing objects
	TArray<UObject*> OuterObjects;
	cPH->GetOuterObjects(OuterObjects);
	// there is no EditingObjects
	if ( OuterObjects.IsEmpty() ) { DefaultPickerAppend(HeaderRow); return; }

	// should be the Object itself
	CurrentlyEditedObject = OuterObjects[0];
/*	// tried doing metadata things, but requires markup in C++ and doesn't support Blueprint Defined T&
	const FString* str = cPH->GetInstanceMetaData(TEXT("UseDefaultPicker"));
	if ( str != nullptr && !str->IsEmpty() )  
	{
		UE_LOG(LogTemp, Display, TEXT("str = %s"), **str);
//		bThisActorOnly = str->ToBool();
	}
	else
	{
		UE_LOG(LogTemp, Display, TEXT("No metaDataFound"));
//		bThisActorOnly = false;
	}
*/

	CollectComponents(CurrentlyEditedObject.Get(), RequiredClass, Components);
	if ( Components.Num() <= 1 ) { DefaultPickerAppend(HeaderRow); return; }

	HeaderRow.NameContent()[cPH->CreatePropertyNameWidget()].ValueContent()
	[
		SNew(SVerticalBox)
		+ SVerticalBox::Slot().AutoHeight().Padding(0,0,0,4)
		[
			SNew(SCheckBox)
			.IsChecked(this, &FActorComponentPicker::GetToggleState)
			.OnCheckStateChanged(this, &FActorComponentPicker::OnToggleChanged)
			[
				SNew(STextBlock).Text(FText::FromString(TEXT("This Actor Only")))
			]
		]
		+ SVerticalBox::Slot().AutoHeight()
		[
			SNew(SComboBox<TSharedRef<FComponentPickerItem>>)
			.OptionsSource(&Components)
			.OnGenerateWidget(this, &FActorComponentPicker::OnGenerateWidget)
			.OnSelectionChanged(this, &FActorComponentPicker::OnSelectionChanged)
			[
				SNew(STextBlock).Text(this, &FActorComponentPicker::GetCurrentSelectionText)
			]
		]
	];
}

void FActorComponentPicker::DefaultPickerAppend(FDetailWidgetRow& HeaderRow)
{
	HeaderRow.NameContent()[cPH->CreatePropertyNameWidget()].ValueContent()
	[
		SNew(SVerticalBox)
		// injecting checkbox into the default, doing so as a sudo header bar to prevent bunching in default
		+ SVerticalBox::Slot().AutoHeight().Padding(0,0,0,4)
		[
			SNew(SCheckBox)
			.IsChecked(this, &FActorComponentPicker::GetToggleState)
			.OnCheckStateChanged(this, &FActorComponentPicker::OnToggleChanged)
			[
				SNew(STextBlock).Text(FText::FromString(TEXT("This Actor Only")))
			]
		]
		+ SVerticalBox::Slot()
		[
			cPH->CreatePropertyValueWidget()
		]
	];
}

//void FActorComponentPicker::CollectComponents(UObject* EditedObject, UClass* RequiredClass, TArray<TWeakObjectPtr<UActorComponent>>& OutComponents)
void FActorComponentPicker::CollectComponents(UObject* EditedObject, UClass* RequiredClass, TArray<TSharedRef<FComponentPickerItem>>& OutComponents)
{
	OutComponents.Empty();

	AActor* OwnerActor = nullptr;
	if ( AActor* AsActor = Cast<AActor>(CurrentlyEditedObject) ) { OwnerActor = AsActor; }
	else { OwnerActor = EditedObject ? EditedObject->GetTypedOuter<AActor>() : nullptr; }

	if ( OwnerActor == nullptr ) { return; }

//	OutComponents.Add(nullptr);
	OutComponents.Add(MakeShared<FComponentPickerItem>(nullptr));

	for ( UActorComponent* Comp : OwnerActor->GetComponents() )
	{
		if ( Comp != nullptr && Comp->IsA(RequiredClass) )
		{
//			OutComponents.Add(Comp);
			OutComponents.Add(MakeShared<FComponentPickerItem>(Comp));
		}
	}
}

//TSharedRef<SWidget> FActorComponentPicker::OnGenerateWidget(TWeakObjectPtr<UActorComponent> Item)
TSharedRef<SWidget> FActorComponentPicker::OnGenerateWidget(TSharedRef<FComponentPickerItem> Item)
{
//	const FString str = Item.IsValid() ? Item.GetName() : TEXT("None");
	const FString str = Item->bIsValid ? Item->Component->GetName() : TEXT("None");
	return SNew(STextBlock).Text(FText::FromString(str));
}

//void FActorComponentPicker::OnSelectionChanged(TWeakObjectPtr<UActorComponent> NewValue, ESelectInfo::Type SelectInfo)
void FActorComponentPicker::OnSelectionChanged(TSharedPtr<FComponentPickerItem> NewValue, ESelectInfo::Type SelectInfo)
{
	if ( !NewValue.IsValid() ) { return; }
//	UObject* Obj = (NewValue.IsValid() ? NewValue.Get() : nullptr);
	UObject* Obj = NewValue->Component.Get();
	if ( cPH.IsValid() )
	{
		// marks Undo-queue
		CurrentlyEditedObject->Modify();

		// supposed to be just for TObjectPtr<T>, but that specific derivative of FObjectProperty is deprecated, so will capture Raw T* as well
		if ( FObjectProperty* objPtrProp = CastField<FObjectProperty>(cPH->GetProperty()) )
		{
			// absolute address
			void* PropValueAddress = objPtrProp->ContainerPtrToValuePtr<void>(CurrentlyEditedObject.Get());
			// actually setting the value
			objPtrProp->SetObjectPropertyValue(PropValueAddress, Obj);
		}
		// this should be all that is required for Raw T* and TWeakObjectPtr<T>, but the above captures Raw T*
		else { cPH->SetValue(Obj); }

		// manually triggering the event for the change, this could trigger double instances and some flicker, but otherwise should be fine
		FPropertyChangedEvent PropChanged(cPH->GetProperty());
		CurrentlyEditedObject->PostEditChangeProperty(PropChanged);
		// mark Package as dirty for metadata
		if ( UPackage* P = CurrentlyEditedObject->GetOutermost() ) { P->SetDirtyFlag(true); }
	}
}

FText FActorComponentPicker::GetCurrentSelectionText() const
{
	UObject* Value = nullptr;
	if ( cPH.IsValid() ) { cPH->GetValue(Value); }
	return FText::FromString(Value ? Value->GetName() : TEXT("None"));
}

void FActorComponentPicker::OnToggleChanged(ECheckBoxState newState)
{
	bThisActorOnly = (newState == ECheckBoxState::Checked);

	if ( cPH.IsValid() )
	{
/*		part of metadata testing
		cPH->SetInstanceMetaData(TEXT("UseDefaultPicker"), (bThisActorOnly ? TEXT("true") : TEXT("false")));
*/
		if ( propUtils.IsValid() ) { propUtils->ForceRefresh(); }
	}
}

To register the customization, in the EditorModule::StartupModule

	FPropertyEditorModule& PropertyEditor = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
	PropertyEditor.RegisterCustomPropertyTypeLayout("ActorComponent", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FActorComponentPicker::MakeInstance));
	PropertyEditor.RegisterCustomPropertyTypeLayout("WeakObjectProperty", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FActorComponentPicker::MakeInstance));
	// this was to capture TObjectPtr<T>, but that wrapper is naturally exposed through reflection of Raw T*
//	PropertyEditor.RegisterCustomPropertyTypeLayout("ObjectProperty", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FActorComponentPicker::MakeInstance));

Editor.Build.cs requirements: “PropertyEditor”, “Slate”, “SlateCore”, “InputCore”

without persistent state to get the default implementation will need to modify cpp check, or struct header (which would require reloading the editor itself)