Files
integration/Plugins/UEWingman/Source/UEWingman/Private/WingPropHandle.cpp
2026-04-03 15:03:59 -04:00

434 lines
13 KiB
C++

#include "WingPropHandle.h"
#include "WingServer.h"
#include "WingActorComponent.h"
#include "WingUtils.h"
#include "WingTypes.h"
#include "Dom/JsonValue.h"
#include "PropertyEditorModule.h"
#include "IDetailTreeNode.h"
#include "Engine/Blueprint.h"
#include "Components/Widget.h"
#include "Components/PanelSlot.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "Materials/MaterialExpression.h"
#include "UObject/EnumProperty.h"
#include "EdGraph/EdGraphPin.h"
/////////////////////////////////////////////////////////////////////////////
//
// Get Root
//
/////////////////////////////////////////////////////////////////////////////
TSharedRef<IPropertyRowGenerator> WingPropHandle::CreateGenerator()
{
FPropertyEditorModule& Module = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
FPropertyRowGeneratorArgs Args;
Args.bShouldShowHiddenProperties = false;
return Module.CreatePropertyRowGenerator(Args);
}
WingPropHandle::Root& WingPropHandle::GetRootForObject(UObject* Obj)
{
for (Root& R : Roots)
{
if (R.Base == (uint8*)Obj) return R;
}
TSharedRef<IPropertyRowGenerator> Gen = CreateGenerator();
Gen->SetObjects({Obj});
Root& R = Roots.AddDefaulted_GetRef();
R.Struct = Obj->GetClass();
R.Base = (uint8*)Obj;
R.End = R.Base + R.Struct->GetStructureSize();
R.Generator = Gen;
return R;
}
WingPropHandle::Root& WingPropHandle::GetRootForStruct(const UStruct* ScriptStruct, uint8* Data)
{
for (Root& R : Roots)
{
if (R.Base == Data) return R;
}
TSharedRef<IPropertyRowGenerator> Gen = CreateGenerator();
TSharedPtr<FStructOnScope> Wrapper = MakeShared<FStructOnScope>(ScriptStruct, Data);
Gen->SetStructure(Wrapper);
Root& R = Roots.AddDefaulted_GetRef();
R.Struct = ScriptStruct;
R.Base = Data;
R.End = Data + ScriptStruct->GetStructureSize();
R.Generator = Gen;
return R;
}
/////////////////////////////////////////////////////////////////////////////
//
// IsInsideRootObject
//
/////////////////////////////////////////////////////////////////////////////
bool WingPropHandle::IsInsideRootObject(const Root& Root, IPropertyHandle& Handle)
{
// Walk up to the topmost property handle that still has a property.
// Keep the shared pointers alive so the raw pointer stays valid.
TSharedPtr<IPropertyHandle> Held;
IPropertyHandle* Top = &Handle;
while (true)
{
TSharedPtr<IPropertyHandle> Parent = Top->GetParentHandle();
if (!Parent.IsValid() || !Parent->GetProperty()) break;
Held = Parent;
Top = Held.Get();
}
// Get the address of the topmost property's data.
void* Addr = nullptr;
if (Top->GetValueData(Addr) != FPropertyAccess::Success || !Addr)
return false;
uint8* DataPtr = (uint8*)Addr;
return DataPtr >= Root.Base && DataPtr < Root.End;
}
/////////////////////////////////////////////////////////////////////////////
//
// All Tree Nodes
//
/////////////////////////////////////////////////////////////////////////////
void WingPropHandle::AllTreeNodesRecursive(const TSharedRef<IDetailTreeNode>& Node, FlatTree& Out)
{
if (Node->GetNodeType() == EDetailNodeType::Category)
{
TArray<TSharedRef<IDetailTreeNode>> Children;
Node->GetChildren(Children);
for (const TSharedRef<IDetailTreeNode>& Child : Children)
{
AllTreeNodesRecursive(Child, Out);
}
return;
}
Out.Add(&*Node);
}
WingPropHandle::FlatTree WingPropHandle::AllTreeNodes(Root& Root)
{
FlatTree Result;
for (const TSharedRef<IDetailTreeNode>& TreeRoot : Root.Generator->GetRootTreeNodes())
{
AllTreeNodesRecursive(TreeRoot, Result);
}
return Result;
}
/////////////////////////////////////////////////////////////////////////////
//
// AllProperties
//
/////////////////////////////////////////////////////////////////////////////
WingPropHandle::Handles WingPropHandle::AllProperties(Root& Root, bool RootFilter, EPropertyFlags Filter)
{
Handles Result;
for (IDetailTreeNode* Node : AllTreeNodes(Root))
{
TSharedPtr<IPropertyHandle> Handle = Node->CreatePropertyHandle();
if (Handle.IsValid() && Handle->GetProperty())
{
if (Filter != CPF_None && !Handle->GetProperty()->HasAllPropertyFlags(Filter))
continue;
if (RootFilter && !IsInsideRootObject(Root, *Handle))
continue;
Result.Add(Handle);
}
}
return Result;
}
WingPropHandle::Handles WingPropHandle::AllProperties(UObject* Obj, bool RootFilter, EPropertyFlags Filter)
{
if (!Obj) return {};
return AllProperties(GetRootForObject(Obj), RootFilter, Filter);
}
WingPropHandle::Handles WingPropHandle::AllProperties(const UStruct* ScriptStruct, uint8* Data, bool RootFilter, EPropertyFlags Filter)
{
if (!ScriptStruct || !Data) return {};
return AllProperties(GetRootForStruct(ScriptStruct, Data), RootFilter, Filter);
}
/////////////////////////////////////////////////////////////////////////////
//
// Named Property
//
/////////////////////////////////////////////////////////////////////////////
TSharedPtr<IPropertyHandle> WingPropHandle::NamedProperty(Root& Root, FName Name, bool RootFilter)
{
for (IDetailTreeNode* Node : AllTreeNodes(Root))
{
if (Node->GetNodeName() != Name) continue;
TSharedPtr<IPropertyHandle> Handle = Node->CreatePropertyHandle();
if (Handle.IsValid() && (!RootFilter || IsInsideRootObject(Root, *Handle)))
return Handle;
}
return nullptr;
}
TSharedPtr<IPropertyHandle> WingPropHandle::NamedProperty(UObject* Obj, FName Name, bool RootFilter)
{
if (!Obj) return nullptr;
return NamedProperty(GetRootForObject(Obj), Name, RootFilter);
}
TSharedPtr<IPropertyHandle> WingPropHandle::NamedProperty(const UStruct* ScriptStruct, uint8* Data, FName Name, bool RootFilter)
{
if (!ScriptStruct || !Data) return nullptr;
return NamedProperty(GetRootForStruct(ScriptStruct, Data), Name, RootFilter);
}
/////////////////////////////////////////////////////////////////////////////
//
// GetDetails
//
/////////////////////////////////////////////////////////////////////////////
WingPropHandle::Handles WingPropHandle::GetDetails(UObject* Obj, bool Mutable)
{
bool RootFilter = false;
EPropertyFlags PropFlags = CPF_Edit;
if (!Obj) return {};
// Blueprints: redirect to the generated class CDO.
if (UBlueprint* BP = Cast<UBlueprint>(Obj))
{
if (!BP->GeneratedClass)
{
UWingServer::Printf(TEXT("ERROR: Blueprint '%s' has no GeneratedClass\n"), *Obj->GetName());
return {};
}
Obj = BP->GeneratedClass->GetDefaultObject();
}
// UWingComponentReference is a class of our own creation, containing
// a pointer to a blueprint and a component name. Sometimes, the
// component is inherited. If so, and if you want to mutate it, you
// first have to create the override template in the child. If you're
// not mutating, you can just use the existing inherited template.
if (UWingComponentReference* Ref = Cast<UWingComponentReference>(Obj))
{
Obj = Mutable ? Ref->GetMutableTemplate() : Ref->GetImmutableTemplate();
if (!Obj)
{
UWingServer::Printf(TEXT("ERROR: Component '%s' has no template\n"), *Ref->VariableName.ToString());
return {};
}
}
// Actors have components, which flood the property listing with hundreds
// of confusing additional properties.
if (Cast<AActor>(Obj)) RootFilter = true;
// Fetch the handles.
Handles Result = AllProperties(Obj, RootFilter, PropFlags);
// Material graph nodes: also collect expression properties.
if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Obj))
{
if (UMaterialExpression* Expr = MatNode->MaterialExpression)
{
Result.Append(AllProperties(Expr, true, CPF_Edit));
}
}
// Widgets: hide the Slot property, add slot properties.
// if (UWidget* Widget = Cast<UWidget>(Obj))
// {
// Result.RemoveAll([](const TSharedPtr<IPropertyHandle>& H)
// {
// return H->GetProperty()->GetFName() == TEXT("Slot");
// });
// if (UPanelSlot* Slot = Widget->Slot)
// {
// Result.Append(AllProperties(Slot, false, CPF_Edit));
// }
// }
return Result;
}
/////////////////////////////////////////////////////////////////////////////
//
// GetText / SetText
//
/////////////////////////////////////////////////////////////////////////////
bool WingPropHandle::IsPinTypeProperty(FProperty* Prop)
{
FStructProperty* StructProp = CastField<FStructProperty>(Prop);
return StructProp && StructProp->Struct == FEdGraphPinType::StaticStruct();
}
FString WingPropHandle::GetText(IPropertyHandle& Handle)
{
// Pin types: use our human-readable format.
if (IsPinTypeProperty(Handle.GetProperty()))
{
void* Data = nullptr;
if (Handle.GetValueData(Data) != FPropertyAccess::Success || !Data)
UE_LOG(LogTemp, Fatal, TEXT("GetValueData failed for pin type property '%s'"),
*Handle.GetProperty()->GetName());
return UWingTypes::TypeToText(*static_cast<FEdGraphPinType*>(Data));
}
// Class properties: use our human-readable type names.
if (CastField<FClassProperty>(Handle.GetProperty()))
{
UObject* ClassObj = nullptr;
if (Handle.GetValue(ClassObj) != FPropertyAccess::Success)
UE_LOG(LogTemp, Fatal, TEXT("GetValue failed for class property '%s'"),
*Handle.GetProperty()->GetName());
if (!ClassObj) return TEXT("None");
return UWingTypes::TypeToText(ClassObj);
}
FString Result;
if (Handle.GetValueAsFormattedString(Result) != FPropertyAccess::Success)
UE_LOG(LogTemp, Fatal, TEXT("GetValueAsFormattedString failed for property '%s'"),
*Handle.GetProperty()->GetName());
return Result;
}
bool WingPropHandle::SetText(IPropertyHandle& Handle, const FString& Text)
{
FProperty* Prop = Handle.GetProperty();
// Pin types: parse with our type parser.
if (IsPinTypeProperty(Prop))
{
FEdGraphPinType PinType;
UWingTypes::Requirements Req;
Req.BlueprintType = true;
Req.Blueprintable = false;
Req.AllowContainer = true;
if (!UWingTypes::TextToType(Text, PinType, Req)) return false;
void* Data = nullptr;
if (Handle.GetValueData(Data) != FPropertyAccess::Success || !Data)
{
UWingServer::Printf(TEXT("ERROR: Cannot access data for property '%s'\n"),
*WingUtils::FormatName(Prop));
return false;
}
Handle.NotifyPreChange();
*static_cast<FEdGraphPinType*>(Data) = PinType;
Handle.NotifyPostChange(EPropertyChangeType::ValueSet);
return true;
}
// Class properties: parse with our type parser.
if (FClassProperty* ClassProp = CastField<FClassProperty>(Prop))
{
UObject* Class = nullptr;
if (!Text.IsEmpty())
{
UWingTypes::Requirements Req;
Req.BlueprintType = true;
Req.Blueprintable = false;
Req.IsChildOf = ClassProp->MetaClass;
Class = UWingTypes::TextToOneObjectType(Text, Req);
if (!Class) return false;
}
if (Handle.SetValue(Class) != FPropertyAccess::Success)
{
UWingServer::Printf(TEXT("ERROR: Failed to set class property '%s'\n"),
*WingUtils::FormatName(Prop));
return false;
}
return true;
}
// Enums: canonicalize the string with our smarter prefix matching.
FString Value = Text;
UEnum* Enum = nullptr;
if (FByteProperty* ByteProp = CastField<FByteProperty>(Prop))
Enum = ByteProp->Enum;
if (FEnumProperty* EnumProp = CastField<FEnumProperty>(Prop))
Enum = EnumProp->GetEnum();
if (Enum != nullptr)
{
int64 EnumValue;
if (!WingUtils::StringToEnum(Enum, Value, EnumValue)) return false;
Value = Enum->GetNameStringByValue(EnumValue);
}
FPropertyAccess::Result Result = Handle.SetValueFromFormattedString(Value);
if (Result != FPropertyAccess::Success)
{
UWingServer::Printf(TEXT("ERROR: Failed to parse '%s' for property '%s' (type: %s)\n"),
*Value, *WingUtils::FormatName(Prop), *Prop->GetCPPType());
return false;
}
return true;
}
/////////////////////////////////////////////////////////////////////////////
//
// SetJson
//
/////////////////////////////////////////////////////////////////////////////
bool WingPropHandle::SetJson(IPropertyHandle& Handle, const TSharedPtr<FJsonValue>& JsonValue)
{
FPropertyAccess::Result Result;
switch (JsonValue->Type)
{
case EJson::String:
return SetText(Handle, JsonValue->AsString());
case EJson::Boolean:
Result = Handle.SetValue(JsonValue->AsBool());
break;
case EJson::Number:
Result = Handle.SetValue(JsonValue->AsNumber());
break;
default:
Result = FPropertyAccess::Fail;
break;
}
if (Result != FPropertyAccess::Success)
{
UWingServer::Printf(TEXT("ERROR: Failed to set property '%s'\n"),
*WingUtils::FormatName(Handle.GetProperty()));
return false;
}
return true;
}
/////////////////////////////////////////////////////////////////////////////
//
// Print
//
/////////////////////////////////////////////////////////////////////////////
void WingPropHandle::Print(IPropertyHandle& Handle, FStringBuilderBase& Out)
{
FString Value = GetText(Handle);
Value.ReplaceInline(TEXT("\r"), TEXT(" "));
Value.ReplaceInline(TEXT("\n"), TEXT(" "));
if (Value.Len() > 100) Value = Value.Left(100) + TEXT("...");
bool bEditable = !Handle.IsEditConst();
Out.Appendf(TEXT(" %s %s %s = %s\n"),
bEditable ? TEXT("editable") : TEXT("readonly"),
*UWingTypes::TypeToText(Handle.GetProperty()),
*WingUtils::FormatName(Handle),
*Value);
}