Better handling of property mutability, and also, walk into structs.

This commit is contained in:
2026-04-04 23:57:59 -04:00
parent bd138e2790
commit c949a4db05
19 changed files with 172 additions and 50 deletions

View File

@@ -281,7 +281,7 @@ public:
}
// Set the 'Class' property.
TArray<FWingProperty> Props = FWingProperty::GetAll(Factory, CPF_Edit);
TArray<FWingProperty> Props = FWingProperty::GetVisible(Factory);
FWingProperty::Remove(Props, TEXT("BlueprintType"));
if (Props.Num() != 1)
{

View File

@@ -29,7 +29,7 @@ public:
UObject* Target = F.Walk(Object).Cast<UObject>();
if (!Target) return;
TArray<FWingProperty> Props = FWingProperty::GetDetails(Target, CPF_Edit, false);
TArray<FWingProperty> Props = FWingProperty::GetDetails(Target, false);
// Group by category, preserving within-category order.
TSortedMap<FString, TArray<FWingProperty>> Categories;

View File

@@ -32,7 +32,7 @@ public:
UObject* Obj = F.Walk(Object).Cast<UObject>();
if (!Obj) return;
TArray<FWingProperty> Props = FWingProperty::GetDetails(Obj, CPF_Edit, false);
TArray<FWingProperty> Props = FWingProperty::GetDetails(Obj, false);
FWingProperty* P = WingUtils::FindOneWithExternalID(Property, Props, TEXT("Property"), WingOut::Stdout);
if (!P) return;

View File

@@ -35,10 +35,10 @@ public:
UObject* Obj = F.Walk(Object).Cast<UObject>();
if (!Obj) return;
TArray<FWingProperty> Props = FWingProperty::GetDetails(Obj, CPF_Edit, true);
TArray<FWingProperty> Props = FWingProperty::GetDetails(Obj, true);
FWingProperty* P = WingUtils::FindOneWithExternalID(Property, Props, TEXT("Property"), WingOut::Stdout);
if (!P) return;
if (P->SetText(Value, WingOut::Stdout))
WingOut::Stdout.Print(TEXT("OK\n"));
}

View File

@@ -38,7 +38,7 @@ public:
return;
}
TArray<FWingProperty> Props = FWingProperty::GetDetails(Obj, CPF_Edit, true);
TArray<FWingProperty> Props = FWingProperty::GetDetails(Obj, true);
// Validation pass — resolve all properties before modifying anything.
for (const auto& Pair : Properties.Json->Values)

View File

