// Copyright Epic Games, Inc. All Rights Reserved. #include "SPropertyBinding.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/SNullWidget.h" #if WITH_EDITOR #include "Engine/BlueprintGeneratedClass.h" #endif // WITH_EDITOR #include "DetailLayoutBuilder.h" #include "Kismet2/BlueprintEditorUtils.h" #include "ScopedTransaction.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/Layout/SSpacer.h" #include "UObject/UObjectIterator.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "BlueprintActionDatabase.h" #include "Preferences/PersonaOptions.h" #include "Styling/SlateTypes.h" #define LOCTEXT_NAMESPACE "PropertyBinding" ///////////////////////////////////////////////////// // SPropertyBinding void SPropertyBinding::Construct(const FArguments& InArgs, UBlueprint* InBlueprint, const TArray& InBindingContextStructs) { Blueprint = InBlueprint; BindingContextStructs = InBindingContextStructs; Args = InArgs._Args; PropertyName = Args.Property != nullptr ? Args.Property->GetFName() : NAME_None; for(int32 Index = 0; Index < BindingContextStructs.Num(); ++Index) { FBindingContextStruct& ContextStruct = BindingContextStructs[Index]; /* BindingContextStructSections contains an entry for each different FBindingContextStruct.Section. Section are then further divided into subcategories based on the FBindingContextStruct.Category Section are displayed as a section while subcategories are displayed as menu and submenu under a section. */ FBindingContextStructCategory* Section = BindingContextStructSections.FindByPredicate([&](const FBindingContextStructCategory& Section) { return Section.Name.IdenticalTo(ContextStruct.Section); }); if(!Section) { Section = &BindingContextStructSections.AddDefaulted_GetRef(); Section->Name = ContextStruct.Section; } //No category the BindingContextStruct will be displayed directly inside the section. if (ContextStruct.Category.IsEmpty()) { Section->BindingContextStructIndices.Add(Index); } else { // Parse the categories and subcategories and construct the data to build the menus. TArray Categories; ContextStruct.Category.ParseIntoArray(Categories, TEXT("|")); BuildContextStructCategoryRecursive(Categories, Section->SubCategories, Index); } } TSharedRef LinkIcon = Args.bUseLinkIconStyle ? SNew(SBox) .HeightOverride(16.0f) [ SNew(SImage) .Image(this, &SPropertyBinding::GetLinkIcon) ] : SNullWidget::NullWidget; ChildSlot [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0f) [ SNew(SComboButton) .ButtonStyle(Args.BindButtonStyle) .ToolTipText(this, &SPropertyBinding::GetCurrentBindingToolTipText) .OnGetMenuContent(this, &SPropertyBinding::OnGenerateDelegateMenu) .ContentPadding(1) .HasDownArrow(!Args.bUseLinkIconStyle) .ButtonContent() [ SNew(SHorizontalBox) .Clipping(EWidgetClipping::ClipToBounds) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ LinkIcon ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SBox) .HeightOverride(16.0f) [ SNew(SImage) .Image(this, &SPropertyBinding::GetCurrentBindingImage) .ColorAndOpacity(this, &SPropertyBinding::GetCurrentBindingColor) ] ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(4, 0, 0, 0) [ SNew(STextBlock) .Text(this, &SPropertyBinding::GetCurrentBindingText) .ColorAndOpacity(this, &SPropertyBinding::GetCurrentBindingTextColor) .Font(IDetailLayoutBuilder::GetDetailFont()) ] ] ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(SButton) .ButtonStyle(FAppStyle::Get(), "HoverHintOnly") .Visibility(this, &SPropertyBinding::GetGotoBindingVisibility) .OnClicked(this, &SPropertyBinding::HandleGotoBindingClicked) .VAlign(VAlign_Center) .ToolTipText(LOCTEXT("GotoFunction", "Goto Function")) [ SNew(SImage) .Image(FAppStyle::GetBrush("Icons.Search")) ] ] ]; } bool SPropertyBinding::IsClassDenied(UClass* InClass) const { if(Args.OnCanBindToClass.IsBound() && Args.OnCanBindToClass.Execute(InClass)) { return false; } return true; } bool SPropertyBinding::IsFieldFromDeniedClass(FFieldVariant Field) const { return IsClassDenied(Field.GetOwnerClass()); } template void SPropertyBinding::ForEachBindableFunction(UClass* FromClass, Predicate Pred) const { if(Args.OnCanBindFunction.IsBound()) { auto CheckBindableClass = [this, &Pred](UClass* InBindableClass) { // Early exit if this class is excluded by permissions if ( !FBlueprintActionDatabase::IsClassAllowed(InBindableClass, FBlueprintActionDatabase::EPermissionsContext::Property) ) { return; } // Walk up class hierarchy for native functions and properties for ( TFieldIterator FuncIt(InBindableClass, EFieldIteratorFlags::IncludeSuper, EFieldIteratorFlags::ExcludeDeprecated, EFieldIteratorFlags::IncludeInterfaces); FuncIt; ++FuncIt ) { UFunction* Function = *FuncIt; // Stop processing functions after reaching a base class that it doesn't make sense to go beyond. if ( IsFieldFromDeniedClass(Function) ) { break; } // Only allow binding pure functions if we're limited to pure function bindings. if ( Args.bGeneratePureBindings && !Function->HasAnyFunctionFlags(FUNC_Const | FUNC_BlueprintPure) ) { continue; } // Only bind to functions that are callable from blueprints if ( !UEdGraphSchema_K2::CanUserKismetCallFunction(Function) ) { continue; } // Only bind to functions that are allowed by permissions if ( !FBlueprintActionDatabase::IsFunctionAllowed(Function, FBlueprintActionDatabase::EPermissionsContext::Property) ) { continue; } bool bValidObjectFunction = false; if(Args.bAllowUObjectFunctions) { FObjectPropertyBase* ObjectPropertyBase = CastField(Function->GetReturnProperty()); if(ObjectPropertyBase != nullptr && Function->NumParms == 1) { bValidObjectFunction = true; } } bool bValidStructFunction = false; if(Args.bAllowStructFunctions) { FStructProperty* StructProperty = CastField(Function->GetReturnProperty()); if(StructProperty != nullptr && Function->NumParms == 1) { bValidStructFunction = true; } } bool bValidThreadSafety = true; if (Args.bAllowOnlyThreadSafeFunctions) { bValidThreadSafety = FBlueprintEditorUtils::HasFunctionBlueprintThreadSafeMetaData(Function); } if(bValidThreadSafety && (bValidObjectFunction || bValidStructFunction || Args.OnCanBindFunction.Execute(Function))) { Pred(FFunctionInfo(Function)); } } }; CheckBindableClass(FromClass); if(Args.bAllowFunctionLibraryBindings) { // Iterate function libraries for (TObjectIterator ClassIt; ClassIt; ++ClassIt) { UClass* TestClass = *ClassIt; if (TestClass->IsChildOf(UBlueprintFunctionLibrary::StaticClass()) && (!TestClass->HasAnyClassFlags(CLASS_Abstract))) { CheckBindableClass(TestClass); } } } } } void SPropertyBinding::AddCategoryToMenu(FMenuBuilder& MenuBuilder, const FBindingContextStructCategory& Category) { if (HasCategorySomethingToDisplayRecursive(Category)) { TSharedRef SubMenu = SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ SNew(SSpacer) .Size(FVector2D(18.0f, 0.0f)) ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(STextBlock) .Text(Category.Name) ]; MenuBuilder.AddSubMenu(SubMenu, FNewMenuDelegate::CreateSP(this, &SPropertyBinding::FillCategoryMenu, &Category)); } } void SPropertyBinding::BuildContextStructCategoryRecursive(TConstArrayView CategoryNames, TArray& ParentSubCategories, int32 ContextStructIndex) { if (CategoryNames.IsEmpty()) { return; } FString CategoryName = CategoryNames[0]; FBindingContextStructCategory* CategoryPtr = ParentSubCategories.FindByPredicate([&](const FBindingContextStructCategory& Category) { return Category.Name.ToString() == CategoryName; }); if (!CategoryPtr) { CategoryPtr = &ParentSubCategories.AddDefaulted_GetRef(); CategoryPtr->Name = FText::FromString(CategoryName); } if(CategoryNames.Num() == 1) { CategoryPtr->BindingContextStructIndices.Add(ContextStructIndex); } else { BuildContextStructCategoryRecursive(CategoryNames.RightChop(1), CategoryPtr->SubCategories, ContextStructIndex); } } bool SPropertyBinding::HasCategorySomethingToDisplayRecursive(const FBindingContextStructCategory& Category) const { for(const FBindingContextStructCategory& SubCategory : Category.SubCategories) { if (HasCategorySomethingToDisplayRecursive(SubCategory)) { return true; } } for(const int32 ContextStructIndex : Category.BindingContextStructIndices) { const FBindingContextStruct& ContextStruct = BindingContextStructs[ContextStructIndex]; UStruct* Struct = const_cast(ContextStruct.Struct); PRAGMA_DISABLE_DEPRECATION_WARNINGS const bool bDeprecatedCanBindToContextStruct = Args.OnCanBindToContextStruct.IsBound() && Args.OnCanBindToContextStruct.Execute(Struct); PRAGMA_ENABLE_DEPRECATION_WARNINGS if (bDeprecatedCanBindToContextStruct || (Args.OnCanBindToContextStructWithIndex.IsBound() && Args.OnCanBindToContextStructWithIndex.Execute(Struct, ContextStructIndex))) { return true; } TArray> BindingChain; BindingChain.Emplace(MakeShared(nullptr, ContextStructIndex)); if (HasBindableProperties(Struct, BindingChain)) { return true; } } return false; } TSharedRef SPropertyBinding::MakeContextStructWidget(const FBindingContextStruct& ContextStruct) const { const FText DisplayText = ContextStruct.DisplayText.IsEmpty() ? ContextStruct.Struct->GetDisplayNameText() : ContextStruct.DisplayText; const FText ToolTipText = ContextStruct.TooltipText.IsEmpty() ? DisplayText : ContextStruct.TooltipText; const FSlateBrush* Icon = ContextStruct.Icon; FLinearColor IconColor = FLinearColor::White; if (Icon == nullptr) { const UEdGraphSchema_K2* Schema = GetDefault(); FEdGraphPinType PinType; if (UClass* Class = Cast(ContextStruct.Struct)) { PinType.PinCategory = UEdGraphSchema_K2::PC_Object; PinType.PinSubCategory = NAME_None; PinType.PinSubCategoryObject = Class; } else if (UScriptStruct* ScriptStruct = Cast(ContextStruct.Struct)) { PinType.PinCategory = UEdGraphSchema_K2::PC_Struct; PinType.PinSubCategory = NAME_None; PinType.PinSubCategoryObject = ScriptStruct; } Icon = FBlueprintEditorUtils::GetIconFromPin(PinType, true); IconColor = Schema->GetPinTypeColor(PinType); } if (ContextStruct.Color.IsSet()) { IconColor = ContextStruct.Color.GetValue(); } return SNew(SHorizontalBox) .ToolTipText(ToolTipText) + SHorizontalBox::Slot() .AutoWidth() [SNew(SSpacer) .Size(FVector2D(18.0f, 0.0f))] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(1.0f, 0.0f) [SNew(SImage) .Image(Icon) .ColorAndOpacity(IconColor)] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(4.0f, 0.0f) [SNew(STextBlock) .Text(DisplayText)]; } template void SPropertyBinding::ForEachBindableProperty(UStruct* InStruct, const TArray>& BindingChain, Predicate Pred) const { PRAGMA_DISABLE_DEPRECATION_WARNINGS const bool bIsOnCanBindPropertyBound = Args.OnCanBindProperty.IsBound() || Args.OnCanBindPropertyWithBindingChain.IsBound(); PRAGMA_ENABLE_DEPRECATION_WARNINGS if(InStruct && bIsOnCanBindPropertyBound) { UBlueprintGeneratedClass* SkeletonClass = Blueprint ? Cast(Blueprint->SkeletonGeneratedClass) : nullptr; for ( TFieldIterator PropIt(InStruct, EFieldIteratorFlags::IncludeSuper); PropIt; ++PropIt ) { FProperty* Property = *PropIt; // Stop processing properties after reaching the stopped base class if ( IsFieldFromDeniedClass(Property) ) { break; } if (!CanAcceptPropertyOrChildren(Property, BindingChain)) { continue; } if (SkeletonClass) { if (!UEdGraphSchema_K2::CanUserKismetAccessVariable(Property, SkeletonClass, UEdGraphSchema_K2::CannotBeDelegate)) { continue; } } // Also ignore advanced properties if (Property->HasAnyPropertyFlags(CPF_AdvancedDisplay | CPF_EditorOnly)) { continue; } // Exit if this property is excluded by permissions if (!GetMutableDefault()->IsAllowedProperty(Property)) { continue; } Pred(Property); } } } bool SPropertyBinding::HasBindableProperties(UStruct* InStruct, TArray>& BindingChain) const { TSet VisitedStructs; return HasBindablePropertiesRecursive(InStruct, VisitedStructs, BindingChain); } bool SPropertyBinding::HasBindablePropertiesRecursive(UStruct* InStruct, TSet& VisitedStructs, TArray>& BindingChain) const { if (VisitedStructs.Contains(InStruct)) { // If we have visited this struct already and it had bindable properties, it has been accounted for already. // Returning false to allow early exit. return false; } VisitedStructs.Add(InStruct); // Arbitrary cut off to avoid infinite loops. if (BindingChain.Num() > Args.MaxDepth) { return false; } int32 BindableCount = 0; ForEachBindableProperty(InStruct, BindingChain, [this, &BindableCount, &VisitedStructs, &BindingChain] (FProperty* Property) { BindingChain.Emplace(MakeShared(Property)); ON_SCOPE_EXIT{ BindingChain.Pop(); }; FArrayProperty* ArrayProperty = CastField(Property); if(CanBindProperty(Property, BindingChain)) { BindableCount++; return; } if(Args.bAllowArrayElementBindings && ArrayProperty != nullptr) { BindingChain.Emplace(MakeShared(Property)); BindingChain.Last()->ArrayIndex = 0; ON_SCOPE_EXIT{ BindingChain.Pop(); }; if(CanBindProperty(ArrayProperty->Inner, BindingChain)) { BindableCount++; return; } } FProperty* InnerProperty = Property; if(Args.bAllowArrayElementBindings && ArrayProperty != nullptr) { InnerProperty = ArrayProperty->Inner; } FObjectPropertyBase* ObjectProperty = CastField(InnerProperty); FStructProperty* StructProperty = CastField(InnerProperty); UStruct* Struct = nullptr; UClass* Class = nullptr; if ( ObjectProperty ) { Struct = Class = ObjectProperty->PropertyClass; } else if ( StructProperty ) { Struct = StructProperty->Struct; } if (Struct) { if (UStruct* ResolvedStruct = ResolveIndirection(BindingChain)) { Struct = ResolvedStruct; } // Recurse into a struct, except if it is the same type as the one we're binding. if (HasBindablePropertiesRecursive(Struct, VisitedStructs, BindingChain)) { if (Class) { // Ignore any subobject properties that are not bindable. // Also ignore any class that is explicitly on the deny list. if (IsClassDenied(Class) || (Args.OnCanBindToSubObjectClass.IsBound() && Args.OnCanBindToSubObjectClass.Execute(Class))) { return; } } if (Args.bAllowArrayElementBindings && ArrayProperty != nullptr) { BindableCount++; } else if (Args.bAllowStructMemberBindings) { BindableCount++; } } } }); if(UClass* Class = Cast(InStruct)) { ForEachBindableFunction(Class, [this, &BindableCount, &VisitedStructs, &BindingChain](const FFunctionInfo& Info) { BindingChain.Emplace(MakeShared(Info.Function)); ON_SCOPE_EXIT{ BindingChain.Pop(); }; FProperty* ReturnProperty = Info.Function->GetReturnProperty(); // We can get here if we accept non-leaf UObject functions, so if so we need to check the return value for compatibility if(!Args.bAllowUObjectFunctions || CanBindProperty(ReturnProperty, BindingChain)) { BindableCount++; } // Only show bindable subobjects, structs and variables if we're generating pure bindings. if(Args.bGeneratePureBindings) { if(FObjectPropertyBase* ObjectPropertyBase = CastField(ReturnProperty)) { if (HasBindablePropertiesRecursive(ObjectPropertyBase->PropertyClass, VisitedStructs, BindingChain)) { if (Args.bAllowUObjectFunctions) { BindableCount++; } } } else if(FStructProperty* StructProperty = CastField(ReturnProperty)) { if (HasBindablePropertiesRecursive(StructProperty->Struct, VisitedStructs, BindingChain)) { BindableCount++; } } } }); } return BindableCount > 0; } TSharedRef SPropertyBinding::OnGenerateDelegateMenu() { // The menu itself is be searchable. const bool bSearchableMenu = true; // The menu are generated through reflection and sometime the API exposes some recursivity (think about a Widget returning its parent which is also a Widget). Just by reflection // it is not possible to determine when the root object is reached. It needs a kind of simulation which is not implemented. Also, even if the recursivity was correctly handled, the possible // permutations tend to grow exponentially. Until a clever solution is found, the simple approach is to disable recursively searching those menus. User can still search the current one though. const bool bRecursivelySearchableMenu = false; const bool bInShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bInShouldCloseWindowAfterMenuSelection, nullptr, Args.MenuExtender, /*bCloseSelfOnly*/false, &FCoreStyle::Get(), bSearchableMenu, NAME_None, bRecursivelySearchableMenu); MenuBuilder.BeginSection("BindingActions", LOCTEXT("Bindings", "Bindings")); if ( CanRemoveBinding() ) { MenuBuilder.AddMenuEntry( LOCTEXT("RemoveBinding", "Remove Binding"), LOCTEXT("RemoveBindingToolTip", "Removes the current binding"), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Cross"), FUIAction(FExecuteAction::CreateSP(this, &SPropertyBinding::HandleRemoveBinding)) ); } if(Args.bAllowNewBindings && Args.BindableSignature != nullptr) { MenuBuilder.AddMenuEntry( LOCTEXT("CreateBinding", "Create Binding"), LOCTEXT("CreateBindingToolTip", "Creates a new function on the widget blueprint that will return the binding data for this property."), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Plus"), FUIAction(FExecuteAction::CreateSP(this, &SPropertyBinding::HandleCreateAndAddBinding)) ); } MenuBuilder.EndSection(); //CreateBinding // Properties { // Get the current skeleton class, think header for the blueprint. UBlueprintGeneratedClass* SkeletonClass = Blueprint ? Cast(Blueprint->SkeletonGeneratedClass) : nullptr; TArray> BindingChain; FillPropertyMenu(MenuBuilder, SkeletonClass, BindingChain); } if (BindingContextStructs.Num() > 0) { // Go thought each top level section and display the sub categories as menu and then the context struct without categories. for(const FBindingContextStructCategory& Section : BindingContextStructSections) { MenuBuilder.BeginSection(FName(), Section.Name); for(const FBindingContextStructCategory& SubCategory : Section.SubCategories) { AddCategoryToMenu(MenuBuilder, SubCategory); } for (int32 Index : Section.BindingContextStructIndices) { FBindingContextStruct& ContextStruct = BindingContextStructs[Index]; // Make first chain element representing the index in the context array. TArray> BindingChain; BindingChain.Emplace(MakeShared(nullptr, Index)); PRAGMA_DISABLE_DEPRECATION_WARNINGS const bool bDeprecatedCanBindToContextStruct = Args.OnCanBindToContextStruct.IsBound() && Args.OnCanBindToContextStruct.Execute(ContextStruct.Struct); PRAGMA_ENABLE_DEPRECATION_WARNINGS if (bDeprecatedCanBindToContextStruct || (Args.OnCanBindToContextStructWithIndex.IsBound() && Args.OnCanBindToContextStructWithIndex.Execute(ContextStruct.Struct, Index))) { // If the struct can be be bound to directly, create action for that. MenuBuilder.AddMenuEntry( FUIAction(FExecuteAction::CreateSP(this, &SPropertyBinding::HandleAddBinding, BindingChain)), MakeContextStructWidget(ContextStruct)); } if (HasBindableProperties(ContextStruct.Struct, BindingChain)) { // Show struct properties. MenuBuilder.AddSubMenu( MakeContextStructWidget(ContextStruct), FNewMenuDelegate::CreateSP(this, &SPropertyBinding::FillPropertyMenu, ContextStruct.Struct, BindingChain)); } } MenuBuilder.EndSection(); } } FDisplayMetrics DisplayMetrics; FSlateApplication::Get().GetCachedDisplayMetrics(DisplayMetrics); return SNew(SVerticalBox) + SVerticalBox::Slot() .MaxHeight(DisplayMetrics.PrimaryDisplayHeight * 0.5) [ MenuBuilder.MakeWidget() ]; } void SPropertyBinding::FillPropertyMenu(FMenuBuilder& MenuBuilder, UStruct* InOwnerStruct, TArray> InBindingChain) { auto MakeArrayElementPropertyWidget = [this](FProperty* InProperty, const TArray>& InNewBindingChain) { const UEdGraphSchema_K2* Schema = GetDefault(); FEdGraphPinType PinType; Schema->ConvertPropertyToPinType(InProperty, PinType); TSharedPtr LeafElement = InNewBindingChain.Last(); return SNew(SHorizontalBox) .ToolTipText(InProperty->GetToolTipText()) +SHorizontalBox::Slot() .AutoWidth() [ SNew(SSpacer) .Size(FVector2D(18.0f, 0.0f)) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(1.0f, 0.0f) [ SNew(SImage) .Image(FBlueprintEditorUtils::GetIconFromPin(PinType, true)) .ColorAndOpacity(Schema->GetPinTypeColor(PinType)) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(4.0f, 0.0f) [ SNew(STextBlock) .Text(FText::FromName(InProperty->GetFName())) ] +SHorizontalBox::Slot() .FillWidth(1.0f) .HAlign(HAlign_Right) .Padding(2.0f, 0.0f) [ SNew(SBox) .MinDesiredWidth(24.0f) [ SNew(SNumericEntryBox) .ToolTipText(LOCTEXT("ArrayIndex", "Array Index")) .Value_Lambda([LeafElement](){ return LeafElement->ArrayIndex == INDEX_NONE ? TOptional() : TOptional(LeafElement->ArrayIndex); }) .OnValueCommitted(this, &SPropertyBinding::HandleSetBindingArrayIndex, InProperty, InNewBindingChain) ] ]; }; auto MakeArrayElementEntry = [this, &MenuBuilder, &MakeArrayElementPropertyWidget](FProperty* InProperty, const TArray>& InNewBindingChain) { MenuBuilder.AddMenuEntry( FUIAction(FExecuteAction::CreateSP(this, &SPropertyBinding::HandleAddBinding, InNewBindingChain)), SNew(SHorizontalBox) +SHorizontalBox::Slot() .FillWidth(1.0f) [ MakeArrayElementPropertyWidget(InProperty, InNewBindingChain) ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SSpacer) .Size(FVector2D(15.0f, 0.0f)) ]); }; auto MakePropertyWidget = [this](FProperty* InProperty) { const UEdGraphSchema_K2* Schema = GetDefault(); FEdGraphPinType PinType; Schema->ConvertPropertyToPinType(InProperty, PinType); FText PropertyDisplayName; if(InProperty->IsNative()) { if(const FString* ScriptNamePtr = InProperty->FindMetaData("ScriptName")) { PropertyDisplayName = FText::FromString(*ScriptNamePtr); } else { PropertyDisplayName = FText::FromString(InProperty->GetName()); } } else { PropertyDisplayName = FText::FromString(InProperty->GetDisplayNameText().ToString()); } return SNew(SHorizontalBox) .ToolTipText(InProperty->GetToolTipText()) +SHorizontalBox::Slot() .AutoWidth() [ SNew(SSpacer) .Size(FVector2D(18.0f, 0.0f)) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(1.0f, 0.0f) [ SNew(SImage) .Image(FBlueprintEditorUtils::GetIconFromPin(PinType, true)) .ColorAndOpacity(Schema->GetPinTypeColor(PinType)) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(4.0f, 0.0f) [ SNew(STextBlock) .Text(PropertyDisplayName) ]; }; auto MakePropertyEntry = [this, &MenuBuilder, &MakePropertyWidget](FProperty* InProperty, const TArray>& InNewBindingChain) { MenuBuilder.AddMenuEntry( FUIAction(FExecuteAction::CreateSP(this, &SPropertyBinding::HandleAddBinding, InNewBindingChain)), MakePropertyWidget(InProperty)); }; auto MakeFunctionWidget = [this](const FFunctionInfo& Info) { static FName FunctionIcon(TEXT("GraphEditor.Function_16x")); const UEdGraphSchema_K2* Schema = GetDefault(); FEdGraphPinType PinType; if(FProperty* ReturnProperty = Info.Function->GetReturnProperty()) { Schema->ConvertPropertyToPinType(ReturnProperty, PinType); } return SNew(SHorizontalBox) .ToolTipText(FText::FromString(Info.Tooltip)) +SHorizontalBox::Slot() .AutoWidth() [ SNew(SSpacer) .Size(FVector2D(18.0f, 0.0f)) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(1.0f, 0.0f) [ SNew(SImage) .Image(FAppStyle::Get().GetBrush(FunctionIcon)) .ColorAndOpacity(Schema->GetPinTypeColor(PinType)) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(4.0f, 0.0f) [ SNew(STextBlock) .Text(Info.DisplayName) ]; }; //--------------------------------------- // Function Bindings if (Args.bAllowFunctionBindings) { if ( UClass* OwnerClass = Cast(InOwnerStruct) ) { static FName FunctionIcon(TEXT("GraphEditor.Function_16x")); MenuBuilder.BeginSection("Functions", LOCTEXT("Functions", "Functions")); { ForEachBindableFunction(OwnerClass, [this, &InBindingChain, &MenuBuilder, &MakeFunctionWidget] (const FFunctionInfo& Info) { TArray> NewBindingChain(InBindingChain); NewBindingChain.Emplace(MakeShared(Info.Function)); FProperty* ReturnProperty = Info.Function->GetReturnProperty(); // We can get here if we accept non-leaf UObject functions, so if so we need to check the return value for compatibility if(!Args.bAllowUObjectFunctions || CanBindProperty(ReturnProperty, InBindingChain)) { MenuBuilder.AddMenuEntry( FUIAction(FExecuteAction::CreateSP(this, &SPropertyBinding::HandleAddBinding, NewBindingChain)), MakeFunctionWidget(Info)); } // Only show bindable subobjects, structs and variables if we're generating pure bindings. if(Args.bGeneratePureBindings) { if(FObjectPropertyBase* ObjectPropertyBase = CastField(ReturnProperty)) { if (HasBindableProperties(ObjectPropertyBase->PropertyClass, NewBindingChain)) { MenuBuilder.AddSubMenu( MakeFunctionWidget(Info), FNewMenuDelegate::CreateSP(this, &SPropertyBinding::FillPropertyMenu, static_cast(ObjectPropertyBase->PropertyClass), NewBindingChain)); } } else if(FStructProperty* StructProperty = CastField(ReturnProperty)) { if (HasBindableProperties(StructProperty->Struct, NewBindingChain)) { MenuBuilder.AddSubMenu( MakeFunctionWidget(Info), FNewMenuDelegate::CreateSP(this, &SPropertyBinding::FillPropertyMenu, static_cast(StructProperty->Struct), NewBindingChain)); } } } }); } MenuBuilder.EndSection(); //Functions } } //--------------------------------------- // Property Bindings // Only show bindable subobjects and variables if we're generating pure bindings. if (Args.bGeneratePureBindings && Args.bAllowPropertyBindings) { FProperty* BindingProperty = nullptr; if (Args.BindableSignature != nullptr) { BindingProperty = Args.BindableSignature->GetReturnProperty(); } else { BindingProperty = Args.Property; } // Find the binder that can handle the delegate return type, don't bother allowing people // to look for bindings that we don't support if (CanBindProperty(BindingProperty, InBindingChain)) { UStruct* BindingStruct = nullptr; FObjectPropertyBase* ObjectProperty = CastField(BindingProperty); FStructProperty* StructProperty = CastField(BindingProperty); if (ObjectProperty) { BindingStruct = ObjectProperty->PropertyClass; } else if (StructProperty) { BindingStruct = StructProperty->Struct; } MenuBuilder.BeginSection("Properties", LOCTEXT("Properties", "Properties")); { ForEachBindableProperty(InOwnerStruct, InBindingChain, [this, &InBindingChain, BindingStruct, &MenuBuilder, &MakeArrayElementPropertyWidget, &MakePropertyWidget, &MakePropertyEntry, &MakeArrayElementEntry](FProperty* Property) { TArray> NewBindingChain(InBindingChain); TArray> NewArrayElementBindingChain; NewBindingChain.Emplace(MakeShared(Property)); if (CanBindProperty(Property, NewBindingChain)) { MakePropertyEntry(Property, NewBindingChain); } FProperty* InnerProperty = Property; FArrayProperty* ArrayProperty = CastField(Property); const bool bUseArrayProperty = Args.bAllowArrayElementBindings && ArrayProperty != nullptr; if (bUseArrayProperty) { NewArrayElementBindingChain = InBindingChain; NewArrayElementBindingChain.Emplace(MakeShared(Property)); NewArrayElementBindingChain.Last()->ArrayIndex = 0; if (CanBindProperty(ArrayProperty->Inner, NewArrayElementBindingChain)) { MakeArrayElementEntry(ArrayProperty->Inner, NewArrayElementBindingChain); } InnerProperty = ArrayProperty->Inner; } FObjectPropertyBase* ObjectProperty = CastField(InnerProperty); FStructProperty* StructProperty = CastField(InnerProperty); UStruct* Struct = nullptr; UClass* Class = nullptr; if (ObjectProperty) { Struct = Class = ObjectProperty->PropertyClass; } else if (StructProperty) { Struct = StructProperty->Struct; } // Recurse into a struct if it has some properties we can bind to. if (Struct) { if (UStruct* ResolvedStruct = ResolveIndirection(NewBindingChain)) { Struct = ResolvedStruct; } if (Class) { // Ignore any subobject properties that are not bindable. // Also ignore any class that is explicitly on the deny list. if (IsClassDenied(Class) || (Args.OnCanBindToSubObjectClass.IsBound() && Args.OnCanBindToSubObjectClass.Execute(Class))) { return; } } TArray>& BindablePropertyBindingChain = bUseArrayProperty ? NewArrayElementBindingChain : NewBindingChain; if (HasBindableProperties(Struct, BindablePropertyBindingChain)) { if (bUseArrayProperty) { MenuBuilder.AddSubMenu( MakeArrayElementPropertyWidget(ArrayProperty->Inner, NewArrayElementBindingChain), FNewMenuDelegate::CreateSP(this, &SPropertyBinding::FillPropertyMenu, Struct, NewArrayElementBindingChain) ); } else if (Args.bAllowStructMemberBindings) { MenuBuilder.AddSubMenu( MakePropertyWidget(Property), FNewMenuDelegate::CreateSP(this, &SPropertyBinding::FillPropertyMenu, Struct, NewBindingChain)); } } } }); } MenuBuilder.EndSection(); //Properties } } // Add 'none' entry only if we just have the search block in the builder if ( InOwnerStruct && MenuBuilder.GetMultiBox()->GetBlocks().Num() == 1 ) { MenuBuilder.BeginSection("None", InOwnerStruct->GetDisplayNameText()); MenuBuilder.AddWidget(SNew(STextBlock).Text(LOCTEXT("None", "None")), FText::GetEmpty()); MenuBuilder.EndSection(); //None } } void SPropertyBinding::FillCategoryMenu(FMenuBuilder& MenuBuilder, const FBindingContextStructCategory* Category) { MenuBuilder.BeginSection(NAME_None, Category->Name); for (const FBindingContextStructCategory& SubCategory : Category->SubCategories) { AddCategoryToMenu(MenuBuilder, SubCategory); } for(int32 Index : Category->BindingContextStructIndices) { FBindingContextStruct& ContextStruct = BindingContextStructs[Index]; // Make first chain element representing the index in the context array. TArray> BindingChain; BindingChain.Emplace(MakeShared(nullptr, Index)); PRAGMA_DISABLE_DEPRECATION_WARNINGS const bool bDeprecatedCanBindToContextStruct = Args.OnCanBindToContextStruct.IsBound() && Args.OnCanBindToContextStruct.Execute(ContextStruct.Struct); PRAGMA_ENABLE_DEPRECATION_WARNINGS if (bDeprecatedCanBindToContextStruct || (Args.OnCanBindToContextStructWithIndex.IsBound() && Args.OnCanBindToContextStructWithIndex.Execute(ContextStruct.Struct, Index))) { // If the struct can be be bound to directly, create action for that. MenuBuilder.AddMenuEntry( FUIAction(FExecuteAction::CreateSP(this, &SPropertyBinding::HandleAddBinding, BindingChain)), MakeContextStructWidget(ContextStruct)); } if (HasBindableProperties(ContextStruct.Struct, BindingChain)) { // Show struct properties. MenuBuilder.AddSubMenu( MakeContextStructWidget(ContextStruct), FNewMenuDelegate::CreateSP(this, &SPropertyBinding::FillPropertyMenu, ContextStruct.Struct, BindingChain)); } } MenuBuilder.EndSection(); } const FSlateBrush* SPropertyBinding::GetLinkIcon() const { bool CheckState = HasAnyBindings(); return (CheckState) ? FAppStyle::GetBrush("Icons.Link") : FAppStyle::GetBrush("Icons.Unlink"); } const FSlateBrush* SPropertyBinding::GetCurrentBindingImage() const { if(Args.CurrentBindingImage.IsSet()) { return Args.CurrentBindingImage.Get(); } return nullptr; } FText SPropertyBinding::GetCurrentBindingText() const { if(Args.CurrentBindingText.IsSet()) { return Args.CurrentBindingText.Get(); } return LOCTEXT("Bind", "Bind"); } FSlateColor SPropertyBinding::GetCurrentBindingTextColor() const { if (Args.CurrentBindingTextColor.IsSet()) { return Args.CurrentBindingTextColor.Get(); } return FSlateColor::UseForeground(); } FText SPropertyBinding::GetCurrentBindingToolTipText() const { if(Args.CurrentBindingToolTipText.IsSet()) { return Args.CurrentBindingToolTipText.Get(); } return GetCurrentBindingText(); } FSlateColor SPropertyBinding::GetCurrentBindingColor() const { if(Args.CurrentBindingColor.IsSet()) { return Args.CurrentBindingColor.Get(); } return FLinearColor(0.25f, 0.25f, 0.25f); } bool SPropertyBinding::CanRemoveBinding() { if(Args.OnCanRemoveBinding.IsBound()) { return Args.OnCanRemoveBinding.Execute(PropertyName); } return false; } void SPropertyBinding::HandleRemoveBinding() { if(Args.OnRemoveBinding.IsBound()) { const FScopedTransaction Transaction(LOCTEXT("UnbindDelegate", "Remove Binding")); Args.OnRemoveBinding.Execute(PropertyName); } } void SPropertyBinding::HandleAddBinding(TArray> InBindingChain) { if(Args.OnAddBinding.IsBound()) { const FScopedTransaction Transaction(LOCTEXT("BindDelegate", "Set Binding")); TArray BindingChain; BindingChain.Reserve(InBindingChain.Num()); Algo::Transform(InBindingChain, BindingChain, [](TSharedPtr InElement) { return *InElement.Get(); }); Args.OnAddBinding.Execute(PropertyName, BindingChain); } } void SPropertyBinding::HandleSetBindingArrayIndex(int32 InArrayIndex, ETextCommit::Type InCommitType, FProperty* InProperty, TArray> InBindingChain) { InBindingChain.Last()->ArrayIndex = InArrayIndex; // If the user hit enter on a compatible property, assume they want to accept if(CanBindProperty(InProperty, InBindingChain) && InCommitType == ETextCommit::OnEnter) { HandleAddBinding(InBindingChain); } } void SPropertyBinding::HandleCreateAndAddBinding() { if (!Blueprint) { return; } const FScopedTransaction Transaction(LOCTEXT("CreateDelegate", "Create Binding")); Blueprint->Modify(); FString Pre = Args.bGeneratePureBindings ? FString(TEXT("Get")) : FString(TEXT("On")); FString WidgetName; if(Args.OnGenerateBindingName.IsBound()) { WidgetName = Args.OnGenerateBindingName.Execute(); } FString Post = (PropertyName != NAME_None) ? PropertyName.ToString() : FString(); Post.RemoveFromStart(TEXT("On")); Post.RemoveFromEnd(TEXT("Event")); // Create the function graph. FString FunctionName = Pre + WidgetName + Post; UEdGraph* FunctionGraph = FBlueprintEditorUtils::CreateNewGraph( Blueprint, FBlueprintEditorUtils::FindUniqueKismetName(Blueprint, FunctionName), UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass()); const bool bUserCreated = true; FBlueprintEditorUtils::AddFunctionGraph(Blueprint, FunctionGraph, bUserCreated, Args.BindableSignature); UFunction* Function = Blueprint->SkeletonGeneratedClass->FindFunctionByName(FunctionGraph->GetFName()); check(Function); Args.OnNewFunctionBindingCreated.ExecuteIfBound(FunctionGraph, Function); // Add the binding to the blueprint TArray> BindingPath; BindingPath.Emplace(MakeShared(Function)); HandleAddBinding(BindingPath); // Only mark bindings as pure that need to be. if ( Args.bGeneratePureBindings ) { const UEdGraphSchema_K2* Schema_K2 = Cast(FunctionGraph->GetSchema()); Schema_K2->AddExtraFunctionFlags(FunctionGraph, FUNC_BlueprintPure); } HandleGotoBindingClicked(); } UStruct* SPropertyBinding::ResolveIndirection(const TArray>& InBindingChain) const { UStruct* ResolvedStruct = nullptr; if (Args.OnResolveIndirection.IsBound()) { TArray BindingChain; BindingChain.Reserve(InBindingChain.Num()); Algo::Transform(InBindingChain, BindingChain, [](TSharedPtr InElement) { return *InElement.Get(); }); ResolvedStruct = Args.OnResolveIndirection.Execute(BindingChain); } return ResolvedStruct; } EVisibility SPropertyBinding::GetGotoBindingVisibility() const { if(Args.OnCanGotoBinding.IsBound()) { if(Args.OnCanGotoBinding.Execute(PropertyName)) { return EVisibility::Visible; } } return EVisibility::Collapsed; } FReply SPropertyBinding::HandleGotoBindingClicked() { if(Args.OnGotoBinding.IsBound()) { if(Args.OnGotoBinding.Execute(PropertyName)) { return FReply::Handled(); } } return FReply::Unhandled(); } bool SPropertyBinding::CanAcceptPropertyOrChildren(FProperty* InProperty, TConstArrayView> InBindingChain) const { PRAGMA_DISABLE_DEPRECATION_WARNINGS if (Args.OnCanAcceptPropertyOrChildren.IsBound() && Args.OnCanAcceptPropertyOrChildren.Execute(InProperty) == false) { return false; } PRAGMA_ENABLE_DEPRECATION_WARNINGS if (Args.OnCanAcceptPropertyOrChildrenWithBindingChain.IsBound()) { TArray> BindingChain; Algo::Transform(InBindingChain, BindingChain, [](TSharedPtr InElement) { return *InElement.Get(); }); if (!Args.OnCanAcceptPropertyOrChildrenWithBindingChain.Execute(InProperty, BindingChain)) { return false; } } return true; } bool SPropertyBinding::CanBindProperty(FProperty* InProperty, TConstArrayView> InBindingChain) const { PRAGMA_DISABLE_DEPRECATION_WARNINGS if (Args.OnCanBindProperty.IsBound() && Args.OnCanBindProperty.Execute(InProperty) == false) { return false; } PRAGMA_ENABLE_DEPRECATION_WARNINGS if (Args.OnCanBindPropertyWithBindingChain.IsBound()) { TArray> BindingChain; Algo::Transform(InBindingChain, BindingChain, [](TSharedPtr InElement) { return *InElement.Get(); }); if (!Args.OnCanBindPropertyWithBindingChain.Execute(InProperty, BindingChain)) { return false; } } return true; } FReply SPropertyBinding::OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) { if (Args.OnDrop.IsBound()) { return Args.OnDrop.Execute(MyGeometry, DragDropEvent); } return FReply::Unhandled(); } bool SPropertyBinding::HasAnyBindings() const { if (Args.OnHasAnyBindings.IsBound()) { return Args.OnHasAnyBindings.Execute(); } return false; } #undef LOCTEXT_NAMESPACE