From 2e2bb89de0d287fc1ed637373f51f0e3f84f3325 Mon Sep 17 00:00:00 2001 From: jyelon Date: Thu, 2 Apr 2026 02:27:14 -0400 Subject: [PATCH] New infrastructure for property manipulation. --- Content/Testing/BP_T1.uasset | 3 - .../UEWingman/Handlers/Blueprint_Create.h | 96 ------- .../UEWingman/Handlers/Create_Blueprint.h | 105 ++++++++ .../UEWingman/Handlers/Create_UsingFactory.h | 14 +- .../UEWingman/Handlers/Material_Create.h | 43 ---- .../UEWingman/Handlers/Property_Dump2.h | 78 ++++++ .../UEWingman/Handlers/SysInfo_Factories.h | 35 ++- .../UEWingman/Private/WingFactories.cpp | 227 +++++------------ .../UEWingman/Private/WingPropHandle.cpp | 234 ++++++++++++++++++ .../Source/UEWingman/Private/WingServer.cpp | 4 +- .../Source/UEWingman/Private/WingUtils.cpp | 2 + .../Source/UEWingman/Public/WingFactories.h | 58 ++--- .../Source/UEWingman/Public/WingHacks.h | 1 - .../Source/UEWingman/Public/WingPropHandle.h | 61 +++++ .../Source/UEWingman/Public/WingServer.h | 2 +- .../Source/UEWingman/Public/WingUtils.h | 2 + .../Source/UEWingman/UEWingman.Build.cs | 8 +- 17 files changed, 608 insertions(+), 365 deletions(-) delete mode 100644 Content/Testing/BP_T1.uasset delete mode 100644 Plugins/UEWingman/Source/UEWingman/Handlers/Blueprint_Create.h create mode 100644 Plugins/UEWingman/Source/UEWingman/Handlers/Create_Blueprint.h delete mode 100644 Plugins/UEWingman/Source/UEWingman/Handlers/Material_Create.h create mode 100644 Plugins/UEWingman/Source/UEWingman/Handlers/Property_Dump2.h create mode 100644 Plugins/UEWingman/Source/UEWingman/Private/WingPropHandle.cpp create mode 100644 Plugins/UEWingman/Source/UEWingman/Public/WingPropHandle.h diff --git a/Content/Testing/BP_T1.uasset b/Content/Testing/BP_T1.uasset deleted file mode 100644 index f98b4da1..00000000 --- a/Content/Testing/BP_T1.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dca01920076a3e51c87d3fbab8c11f1e57f069086b7aeb67876f8f6c21999a3a -size 14090 diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Blueprint_Create.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Blueprint_Create.h deleted file mode 100644 index f194f73f..00000000 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Blueprint_Create.h +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "WingServer.h" -#include "WingHandler.h" -#include "WingFetcher.h" -#include "WingUtils.h" -#include "WingTypes.h" -#include "WingPackageMaker.h" -#include "Engine/Blueprint.h" -#include "Kismet/BlueprintFunctionLibrary.h" -#include "Kismet2/KismetEditorUtilities.h" -#include "Blueprint_Create.generated.h" - - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UWing_Blueprint_Create : public UWingHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Full asset path for the new Blueprint")) - FString AssetPath; - - UPROPERTY(meta=(Description="Parent class, expressed as a type")) - FString ParentClass; - - UPROPERTY(meta=(Optional, Description="Normal, Interface, FunctionLibrary, or MacroLibrary")) - TEnumAsByte BlueprintType = BPTYPE_Normal; - - virtual void Register() override - { - UWingServer::AddHandler(this, - TEXT("Create a new Blueprint asset with a specified parent class and type.")); - } - virtual void Handle() override - { - WingPackageMaker Maker(AssetPath); - if (!Maker.Ok()) return; - - // Resolve parent class based on blueprint type - UWingTypes::Requirements Req; - Req.BlueprintType = false; - Req.Blueprintable = true; - Req.AllowContainer = false; - UClass* ParentClassObj = nullptr; - switch (BlueprintType) - { - case BPTYPE_Normal: - ParentClassObj = UWingTypes::TextToOneObjectType(ParentClass, Req); - if (!ParentClassObj) return; - break; - case BPTYPE_MacroLibrary: - ParentClassObj = UWingTypes::TextToOneObjectType(ParentClass, Req); - if (!ParentClassObj) return; - break; - case BPTYPE_Interface: - ParentClassObj = UInterface::StaticClass(); - break; - case BPTYPE_FunctionLibrary: - ParentClassObj = UBlueprintFunctionLibrary::StaticClass(); - break; - default: - UWingServer::Print(TEXT("ERROR: BlueprintType must be Normal, Interface, FunctionLibrary, or MacroLibrary\n")); - return; - } - - // Create the package and Blueprint - if (!Maker.Make()) return; - - UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint( - ParentClassObj, - Maker.Package(), - Maker.GetFName(), - BlueprintType, - UBlueprint::StaticClass(), - UBlueprintGeneratedClass::StaticClass() - ); - - if (!NewBP) - { - UWingServer::Print(TEXT("ERROR: FKismetEditorUtilities::CreateBlueprint returned null\n")); - return; - } - - // Compile - FKismetEditorUtilities::CompileBlueprint(NewBP); - - // Report result - UWingServer::Printf(TEXT("Created: %s\n"), *WingUtils::FormatName(NewBP)); - } -}; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Create_Blueprint.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Create_Blueprint.h new file mode 100644 index 00000000..3777a58f --- /dev/null +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Create_Blueprint.h @@ -0,0 +1,105 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "WingServer.h" +#include "WingHandler.h" +#include "WingFactories.h" +#include "WingTypes.h" +#include "Factories/BlueprintFactory.h" +#include "Factories/BlueprintMacroFactory.h" +#include "Factories/AnimBlueprintFactory.h" +#include "WidgetBlueprintFactory.h" +#include "EditorUtilityBlueprintFactory.h" +#include "EditorUtilityWidgetBlueprintFactory.h" +#include "Create_Blueprint.generated.h" + +UCLASS() +class UWing_Create_Blueprint : public UWingHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Full asset path for the new asset (e.g. '/Game/MyFolder/MyAsset')")) + FString Path; + + UPROPERTY(meta=(Optional, Description="The parent class for the new blueprint")) + FString ParentClass; + + static UClass *BlueprintInterfaceFactoryStaticClass() + { + return FindObject(nullptr, TEXT("/Script/UnrealEd.BlueprintInterfaceFactory")); + } + + virtual void Register() override + { + UWingServer::AddHandler(this, TEXT("Create_Blueprint"), + UBlueprintFactory::StaticClass(), EWingHandlerKind::Create, + TEXT("Create a normal blueprint")); + UWingServer::AddHandler(this, TEXT("Create_BlueprintMacroLibrary"), + UBlueprintMacroFactory::StaticClass(), EWingHandlerKind::Create, + TEXT("Create a blueprint macro library")); + UWingServer::AddHandler(this, TEXT("Create_WidgetBlueprint"), + UWidgetBlueprintFactory::StaticClass(), EWingHandlerKind::Create, + TEXT("Create a widget blueprint")); + UWingServer::AddHandler(this, TEXT("Create_EditorUtilityBlueprint"), + UEditorUtilityBlueprintFactory::StaticClass(), EWingHandlerKind::Create, + TEXT("Create an editor utility blueprint")); + UWingServer::AddHandler(this, TEXT("Create_EditorUtilityWidgetBlueprint"), + UEditorUtilityWidgetBlueprintFactory::StaticClass(), EWingHandlerKind::Create, + TEXT("Create an editor utility widget blueprint")); + if (BlueprintInterfaceFactoryStaticClass() != nullptr) + UWingServer::AddHandler(this, TEXT("Create_BlueprintInterface"), + BlueprintInterfaceFactoryStaticClass(), EWingHandlerKind::Create, + TEXT("Create a blueprint interface")); + } + + virtual void Handle() override + { + if (!WingFactories::CheckNewAssetPath(Path)) return; + + // Resolve parent class, if specified. + UClass *ParentClassObj = nullptr; + if (!ParentClass.IsEmpty()) + { + UWingTypes::Requirements Req; + Req.BlueprintType = false; + Req.Blueprintable = true; + Req.AllowContainer = false; + ParentClassObj = UWingTypes::TextToOneObjectType(ParentClass, Req); + if (!ParentClassObj) return; + } + + // Make the factory. + UClass *FactoryClass = Cast(ConfigurationObject); + UFactory *Factory = NewObject(GetTransientPackage(), FactoryClass); + + // Get the 'ParentClass' property. + FProperty *Prop = FactoryClass->FindPropertyByName(FName(TEXT("ParentClass"))); + FClassProperty *CProp = CastField(Prop); + + // Check that things are on track. + if ((Factory == nullptr) || (CProp == nullptr)) + { + UWingServer::Printf(TEXT("In Create_Blueprint, factory creation is buggy\n")); + return; + } + + // Store the parentclass, if specified. + if (ParentClassObj) + { + if (!ParentClassObj->IsChildOf(CProp->MetaClass)) + { + UWingServer::Printf(TEXT("ParentClass must be child of %s.\n"), + *WingUtils::FormatName(CProp->MetaClass)); + return; + } + CProp->SetObjectPropertyValue_InContainer(Factory, ParentClassObj); + } + + // Create the asset. + UObject *Blueprint = WingFactories::CreateAsset(Path, Factory); + if (Blueprint == nullptr) return; + UWingServer::Printf(TEXT("Created.\n")); + } +}; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Create_UsingFactory.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Create_UsingFactory.h index 654c83ee..eb509e89 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Create_UsingFactory.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Create_UsingFactory.h @@ -6,6 +6,7 @@ #include "WingHandler.h" #include "WingFactories.h" #include "WingProperty.h" +#include "Factories/BlueprintFunctionLibraryFactory.h" #include "Create_UsingFactory.generated.h" UCLASS() @@ -15,7 +16,7 @@ class UWing_Create_UsingFactory : public UWingHandler public: UPROPERTY(meta=(Description="Full asset path for the new asset (e.g. '/Game/MyFolder/MyAsset')")) - FString AssetPath; + FString Path; virtual void Register() override { @@ -32,17 +33,22 @@ public: TArray ConfigProps = FWingProperty::GetNames(Class, CPF_Edit); if (ConfigProps.Num() > 0) continue; - FString FactoryName = UWingFactories::DeriveFactoryName(Class); + FString FactoryName = WingFactories::DeriveFactoryName(Class); FString CommandName = FString::Printf(TEXT("Create_%s"), *FactoryName); FString Doc = FString::Printf(TEXT("Create a new %s asset."), *FactoryName); - UWingServer::AddHandler(this, CommandName, Doc, Class, EWingHandlerKind::Create); + UWingServer::AddHandler(this, CommandName, Class, EWingHandlerKind::Create, Doc); } + + UWingServer::AddHandler(this, TEXT("Create_BlueprintFunctionLibrary"), + UBlueprintFunctionLibraryFactory::StaticClass(), EWingHandlerKind::Create, + TEXT("Create a blueprint function library")); } virtual void Handle() override { UClass* FactoryClass = Cast(ConfigurationObject); UFactory* Factory = NewObject(GetTransientPackage(), FactoryClass); - UWingFactories::CreateAsset(AssetPath, Factory); + WingFactories::CreateAsset(Path, Factory); + UWingServer::Printf(TEXT("Created.\n")); } }; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Material_Create.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Material_Create.h deleted file mode 100644 index 3b399964..00000000 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Material_Create.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "WingServer.h" -#include "WingHandler.h" -#include "WingUtils.h" -#include "Materials/Material.h" -#include "MaterialDomain.h" -#include "Factories/MaterialFactoryNew.h" -#include "WingPackageMaker.h" -#include "Material_Create.generated.h" - - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UWing_Material_Create : public UWingHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Full asset path for the new material")) - FString Material; - - virtual void Register() override - { - UWingServer::AddHandler(this, - TEXT("Create a new UMaterial asset")); - } - virtual void Handle() override - { - WingPackageMaker Maker(Material); - if (!Maker.Ok()) return; - - // Create via IAssetTools + factory. - UMaterial* MaterialObj = Maker.CreateAsset(); - if (!MaterialObj) return; - - UWingServer::Printf(TEXT("Created %s\n"), *MaterialObj->GetPathName()); - } -}; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Property_Dump2.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Property_Dump2.h new file mode 100644 index 00000000..352f59b9 --- /dev/null +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Property_Dump2.h @@ -0,0 +1,78 @@ +#pragma once + +#include "CoreMinimal.h" +#include "WingServer.h" +#include "WingHandler.h" +#include "WingFetcher.h" +#include "WingPropHandle.h" +#include "WingUtils.h" +#include "WingTypes.h" +#include "Property_Dump2.generated.h" + +UCLASS() +class UWing_Property_Dump2 : public UWingHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Target object")) + FString Object; + + UPROPERTY(meta=(Optional, Description="Substring filter for property names")) + FString Query; + + virtual void Register() override + { + UWingServer::AddHandler(this, + TEXT("Test handler: dump properties using IPropertyHandle.")); + } + + virtual void Handle() override + { + WingFetcher F; + UObject* Target = F.Walk(Object).Cast(); + if (!Target) return; + + WingPropHandle Props; + WingPropHandle::Handles Handles = Props.GetDetails(Target, false, CPF_Edit); + + // Sort by category name for consistent grouping. + Handles.Sort([](const TSharedPtr& A, const TSharedPtr& B) + { + return A->GetProperty()->GetMetaData(TEXT("Category")) < B->GetProperty()->GetMetaData(TEXT("Category")); + }); + + FString QueryLower = Query.ToLower(); + FString CurrentCategory; + + for (const TSharedPtr& H : Handles) + { + FProperty* Prop = H->GetProperty(); + FString Name = WingUtils::FormatName(Prop); + if (!QueryLower.IsEmpty() && !Name.ToLower().Contains(QueryLower)) + continue; + + FString Category = Prop->GetMetaData(TEXT("Category")); + if (Category.IsEmpty()) Category = TEXT("Unclassified"); + if (Category != CurrentCategory) + { + if (!CurrentCategory.IsEmpty()) + UWingServer::Print(TEXT("\n")); + CurrentCategory = Category; + UWingServer::Printf(TEXT("%s:\n"), *CurrentCategory); + } + + FString Value; + H->GetValueAsFormattedString(Value); + if (Value.Len() > 100) { Value.LeftInline(100); Value += TEXT("..."); } + + bool bEditable = !H->IsEditConst(); + + UWingServer::Printf(TEXT(" %s %s %s = %s\n"), + bEditable ? TEXT("editable") : TEXT("readonly"), + *UWingTypes::TypeToText(Prop), + *Name, + *Value); + } + } +}; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/SysInfo_Factories.h b/Plugins/UEWingman/Source/UEWingman/Handlers/SysInfo_Factories.h index 7a39c490..7304c012 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/SysInfo_Factories.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/SysInfo_Factories.h @@ -3,6 +3,7 @@ #include "CoreMinimal.h" #include "WingServer.h" #include "WingHandler.h" +#include "WingProperty.h" #include "WingFactories.h" #include "SysInfo_Factories.generated.h" @@ -24,25 +25,33 @@ public: "is developing this plugin, they help him to understand. " "unreal's internals better.")); } + virtual void Handle() override { - const TArray& All = UWingFactories::AllFactories(); + TArray FactoryClasses; + GetDerivedClasses(UFactory::StaticClass(), FactoryClasses); - for (int nparam = 0; nparam < 10; nparam++) + TArray Results; + + for (UClass *Factory : FactoryClasses) { - for (const UWingFactories::Info& Entry : All) + if (Factory->HasAnyClassFlags(CLASS_Abstract)) continue; + UFactory *CDO = Factory->GetDefaultObject(); + if (!CDO->CanCreateNew()) continue; + if (!CDO->ShouldShowInNewMenu()) continue; + TArray Params = FWingProperty::GetNames(Factory, CPF_Edit); + TStringBuilder<512> Line; + Line.Appendf(TEXT("%2d %s "), Params.Num(), *Factory->GetName()); + for (const FName& Prop : Params) { - if (Entry.Config.Num() != nparam) continue; - if (!Entry.CanCreateNew()) continue; - UWingServer::Printf(TEXT("%s"), *Entry.Name); - - for (const FName& Prop : Entry.Config) - { - UWingServer::Printf(TEXT(" PARAM: %s"), *Prop.ToString()); - } - UWingServer::Printf(TEXT("\n")); + Line.Appendf(TEXT(" %s"), *Prop.ToString()); } - UWingServer::Printf(TEXT("\n")); + Results.Add(Line.ToString()); + } + Results.Sort(); + for (const FString &Line : Results) + { + UWingServer::Printf(TEXT("%s\n"), *Line); } } }; diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingFactories.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingFactories.cpp index 708fb4c0..77966580 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingFactories.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingFactories.cpp @@ -1,69 +1,11 @@ #include "WingFactories.h" #include "WingServer.h" -#include "Editor.h" #include "WingUtils.h" -#include "WingProperty.h" #include "PackageTools.h" -#include "Kismet2/KismetEditorUtilities.h" +#include "AssetRegistry/AssetRegistryModule.h" #include "Kismet2/EnumEditorUtils.h" -#include "Factories/BlueprintFactory.h" -#include "Factories/EnumFactory.h" -#include "Algo/BinarySearch.h" -void UWingFactories::Initialize(FSubsystemCollectionBase& Collection) -{ - Super::Initialize(Collection); - PopulateRegistry(); -} - -const TArray& UWingFactories::AllFactories() -{ - return GEditor->GetEditorSubsystem()->Registry; -} - -bool UWingFactories::Info::CanCreateNew() const -{ - UFactory *CDO = FactoryClass->GetDefaultObject(); - return CDO->CanCreateNew() && CDO->ShouldShowInNewMenu(); -} - -void UWingFactories::PopulateRegistry() -{ - TArray FactoryClasses; - GetDerivedClasses(UFactory::StaticClass(), FactoryClasses); - - // Populate it with initial data. - for (UClass* Class : FactoryClasses) - { - if (Class->HasAnyClassFlags(CLASS_Abstract)) continue; - - Info Entry; - Entry.Name = DeriveFactoryName(Class); - Entry.FactoryClass = Class; - Entry.Config = FWingProperty::GetNames(Class, CPF_Edit); - Registry.Add(MoveTemp(Entry)); - } - - // Sort the registry. - Registry.Sort([](const Info& A, const Info& B) { return A.Name < B.Name; }); - - // Blacklist certain bad factories. - DisableFactory(TEXT("PhysicsAsset")); // PhysicsAsset factory pops a modal dialog -} - -void UWingFactories::DisableFactory(const TCHAR* Name) -{ - Info* Entry = Find(Name); - if (!Entry) - { - UE_LOG(LogTemp, Fatal, TEXT("UWingFactories::DisableFactory: factory '%s' not found"), Name); - return; - } - Entry->Disabled = true; -} - - -bool UWingFactories::CheckNewAssetPath(const FString& Path) +bool WingFactories::CheckNewAssetPath(const FString& Path) { if (UPackageTools::SanitizePackageName(Path) != Path) { @@ -88,34 +30,36 @@ bool UWingFactories::CheckNewAssetPath(const FString& Path) return true; } -UPackage *UWingFactories::CreateNewPackage(const FString &Path) +bool WingFactories::IsBlacklisted(UClass* FactoryClass) { - UPackage *Pkg = CreatePackage(*Path); - if (!Pkg) + FName Name = FactoryClass->GetFName(); + if (Name == TEXT("PhysicsAssetFactory")) return true; // Pops a modal dialog + if (Name == TEXT("EnumFactory")) return true; // Pops a modal dialog + return false; +} + +UObject* WingFactories::CreateAsset(const FString& Path, UFactory* Factory) +{ + // Check the blacklist. + if (IsBlacklisted(Factory->GetClass())) + { + UWingServer::Printf(TEXT("ERROR: Factory '%s' is blacklisted\n"), *Factory->GetClass()->GetName()); + return nullptr; + } + + // Validate the path, and that there's not already something there. + if (!CheckNewAssetPath(Path)) return nullptr; + FName Name = FName(FPackageName::GetShortName(Path)); + + // Create the package. + UPackage *Package = CreatePackage(*Path); + if (!Package) { UWingServer::Printf(TEXT("ERROR: Failed to create package at '%s'\n"), *Path); return nullptr; } - Pkg->ClearFlags(RF_Transient); - Pkg->SetIsExternallyReferenceable(true); - Pkg->MarkPackageDirty(); - return Pkg; -} - - -UObject* UWingFactories::CreateAsset(const FString& Path, UFactory* Factory) -{ - // Validate the path, and that there's not already something there. - if (!CheckNewAssetPath(Path)) return nullptr; - FName Name = FName(FPackageName::GetShortName(Path)); - - // Pre-check: block factories that would cause problems. - // In particular, this blocks those that would pop a dialog. - if (!PreCheck(Factory, Name, Path)) return nullptr; - // Create the asset. - UPackage *Package = CreateNewPackage(Path); UObject* NewAsset = Factory->FactoryCreateNew( Factory->GetSupportedClass(), Package, @@ -124,105 +68,50 @@ UObject* UWingFactories::CreateAsset(const FString& Path, UFactory* Factory) nullptr, GWarn ); - if (!NewAsset) + + // If the asset was created, turn the package into a + // permanent package and encourge the editor to save it + // to disk. Otherwise, mark the package for deletion. + if (NewAsset) { - UWingServer::Printf(TEXT("ERROR: Factory '%s' returned null\n"), *Factory->GetClass()->GetName()); + Package->ClearFlags(RF_Transient); + Package->SetIsExternallyReferenceable(true); + Package->MarkPackageDirty(); + FAssetRegistryModule::AssetCreated(NewAsset); + UWingServer::AddTouchedObject(NewAsset); + } + else + { + UWingServer::Printf(TEXT("ERROR: Factory '%s' failed to create an object\n"), *Factory->GetClass()->GetName()); + Package->ClearDirtyFlag(); + Package->MarkAsGarbage(); return nullptr; } - - UWingServer::Printf(TEXT("Created: %s\n"), *WingUtils::ExternalizeID(Name)); return NewAsset; } -UObject* UWingFactories::CreateAsset(const FString& Path, const FString& FactoryName, FWingJsonObject& Config) -{ - UWingFactories* Self = GEditor->GetEditorSubsystem(); - - // Look up the factory info. - const Info* FactoryInfo = Self->Find(FactoryName); - if (!FactoryInfo) - { - UWingServer::Printf(TEXT("ERROR: Unknown factory '%s'\n"), *FactoryName); - return nullptr; - } - - // Make sure this is a creation factory, as opposed to an import factory. - if (!FactoryInfo->CanCreateNew()) - { - UWingServer::Printf(TEXT("ERROR: Factory '%s' cannot create objects from scratch\n"), *FactoryName); - return nullptr; - } - - // Create the factory instance. - UFactory* Factory = NewObject(GetTransientPackage(), FactoryInfo->FactoryClass); - - // Get the editable properties - TArray Props = - FWingProperty::GetNamed(Factory->GetClass(), Factory, FactoryInfo->Config); - - // if there is no config table, make a blank config table. - TSharedPtr ConfigJson = Config.Json; - if (ConfigJson == nullptr) ConfigJson = MakeShared(); - - // Populate the configuration properties from the json. - if (!FWingProperty::PopulateFromJson(Props, ConfigJson.Get(), false)) - return nullptr; - - return CreateAsset(Path, Factory); -} - -bool UWingFactories::PreCheck(UFactory* Factory, FName Name, const FString &Path) -{ - // Blueprint factories: FactoryCreateNew pops FMessageDialog if ParentClass is invalid. - if (UBlueprintFactory* BPFactory = Cast(Factory)) - { - if (!BPFactory->ParentClass) - { - UWingServer::Print(TEXT("ERROR: ParentClass must be set\n")); - return false; - } - if (!FKismetEditorUtilities::CanCreateBlueprintOfClass(BPFactory->ParentClass)) - { - UWingServer::Printf(TEXT("ERROR: Cannot create a blueprint based on class '%s'\n"), *BPFactory->ParentClass->GetName()); - return false; - } - } - - // Enum factory: FactoryCreateNew pops FMessageDialog if the name already exists. - if (UEnumFactory* EFactory = Cast(Factory)) - { - if(!FEnumEditorUtils::IsNameAvailebleForUserDefinedEnum(Name)) - { - UWingServer::Printf(TEXT("Enum name is already taken: %s"), - *WingUtils::ExternalizeID(Name)); - } - } - - return true; -} - -UWingFactories::Info* UWingFactories::Find(const FString& Name) -{ - int32 Index = Algo::LowerBound(Registry, Name, [](Info& Entry, const FString& N) { - return Entry.Name < N; - }); - if (Index < Registry.Num() && Registry[Index].Name == Name) - { - return &Registry[Index]; - } - return nullptr; -} - -FString UWingFactories::DeriveFactoryName(UClass* FactoryClass) +FString WingFactories::DeriveFactoryName(UClass* FactoryClass) { FString Name = FactoryClass->GetName(); - if (Name.EndsWith(TEXT("FactoryNew"))) + int32 Index = Name.Find(TEXT("Factory")); + if (Index != INDEX_NONE) { - Name.LeftChopInline(10); + Name.LeftInline(Index); } - else if (Name.EndsWith(TEXT("Factory"))) + if (Name.EndsWith(TEXT("_"))) { - Name.LeftChopInline(7); + Name.LeftChopInline(1); } return Name; } + +UObject* UEnumFactoryWing::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + if (!FEnumEditorUtils::IsNameAvailebleForUserDefinedEnum(Name)) + { + UWingServer::Printf(TEXT("ERROR: Enum name is already taken: %s\n"), *Name.ToString()); + return nullptr; + } + return FEnumEditorUtils::CreateUserDefinedEnum(InParent, Name, Flags); +} + diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingPropHandle.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingPropHandle.cpp new file mode 100644 index 00000000..89d319d4 --- /dev/null +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingPropHandle.cpp @@ -0,0 +1,234 @@ +#include "WingPropHandle.h" +#include "WingServer.h" +#include "WingActorComponent.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 "DetailTreeNode.h" +#include "PropertyNode.h" +#include "ObjectPropertyNode.h" +#include "PropertyNode.h" +#include "ObjectPropertyNode.h" +#include "ItemPropertyNode.h" +#include "CategoryPropertyNode.h" + +///////////////////////////////////////////////////////////////////////////// +// +// IsSubObject +// +///////////////////////////////////////////////////////////////////////////// + +bool WingPropHandle::IsSubObject(const TSharedRef& Node) +{ + FDetailTreeNode& DetailNode = static_cast(*Node); + TSharedPtr PropNode = DetailNode.GetPropertyNode(); + if (!PropNode.IsValid()) return false; + + FPropertyNode* Current = PropNode.Get(); + while (true) + { + if (Current == nullptr) return false; + FPropertyNode *Parent = Current->GetParentNode(); + if (Current->AsObjectNode()) return (Parent != nullptr); + Current = Parent; + } +} + +///////////////////////////////////////////////////////////////////////////// +// +// Get Generator +// +///////////////////////////////////////////////////////////////////////////// + +TSharedRef WingPropHandle::CreateGenerator() +{ + FPropertyEditorModule& Module = FModuleManager::GetModuleChecked("PropertyEditor"); + FPropertyRowGeneratorArgs Args; + Args.bShouldShowHiddenProperties = false; + return Module.CreatePropertyRowGenerator(Args); +} + +TSharedRef WingPropHandle::GetGeneratorForObject(UObject* Obj) +{ + for (auto& Pair : Generators) + { + if (Pair.Key == Obj) return Pair.Value.ToSharedRef(); + } + TSharedRef Gen = CreateGenerator(); + Gen->SetObjects({Obj}); + Generators.Add({Obj, Gen}); + return Gen; +} + +TSharedRef WingPropHandle::GetGeneratorForStruct(const UStruct* ScriptStruct, uint8* Data) +{ + for (auto& Pair : Generators) + { + if (Pair.Key == Data) return Pair.Value.ToSharedRef(); + } + TSharedRef Gen = CreateGenerator(); + TSharedPtr Wrapper = MakeShared(ScriptStruct, Data); + Gen->SetStructure(Wrapper); + Generators.Add({Data, Gen}); + return Gen; +} + +///////////////////////////////////////////////////////////////////////////// +// +// All Tree Nodes +// +///////////////////////////////////////////////////////////////////////////// + +void WingPropHandle::AllTreeNodesRecursive(const TSharedRef& Node, FlatTree& Out) +{ + if (IsSubObject(Node)) return; + if (Node->GetNodeType() == EDetailNodeType::Category) + { + TArray> Children; + Node->GetChildren(Children); + for (const TSharedRef& Child : Children) + { + AllTreeNodesRecursive(Child, Out); + } + return; + } + + Out.Add(&*Node); +} + +WingPropHandle::FlatTree WingPropHandle::AllTreeNodes(TSharedRef Generator) +{ + FlatTree Result; + for (const TSharedRef& Root : Generator->GetRootTreeNodes()) + { + AllTreeNodesRecursive(Root, Result); + } + return Result; +} + +///////////////////////////////////////////////////////////////////////////// +// +// AllProperties +// +///////////////////////////////////////////////////////////////////////////// + +WingPropHandle::Handles WingPropHandle::AllProperties(TSharedRef Generator, EPropertyFlags Filter) +{ + Handles Result; + for (IDetailTreeNode* Node : AllTreeNodes(Generator)) + { + TSharedPtr Handle = Node->CreatePropertyHandle(); + if (Handle.IsValid() && Handle->GetProperty()) + { + if (Filter == CPF_None || Handle->GetProperty()->HasAllPropertyFlags(Filter)) + { + Result.Add(Handle); + } + } + } + return Result; +} + +WingPropHandle::Handles WingPropHandle::AllProperties(UObject* Obj, EPropertyFlags Filter) +{ + if (!Obj) return {}; + return AllProperties(GetGeneratorForObject(Obj), Filter); +} + +WingPropHandle::Handles WingPropHandle::AllProperties(const UStruct* ScriptStruct, uint8* Data, EPropertyFlags Filter) +{ + if (!ScriptStruct || !Data) return {}; + return AllProperties(GetGeneratorForStruct(ScriptStruct, Data), Filter); +} + +///////////////////////////////////////////////////////////////////////////// +// +// Named Property +// +///////////////////////////////////////////////////////////////////////////// + +TSharedPtr WingPropHandle::NamedProperty(TSharedRef Generator, FName Name) +{ + for (IDetailTreeNode* Node : AllTreeNodes(Generator)) + { + if (Node->GetNodeName() == Name) + return Node->CreatePropertyHandle(); + } + return nullptr; +} + +TSharedPtr WingPropHandle::NamedProperty(UObject* Obj, FName Name) +{ + if (!Obj) return nullptr; + return NamedProperty(GetGeneratorForObject(Obj), Name); +} + +TSharedPtr WingPropHandle::NamedProperty(const UStruct* ScriptStruct, uint8* Data, FName Name) +{ + if (!ScriptStruct || !Data) return nullptr; + return NamedProperty(GetGeneratorForStruct(ScriptStruct, Data), Name); +} + +///////////////////////////////////////////////////////////////////////////// +// +// GetDetails +// +///////////////////////////////////////////////////////////////////////////// + +WingPropHandle::Handles WingPropHandle::GetDetails(UObject* Obj, bool Mutable, EPropertyFlags Filter) +{ + if (!Obj) return {}; + + // Blueprints: redirect to the generated class CDO. + if (UBlueprint* BP = Cast(Obj)) + { + if (!BP->GeneratedClass) + { + UWingServer::Printf(TEXT("ERROR: Blueprint '%s' has no GeneratedClass\n"), *Obj->GetName()); + return {}; + } + Obj = BP->GeneratedClass->GetDefaultObject(); + } + + // Component references: redirect to the template. + if (UWingComponentReference* Ref = Cast(Obj)) + { + Obj = Mutable ? Ref->GetMutableTemplate() : Ref->GetImmutableTemplate(); + if (!Obj) + { + UWingServer::Printf(TEXT("ERROR: Component '%s' has no template\n"), *Ref->VariableName.ToString()); + return {}; + } + } + + Handles Result = AllProperties(Obj, Filter); + + // Material graph nodes: also collect expression properties. + if (UMaterialGraphNode* MatNode = Cast(Obj)) + { + if (UMaterialExpression* Expr = MatNode->MaterialExpression) + { + Result.Append(AllProperties(Expr, Filter)); + } + } + + // Widgets: hide the Slot property, add slot properties. + if (UWidget* Widget = Cast(Obj)) + { + Result.RemoveAll([](const TSharedPtr& H) + { + return H->GetProperty()->GetFName() == TEXT("Slot"); + }); + if (UPanelSlot* Slot = Widget->Slot) + { + Result.Append(AllProperties(Slot, Filter)); + } + } + + return Result; +} + diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp index 41dbf174..c440720a 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp @@ -454,10 +454,10 @@ void UWingServer::ClientThreadFunc(UWingServer* Server, TSharedPtrGetClass()), Documentation, nullptr, EWingHandlerKind::Normal); + AddHandler(Obj, WingUtils::GetHandlerName(Obj->GetClass()), nullptr, EWingHandlerKind::Normal, Documentation); } -void UWingServer::AddHandler(UObject* Obj, const FString& Name, const FString& Documentation, UObject* Config, EWingHandlerKind Kind) +void UWingServer::AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, const FString& Documentation) { FWingHandlerConfig H; H.Name = Name; diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp index d24b1518..9ae716b7 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp @@ -5,6 +5,7 @@ #include "WingServer.h" #include "WingHandler.h" #include "WingTokenizer.h" +#include "PropertyHandle.h" #include "Engine/Blueprint.h" #include "Engine/MemberReference.h" #include "Engine/World.h" @@ -56,6 +57,7 @@ // ============================================================ FName WingUtils::GetFName(const FWingProperty &Prop) { return Prop.Prop->GetFName(); } +FName WingUtils::GetFName(const TSharedPtr &H) { return H->GetProperty()->GetFName(); } FString WingUtils::ExternalizeID(FName Name) { diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingFactories.h b/Plugins/UEWingman/Source/UEWingman/Public/WingFactories.h index ac50b092..25a3dc8e 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingFactories.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingFactories.h @@ -1,47 +1,41 @@ #pragma once #include "CoreMinimal.h" -#include "EditorSubsystem.h" #include "Factories/Factory.h" -#include "WingHandler.h" +#include "Factories/EnumFactory.h" #include "WingFactories.generated.h" -UCLASS() -class UWingFactories : public UEditorSubsystem + + +class WingFactories { - GENERATED_BODY() - public: - struct Info - { - FString Name; - UClass* FactoryClass = nullptr; - TArray Config; - bool Disabled = false; - bool CanCreateNew() const; - }; - - virtual void Initialize(FSubsystemCollectionBase& Collection) override; - virtual void Deinitialize() override {} - - static const TArray& AllFactories(); - - // Create an asset on disk, using a factory instance. Returns the main object. - // If there are problems, prints error messages and returns nullptr. + // Create an asset on disk, using a factory instance. + // Returns the main object. If there are problems, + // prints error messages and returns nullptr. static UObject *CreateAsset(const FString &Path, UFactory *Factory); - // Create an asset on disk, looking up the factory by name and configuring it. - static UObject *CreateAsset(const FString &Path, const FString &FactoryName, FWingJsonObject &Config); + // Some factories are blacklisted, mainly because they + // pop up dialog boxes. In those cases, we deal with it + // primarily by adding those factories to the blacklist, + // and then implementing replacement factories. + static bool IsBlacklisted(UClass* FactoryClass); + // This takes a factory name and turns it into a string + // that we can present to the user. Mainly, it removes + // the word 'Factory', and anything that comes after. static FString DeriveFactoryName(UClass* FactoryClass); -private: - TArray Registry; - - Info* Find(const FString& Name); - static UPackage *CreateNewPackage(const FString &Path); + // Verifies that the asset path is a valid path, and also + // that there's not something already there at that path. static bool CheckNewAssetPath(const FString &Path); - void PopulateRegistry(); - void DisableFactory(const TCHAR* Name); - static bool PreCheck(UFactory *Factory, FName Name, const FString &Path); }; + +// The original UEnumFactory may pop a dialog. We made a better one. +UCLASS() +class UEnumFactoryWing : public UEnumFactory +{ + GENERATED_BODY() +public: + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; +}; \ No newline at end of file diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingHacks.h b/Plugins/UEWingman/Source/UEWingman/Public/WingHacks.h index 3c2b6855..0793d1bb 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingHacks.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingHacks.h @@ -5,7 +5,6 @@ struct FToolMenuEntry; struct FToolUIActionChoice; class FBlueprintEditor; - // Utility class that uses the C++ template explicit-instantiation // loophole to access private members of engine classes. // See WingHacks.cpp for details on the technique. diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingPropHandle.h b/Plugins/UEWingman/Source/UEWingman/Public/WingPropHandle.h new file mode 100644 index 00000000..56a0a308 --- /dev/null +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingPropHandle.h @@ -0,0 +1,61 @@ +#pragma once + +#include "CoreMinimal.h" +#include "IPropertyRowGenerator.h" +#include "PropertyHandle.h" + +// WingPropHandle: A wrapper around IPropertyRowGenerator that +// provides easy access to IPropertyHandle objects. +// +// Usage: +// WingPropHandle Props; +// TArray> Handles = Props.AddObject(MyObject); +// +// The generators and all handles are valid for the lifetime of +// this object. + +class WingPropHandle +{ +public: + using Handles = TArray>; + using FlatTree = TArray>; + + WingPropHandle() {} + + // Get all properties of a UObject. Returns the handles. + // Only properties that have all the specified flags are included. + Handles AllProperties(UObject* Obj, EPropertyFlags Filter = CPF_None); + + // Get all properties of a struct. Does not copy — the data + // pointer must remain valid for the lifetime of this object. + // Only properties that have all the specified flags are included. + Handles AllProperties(const UStruct* ScriptStruct, uint8* Data, EPropertyFlags Filter = CPF_None); + + // Get "details panel" properties for an object. Handles special + // cases: blueprints (redirects to CDO), component references + // (redirects to template), material graph nodes (includes + // expression properties), widgets (includes slot properties). + Handles GetDetails(UObject* Obj, bool Mutable, EPropertyFlags Filter = CPF_None); + + // Get a single named property from a UObject. + TSharedPtr NamedProperty(UObject* Obj, FName Name); + + // Get a single named property from a struct. + TSharedPtr NamedProperty(const UStruct* ScriptStruct, uint8* Data, FName Name); + +private: + TArray>> Generators; + + // Check whether a detail tree node's property comes from a sub-object. + static bool IsSubObject(const TSharedRef& Node); + + static TSharedRef CreateGenerator(); + TSharedRef GetGeneratorForObject(UObject* Obj); + TSharedRef GetGeneratorForStruct(const UStruct* ScriptStruct, uint8* Data); + + static void AllTreeNodesRecursive(const TSharedRef& Node, FlatTree& Out); + static FlatTree AllTreeNodes(TSharedRef Generator); + + Handles AllProperties(TSharedRef Generator, EPropertyFlags Filter); + static TSharedPtr NamedProperty(TSharedRef Generator, FName Name); +}; diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingServer.h b/Plugins/UEWingman/Source/UEWingman/Public/WingServer.h index 155ceaca..1659d7df 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingServer.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingServer.h @@ -75,7 +75,7 @@ public: // ----- Tool dispatch ----- static void AddHandler(UObject* Obj, const FString& Documentation); - static void AddHandler(UObject* Obj, const FString& Name, const FString& Documentation, UObject* Config, EWingHandlerKind Kind); + static void AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, const FString& Documentation); static const TArray& AllHandlers() { return GWingServer->WingHandlerRegistry; } private: diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h b/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h index 7c2eeb4f..b854d4ba 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h @@ -24,6 +24,7 @@ class UScriptStruct; class UEnum; struct FBPInterfaceDescription; struct FWingProperty; +class IPropertyHandle; #include "Engine/World.h" #include "Materials/Material.h" @@ -65,6 +66,7 @@ public: static FName GetFName(const UWingComponentReference *Ref) { return Ref->VariableName; } static FName GetFName(const UWidget *Widget) { return Widget->GetFName(); } static FName GetFName(const WingVariables::Var &Var) { return Var.Name; } + static FName GetFName(const TSharedPtr &H); //////////////////////////////////////////////////////// // diff --git a/Plugins/UEWingman/Source/UEWingman/UEWingman.Build.cs b/Plugins/UEWingman/Source/UEWingman/UEWingman.Build.cs index 3946e0fa..f29532f0 100644 --- a/Plugins/UEWingman/Source/UEWingman/UEWingman.Build.cs +++ b/Plugins/UEWingman/Source/UEWingman/UEWingman.Build.cs @@ -6,6 +6,10 @@ public class UEWingman : ModuleRules { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + // Needed for debug dump of FPropertyNode tree (private headers) + PrivateIncludePaths.Add(System.IO.Path.Combine(EngineDirectory, "Source/Editor/PropertyEditor/Private")); + + PublicDependencyModuleNames.AddRange(new string[] { "Core", @@ -34,7 +38,9 @@ public class UEWingman : ModuleRules "SlateCore", "ToolMenus", "UMG", - "UMGEditor" + "UMGEditor", + "Blutility", + "PropertyEditor" }); } }