More work on properties

This commit is contained in:
2026-04-03 11:29:58 -04:00
parent 4f09d6caf0
commit 78a3ca3e72
6 changed files with 118 additions and 86 deletions

View File

@@ -34,7 +34,7 @@ public:
if (!Target) return; if (!Target) return;
WingPropHandle Props; WingPropHandle Props;
WingPropHandle::Handles Handles = Props.GetDetails(Target, false, CPF_Edit); WingPropHandle::Handles Handles = Props.GetDetails(Target, false);
// Sort by category name for consistent grouping. // Sort by category name for consistent grouping.
Handles.Sort([](const TSharedPtr<IPropertyHandle>& A, const TSharedPtr<IPropertyHandle>& B) Handles.Sort([](const TSharedPtr<IPropertyHandle>& A, const TSharedPtr<IPropertyHandle>& B)

View File

@@ -1,5 +1,4 @@
#include "WingGraphExport.h" #include "WingGraphExport.h"
#include "WingProperty.h"
#include "WingTypes.h" #include "WingTypes.h"
#include "WingUtils.h" #include "WingUtils.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
@@ -204,8 +203,8 @@ void WingGraphExport::EmitNode(UEdGraphNode* Node)
Output.Appendf(TEXT("\nnode %s: %s\n"), *WingUtils::FormatName(Node), *WingUtils::FormatNodeTitle(Node)); Output.Appendf(TEXT("\nnode %s: %s\n"), *WingUtils::FormatName(Node), *WingUtils::FormatNodeTitle(Node));
// Emit material expression properties (if applicable). // Emit node properties (if applicable).
EmitMaterialProperties(Node, Output, true); EmitNodeProperties(Node, Output, true);
// Emit input data pins. // Emit input data pins.
for (UEdGraphPin* Pin : FilterPins(Node, EGPD_Input)) for (UEdGraphPin* Pin : FilterPins(Node, EGPD_Input))
@@ -249,36 +248,26 @@ void WingGraphExport::EmitNode(UEdGraphNode* Node)
} }
} }
void WingGraphExport::EmitMaterialProperty(const FWingProperty& WP, FStringBuilderBase& Out) void WingGraphExport::EmitNodeProperties(UEdGraphNode* Node, FStringBuilderBase& Out, bool bPrimary)
{ {
FString ValueStr = WP.GetText();
ValueStr.ReplaceInline(TEXT("\r\n"), TEXT(" "));
ValueStr.ReplaceInline(TEXT("\n"), TEXT(" "));
if (ValueStr.Len() > 80)
ValueStr = ValueStr.Left(80) + TEXT("...");
bool bEditable = !WP->HasAnyPropertyFlags(CPF_EditConst); FString PrimaryCategory;
Out.Appendf(TEXT(" %s %s %s = %s\n"), if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Node))
bEditable ? TEXT("mxeditable") : TEXT("mxreadonly"),
*UWingTypes::TypeToText(WP.Prop),
*WingUtils::FormatName(WP.Prop),
*ValueStr);
}
void WingGraphExport::EmitMaterialProperties(UEdGraphNode* Node, FStringBuilderBase& Out, bool bPrimary)
{ {
UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Node); WingPropHandle::Handles Handles = Props.GetDetails(Node, false);
if (!MatNode || !MatNode->MaterialExpression) return; PrimaryCategory = MatNode->MaterialExpression->GetClass()->GetName();
for (const TSharedPtr<IPropertyHandle>& H : Handles)
UMaterialExpression* Expression = MatNode->MaterialExpression;
FString PrimaryCategory = Expression->GetClass()->GetName();
TArray<FWingProperty> Props = FWingProperty::GetDetailsMutable(Expression, CPF_Edit);
for (const FWingProperty& WP : Props)
{ {
FString Category = WP->HasMetaData(TEXT("Category")) ? WP->GetMetaData(TEXT("Category")) : FString(); FString Category = H->GetProperty()->GetMetaData(TEXT("Category"));
if ((Category == PrimaryCategory) == bPrimary) if ((Category == PrimaryCategory) == bPrimary)
EmitMaterialProperty(WP, Out); WingPropHandle::Print(*H, Out);
}
}
else if (bPrimary)
{
WingPropHandle::Handles Handles = Props.GetDetails(Node, false);
for (const TSharedPtr<IPropertyHandle>& H : Handles)
WingPropHandle::Print(*H, Out);
} }
} }
@@ -307,7 +296,7 @@ void WingGraphExport::EmitDetails()
Details.Appendf(TEXT("\ndetails %s\n"), *WingUtils::FormatName(Node)); Details.Appendf(TEXT("\ndetails %s\n"), *WingUtils::FormatName(Node));
Details.Appendf(TEXT(" pos %d, %d\n"), Node->NodePosX, Node->NodePosY); Details.Appendf(TEXT(" pos %d, %d\n"), Node->NodePosX, Node->NodePosY);
EmitMaterialProperties(Node, Details, false); EmitNodeProperties(Node, Details, false);
} }
} }

