Stricter name validation, and better disambiguation of graph actions

This commit is contained in:
2026-04-09 22:01:19 -04:00
parent 46ac6b34a8
commit 224e6604e6
13 changed files with 153 additions and 63 deletions

View File

@@ -51,7 +51,7 @@ public:
FName InternalID = WingUtils::CheckProposedName(Component, WingOut::Stdout);
if (InternalID.IsNone()) return;
TSet<FName> Names;
FBlueprintEditorUtils::GetClassVariableList(BP, Names);
WingUtils::GetAllInUseNames(BP, Names);
if (!WingUtils::FindNoDuplicateName(Names, InternalID, TEXT("variable or component"), WingOut::Stdout)) return;
// Resolve the component class by name

View File

@@ -46,7 +46,7 @@ public:
FName InternalID = WingUtils::CheckProposedName(Dispatcher, WingOut::Stdout);
if (InternalID.IsNone()) return;
TSet<FName> Names;
FBlueprintEditorUtils::GetClassVariableList(BP, Names);
WingUtils::GetAllInUseNames(BP, Names);
if (!WingUtils::FindNoDuplicateName(Names, InternalID, TEXT("variable or component"), WingOut::Stdout)) return;
// Parse the arguments.

View File

@@ -51,7 +51,6 @@ public:
WingFetcher F(WingOut::Stdout);
UWidgetBlueprint* BP = F.Walk(Blueprint).Cast<UWidgetBlueprint>();
if (!BP) return;
UWidgetTree* Tree = BP->WidgetTree;
// Resolve the widget type.
WingWidgets WidgetMenu;
@@ -71,15 +70,14 @@ public:
if (InternalID.IsNone()) return;
// Check that the name is unique among existing widgets.
TArray<UWidget*> AllWidgets;
Tree->GetAllWidgets(AllWidgets);
TSet<FName> Names;
FBlueprintEditorUtils::GetClassVariableList(BP, Names);
if (!WingUtils::FindNoDuplicateNames(Names, AllWidgets, TEXT("widget or variable"), WingOut::Stdout)) return;
WingUtils::GetAllInUseNames(BP, Names);
if (!WingUtils::FindNoDuplicateName(Names, InternalID, TEXT("widget or variable"), WingOut::Stdout)) return;
// If a parent is specified, find it and verify it's a panel.
UPanelWidget* ParentPanel = nullptr;
TArray<UWidget*> AllWidgets;
BP->WidgetTree->GetAllWidgets(AllWidgets);
if (!Parent.IsEmpty())
{
UWidget* ParentWidget = WingUtils::FindOneWithExternalID(Parent, AllWidgets, TEXT("Widget"), WingOut::Stdout);
@@ -89,7 +87,7 @@ public:
}
else
{
if (Tree->RootWidget != nullptr)
if (BP->WidgetTree->RootWidget != nullptr)
{
WingOut::Stdout.Printf(TEXT("ERROR: Widget tree already has a root widget. Specify a Parent.\n"));
return;
@@ -99,9 +97,9 @@ public:
// Create the widget.
UWidget* NewWidget;
if (WidgetClass->IsChildOf(UUserWidget::StaticClass()))
NewWidget = CreateWidget<UUserWidget>(Tree, WidgetClass, InternalID);
NewWidget = CreateWidget<UUserWidget>(BP->WidgetTree, WidgetClass, InternalID);
else
NewWidget = NewObject<UWidget>(Tree, WidgetClass, InternalID, RF_Transactional);
NewWidget = NewObject<UWidget>(BP->WidgetTree, WidgetClass, InternalID, RF_Transactional);
if (!NewWidget)
{
@@ -120,7 +118,7 @@ public:
}
else
{
Tree->RootWidget = NewWidget;
BP->WidgetTree->RootWidget = NewWidget;
}
WingOut::Stdout.Printf(TEXT("Created widget '%s' of type '%s'\n"), *Name, *Type);

View File

@@ -150,7 +150,7 @@ bool UWingComponent::AddComponent(UBlueprint *BP, UClass *Class, UWingComponentR
return false;
}
TSet<FName> Names;
FBlueprintEditorUtils::GetClassVariableList(BP, Names);
WingUtils::GetAllInUseNames(BP, Names);
if (Names.Contains(Name))
{
Errors.Printf(TEXT("There is already a variable or component named %s in %s.\n"),

View File

@@ -1,10 +1,75 @@
#include "WingGraphActions.h"
#include "EdGraph/EdGraphSchema.h"
#include "AnimNotifyEventNodeSpawner.h"
#include "BlueprintActionFilter.h"
#include "BlueprintActionDatabase.h"
#include "BlueprintBoundEventNodeSpawner.h"
#include "BlueprintComponentNodeSpawner.h"
#include "BlueprintDelegateNodeSpawner.h"
#include "BlueprintEventNodeSpawner.h"
#include "BlueprintFunctionNodeSpawner.h"
#include "BlueprintNodeSpawner.h"
#include "BlueprintVariableNodeSpawner.h"
#include "EdGraph/EdGraphSchema.h"
#include "EdGraphSchema_K2.h"
#include "Animation/AnimInstance.h"
#include "Engine/Blueprint.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "WingUtils.h"
FName FWingGraphAction::GetPropertyClassName(const FProperty *Prop)
{
if (Prop == nullptr) return FName();
UClass *Class = Prop->GetOwnerClass();
if (Class == nullptr) return FName();
return Class->GetFName();
}
FName FWingGraphAction::GetSpawnerOwnerClass(const UBlueprintNodeSpawner* Spawner)
{
if (const UBlueprintFunctionNodeSpawner* FunctionSpawner = Cast<UBlueprintFunctionNodeSpawner>(Spawner))
{
return FunctionSpawner->GetFunction()->GetOwnerClass()->GetFName();
}
if (const UBlueprintVariableNodeSpawner* VariableSpawner = Cast<UBlueprintVariableNodeSpawner>(Spawner))
{
return GetPropertyClassName(VariableSpawner->GetVarProperty());
}
if (const UBlueprintDelegateNodeSpawner* DelegateSpawner = Cast<UBlueprintDelegateNodeSpawner>(Spawner))
{
return GetPropertyClassName(DelegateSpawner->GetDelegateProperty());
}
if (const UBlueprintBoundEventNodeSpawner* BoundEventSpawner = Cast<UBlueprintBoundEventNodeSpawner>(Spawner))
{
return GetPropertyClassName(BoundEventSpawner->GetEventDelegate());
}
if (Cast<UAnimNotifyEventNodeSpawner>(Spawner))
{
return UAnimInstance::StaticClass()->GetFName();
}
if (const UBlueprintEventNodeSpawner* EventSpawner = Cast<UBlueprintEventNodeSpawner>(Spawner))
{
if (EventSpawner->IsForCustomEvent()) return FName();
return EventSpawner->GetEventFunction()->GetOwnerClass()->GetFName();
}
if (const UBlueprintComponentNodeSpawner* ComponentSpawner = Cast<UBlueprintComponentNodeSpawner>(Spawner))
{
if (const UClass* ComponentClass = ComponentSpawner->GetComponentClass())
{
return ComponentClass->GetFName();
}
// todo: get a class name from the asset data.
return FName();
}
return FName();
}
FWingGraphAction::FWingGraphAction(TSharedPtr<FEdGraphSchemaAction> &iAction, UEdGraph *iGraph)
{
Action = iAction;
@@ -20,10 +85,13 @@ FWingGraphAction::FWingGraphAction(UBlueprintNodeSpawner *iSpawner, UEdGraph *iG
Spawner = iSpawner;
Graph = iGraph;
const FBlueprintActionUiSpec& UiSpec = Spawner->PrimeDefaultUiSpec();
FString Category = UiSpec.Category.ToString();
FString MenuName = UiSpec.MenuName.ToString();
Name = WingUtils::StandardizeMenuItem(Category + TEXT("|") + MenuName);
Keywords = Spawner->PrimeDefaultUiSpec().Keywords.ToString();
Name = UiSpec.Category.ToString() + TEXT("|") + UiSpec.MenuName.ToString();
Keywords = UiSpec.Keywords.ToString();
if (FName OwnerClass = GetSpawnerOwnerClass(Spawner); !OwnerClass.IsNone())
{
Name = OwnerClass.ToString() + TEXT("|") + Name;
}
Name = WingUtils::StandardizeMenuItem(Name);
}
UEdGraphNode* FWingGraphAction::Execute(const FVector2D &Location) const
@@ -72,18 +140,28 @@ void FWingGraphActions::CollectActions()
void FWingGraphActions::CollectSpawners()
{
UBlueprint* Blueprint = FBlueprintEditorUtils::FindBlueprintForGraph(Graph);
if (!Blueprint) return;
FBlueprintActionContext Context;
Context.Blueprints.Add(Blueprint);
Context.Graphs.Add(Graph);
FBlueprintActionFilter Filter(FBlueprintActionFilter::BPFILTER_NoFlags);
Filter.Context = Context;
for (const auto& Pair : FBlueprintActionDatabase::Get().GetAllActions())
{
UObject* ActionObject = Pair.Key.ResolveObjectPtr();
if (!ActionObject) continue;
for (UBlueprintNodeSpawner* Spawner : Pair.Value)
{
if (Spawner->NodeClass)
{
UEdGraphNode* NodeCDO = CastChecked<UEdGraphNode>(Spawner->NodeClass->ClassDefaultObject);
if (NodeCDO->IsCompatibleWithGraph(Graph))
{
Actions.Emplace(Spawner, Graph);
}
}
if (!Spawner->NodeClass) continue;
FBlueprintActionInfo ActionInfo(ActionObject, Spawner);
if (Filter.IsFiltered(ActionInfo)) continue;
Actions.Emplace(Spawner, Graph);
}
}
}

View File

@@ -474,7 +474,8 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
}
// If it's a Widget, hide the slot property, and add the
// slot properties.
// slot properties. Then, add the 'bIsVariable' property and
// make it editable.
if (UWidget* Widget = Cast<UWidget>(Obj))
{
FWingProperty::Remove(Result, TEXT("Slot"));
@@ -482,6 +483,8 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
{
Result.Append(GetVisible(Slot));
}
FProperty *VarProp = Widget->GetClass()->FindPropertyByName(TEXT("bIsVariable"));
if (VarProp) Result.Add(FWingProperty(VarProp, Widget, true));
}
if (!Mutable) StripEditable(Result);

View File

@@ -10,6 +10,8 @@
#include "Kismet2/Kismet2NameValidators.h"
#include "UObject/UObjectIterator.h"
#include "UObject/UnrealType.h"
#include "WidgetBlueprint.h"
#include "Blueprint/WidgetTree.h"
// Reparent validation
#include "Engine/LevelScriptActor.h"
@@ -62,6 +64,11 @@ FName WingUtils::CheckInternalizeID(const FString &ExternalID, WingOut Errors)
FName WingUtils::CheckProposedName(const FString &ExternalID, WingOut Errors)
{
FName InternalID = CheckInternalizeID(ExternalID, Errors);
if (InternalID.ToString().Len() > 100)
{
Errors.Printf(TEXT("ERROR: id %s is too long"), *ExternalID);
return FName();
}
if (!InternalID.IsNone() && !WingTokenizer::WouldExternalizeReadably(InternalID))
{
Errors.Printf(TEXT("ERROR: id %s would not be a readable id, may not create item with this name"),
@@ -476,6 +483,21 @@ UObject *WingUtils::GetGeneratedCDO(UBlueprint *BP)
return BP->GeneratedClass->GetDefaultObject();
}
void WingUtils::GetAllInUseNames(UBlueprint *BP, TSet<FName> &Names)
{
FBlueprintEditorUtils::GetClassVariableList(BP, Names, true);
FBlueprintEditorUtils::GetFunctionNameList(BP, Names);
FBlueprintEditorUtils::GetAllGraphNames(BP, Names);
FBlueprintEditorUtils::GetSCSVariableNameList(BP, Names);
FBlueprintEditorUtils::GetImplementingBlueprintsFunctionNameList(BP, Names);
if (UWidgetBlueprint *WBP = Cast<UWidgetBlueprint>(BP))
{
TArray<UWidget*> AllWidgets;
WBP->WidgetTree->GetAllWidgets(AllWidgets);
WingUtils::AddNamesToSet(Names, AllWidgets);
}
}
// ============================================================
// Graph Pin Helpers
// ============================================================

View File

@@ -612,7 +612,7 @@ bool WingVariables::CreateBlueprint(WingOut Errors)
// Check for name collisions against existing variables, components, and the like.
TSet<FName> Names;
FBlueprintEditorUtils::GetClassVariableList(Blueprint.Get(), Names);
WingUtils::GetAllInUseNames(Blueprint.Get(), Names);
if (!WingUtils::FindNoDuplicateNames(Names, BlueprintVariables.Variables, TEXT("variable or component"), Errors)) return false;
// Create the variables.

View File

@@ -5,6 +5,7 @@
class UBlueprintNodeSpawner;
class UEdGraph;
class UEdGraphNode;
struct FBlueprintActionContext;
struct FEdGraphSchemaAction;
@@ -20,6 +21,10 @@ struct FWingGraphAction
FWingGraphAction(UBlueprintNodeSpawner *iSpawner, UEdGraph *iGraph);
UEdGraphNode *Execute(const FVector2D &Location) const;
private:
static FName GetSpawnerOwnerClass(const UBlueprintNodeSpawner* Spawner);
static FName GetPropertyClassName(const FProperty *Prop);
};

View File

@@ -136,6 +136,12 @@ public:
return Result;
}
template<typename ArrayType>
static void AddNamesToSet(TSet<FName> &Collection, ArrayType &&Array)
{
for (auto &Elt : Array) Collection.Add(GetFName(Elt));
}
static bool FindNoDuplicateName(TSet<FName> &Collection, FName InternalID, const TCHAR *Kind, WingOut Errors)
{
if (Collection.Contains(InternalID))
@@ -244,6 +250,7 @@ public:
static TArray<UEdGraphNode*> AllNodes(UEdGraph *Graph);
static TArray<UBlueprint*> GetAncestorBlueprints(UBlueprint *BP, bool OldestFirst = false);
static UObject *GetGeneratedCDO(UBlueprint *BP);
static void GetAllInUseNames(UBlueprint *BP, TSet<FName> &Names);
// ----- Graph pin helpers -----
static UEdGraphPin* CheckGetPin(UEdGraphNode* Node, FName PinName, WingOut Errors);