@@ -66,7 +66,7 @@ public:
// Parse the json array, turning it into an array of spawn node entries.
TArray<FSpawnNodeEntry> Entries;
FSpawnNodeEntry Entry;
TArray<FWingProperty> Props = FWingProperty::GetAll(&Entry, CPF_None);
TArray<FWingProperty> Props = FWingProperty::GetAll(&Entry);
for (const TSharedPtr<FJsonValue>& Elt : Nodes.Array)
{
if (!FWingProperty::PopulateFromJson(Props, *Elt, false, WingOut::Stdout)) return;

View File

@@ -91,7 +91,7 @@ public:
UEdGraphNode* Node = F.Node(Entry.Node).Cast<UEdGraphNode>();
if (!Node) return;
TArray<FWingProperty> All = FWingProperty::GetDetails(Node, CPF_Edit, true);
TArray<FWingProperty> All = FWingProperty::GetDetails(Node, true);
FWingProperty *P = WingUtils::FindOneWithExternalID(Entry.Name, All, TEXT("Property"), WingOut::Stdout);
if (!P) return;
@@ -121,7 +121,7 @@ public:
}
FSetNodeDefaultEntry Entry;
TArray<FWingProperty> Props = FWingProperty::GetAll(&Entry, CPF_None);
TArray<FWingProperty> Props = FWingProperty::GetAll(&Entry);
for (const TSharedPtr<FJsonValue>& PinVal : Pins.Array)
{
if (!FWingProperty::PopulateFromJson(Props, *PinVal, false, WingOut::Stdout)) continue;

View File

@@ -57,7 +57,7 @@ public:
int32 SuccessCount = 0;
FMoveNodeEntry Entry;
TArray<FWingProperty> Props = FWingProperty::GetAll(&Entry, CPF_None);
TArray<FWingProperty> Props = FWingProperty::GetAll(&Entry);
for (const TSharedPtr<FJsonValue>& Elt : Nodes.Array)
{
if (!FWingProperty::PopulateFromJson(Props, *Elt, false, WingOut::Stdout)) continue;

View File

@@ -57,7 +57,7 @@ public:
int32 TotalCount = Connections.Array.Num();
FConnectPinsEntry Entry;
TArray<FWingProperty> EntryProps = FWingProperty::GetAll(&Entry, CPF_None);
TArray<FWingProperty> EntryProps = FWingProperty::GetAll(&Entry);
for (const TSharedPtr<FJsonValue>& ConnVal : Connections.Array)
{
if (!FWingProperty::PopulateFromJson(EntryProps, *ConnVal, false, WingOut::Stdout))

View File

@@ -57,7 +57,7 @@ public:
int32 TotalDisconnected = 0;
FDisconnectPinEntry Entry;
TArray<FWingProperty> EntryProps = FWingProperty::GetAll(&Entry, CPF_None);
TArray<FWingProperty> EntryProps = FWingProperty::GetAll(&Entry);
for (const TSharedPtr<FJsonValue>& DiscVal : Disconnections.Array)
{
if (!FWingProperty::PopulateFromJson(EntryProps, *DiscVal, false, WingOut::Stdout)) continue;

View File

@@ -50,7 +50,7 @@ bool WingFactories::CanCreate(TSubclassOf<UFactory> FactoryClass)
TArray<FName> WingFactories::GetParameterNames(TSubclassOf<UFactory> FactoryClass)
{
return FWingProperty::GetNames(FactoryClass, CPF_Edit);
return FWingProperty::GetVisibleNames(FactoryClass);
}
UObject* WingFactories::CreateAsset(const FString& Path, UFactory* Factory, WingOut Errors)

View File

@@ -1,5 +1,6 @@
#include "WingFetcher.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingUtils.h"
#include "WingActorComponent.h"
#include "Engine/Blueprint.h"
@@ -29,6 +30,7 @@ WingFetcher::WalkFunc WingFetcher::GetWalker(const FString& Step)
if (Step.Equals(TEXT("component"), ESearchCase::IgnoreCase)) return &WingFetcher::Component;
if (Step.Equals(TEXT("widget"), ESearchCase::IgnoreCase)) return &WingFetcher::Widget;
if (Step.Equals(TEXT("levelblueprint"), ESearchCase::IgnoreCase)) return &WingFetcher::LevelBlueprint;
if (Step.Equals(TEXT("structprop"), ESearchCase::IgnoreCase)) return &WingFetcher::StructProp;
return nullptr;
}
@@ -370,3 +372,50 @@ WingFetcher& WingFetcher::LevelBlueprint(const FString& Value)
SetObj(LevelBP);
return *this;
}
WingFetcher& WingFetcher::StructProp(const FString& Value)
{
if (bError) return *this;
FName InternalID = WingUtils::CheckInternalizeID(Value, Errors);
if (InternalID.IsNone()) return SetError();
if (!Obj)
{
TypeMismatch(TEXT("structprop"), TEXT("UObject"));
return SetError();
}
FStructProperty* StructProp = nullptr;
// The "host" is the object containing this property.
UObject *HostObject = Obj;
void *HostBase = Obj;
UStruct *HostType = Obj->GetClass();
bool HostEditable = true;
// If we are *already* inside a UWingStructPointer, update the host
// fields, to make it possible to navigate even further inside.
if (UWingStructPointer *SPtr = ::Cast<UWingStructPointer>(Obj))
{
HostObject = SPtr->Object;
HostBase = SPtr->StructBase;
HostType = SPtr->StructType;
HostEditable = SPtr->Editable;
}
StructProp = FindFProperty<FStructProperty>(HostType, InternalID);
if (!StructProp)
{
Errors.Printf(TEXT("ERROR: No struct property '%s' found on %s\n"), *Value, *HostType->GetName());
return SetError();
}
UWingStructPointer* Ptr = NewObject<UWingStructPointer>();
Ptr->Object = HostObject;
Ptr->StructType = StructProp->Struct;
Ptr->StructBase = StructProp->ContainerPtrToValuePtr<void>(HostBase);
Ptr->Editable = HostEditable && StructProp->HasAllPropertyFlags(CPF_Edit);
SetObj(Ptr);
return *this;
}

View File

@@ -250,7 +250,7 @@ void WingGraphExport::EmitNode(UEdGraphNode* Node)
void WingGraphExport::EmitNodeProperties(UEdGraphNode* Node, WingOut Out, bool bPrimary)
{
TArray<FWingProperty> Props = FWingProperty::GetDetails(Node, CPF_Edit, false);
TArray<FWingProperty> Props = FWingProperty::GetDetails(Node, false);
FString PrimaryCategory;
if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Node))

View File

@@ -16,6 +16,7 @@
bool FWingProperty::SetObject(UObject *Obj, WingOut Errors) const
{
if (!CheckEditable(Errors)) return false;
FObjectPropertyBase *OProp = CastField<FObjectPropertyBase>(Prop);
if (!OProp)
{
@@ -47,6 +48,7 @@ bool FWingProperty::SetObject(UObject *Obj, WingOut Errors) const
bool FWingProperty::SetDouble(double D, WingOut Errors) const
{
if (!CheckEditable(Errors)) return false;
FNumericProperty *NProp = CastField<FNumericProperty>(Prop);
if (!NProp)
{
@@ -95,6 +97,7 @@ bool FWingProperty::SetDouble(double D, WingOut Errors) const
bool FWingProperty::SetInt64(int64 I, WingOut Errors) const
{
if (!CheckEditable(Errors)) return false;
FNumericProperty *NProp = CastField<FNumericProperty>(Prop);
if (!NProp)
{
@@ -133,6 +136,7 @@ bool FWingProperty::SetInt64(int64 I, WingOut Errors) const
bool FWingProperty::SetBool(bool B, WingOut Errors) const
{
if (!CheckEditable(Errors)) return false;
if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop))
{
Prop->SetValue_InContainer(Container, &B);
@@ -144,6 +148,8 @@ bool FWingProperty::SetBool(bool B, WingOut Errors) const
bool FWingProperty::SetText(FString Value, WingOut Errors) const
{
if (!CheckEditable(Errors)) return false;
// Pin types get parsed by UWingTypes.
if (IsPinTypeProperty(Prop))
{
@@ -201,6 +207,8 @@ bool FWingProperty::SetText(FString Value, WingOut Errors) const
bool FWingProperty::SetJson(const FJsonValue &JsonValue, WingOut Errors) const
{
if (!CheckEditable(Errors)) return false;
if (JsonValue.Type == EJson::String)
{
return SetText(JsonValue.AsString(), Errors);
@@ -376,28 +384,35 @@ FString FWingProperty::GetCategory() const
return Result;
}
void FWingProperty::GetAll(FWingStructAndUStruct Obj, EPropertyFlags Flags, TArray<FWingProperty> &Props)
{
for (TFieldIterator<FProperty> It(Obj.UStructPtr); It; ++It)
{
if (Flags != 0 && !It->HasAnyPropertyFlags(Flags)) continue;
Props.Add(FWingProperty(*It, Obj.StructPtr));
}
}
TArray<FWingProperty> FWingProperty::GetAll(FWingStructAndUStruct Obj, EPropertyFlags Flags)
TArray<FWingProperty> FWingProperty::GetAll(FWingStructAndUStruct Obj)
{
TArray<FWingProperty> Result;
GetAll(Obj, Flags, Result);
for (TFieldIterator<FProperty> It(Obj.UStructPtr); It; ++It)
{
bool Editable = !It->HasAnyPropertyFlags(CPF_EditConst);
Result.Add(FWingProperty(*It, Obj.StructPtr, Editable));
}
return Result;
}
TArray<FName> FWingProperty::GetNames(UStruct *US, EPropertyFlags Flags)
TArray<FWingProperty> FWingProperty::GetVisible(FWingStructAndUStruct Obj)
{
TArray<FWingProperty> Result;
for (TFieldIterator<FProperty> It(Obj.UStructPtr); It; ++It)
{
if (!It->HasAllPropertyFlags(CPF_Edit)) continue;
bool Editable = !It->HasAnyPropertyFlags(CPF_EditConst);
Result.Add(FWingProperty(*It, Obj.StructPtr, Editable));
}
return Result;
}
TArray<FName> FWingProperty::GetVisibleNames(UStruct *US)
{
TArray<FName> Result;
for (TFieldIterator<FProperty> It(US); It; ++It)
{
if (Flags != 0 && !It->HasAnyPropertyFlags(Flags)) continue;
if (!It->HasAllPropertyFlags(CPF_Edit)) continue;
Result.Add(It->GetFName());
}
return Result;
@@ -419,10 +434,20 @@ void FWingProperty::Move(TArray<FWingProperty> &Out, TArray<FWingProperty> &In,
In.SetNum(Dst);
}
TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, EPropertyFlags Flags, bool Mutable)
TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
{
if (!Obj) return {};
// If it's a UWingStructPointer, return the properties
// of the struct instead. Propagate editability of the host.
if (UWingStructPointer *SP = Cast<UWingStructPointer>(Obj))
{
TArray<FWingProperty> Result =
GetVisible(FWingStructAndUStruct(SP->StructBase, SP->StructType));
if (!Mutable || (!SP->Editable)) StripEditable(Result);
return Result;
}
// Blueprints don't have editable properties. So
// instead, we fetch properties from the generated CDO.
if (UBlueprint *BP = ::Cast<UBlueprint>(Obj))
@@ -446,7 +471,7 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, EPropertyFlags Fla
}
}
TArray<FWingProperty> Result = GetAll(Obj, Flags);
TArray<FWingProperty> Result = GetVisible(Obj);
// If it's a Material Graph node, also collect properties from
// the associated material expression.
@@ -455,7 +480,7 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, EPropertyFlags Fla
{
if (UMaterialExpression* Expr = MatNode->MaterialExpression)
{
GetAll(Expr, Flags, Result);
Result.Append(GetVisible(Expr));
}
}
@@ -466,10 +491,11 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, EPropertyFlags Fla
FWingProperty::Remove(Result, TEXT("Slot"));
if (UPanelSlot* Slot = Widget->Slot)
{
GetAll(Slot, Flags, Result);
Result.Append(GetVisible(Slot));
}
}
if (!Mutable) StripEditable(Result);
return Result;
}
@@ -555,4 +581,20 @@ bool FWingProperty::CheckImportTextResult(const FString &Value, WingOut Errors)
}
}
return true;
}
void FWingProperty::StripEditable(TArray<FWingProperty> &Props)
{
for (FWingProperty &Elt : Props) Elt.Editable = false;
}
bool FWingProperty::CheckEditable(WingOut Errors) const
{
if (!Editable)
{
Errors.Printf(TEXT("ERROR: Cannot edit property %s, not marked editable"),
*WingUtils::FormatName(Prop));
return false;
}
return true;
}

View File

@@ -331,7 +331,7 @@ void UWingServer::TryCallHandler(const FString &Line)
Handler->Configuration = Found;
// Populate the handler object with the request parameters.
TArray<FWingProperty> Props = FWingProperty::GetAll(Handler, CPF_Edit);
TArray<FWingProperty> Props = FWingProperty::GetVisible(Handler);
if (!FWingProperty::PopulateFromJson(Props, *Request, false, WingOut::Stdout))
{
UWingServer::SuggestManual(WingManual::Section::HandlerHelp);
@@ -365,8 +365,6 @@ void UWingServer::AcceptNewConnections()
Client->Socket = ClientSocket;
Client->ThreadFuture = Async(EAsyncExecution::Thread, [this, Client]() { ClientThreadFunc(this, Client); });
Clients.Add(Client);
UE_LOG(LogTemp, Display, TEXT("UEWingman: Client connected."));
}
void UWingServer::CleanupFinishedClients()
@@ -448,10 +446,8 @@ void UWingServer::ClientThreadFunc(UWingServer* Server, TSharedPtr<FClientConnec
}
Client->bDone = true;
UE_LOG(LogTemp, Display, TEXT("UEWingman: Client disconnected."));
}
// ============================================================
// BuildWingHandlerRegistry
// ============================================================

View File

@@ -319,7 +319,7 @@ WingVariables::Var WingVariables::LoadBlueprintVariableDescription(FBPVariableDe
FProperty* Prop = CDO->GetClass()->FindPropertyByName(Desc.VarName);
if (Prop)
{
Result.DefaultValue = FWingProperty(Prop, CDO).GetText();
Result.DefaultValue = FWingProperty(Prop, CDO, false).GetText();
Result.DefaultSpecified = true;
}
}
@@ -494,7 +494,7 @@ bool WingVariables::ModifyBlueprintDefaults(WingOut Errors)
*WingTokenizer::ExternalizeID(Input.Name));
return false;
}
if (!FWingProperty(Prop, CDO).SetText(Input.DefaultValue, Errors)) return false;
if (!FWingProperty(Prop, CDO, true).SetText(Input.DefaultValue, Errors)) return false;
}
}
return true;