View File

@@ -127,7 +127,7 @@ WingPropHandle::FlatTree WingPropHandle::AllTreeNodes(Root& Root)
// //
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
WingPropHandle::Handles WingPropHandle::AllProperties(Root& Root, EPropertyFlags Filter) WingPropHandle::Handles WingPropHandle::AllProperties(Root& Root, bool RootFilter, EPropertyFlags Filter)
{ {
Handles Result; Handles Result;
for (IDetailTreeNode* Node : AllTreeNodes(Root)) for (IDetailTreeNode* Node : AllTreeNodes(Root))
@@ -137,7 +137,7 @@ WingPropHandle::Handles WingPropHandle::AllProperties(Root& Root, EPropertyFlags
{ {
if (Filter != CPF_None && !Handle->GetProperty()->HasAllPropertyFlags(Filter)) if (Filter != CPF_None && !Handle->GetProperty()->HasAllPropertyFlags(Filter))
continue; continue;
if (!IsInsideRootObject(Root, *Handle)) if (RootFilter && !IsInsideRootObject(Root, *Handle))
continue; continue;
Result.Add(Handle); Result.Add(Handle);
} }
@@ -145,16 +145,16 @@ WingPropHandle::Handles WingPropHandle::AllProperties(Root& Root, EPropertyFlags
return Result; return Result;
} }
WingPropHandle::Handles WingPropHandle::AllProperties(UObject* Obj, EPropertyFlags Filter) WingPropHandle::Handles WingPropHandle::AllProperties(UObject* Obj, bool RootFilter, EPropertyFlags Filter)
{ {
if (!Obj) return {}; if (!Obj) return {};
return AllProperties(GetRootForObject(Obj), Filter); return AllProperties(GetRootForObject(Obj), RootFilter, Filter);
} }
WingPropHandle::Handles WingPropHandle::AllProperties(const UStruct* ScriptStruct, uint8* Data, EPropertyFlags Filter) WingPropHandle::Handles WingPropHandle::AllProperties(const UStruct* ScriptStruct, uint8* Data, bool RootFilter, EPropertyFlags Filter)
{ {
if (!ScriptStruct || !Data) return {}; if (!ScriptStruct || !Data) return {};
return AllProperties(GetRootForStruct(ScriptStruct, Data), Filter); return AllProperties(GetRootForStruct(ScriptStruct, Data), RootFilter, Filter);
} }
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
@@ -163,28 +163,28 @@ WingPropHandle::Handles WingPropHandle::AllProperties(const UStruct* ScriptStruc
// //
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
TSharedPtr<IPropertyHandle> WingPropHandle::NamedProperty(Root& Root, FName Name) TSharedPtr<IPropertyHandle> WingPropHandle::NamedProperty(Root& Root, FName Name, bool RootFilter)
{ {
for (IDetailTreeNode* Node : AllTreeNodes(Root)) for (IDetailTreeNode* Node : AllTreeNodes(Root))
{ {
if (Node->GetNodeName() != Name) continue; if (Node->GetNodeName() != Name) continue;
TSharedPtr<IPropertyHandle> Handle = Node->CreatePropertyHandle(); TSharedPtr<IPropertyHandle> Handle = Node->CreatePropertyHandle();
if (Handle.IsValid() && IsInsideRootObject(Root, *Handle)) if (Handle.IsValid() && (!RootFilter || IsInsideRootObject(Root, *Handle)))
return Handle; return Handle;
} }
return nullptr; return nullptr;
} }
TSharedPtr<IPropertyHandle> WingPropHandle::NamedProperty(UObject* Obj, FName Name) TSharedPtr<IPropertyHandle> WingPropHandle::NamedProperty(UObject* Obj, FName Name, bool RootFilter)
{ {
if (!Obj) return nullptr; if (!Obj) return nullptr;
return NamedProperty(GetRootForObject(Obj), Name); return NamedProperty(GetRootForObject(Obj), Name, RootFilter);
} }
TSharedPtr<IPropertyHandle> WingPropHandle::NamedProperty(const UStruct* ScriptStruct, uint8* Data, FName Name) TSharedPtr<IPropertyHandle> WingPropHandle::NamedProperty(const UStruct* ScriptStruct, uint8* Data, FName Name, bool RootFilter)
{ {
if (!ScriptStruct || !Data) return nullptr; if (!ScriptStruct || !Data) return nullptr;
return NamedProperty(GetRootForStruct(ScriptStruct, Data), Name); return NamedProperty(GetRootForStruct(ScriptStruct, Data), Name, RootFilter);
} }
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
@@ -193,8 +193,11 @@ TSharedPtr<IPropertyHandle> WingPropHandle::NamedProperty(const UStruct* ScriptS
// //
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
WingPropHandle::Handles WingPropHandle::GetDetails(UObject* Obj, bool Mutable, EPropertyFlags Filter) WingPropHandle::Handles WingPropHandle::GetDetails(UObject* Obj, bool Mutable)
{ {
bool RootFilter = false;
EPropertyFlags PropFlags = CPF_Edit;
if (!Obj) return {}; if (!Obj) return {};
// Blueprints: redirect to the generated class CDO. // Blueprints: redirect to the generated class CDO.
@@ -208,7 +211,11 @@ WingPropHandle::Handles WingPropHandle::GetDetails(UObject* Obj, bool Mutable, E
Obj = BP->GeneratedClass->GetDefaultObject(); Obj = BP->GeneratedClass->GetDefaultObject();
} }
// Component references: redirect to the template. // 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)) if (UWingComponentReference* Ref = Cast<UWingComponentReference>(Obj))
{ {
Obj = Mutable ? Ref->GetMutableTemplate() : Ref->GetImmutableTemplate(); Obj = Mutable ? Ref->GetMutableTemplate() : Ref->GetImmutableTemplate();
@@ -219,29 +226,34 @@ WingPropHandle::Handles WingPropHandle::GetDetails(UObject* Obj, bool Mutable, E
} }
} }
Handles Result = AllProperties(Obj, Filter); // 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. // Material graph nodes: also collect expression properties.
if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Obj)) if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Obj))
{ {
if (UMaterialExpression* Expr = MatNode->MaterialExpression) if (UMaterialExpression* Expr = MatNode->MaterialExpression)
{ {
Result.Append(AllProperties(Expr, Filter)); Result.Append(AllProperties(Expr, true, CPF_Edit));
} }
} }
// Widgets: hide the Slot property, add slot properties. // Widgets: hide the Slot property, add slot properties.
if (UWidget* Widget = Cast<UWidget>(Obj)) // if (UWidget* Widget = Cast<UWidget>(Obj))
{ // {
Result.RemoveAll([](const TSharedPtr<IPropertyHandle>& H) // Result.RemoveAll([](const TSharedPtr<IPropertyHandle>& H)
{ // {
return H->GetProperty()->GetFName() == TEXT("Slot"); // return H->GetProperty()->GetFName() == TEXT("Slot");
}); // });
if (UPanelSlot* Slot = Widget->Slot) // if (UPanelSlot* Slot = Widget->Slot)
{ // {
Result.Append(AllProperties(Slot, Filter)); // Result.Append(AllProperties(Slot, false, CPF_Edit));
} // }
} // }
return Result; return Result;
} }
@@ -326,3 +338,27 @@ bool WingPropHandle::SetText(IPropertyHandle& Handle, const FString& Text)
} }
return true; return true;
} }
/////////////////////////////////////////////////////////////////////////////
//
// Print
//
/////////////////////////////////////////////////////////////////////////////
void WingPropHandle::Print(IPropertyHandle& Handle, FStringBuilderBase& Out)
{
FProperty* Prop = Handle.GetProperty();
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(Prop),
*WingUtils::FormatName(Prop),
*Value);
}

