// Copyright Epic Games, Inc. All Rights Reserved. #include "MovieSceneEventCustomization.h" #include "Modules/ModuleManager.h" #include "MovieSceneEventUtils.h" #include "UObject/UnrealType.h" #include "Channels/MovieSceneEvent.h" #include "Tracks/MovieSceneEventTrack.h" #include "Sections/MovieSceneEventSectionBase.h" #include "Algo/Find.h" #include "MovieSceneSequence.h" #include "ISequencerModule.h" #include "ScopedTransaction.h" #include "Framework/Application/SlateApplication.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Images/SImage.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Input/SHyperlink.h" #include "Widgets/Input/SEditableTextBox.h" #include "Widgets/Views/SExpanderArrow.h" #include "PropertyHandle.h" #include "IPropertyUtilities.h" #include "PropertyCustomizationHelpers.h" #include "IDetailChildrenBuilder.h" #include "DetailLayoutBuilder.h" #include "DetailCategoryBuilder.h" #include "K2Node_Variable.h" #include "K2Node_CustomEvent.h" #include "K2Node_CallFunction.h" #include "K2Node_FunctionEntry.h" #include "Kismet2/KismetEditorUtilities.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/Kismet2NameValidators.h" #include "SGraphActionMenu.h" #include "BlueprintActionMenuUtils.h" #include "BlueprintActionMenuBuilder.h" #include "BlueprintActionMenuItem.h" #include "BlueprintFunctionNodeSpawner.h" #include "Styling/AppStyle.h" #include "EditorFontGlyphs.h" #define LOCTEXT_NAMESPACE "MovieSceneEventCustomization" namespace UE_MovieSceneEventCustomization { void CollectQuickBindActions(FGraphActionListBuilderBase& OutAllActions, UBlueprint* Blueprint, UClass* BoundObjectPinClass) { // Build up the context object auto RejectAnyNonFunctions = [](const FBlueprintActionFilter& Filter, FBlueprintActionInfo& BlueprintAction) { const UFunction* Function = BlueprintAction.GetAssociatedFunction(); return Function == nullptr || Function->HasAnyFunctionFlags(FUNC_BlueprintPure); }; auto RejectAnyUnboundActions = [](const FBlueprintActionFilter& Filter, FBlueprintActionInfo& BlueprintAction) { return BlueprintAction.GetBindings().Num() <= 0; }; FBlueprintActionMenuBuilder ContextMenuBuilder(nullptr); if (BoundObjectPinClass) { // Add actions that are relevant to the bound object from the pin class FBlueprintActionFilter CallOnMemberFilter(FBlueprintActionFilter::BPFILTER_RejectGlobalFields | FBlueprintActionFilter::BPFILTER_RejectPermittedSubClasses); CallOnMemberFilter.PermittedNodeTypes.Add(UK2Node_CallFunction::StaticClass()); CallOnMemberFilter.Context.Blueprints.Add(Blueprint); for (FObjectProperty* ObjectProperty : TFieldRange(BoundObjectPinClass)) { if (ObjectProperty->HasAnyPropertyFlags(CPF_BlueprintVisible) && (ObjectProperty->HasMetaData(FBlueprintMetadata::MD_ExposeFunctionCategories) || FBlueprintEditorUtils::IsSCSComponentProperty(ObjectProperty))) { CallOnMemberFilter.Context.SelectedObjects.Add(ObjectProperty); FBlueprintActionFilter::AddUnique(CallOnMemberFilter.TargetClasses, ObjectProperty->PropertyClass); } } FBlueprintActionFilter::AddUnique(CallOnMemberFilter.TargetClasses, BoundObjectPinClass); // This removes duplicate entries (ie. Set Static Mesh and Set Static Mesh (StaticMeshComponent)), // but also prevents displaying functions on BP components. Comment out for now. //CallOnMemberFilter.AddRejectionTest(FBlueprintActionFilter::FRejectionTestDelegate::CreateStatic(RejectAnyUnboundActions)); CallOnMemberFilter.AddRejectionTest(FBlueprintActionFilter::FRejectionTestDelegate::CreateStatic(RejectAnyNonFunctions)); ContextMenuBuilder.AddMenuSection(CallOnMemberFilter, FText::FromName(BoundObjectPinClass->GetFName()), 0); } { // Add all actions that are relevant to the sequence director BP itself FBlueprintActionFilter MenuFilter(FBlueprintActionFilter::BPFILTER_RejectGlobalFields | FBlueprintActionFilter::BPFILTER_RejectPermittedSubClasses); MenuFilter.PermittedNodeTypes.Add(UK2Node_CallFunction::StaticClass()); MenuFilter.Context.Blueprints.Add(Blueprint); MenuFilter.Context.Graphs.Append(Blueprint->UbergraphPages); MenuFilter.Context.Graphs.Append(Blueprint->FunctionGraphs); if (Blueprint->SkeletonGeneratedClass) { FBlueprintActionFilter::AddUnique(MenuFilter.TargetClasses, Blueprint->SkeletonGeneratedClass); } MenuFilter.AddRejectionTest(FBlueprintActionFilter::FRejectionTestDelegate::CreateStatic(RejectAnyNonFunctions)); ContextMenuBuilder.AddMenuSection(MenuFilter, LOCTEXT("SequenceDirectorMenu", "This Sequence"), 0); } ContextMenuBuilder.RebuildActionList(); OutAllActions.Append(ContextMenuBuilder); } void CollectAllRebindActions(FGraphActionListBuilderBase& OutAllActions, UBlueprint* Blueprint) { // Build up the context object auto RejectAnyForeignFunctions = [Blueprint](const FBlueprintActionFilter& Filter, FBlueprintActionInfo& BlueprintAction) { const UBlueprintFunctionNodeSpawner* FunctionNodeSpawner = Cast(BlueprintAction.NodeSpawner); const UFunction* FunctionToCall = FunctionNodeSpawner ? FunctionNodeSpawner->GetFunction() : nullptr; if (!FunctionToCall || FunctionToCall->HasAnyFunctionFlags(FUNC_BlueprintPure)) { return true; } UClass* OuterClass = CastChecked(FunctionToCall->GetOuter()); return OuterClass->ClassGeneratedBy != Blueprint; }; FBlueprintActionFilter MenuFilter(FBlueprintActionFilter::BPFILTER_RejectGlobalFields | FBlueprintActionFilter::BPFILTER_RejectPermittedSubClasses); MenuFilter.PermittedNodeTypes.Add(UK2Node_CallFunction::StaticClass()); MenuFilter.Context.Blueprints.Add(Blueprint); MenuFilter.AddRejectionTest(FBlueprintActionFilter::FRejectionTestDelegate::CreateLambda(RejectAnyForeignFunctions)); if (Blueprint->SkeletonGeneratedClass) { FBlueprintActionFilter::AddUnique(MenuFilter.TargetClasses, Blueprint->SkeletonGeneratedClass); } FBlueprintActionMenuBuilder ContextMenuBuilder(nullptr); ContextMenuBuilder.AddMenuSection(MenuFilter, LOCTEXT("SequenceDirectorMenu", "This Sequence"), 0); ContextMenuBuilder.RebuildActionList(); OutAllActions.Append(ContextMenuBuilder); } } TSharedRef FMovieSceneEventCustomization::MakeInstance() { return MakeShared(); } TSharedRef FMovieSceneEventCustomization::MakeInstance(UMovieSceneSection* InSection) { TSharedRef Custo = MakeShared(); Custo->WeakExternalSection = InSection; return Custo; } void FMovieSceneEventCustomization::GetEditObjects(TArray& OutObjects) const { UMovieSceneEventSectionBase* ExternalSection = Cast(WeakExternalSection.Get()); if (ExternalSection) { OutObjects.Add(ExternalSection); } else { PropertyHandle->GetOuterObjects(OutObjects); } } void FMovieSceneEventCustomization::CustomizeHeader(TSharedRef InPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) {} void FMovieSceneEventCustomization::CustomizeChildren(TSharedRef InPropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils) { PropertyHandle = InPropertyHandle; PropertyUtilities = CustomizationUtils.GetPropertyUtilities(); TArray RawData; PropertyHandle->AccessRawData(RawData); ChildBuilder.AddCustomRow(FText()) .NameContent() [ SNew(STextBlock) .Font(CustomizationUtils.GetRegularFont()) .Text(LOCTEXT("EventValueText", "Event")) ] .ValueContent() .MinDesiredWidth(200.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() [ SNew(SComboButton) .ButtonStyle(FAppStyle::Get(), "HoverHintOnly") .ForegroundColor(FSlateColor::UseForeground()) .OnGetMenuContent(this, &FMovieSceneEventCustomization::GetMenuContent) .CollapseMenuOnParentFocus(true) .ContentPadding(FMargin(4.f, 0.f)) .ButtonContent() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(FMargin(0.f, 0.f, 4.f, 0.f)) .VAlign(VAlign_Center) .AutoWidth() [ SNew(SImage) .Image(this, &FMovieSceneEventCustomization::GetEventIcon) ] + SHorizontalBox::Slot() .Padding(FMargin(0.f, 0.f, 4.f, 0.f)) .VAlign(VAlign_Center) [ SNew(STextBlock) .Font(CustomizationUtils.GetRegularFont()) .Text(this, &FMovieSceneEventCustomization::GetEventName) ] ] ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() [ PropertyCustomizationHelpers::MakeBrowseButton(FSimpleDelegate::CreateSP(this, &FMovieSceneEventCustomization::NavigateToDefinition), LOCTEXT("NavigateToDefinition_Tip", "Navigate to this event's definition")) ] ]; const bool bAnyBoundEvents = GetAllValidEndpoints().Num() != 0; if (bAnyBoundEvents) { FText CallInEditorText = LOCTEXT("CallInEditor_Label", "Call In Editor"); FText CallInEditorTooltip = LOCTEXT("CallInEditor_Tooltip", "When checked, this event will be triggered in the Editor outside of PIE.\n\nBEWARE: ANY CHANGES MADE AS A RESULT OF THIS EVENT BEING CALLED MAY END UP BEING SAVED IN THE CURRENT LEVEL OR ASSET."); ChildBuilder.AddCustomRow(CallInEditorText) .NameContent() [ SNew(STextBlock) .Text(CallInEditorText) .ToolTipText(CallInEditorTooltip) .Font(IDetailLayoutBuilder::GetDetailFont()) ] .ValueContent() [ SNew(SCheckBox) .ToolTipText(CallInEditorTooltip) .IsChecked(this, &FMovieSceneEventCustomization::GetCallInEditorCheckState) .OnCheckStateChanged(this, &FMovieSceneEventCustomization::OnSetCallInEditorCheckState) ]; } UK2Node* CommonEndpoint = GetCommonEndpoint(); if (!CommonEndpoint) { return; } TArray ValidBoundObjectPins; for (UEdGraphPin* Pin : CommonEndpoint->Pins) { if (Pin->Direction == EGPD_Output && (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Object || Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Interface)) { ValidBoundObjectPins.Add(Pin); } } if (ValidBoundObjectPins.Num() > 0) { FText BoundObjectPinText = LOCTEXT("BoundObjectPin_Label", "Pass Bound Object To"); FText BoundObjectPinTooltip = LOCTEXT("BoundObjectPin_Tooltip", "Specifies a pin to pass the bound object(s) through when the event is triggered. Interface and object pins are both supported."); ChildBuilder.AddCustomRow(BoundObjectPinText) .NameContent() [ SNew(STextBlock) .Text(BoundObjectPinText) .ToolTipText(BoundObjectPinTooltip) .Font(IDetailLayoutBuilder::GetDetailFont()) ] .ValueContent() [ SNew(SComboButton) .ButtonStyle(FAppStyle::Get(), "HoverHintOnly") .ForegroundColor(FSlateColor::UseForeground()) .ToolTipText(BoundObjectPinTooltip) .OnGetMenuContent(this, &FMovieSceneEventCustomization::GetBoundObjectPinMenuContent) .CollapseMenuOnParentFocus(true) .ContentPadding(FMargin(4.f, 0.f)) .ButtonContent() [ SNew(SBox) .HeightOverride(21.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(FMargin(0.f, 0.f, 4.f, 0.f)) .VAlign(VAlign_Center) .AutoWidth() [ SNew(SImage) .Image(this, &FMovieSceneEventCustomization::GetBoundObjectPinIcon) ] + SHorizontalBox::Slot() .Padding(FMargin(0.f, 0.f, 4.f, 0.f)) .VAlign(VAlign_Center) [ SNew(STextBlock) .Font(CustomizationUtils.GetRegularFont()) .Text(this, &FMovieSceneEventCustomization::GetBoundObjectPinText) ] ] ] ]; } UFunction* CommonFunction = nullptr; UBlueprint* Blueprint = CommonEndpoint->GetBlueprint(); if (Blueprint) { if (UK2Node_Event* Event = Cast(CommonEndpoint)) { CommonFunction = Blueprint->SkeletonGeneratedClass ? Blueprint->SkeletonGeneratedClass->FindFunctionByName(Event->GetFunctionName()) : nullptr; } else if (UK2Node_FunctionEntry* EndpointEntry = Cast(CommonEndpoint)) { CommonFunction = EndpointEntry->FindSignatureFunction(); } else { // @todo: Error not supported } Blueprint->OnCompiled().AddSP(this, &FMovieSceneEventCustomization::OnBlueprintCompiled); } if (CommonFunction) { IDetailCategoryBuilder& DetailCategoryBuilder = ChildBuilder.GetParentCategory().GetParentLayout().EditCategory("Payload", LOCTEXT("PayloadLabel", "Payload"), ECategoryPriority::Uncommon); bool bPayloadUpToDate = true; for (int32 Index = 0; Index < RawData.Num(); ++Index) { TSharedPtr StructData = MakeShared(CommonFunction); const FMovieSceneEvent* EntryPoint = static_cast(RawData[Index]); TArray> AllValidNames; for (FProperty* Field : TFieldRange(CommonFunction)) { if (Field->HasAnyPropertyFlags(CPF_OutParm | CPF_ReturnParm | CPF_ReferenceParm) || Field->GetFName() == EntryPoint->BoundObjectPinName) { continue; } const FMovieSceneEventPayloadVariable* PayloadVariable = EntryPoint->PayloadVariables.Find(Field->GetFName()); if (PayloadVariable) { AllValidNames.Add(Field->GetFName()); // We have an override for this variable const bool bImportSuccess = FBlueprintEditorUtils::PropertyValueFromString(Field, PayloadVariable->Value, StructData->GetStructMemory()); if (!bImportSuccess) { // @todo: error } } else if (UEdGraphPin* Pin = CommonEndpoint->FindPin(Field->GetFName(), EGPD_Output)) { if (!Pin->DefaultValue.IsEmpty()) { const bool bImportSuccess = FBlueprintEditorUtils::PropertyValueFromString(Field, Pin->DefaultValue, StructData->GetStructMemory()); if (!bImportSuccess) { // @todo: error } } } IDetailPropertyRow* ExternalRow = DetailCategoryBuilder.AddExternalStructureProperty(StructData.ToSharedRef(), Field->GetFName(), EPropertyLocation::Default, FAddPropertyParams().ForceShowProperty()); TSharedPtr LocalVariableProperty = ExternalRow->GetPropertyHandle(); FSimpleDelegate Delegate = FSimpleDelegate::CreateSP(this, &FMovieSceneEventCustomization::OnPayloadVariableChanged, StructData.ToSharedRef(), LocalVariableProperty); LocalVariableProperty->SetOnPropertyValueChanged(Delegate); LocalVariableProperty->SetOnChildPropertyValueChanged(Delegate); } bPayloadUpToDate &= (AllValidNames.Num() == EntryPoint->PayloadVariables.Num()); } if (!bPayloadUpToDate) { DetailCategoryBuilder.AddCustomRow(FText()) .WholeRowContent() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(FMargin(5.f, 0.f, 5.f, 0.f)) .AutoWidth() [ SNew(STextBlock) .TextStyle(FAppStyle::Get(), "Log.Warning") .Font(FAppStyle::GetFontStyle("FontAwesome.10")) .Text(FEditorFontGlyphs::Exclamation_Triangle) ] + SHorizontalBox::Slot() .Padding(FMargin(0.f, 0.f, 5.f, 0.f)) [ SNew(STextBlock) .TextStyle(FAppStyle::Get(), "Log.Warning") .Text(LOCTEXT("PayloadOutOfDateError", "Payload variables may be out-of-date. Please compile the blueprint.")) ] ]; } } } void FMovieSceneEventCustomization::OnPayloadVariableChanged(TSharedRef InStructData, TSharedPtr LocalVariableProperty) { // This function should only ever be bound if all the entry points call the same function FProperty* Property = LocalVariableProperty->GetProperty(); if (!Property) { return; } FScopedTransaction Transaction(LOCTEXT("SetPayloadValue", "Set payload value")); bool bChangedAnything = false; UBlueprint* Blueprint = nullptr; FString NewValueString; const bool bSuccessfulTextExport = FBlueprintEditorUtils::PropertyValueToString(Property, InStructData->GetStructMemory(), NewValueString, nullptr); if (!bSuccessfulTextExport) { // @todo: error return; } TArray EditObjects; GetEditObjects(EditObjects); TArray RawData; PropertyHandle->AccessRawData(RawData); if (!ensure(RawData.Num() == EditObjects.Num())) { return; } for (int32 Index = 0; Index < RawData.Num(); ++Index) { UMovieSceneEventSectionBase* EventSection = Cast(EditObjects[Index]); FMovieSceneEvent* EntryPoint = static_cast(RawData[Index]); if (!EventSection || !EntryPoint) { continue; } EventSection->Modify(); FMovieSceneEventPayloadVariable* PayloadVariable = EntryPoint->PayloadVariables.Find(Property->GetFName()); if (!PayloadVariable) { PayloadVariable = &EntryPoint->PayloadVariables.Add(Property->GetFName()); } PayloadVariable->Value = NewValueString; bChangedAnything = true; } if (bChangedAnything) { UBlueprint* BP = GetCommonEndpoint()->GetBlueprint(); FBlueprintEditorUtils::MarkBlueprintAsModified(BP); } else { Transaction.Cancel(); } } ECheckBoxState FMovieSceneEventCustomization::GetCallInEditorCheckState() const { ECheckBoxState CheckState = ECheckBoxState::Undetermined; for (UK2Node* Endpoint : GetAllValidEndpoints()) { bool bCallInEditor = false; if (UK2Node_CustomEvent* CustomEvent = Cast(Endpoint)) { bCallInEditor = CustomEvent->bCallInEditor; } else if (UK2Node_FunctionEntry* FunctionEntry = Cast(Endpoint)) { bCallInEditor = FunctionEntry->MetaData.bCallInEditor; } if (CheckState == ECheckBoxState::Undetermined) { CheckState = bCallInEditor ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } else if (bCallInEditor != (CheckState == ECheckBoxState::Checked) ) { return ECheckBoxState::Undetermined; } } return CheckState; } void FMovieSceneEventCustomization::OnSetCallInEditorCheckState(ECheckBoxState NewCheckedState) { FScopedTransaction Transaction(LOCTEXT("SetCallInEditor", "Set Call in Editor")); const bool bCallInEditor = (NewCheckedState == ECheckBoxState::Checked); TSet Blueprints; for (UK2Node* Endpoint : GetAllValidEndpoints()) { if (UK2Node_CustomEvent* CustomEvent = Cast(Endpoint)) { CustomEvent->Modify(); CustomEvent->bCallInEditor = bCallInEditor; Blueprints.Add(CustomEvent->GetBlueprint()); } else if (UK2Node_FunctionEntry* FunctionEntry = Cast(Endpoint)) { FunctionEntry->Modify(); FunctionEntry->MetaData.bCallInEditor = bCallInEditor; Blueprints.Add(FunctionEntry->GetBlueprint()); } } for (UBlueprint* Blueprint : Blueprints) { FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint); } } void FMovieSceneEventCustomization::OnBlueprintCompiled(UBlueprint*) { PropertyUtilities->ForceRefresh(); } UMovieSceneSequence* FMovieSceneEventCustomization::GetCommonSequence() const { TArray EditObjects; GetEditObjects(EditObjects); UMovieSceneSequence* CommonEventSequence = nullptr; for (UObject* Obj : EditObjects) { UMovieSceneSequence* ThisSequence = Obj ? Obj->GetTypedOuter() : nullptr; if (CommonEventSequence && CommonEventSequence != ThisSequence) { return nullptr; } CommonEventSequence = ThisSequence; } return CommonEventSequence; } UMovieSceneEventTrack* FMovieSceneEventCustomization::GetCommonTrack() const { TArray EditObjects; GetEditObjects(EditObjects); UMovieSceneEventTrack* CommonEventTrack = nullptr; for (UObject* Obj : EditObjects) { UMovieSceneEventTrack* ThisTrack = Obj ? Obj->GetTypedOuter() : nullptr; if (CommonEventTrack && CommonEventTrack != ThisTrack) { return nullptr; } CommonEventTrack = ThisTrack; } return CommonEventTrack; } void FMovieSceneEventCustomization::IterateEndpoints(TFunctionRef Callback) const { TArray EditObjects; GetEditObjects(EditObjects); TArray RawData; PropertyHandle->AccessRawData(RawData); if (!ensure(RawData.Num() == EditObjects.Num())) { return; } for (int32 Index = 0; Index < RawData.Num(); ++Index) { UMovieSceneEventSectionBase* EventSection = Cast(EditObjects[Index]); FMovieSceneEvent* EntryPoint = static_cast(RawData[Index]); if (!EventSection || !EntryPoint) { continue; } UMovieSceneSequence* Sequence = EventSection->GetTypedOuter(); FMovieSceneSequenceEditor* SequenceEditor = FMovieSceneSequenceEditor::Find(Sequence); if (!SequenceEditor) { continue; } UBlueprint* SequenceDirectorBP = SequenceEditor->FindDirectorBlueprint(Sequence); if (SequenceDirectorBP) { UK2Node* Endpoint = FMovieSceneEventUtils::FindEndpoint(EntryPoint, EventSection, SequenceDirectorBP); if (!Callback(Endpoint)) { return; } } } } TArray FMovieSceneEventCustomization::GetAllValidEndpoints() const { TArray Endpoints; IterateEndpoints( [&Endpoints](UK2Node* Endpoint) { if (Endpoint) { Endpoints.Add(Endpoint); } return true; } ); return Endpoints; } UK2Node* FMovieSceneEventCustomization::GetCommonEndpoint() const { UK2Node* CommonEndpoint = nullptr; IterateEndpoints( [&CommonEndpoint](UK2Node* Endpoint) { if (CommonEndpoint && Endpoint != CommonEndpoint) { CommonEndpoint = nullptr; return false; } CommonEndpoint = Endpoint; return true; } ); return CommonEndpoint; } TSharedRef FMovieSceneEventCustomization::GetMenuContent() { FMenuBuilder MenuBuilder(true, nullptr, nullptr, true); UMovieSceneSequence* Sequence = GetCommonSequence(); FMovieSceneSequenceEditor* SequenceEditor = FMovieSceneSequenceEditor::Find(Sequence); UBlueprint* DirectorBP = SequenceEditor ? SequenceEditor->FindDirectorBlueprint(Sequence) : nullptr; MenuBuilder.AddMenuEntry( LOCTEXT("CreateEventEndpoint_Text", "Create New Endpoint"), LOCTEXT("CreateEventEndpoint_Tooltip", "Creates a new event endpoint in this sequence's blueprint."), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Sequencer.CreateEventBinding"), FUIAction( FExecuteAction::CreateSP(this, &FMovieSceneEventCustomization::CreateEventEndpoint) ) ); const bool bAnyBoundEvents = GetAllValidEndpoints().Num() != 0; if (!bAnyBoundEvents && Sequence) { MenuBuilder.AddSubMenu( LOCTEXT("CreateQuickBinding_Text", "Quick Bind"), LOCTEXT("CreateQuickBinding_Tooltip", "Shows a list of functions on this object binding that can be bound directly to this event."), FNewMenuDelegate::CreateSP(this, &FMovieSceneEventCustomization::PopulateQuickBindSubMenu, Sequence), false /* bInOpenSubMenuOnClick */, FSlateIcon(FAppStyle::GetAppStyleSetName(), "Sequencer.CreateQuickBinding"), false /* bInShouldWindowAfterMenuSelection */ ); } else { MenuBuilder.AddSubMenu( LOCTEXT("Rebind_Text", "Rebind To"), LOCTEXT("Rebind_Text_Tooltip", "Rebinds this event to a different function call or event node."), FNewMenuDelegate::CreateSP(this, &FMovieSceneEventCustomization::PopulateRebindSubMenu, Sequence), false /* bInOpenSubMenuOnClick */, FSlateIcon(FAppStyle::GetAppStyleSetName(), "Sequencer.CreateQuickBinding"), false /* bInShouldCloseWindowAfterMenuSelection */ ); MenuBuilder.AddMenuEntry( LOCTEXT("ClearEventEndpoint_Text", "Clear"), LOCTEXT("ClearEventEndpoint_Tooltip", "Unbinds this event from its current binding."), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Sequencer.ClearEventBinding"), FUIAction( FExecuteAction::CreateSP(this, &FMovieSceneEventCustomization::ClearEventEndpoint) ) ); } return MenuBuilder.MakeWidget(); } TSharedRef FMovieSceneEventCustomization::GetBoundObjectPinMenuContent() { UK2Node* CommonEndpoint = GetCommonEndpoint(); if (CommonEndpoint) { bool bAnyValidPins = false; bool bBoundObjectPinIsSet = false; FMenuBuilder MenuBuilder(true, nullptr, nullptr, true); for (UEdGraphPin* Pin : CommonEndpoint->Pins) { if (Pin->Direction == EGPD_Output && (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Object || Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Interface)) { bAnyValidPins = true; FText PinText = FText::FromName(Pin->PinName); MenuBuilder.AddMenuEntry( PinText, FText::Format(LOCTEXT("SetBoundObjectPin_Tooltip", "When calling this event with a bound object, pass the object through pin {0}."), PinText), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &FMovieSceneEventCustomization::SetBoundObjectPinName, Pin->PinName), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &FMovieSceneEventCustomization::CompareBoundObjectPinName, Pin->PinName) ), NAME_None, EUserInterfaceActionType::RadioButton ); } } MenuBuilder.AddMenuEntry( LOCTEXT("ClearBoundObjectPin_Label", "Clear"), LOCTEXT("ClearBoundObjectPin_Tooltip", "Clears the reference to the bound object pin, meaning any bound objects will not be passed to the event"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &FMovieSceneEventCustomization::SetBoundObjectPinName, FName()) ) ); if (bAnyValidPins || bBoundObjectPinIsSet) { return MenuBuilder.MakeWidget(); } else { return SNew(STextBlock).Text(LOCTEXT("BoundObjectPinError_NoPins", "No compatible pins were found. Only Object and Interface pins can be bound to objects.")); } } return SNew(STextBlock).Text(LOCTEXT("BoundObjectPinError_MultipleEvents", "Cannot choose a bound object pin with multiple events selected")); } bool FMovieSceneEventCustomization::CompareBoundObjectPinName(FName InPinName) const { bool bMatch = false; auto Enumerator = [&bMatch, InPinName](void* Ptr, const int32, const int32) { bMatch = (static_cast(Ptr)->BoundObjectPinName == InPinName); return bMatch; }; PropertyHandle->EnumerateRawData(Enumerator); return bMatch; } void FMovieSceneEventCustomization::SetBoundObjectPinName(FName InNewBoundObjectPinName) { FScopedTransaction Transaction(LOCTEXT("SetBoundObjectPinTransaction", "Set Bound Object Pin")); TArray EditObjects; GetEditObjects(EditObjects); for (UObject* Object : EditObjects) { Object->Modify(); } auto Enumerator = [InNewBoundObjectPinName](void* Ptr, const int32, const int32) { static_cast(Ptr)->BoundObjectPinName = InNewBoundObjectPinName; return true; }; PropertyHandle->EnumerateRawData(Enumerator); // Ensure that anything listening for property changed notifications are notified of the new binding PropertyHandle->NotifyFinishedChangingProperties(); PropertyUtilities->ForceRefresh(); } const FSlateBrush* FMovieSceneEventCustomization::GetBoundObjectPinIcon() const { if (CompareBoundObjectPinName(NAME_None)) { return nullptr; } return FAppStyle::GetBrush("Graph.Pin.Disconnected_VarA"); } FText FMovieSceneEventCustomization::GetBoundObjectPinText() const { TArray RawData; PropertyHandle->AccessRawData(RawData); FName CommonPinName; for (void* Ptr : RawData) { FName PinName = static_cast(Ptr)->BoundObjectPinName; if (CommonPinName == NAME_None) { CommonPinName = PinName; } else if (CommonPinName != PinName) { return LOCTEXT("MultiplePinValues", "Multiple Values"); } } return FText::FromName(CommonPinName); } void FMovieSceneEventCustomization::PopulateQuickBindSubMenu(FMenuBuilder& MenuBuilder, UMovieSceneSequence* Sequence) { FMovieSceneSequenceEditor* SequenceEditor = FMovieSceneSequenceEditor::Find(Sequence); if (!SequenceEditor) { return; } UBlueprint* Blueprint = SequenceEditor->GetOrCreateDirectorBlueprint(Sequence); if (!Blueprint) { return; } UMovieScene* MovieScene = Sequence->GetMovieScene(); TOptional CommonObjectBindingID; { TArray EditObjects; GetEditObjects(EditObjects); for (UObject* Outer : EditObjects) { FGuid ThisBindingID; MovieScene->FindTrackBinding(*Outer->GetTypedOuter(), ThisBindingID); if (CommonObjectBindingID.IsSet() && CommonObjectBindingID != ThisBindingID) { CommonObjectBindingID.Reset(); break; } CommonObjectBindingID = ThisBindingID; } } FMovieSceneEventEndpointParameters EventParams = FMovieSceneEventEndpointParameters::Generate(MovieScene, CommonObjectBindingID.Get(FGuid())); TSharedRef ActionMenu = SNew(SGraphActionMenu) .OnCreateCustomRowExpander_Static([](const FCustomExpanderData& Data) -> TSharedRef { return SNew(SExpanderArrow, Data.TableRow); }) .OnCollectAllActions_Static(UE_MovieSceneEventCustomization::CollectQuickBindActions, Blueprint, EventParams.BoundObjectPinClass) .OnActionSelected(this, &FMovieSceneEventCustomization::HandleQuickBindActionSelected, Blueprint, EventParams); ActionMenu->RegisterActiveTimer(0.f, FWidgetActiveTimerDelegate::CreateLambda( [FilterTextBox = ActionMenu->GetFilterTextBox()](double, float) { FSlateApplication::Get().SetKeyboardFocus(FilterTextBox); return EActiveTimerReturnType::Stop; } )); MenuBuilder.AddWidget( SNew(SBox) .WidthOverride(300.f) .MaxDesiredHeight(500.f) [ ActionMenu ], FText() ); } void FMovieSceneEventCustomization::HandleQuickBindActionSelected(const TArray>& SelectedAction, ESelectInfo::Type InSelectionType, UBlueprint* Blueprint, FMovieSceneEventEndpointParameters Params) { if (InSelectionType != ESelectInfo::OnMouseClick && InSelectionType != ESelectInfo::OnKeyPress) { return; } for (TSharedPtr Action : SelectedAction) { if (Action->GetTypeId() == FBlueprintActionMenuItem::StaticGetTypeId()) { const UBlueprintFunctionNodeSpawner* FunctionNodeSpawner = Cast(static_cast(Action.Get())->GetRawAction()); const UFunction* FunctionToCall = FunctionNodeSpawner ? FunctionNodeSpawner->GetFunction() : nullptr; if (FunctionToCall) { UClass* OuterClass = CastChecked(FunctionToCall->GetOuter()); if (OuterClass->ClassGeneratedBy == Blueprint && Blueprint->SkeletonGeneratedClass) { // Attempt to locate a custom event or a function graph of this name on the blueprint for (UEdGraph* Graph : Blueprint->UbergraphPages) { for (UEdGraphNode* Node : Graph->Nodes) { UK2Node_CustomEvent* CustomEvent = Cast(Node); if (CustomEvent && Blueprint->SkeletonGeneratedClass->FindFunctionByName(CustomEvent->GetFunctionName()) == FunctionToCall) { // Use this custom event UEdGraphPin* BoundObjectPin = FMovieSceneEventUtils::FindBoundObjectPin(CustomEvent, Params.BoundObjectPinClass); this->SetEventEndpoint(CustomEvent, BoundObjectPin, CustomEvent, EAutoCreatePayload::Variables); return; } } } for (UEdGraph* Graph : Blueprint->FunctionGraphs) { if (Blueprint->SkeletonGeneratedClass->FindFunctionByName(Graph->GetFName()) != FunctionToCall) { continue; } // Use this function graph for the event endpoint for (UEdGraphNode* Node : Graph->Nodes) { UK2Node_FunctionEntry* FunctionEntry = Cast(Node); if (FunctionEntry) { UEdGraphPin* BoundObjectPin = FMovieSceneEventUtils::FindBoundObjectPin(FunctionEntry, Params.BoundObjectPinClass); this->SetEventEndpoint(FunctionEntry, BoundObjectPin, FunctionEntry, EAutoCreatePayload::Variables); return; } } } } } } if (Action) { UK2Node_CustomEvent* NewEventEndpoint = FMovieSceneEventUtils::CreateUserFacingEvent(Blueprint, Params); UEdGraphPin* ThenPin = NewEventEndpoint->FindPin(UEdGraphSchema_K2::PN_Then, EGPD_Output); UEdGraphPin* BoundObjectPin = FMovieSceneEventUtils::FindBoundObjectPin(NewEventEndpoint, Params.BoundObjectPinClass); FVector2D NodePosition(NewEventEndpoint->NodePosX + 400.f, NewEventEndpoint->NodePosY); UEdGraphNode* NewNode = Action->PerformAction(NewEventEndpoint->GetGraph(), BoundObjectPin ? BoundObjectPin : ThenPin, NodePosition); if (NewNode) { UEdGraphPin* ExecPin = NewNode->FindPin(UEdGraphSchema_K2::PN_Execute, EGPD_Input); if (ensure(ThenPin && ExecPin)) { ThenPin->MakeLinkTo(ExecPin); } } this->SetEventEndpoint(NewEventEndpoint, BoundObjectPin, Cast(NewNode), EAutoCreatePayload::Pins | EAutoCreatePayload::Variables); } } } void FMovieSceneEventCustomization::PopulateRebindSubMenu(FMenuBuilder& MenuBuilder, UMovieSceneSequence* Sequence) { FMovieSceneSequenceEditor* SequenceEditor = FMovieSceneSequenceEditor::Find(Sequence); if (!SequenceEditor) { return; } UBlueprint* Blueprint = SequenceEditor->GetOrCreateDirectorBlueprint(Sequence); if (!Blueprint) { return; } UMovieScene* MovieScene = Sequence->GetMovieScene(); TOptional CommonObjectBindingID; { TArray EditObjects; GetEditObjects(EditObjects); for (UObject* Outer : EditObjects) { FGuid ThisBindingID; MovieScene->FindTrackBinding(*Outer->GetTypedOuter(), ThisBindingID); if (CommonObjectBindingID.IsSet() && CommonObjectBindingID != ThisBindingID) { CommonObjectBindingID.Reset(); break; } CommonObjectBindingID = ThisBindingID; } } FMovieSceneEventEndpointParameters EventParams = FMovieSceneEventEndpointParameters::Generate(MovieScene, CommonObjectBindingID.Get(FGuid())); TSharedRef ActionMenu = SNew(SGraphActionMenu) .OnCreateCustomRowExpander_Static([](const FCustomExpanderData& Data) -> TSharedRef { return SNew(SExpanderArrow, Data.TableRow); }) .OnCollectAllActions_Static(UE_MovieSceneEventCustomization::CollectAllRebindActions, Blueprint) .OnActionSelected(this, &FMovieSceneEventCustomization::HandleRebindActionSelected, Blueprint, EventParams.BoundObjectPinClass); ActionMenu->RegisterActiveTimer(0.f, FWidgetActiveTimerDelegate::CreateLambda( [FilterTextBox = ActionMenu->GetFilterTextBox()](double, float) { FSlateApplication::Get().SetKeyboardFocus(FilterTextBox); return EActiveTimerReturnType::Stop; } )); MenuBuilder.AddWidget( SNew(SBox) .WidthOverride(300.f) .MaxDesiredHeight(500.f) [ ActionMenu ], FText() ); } void FMovieSceneEventCustomization::HandleRebindActionSelected(const TArray>& SelectedAction, ESelectInfo::Type InSelectionType, UBlueprint* Blueprint, UClass* BoundObjectPinClass) { if (InSelectionType != ESelectInfo::OnMouseClick && InSelectionType != ESelectInfo::OnKeyPress) { return; } if (!Blueprint->SkeletonGeneratedClass) { return; } for (TSharedPtr Action : SelectedAction) { if (Action->GetTypeId() != FBlueprintActionMenuItem::StaticGetTypeId()) { continue; } const UBlueprintFunctionNodeSpawner* FunctionNodeSpawner = Cast(static_cast(Action.Get())->GetRawAction()); const UFunction* FunctionToCall = FunctionNodeSpawner ? FunctionNodeSpawner->GetFunction() : nullptr; if (!FunctionToCall) { continue; } UClass* OuterClass = CastChecked(FunctionToCall->GetOuter()); if (OuterClass->ClassGeneratedBy != Blueprint) { continue; } // Attempt to locate a custom event or a function graph of this name on the blueprint for (UEdGraph* Graph : Blueprint->UbergraphPages) { for (UEdGraphNode* Node : Graph->Nodes) { UK2Node_CustomEvent* CustomEvent = Cast(Node); if (CustomEvent && Blueprint->SkeletonGeneratedClass->FindFunctionByName(CustomEvent->GetFunctionName()) == FunctionToCall) { // Use this custom event UEdGraphPin* BoundObjectPin = FMovieSceneEventUtils::FindBoundObjectPin(CustomEvent, BoundObjectPinClass); this->SetEventEndpoint(CustomEvent, BoundObjectPin, CustomEvent, EAutoCreatePayload::Variables); return; } } } for (UEdGraph* Graph : Blueprint->FunctionGraphs) { if (Blueprint->SkeletonGeneratedClass->FindFunctionByName(Graph->GetFName()) != FunctionToCall) { continue; } // Use this function graph for the event endpoint for (UEdGraphNode* Node : Graph->Nodes) { UK2Node_FunctionEntry* FunctionEntry = Cast(Node); if (FunctionEntry) { UEdGraphPin* BoundObjectPin = FMovieSceneEventUtils::FindBoundObjectPin(FunctionEntry, BoundObjectPinClass); this->SetEventEndpoint(FunctionEntry, BoundObjectPin, FunctionEntry, EAutoCreatePayload::Variables); return; } } } } ensureMsgf(false, TEXT("Unknown blueprint action type encountered for rebinding")); } const FSlateBrush* FMovieSceneEventCustomization::GetEventIcon() const { UK2Node* CommonEndpoint = GetCommonEndpoint(); if (CommonEndpoint) { FLinearColor Color; FSlateIcon EndpointIcon = CommonEndpoint->GetIconAndTint(Color); return EndpointIcon.GetIcon(); } else { TArray RawData; PropertyHandle->AccessRawData(RawData); if (RawData.Num() > 1) { return FAppStyle::GetBrush("Sequencer.MultipleEvents"); } } return FAppStyle::GetBrush("Sequencer.UnboundEvent"); } FText FMovieSceneEventCustomization::GetEventName() const { UEdGraphNode* CommonEndpoint = GetCommonEndpoint(); if (CommonEndpoint) { return CommonEndpoint->GetNodeTitle(ENodeTitleType::MenuTitle); } else { TArray RawData; PropertyHandle->AccessRawData(RawData); if (RawData.Num() != 1) { return LOCTEXT("MultipleValuesText", "Multiple Values"); } } return LOCTEXT("UnboundText", "Unbound"); } void FMovieSceneEventCustomization::SetEventEndpoint(UK2Node* NewEndpoint, UEdGraphPin* BoundObjectPin, UK2Node* PayloadTemplate, EAutoCreatePayload AutoCreatePayload) { FScopedTransaction Transaction(LOCTEXT("SetEventEndpoint", "Set Event Endpoint")); // Modify and assign the blueprint for outer sections TArray EditObjects; GetEditObjects(EditObjects); TArray AllBlueprints; // If we're assigning a new valid endpoint, it must reside within the same blueprint as everything we're assigning it to. // Anything else must be implemented as a call function node connected to a custom event node UBlueprint* Blueprint = NewEndpoint ? NewEndpoint->GetBlueprint() : nullptr; if (Blueprint) { for (UObject* Outer : EditObjects) { UMovieSceneSequence* Sequence = Outer->GetTypedOuter(); FMovieSceneSequenceEditor* SequenceEditor = FMovieSceneSequenceEditor::Find(Sequence); UBlueprint* SequenceDirectorBP = SequenceEditor ? SequenceEditor->GetOrCreateDirectorBlueprint(Sequence) : nullptr; if (!ensureAlwaysMsgf(SequenceDirectorBP == Blueprint, TEXT("Attempting to assign an event endpoint to an event with a different sequence director Blueprint."))) { Transaction.Cancel(); return; } } Blueprint->Modify(); } for (UObject* Outer : EditObjects) { UMovieSceneEventSectionBase* BaseEventSection = Cast(Outer); if (BaseEventSection) { BaseEventSection->Modify(); if (Blueprint) { FMovieSceneEventUtils::BindEventSectionToBlueprint(BaseEventSection, Blueprint); } } } TArray PayloadNames; if (PayloadTemplate && EnumHasAnyFlags(AutoCreatePayload, EAutoCreatePayload::Variables | EAutoCreatePayload::Pins)) { UK2Node_EditablePinBase* EditableNode = Cast(NewEndpoint); for (UEdGraphPin* PayloadPin : PayloadTemplate->Pins) { if (PayloadPin != BoundObjectPin && PayloadPin->Direction == EGPD_Input && PayloadPin->PinType.PinCategory != UEdGraphSchema_K2::PC_Exec && PayloadPin->LinkedTo.Num() == 0 && PayloadPin->PinName != UEdGraphSchema_K2::PN_Self) { // Make a payload variable for this pin if (EnumHasAnyFlags(AutoCreatePayload, EAutoCreatePayload::Variables)) { PayloadNames.Add(PayloadPin->PinName); } if (EditableNode && EnumHasAnyFlags(AutoCreatePayload, EAutoCreatePayload::Pins)) { UEdGraphPin* NewPin = EditableNode->CreateUserDefinedPin(PayloadPin->PinName, PayloadPin->PinType, EGPD_Output); if (PayloadTemplate != NewEndpoint && NewPin) { NewPin->MakeLinkTo(PayloadPin); } } } } } // Assign the endpoints to all events TArray RawData; PropertyHandle->AccessRawData(RawData); check(EditObjects.Num() == RawData.Num()); for (int32 Index = 0; Index < RawData.Num(); ++Index) { UMovieSceneEventSectionBase* EventSection = Cast(EditObjects[Index]); FMovieSceneEvent* EntryPoint = static_cast(RawData[Index]); if (EntryPoint) { FMovieSceneEventUtils::SetEndpoint(EntryPoint, EventSection, NewEndpoint, BoundObjectPin); for (FName PayloadVar : PayloadNames) { if (!EntryPoint->PayloadVariables.Contains(PayloadVar)) { EntryPoint->PayloadVariables.Add(PayloadVar); } } } } // Ensure that anything listening for property changed notifications are notified of the new binding PropertyHandle->NotifyFinishedChangingProperties(); // Compile the blueprint now that clients have had a chance to update underlying data (we do this after to ensure we are compiling the correct data if (Blueprint) { FKismetEditorUtilities::CompileBlueprint(Blueprint); } // Forcibly update the panel now that our endpoint has changed PropertyUtilities->ForceRefresh(); if (NewEndpoint) { FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(NewEndpoint, false); } } void FMovieSceneEventCustomization::CreateEventEndpoint() { struct FSectionData { TArray> EntryPoints; }; struct FTrackData { TSortedMap Sections; }; struct FSequenceData { TSortedMap Tracks; }; TSortedMap PerSequenceData; // Populate all the sequences represented by this customization { TArray RawData; PropertyHandle->AccessRawData(RawData); TArray EditObjects; GetEditObjects(EditObjects); check(RawData.Num() == EditObjects.Num()); for (int32 Index = 0; Index < RawData.Num(); ++Index) { UMovieSceneEventSectionBase* EventSection = Cast(EditObjects[Index]); FMovieSceneEvent* EntryPoint = static_cast(RawData[Index]); if (EventSection) { FSequenceData& SequenceData = PerSequenceData.FindOrAdd(EventSection->GetTypedOuter()); FTrackData& TrackData = SequenceData.Tracks.FindOrAdd(EventSection->GetTypedOuter()); FSectionData& SectionData = TrackData.Sections.FindOrAdd(EventSection); SectionData.EntryPoints.Add(EntryPoint); } } } UK2Node_CustomEvent* NewEventEndpoint = nullptr; FScopedTransaction Transaction(LOCTEXT("CreateEventEndpoint", "Create Event Endpoint")); for (const TPair& SequencePair : PerSequenceData) { FMovieSceneSequenceEditor* SequenceEditor = FMovieSceneSequenceEditor::Find(SequencePair.Key); if (!SequenceEditor) { continue; } UBlueprint* SequenceDirectorBP = SequenceEditor->GetOrCreateDirectorBlueprint(SequencePair.Key); if (!SequenceDirectorBP) { continue; } UMovieScene* MovieScene = SequencePair.Key->GetMovieScene(); TOptional CommonObjectBindingID; for (const TPair& TrackPair : SequencePair.Value.Tracks) { FGuid ThisBindingID; MovieScene->FindTrackBinding(*TrackPair.Key, ThisBindingID); if (CommonObjectBindingID.IsSet() && CommonObjectBindingID != ThisBindingID) { CommonObjectBindingID.Reset(); break; } CommonObjectBindingID = ThisBindingID; } FMovieSceneEventEndpointParameters Parameters = FMovieSceneEventEndpointParameters::Generate(MovieScene, CommonObjectBindingID.Get(FGuid())); SequenceDirectorBP->Modify(); NewEventEndpoint = FMovieSceneEventUtils::CreateUserFacingEvent(SequenceDirectorBP, Parameters); if (!NewEventEndpoint) { continue; } UEdGraphPin* BoundObjectPin = FMovieSceneEventUtils::FindBoundObjectPin(NewEventEndpoint, Parameters.BoundObjectPinClass); for (const TPair& TrackPair : SequencePair.Value.Tracks) { for (const TPair& SectionPair : TrackPair.Value.Sections) { SectionPair.Key->Modify(); FMovieSceneEventUtils::BindEventSectionToBlueprint(SectionPair.Key, SequenceDirectorBP); for (FMovieSceneEvent* EntryPoint : SectionPair.Value.EntryPoints) { FMovieSceneEventUtils::SetEndpoint(EntryPoint, SectionPair.Key, NewEventEndpoint, BoundObjectPin); } } } FBlueprintEditorUtils::MarkBlueprintAsModified(SequenceDirectorBP); } // Ensure that anything listening for property changed notifications are notified of the new binding PropertyHandle->NotifyFinishedChangingProperties(); PropertyUtilities->ForceRefresh(); if (NewEventEndpoint) { FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(NewEventEndpoint, false); } } void FMovieSceneEventCustomization::ClearEventEndpoint() { SetEventEndpoint(nullptr, nullptr, nullptr, EAutoCreatePayload::None); } void FMovieSceneEventCustomization::NavigateToDefinition() { UEdGraphNode* CommonEndpoint = GetCommonEndpoint(); if (CommonEndpoint) { FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(CommonEndpoint, false); } } #undef LOCTEXT_NAMESPACE