View File

@@ -54,6 +54,7 @@ public:
WingFetcher& Component(const FString& Value);
WingFetcher& Widget(const FString& Value);
WingFetcher& LevelBlueprint(const FString& Value);
WingFetcher& StructProp(const FString& Value);
// Return true if there haven't been any errors.
// Note that errors always automatically generate

View File

@@ -94,6 +94,9 @@ struct FWingStructAndUStruct
void *StructPtr;
UStruct *UStructPtr;
// Explicit constructor.
explicit FWingStructAndUStruct(void *Base, UStruct *S) : StructPtr(Base), UStructPtr(S) {}
// Copy constructor.
FWingStructAndUStruct(FWingStructAndUStruct &Src) : StructPtr(Src.StructPtr), UStructPtr(Src.UStructPtr) {}
@@ -103,4 +106,29 @@ struct FWingStructAndUStruct
// Construct from a UStruct pointer.
template<class T, typename = std::enable_if_t<!std::is_base_of_v<UObject, T>>>
FWingStructAndUStruct(T *Struct) : StructPtr(Struct), UStructPtr(Struct->StaticStruct()) {}
};
// WingStructPointer: Allows us to store a pointer to a
// ustruct in a variable of class UObject, through the magic
// of an extra level of indirection. If the struct is part of
// a UObject, then the Object field should be point at the
// object to ensure it doesn't get garbage collected. If the
// editable flag is false, it means the struct is for viewing
// only.
UCLASS()
class UWingStructPointer : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
UObject* Object;
UPROPERTY()
UStruct* StructType;
void *StructBase;
bool Editable;
};