View File

@@ -23,13 +23,13 @@ public:
UPROPERTY() UPROPERTY()
UBlueprint* BP = nullptr; UBlueprint* BP = nullptr;
// The raw component name, not sanitized yet. // The component name.
FName VariableName; FName VariableName;
// Sanitized Parent Name (for display only) // Externalized Parent Name (for display only)
FString ParentName; FString ParentName;
// Sanitized TypeName of the component (for display only) // Externalized TypeName of the component (for display only)
FString TypeName; FString TypeName;
// True if the component is native (for display only) // True if the component is native (for display only)

View File

@@ -1,14 +1,12 @@
#pragma once #pragma once
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "WingPropHandle.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h" #include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h" #include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h" #include "EdGraph/EdGraphPin.h"
class UMaterialExpression;
struct FWingProperty;
class WingGraphExport class WingGraphExport
{ {
public: public:
@@ -64,8 +62,7 @@ private:
void Traverse(UEdGraphNode* Node); void Traverse(UEdGraphNode* Node);
void SortNodes(); void SortNodes();
void EmitNode(UEdGraphNode* Node); void EmitNode(UEdGraphNode* Node);
void EmitMaterialProperty(const FWingProperty& WP, FStringBuilderBase& Out); void EmitNodeProperties(UEdGraphNode* Node, FStringBuilderBase& Out, bool bPrimary);
void EmitMaterialProperties(UEdGraphNode* Node, FStringBuilderBase& Out, bool bPrimary);
void EmitLocalVariables(); void EmitLocalVariables();
void EmitGraph(); void EmitGraph();
void EmitDetails(); void EmitDetails();
@@ -79,6 +76,7 @@ private:
UEdGraph* Graph; UEdGraph* Graph;
WingPropHandle Props;
// Data populated by passes. // Data populated by passes.
TArray<UEdGraphNode*> SortedNodes; TArray<UEdGraphNode*> SortedNodes;

View File

@@ -4,15 +4,16 @@
#include "IPropertyRowGenerator.h" #include "IPropertyRowGenerator.h"
#include "PropertyHandle.h" #include "PropertyHandle.h"
// WingPropHandle: A wrapper around IPropertyRowGenerator that // WingPropHandle: A module that provides easy access to
// provides easy access to IPropertyHandle objects. // IPropertyHandle objects. To use this module, construct a
// WingPropHandle, then use it to fetch properties from
// objects. The property handles returned are valid until
// the WingPropHandle is destroyed.
// //
// Usage: // In UE Wingman, we are encouraging the use of
// WingPropHandle Props; // IPropertyHandle over FProperty. The FProperty API sucks:
// TArray<TSharedPtr<IPropertyHandle>> Handles = Props.AddObject(MyObject); // it's buggy and unreliable. Use IPropertyHandle instead.
// //
// The generators and all handles are valid for the lifetime of
// this object.
class WingPropHandle class WingPropHandle
{ {
@@ -24,24 +25,28 @@ public:
// Get all properties of a UObject. Returns the handles. // Get all properties of a UObject. Returns the handles.
// Only properties that have all the specified flags are included. // Only properties that have all the specified flags are included.
Handles AllProperties(UObject* Obj, EPropertyFlags Filter = CPF_None); // If RootFilter is true, only properties inside the root object are returned.
Handles AllProperties(UObject* Obj, bool RootFilter, EPropertyFlags Filter);
// Get all properties of a struct. Does not copy — the data // Get all properties of a struct. Does not copy — the data
// pointer must remain valid for the lifetime of this object. // pointer must remain valid for the lifetime of this object.
// Only properties that have all the specified flags are included. // Only properties that have all the specified flags are included.
Handles AllProperties(const UStruct* ScriptStruct, uint8* Data, EPropertyFlags Filter = CPF_None); // If RootFilter is true, only properties inside the root object are returned.
Handles AllProperties(const UStruct* ScriptStruct, uint8* Data, bool RootFilter, EPropertyFlags Filter);
// Get a single named property from a UObject.
// If RootFilter is true, only properties inside the root object are returned.
TSharedPtr<IPropertyHandle> NamedProperty(UObject* Obj, FName Name, bool RootFilter);
// Get a single named property from a struct.
// If RootFilter is true, only properties inside the root object are returned.
TSharedPtr<IPropertyHandle> NamedProperty(const UStruct* ScriptStruct, uint8* Data, FName Name, bool RootFilter);
// Get "details panel" properties for an object. Handles special // Get "details panel" properties for an object. Handles special
// cases: blueprints (redirects to CDO), component references // cases: blueprints (redirects to CDO), component references
// (redirects to template), material graph nodes (includes // (redirects to template), material graph nodes (includes
// expression properties), widgets (includes slot properties). // expression properties), widgets (includes slot properties).
Handles GetDetails(UObject* Obj, bool Mutable, EPropertyFlags Filter = CPF_None); Handles GetDetails(UObject* Obj, bool Mutable);
// Get a single named property from a UObject.
TSharedPtr<IPropertyHandle> NamedProperty(UObject* Obj, FName Name);
// Get a single named property from a struct.
TSharedPtr<IPropertyHandle> NamedProperty(const UStruct* ScriptStruct, uint8* Data, FName Name);
// Get/set text with special handling for enums (smarter prefix // Get/set text with special handling for enums (smarter prefix
// matching via WingUtils::StringToEnum) and FEdGraphPinType // matching via WingUtils::StringToEnum) and FEdGraphPinType
@@ -49,6 +54,10 @@ public:
static FString GetText(IPropertyHandle& Handle); static FString GetText(IPropertyHandle& Handle);
static bool SetText(IPropertyHandle& Handle, const FString& Text); static bool SetText(IPropertyHandle& Handle, const FString& Text);
// Print a single property in a standardized format:
// editable|readonly Type Name = Value
static void Print(IPropertyHandle& Handle, FStringBuilderBase& Out);
private: private:
struct Root struct Root
{ {
@@ -68,6 +77,6 @@ private:
static void AllTreeNodesRecursive(const TSharedRef<IDetailTreeNode>& Node, FlatTree& Out); static void AllTreeNodesRecursive(const TSharedRef<IDetailTreeNode>& Node, FlatTree& Out);
static FlatTree AllTreeNodes(Root& Root); static FlatTree AllTreeNodes(Root& Root);
Handles AllProperties(Root& Root, EPropertyFlags Filter); Handles AllProperties(Root& Root, bool RootFilter, EPropertyFlags Filter);
static TSharedPtr<IPropertyHandle> NamedProperty(Root& Root, FName Name); static TSharedPtr<IPropertyHandle> NamedProperty(Root& Root, FName Name, bool RootFilter);
}; };