View File

@@ -12,11 +12,12 @@ struct FWingProperty
FProperty* Prop = nullptr;
void* Container = nullptr;
bool Editable = false;
// Construct a property reference.
//
FWingProperty(FProperty* InProp, void* InContainer)
: Prop(InProp), Container(InContainer) {}
FWingProperty(FProperty* InProp, void* InContainer, bool Edit)
: Prop(InProp), Container(InContainer), Editable(Edit) {}
// Construct a null property reference.
//
@@ -57,7 +58,7 @@ struct FWingProperty
//
FString GetTruncatedText(int32 MaxLen) const;
// Print the property's type, name, and value.
// Print the property's editflag, type, name, and value.
//
void Print(WingOut Out) const;
@@ -69,16 +70,22 @@ struct FWingProperty
//
explicit operator bool() const { return Prop != nullptr; }
// Get the raw properties of the specified object or struct.
// Get all the properties of the specified object or struct.
//
// This gets the properties that are literally present in the
// specified object or struct. No special interpretation is done.
//
static TArray<FWingProperty> GetAll(FWingStructAndUStruct Obj, EPropertyFlags Flags);
static TArray<FWingProperty> GetAll(FWingStructAndUStruct Obj);
// Get all the visible properties of the specified object or struct.
//
// This gets the properties that have CPF_Edit marked on them.
//
static TArray<FWingProperty> GetVisible(FWingStructAndUStruct Obj);
// Get just the names of the properties of the specified struct/class.
//
static TArray<FName> GetNames(UStruct *US, EPropertyFlags Flags);
static TArray<FName> GetVisibleNames(UStruct *US);
// Remove any properties with the specified name.
//
@@ -98,12 +105,10 @@ struct FWingProperty
// click a material graph node, the details panel shows you properties
// of the node, but also properties of the material expression.
//
// When editing an inherited ActorComponent, you're actually editing
// properties that *override* the original properties of the ActorComponent.
// The 'mutable' flag tells it whether to create overrides for these
// properties.
// If you do not specify mutable, then all properties will be marked
// non-editable.
//
static TArray<FWingProperty> GetDetails(UObject* Obj, EPropertyFlags Flags, bool Mutable);
static TArray<FWingProperty> GetDetails(UObject* Obj, bool Mutable);
// Functions to populate properties from a JSON object.
//
@@ -113,8 +118,9 @@ struct FWingProperty
bool AllOptional, WingOut Errors);
private:
static void GetAll(FWingStructAndUStruct Obj, EPropertyFlags Flags, TArray<FWingProperty> &Props);
static void StripEditable(TArray<FWingProperty> &Props);
static bool IsPinTypeProperty(FProperty *Prop);
void PrintExpectsReceived(const TCHAR *Type, WingOut Errors) const;
bool CheckImportTextResult(const FString &Value, WingOut Errors) const;
bool CheckEditable(WingOut Errors) const;
};