// Copyright Epic Games, Inc. All Rights Reserved. #include "SGraphEditorImpl.h" #include "Containers/EnumAsByte.h" #include "Containers/Map.h" #include "Containers/UnrealString.h" #include "EdGraph/EdGraph.h" #include "Editor.h" #include "Editor/EditorEngine.h" #include "MaterialGraph/MaterialGraphNode_Root.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Commands/UIAction.h" #include "Framework/Commands/UICommandInfo.h" #include "Framework/Commands/UICommandList.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/MultiBox/MultiBoxExtender.h" #include "GenericPlatform/GenericApplication.h" #include "GraphEditAction.h" #include "GraphEditorActions.h" #include "GraphEditorModule.h" #include "GraphEditorSettings.h" #include "Input/Events.h" #include "InputCoreTypes.h" #include "Internationalization/Internationalization.h" #include "Layout/Children.h" #include "Layout/Margin.h" #include "Layout/SlateRect.h" #include "Misc/AssertionMacros.h" #include "Misc/CString.h" #include "Modules/ModuleManager.h" #include "SGraphEditorActionMenu.h" #include "SGraphPanel.h" #include "SNodePanel.h" #include "ScopedTransaction.h" #include "SlateGlobals.h" #include "SlotBase.h" #include "Styling/AppStyle.h" #include "Styling/CoreStyle.h" #include "Styling/ISlateStyle.h" #include "Textures/SlateIcon.h" #include "ToolMenu.h" #include "ToolMenuContext.h" #include "ToolMenuDelegates.h" #include "ToolMenuEntry.h" #include "ToolMenuMisc.h" #include "ToolMenuSection.h" #include "ToolMenus.h" #include "Toolkits/AssetEditorToolkit.h" #include "Toolkits/AssetEditorToolkitMenuContext.h" #include "UObject/Class.h" #include "UObject/ObjectPtr.h" #include "UObject/UObjectGlobals.h" #include "UObject/UnrealNames.h" #include "UObject/UnrealType.h" #include "Widgets/Input/SEditableTextBox.h" #include "Widgets/Input/SMultiLineEditableTextBox.h" #include "Widgets/Input/SButton.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Notifications/SNotificationList.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SCompoundWidget.h" #include "Widgets/SNullWidget.h" #include "Widgets/SWidget.h" #include "Widgets/Text/STextBlock.h" #include "HAL/IConsoleManager.h" #include "RenderUtils.h" #include "Materials/Material.h" class FActiveTimerHandle; struct FGeometry; struct FSlateBrush; #define LOCTEXT_NAMESPACE "GraphEditorModule" FVector2f GetNodeSize(const SGraphEditor& GraphEditor, const UEdGraphNode* Node) { FSlateRect Rect; if (GraphEditor.GetBoundsForNode(Node, Rect, 0.f)) { return Rect.GetSize(); } return Node->GetSize(); } ///////////////////////////////////////////////////// // FAlignmentHelper FAlignmentData FAlignmentHelper::GetAlignmentDataForNode(UEdGraphNode* Node) { float PropertyOffset = 0.f; const float NodeSize = Orientation == Orient_Horizontal ? GetNodeSize(*GraphEditor, Node).X : GetNodeSize(*GraphEditor, Node).Y; switch (AlignType) { case EAlignType::Minimum: PropertyOffset = 0.f; break; case EAlignType::Middle: PropertyOffset = NodeSize * .5f; break; case EAlignType::Maximum: PropertyOffset = NodeSize; break; } int32* Property = Orientation == Orient_Horizontal ? &Node->NodePosX : &Node->NodePosY; return FAlignmentData(Node, *Property, PropertyOffset); } ///////////////////////////////////////////////////// // SGraphEditorImpl UE::Slate::FDeprecateVector2DResult SGraphEditorImpl::GetPasteLocation2f() const { return GraphPanel->GetPastePosition(); } bool SGraphEditorImpl::IsNodeTitleVisible( const UEdGraphNode* Node, bool bEnsureVisible ) { return GraphPanel->IsNodeTitleVisible(Node, bEnsureVisible); } void SGraphEditorImpl::JumpToNode( const UEdGraphNode* JumpToMe, bool bRequestRename, bool bSelectNode ) { GraphPanel->JumpToNode(JumpToMe, bRequestRename, bSelectNode); FocusLockedEditorHere(); } void SGraphEditorImpl::JumpToPin( const UEdGraphPin* JumpToMe ) { GraphPanel->JumpToPin(JumpToMe); FocusLockedEditorHere(); } bool SGraphEditorImpl::SupportsKeyboardFocus() const { return true; } FReply SGraphEditorImpl::OnFocusReceived( const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent ) { OnFocused.ExecuteIfBound(SharedThis(this)); return FReply::Handled(); } FReply SGraphEditorImpl::OnMouseButtonDown( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent ) { if(MouseEvent.IsMouseButtonDown(EKeys::ThumbMouseButton)) { OnNavigateHistoryBack.ExecuteIfBound(); } else if(MouseEvent.IsMouseButtonDown(EKeys::ThumbMouseButton2)) { OnNavigateHistoryForward.ExecuteIfBound(); } return FReply::Handled().SetUserFocus(SharedThis(this), EFocusCause::Mouse); } FReply SGraphEditorImpl::OnMouseMove( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent ) { if (!MouseEvent.GetCursorDelta().IsZero()) { NumNodesAddedSinceLastPointerPosition = 0; } return SGraphEditor::OnMouseMove(MyGeometry, MouseEvent); } FReply SGraphEditorImpl::OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent ) { int32 NumNodes = GetCurrentGraph()->Nodes.Num(); if (Commands->ProcessCommandBindings( InKeyEvent ) ) { bool bPasteOperation = InKeyEvent.IsControlDown() && InKeyEvent.GetKey() == EKeys::V; if( !bPasteOperation && GetCurrentGraph()->Nodes.Num() > NumNodes ) { OnNodeSpawnedByKeymap.ExecuteIfBound(); } return FReply::Handled(); } else { return SCompoundWidget::OnKeyDown(MyGeometry, InKeyEvent); } } void SGraphEditorImpl::NotifyGraphChanged() { GetCurrentGraph()->NotifyGraphChanged(); } void SGraphEditorImpl::OnGraphChanged(const FEdGraphEditAction& InAction) { const bool bWasAddAction = (InAction.Action & GRAPHACTION_AddNode) != 0; if (bWasAddAction) { NumNodesAddedSinceLastPointerPosition++; } } void SGraphEditorImpl::GetPinContextMenuActionsForSchema(UToolMenu* InMenu) const { FToolMenuSection& Section = InMenu->FindOrAddSection("EdGraphSchemaPinActions"); Section.InitSection("EdGraphSchemaPinActions", LOCTEXT("PinActionsMenuHeader", "Pin Actions"), FToolMenuInsert()); // Select Output/Input Nodes { FToolUIAction SelectConnectedNodesAction; SelectConnectedNodesAction.ExecuteAction = FToolMenuExecuteAction::CreateSP(this, &SGraphEditorImpl::ExecuteSelectConnectedNodesFromPin); SelectConnectedNodesAction.IsActionVisibleDelegate = FToolMenuIsActionButtonVisible::CreateSP(this, &SGraphEditorImpl::IsSelectConnectedNodesFromPinVisible, EEdGraphPinDirection::EGPD_Output); TSharedPtr SelectAllOutputNodesCommand = FGraphEditorCommands::Get().SelectAllOutputNodes; Section.AddMenuEntry( SelectAllOutputNodesCommand->GetCommandName(), SelectAllOutputNodesCommand->GetLabel(), SelectAllOutputNodesCommand->GetDescription(), SelectAllOutputNodesCommand->GetIcon(), SelectConnectedNodesAction ); SelectConnectedNodesAction.IsActionVisibleDelegate = FToolMenuIsActionButtonVisible::CreateSP(this, &SGraphEditorImpl::IsSelectConnectedNodesFromPinVisible, EEdGraphPinDirection::EGPD_Input); TSharedPtr SelectAllInputNodesCommand = FGraphEditorCommands::Get().SelectAllInputNodes; Section.AddMenuEntry( SelectAllInputNodesCommand->GetCommandName(), SelectAllInputNodesCommand->GetLabel(), SelectAllInputNodesCommand->GetDescription(), SelectAllInputNodesCommand->GetIcon(), SelectConnectedNodesAction ); } auto GetMenuEntryForPin = [](const UEdGraphPin* TargetPin, const FToolUIAction& Action, const FText& SingleDescFormat, const FText& MultiDescFormat, TMap< FString, uint32 >& LinkTitleCount) { FText Title = TargetPin->GetOwningNode()->GetNodeTitle(ENodeTitleType::ListView); const FText PinDisplayName = TargetPin->GetDisplayName(); if (!PinDisplayName.IsEmpty()) { // Add name of connection if possible FFormatNamedArguments Args; Args.Add(TEXT("NodeTitle"), Title); Args.Add(TEXT("PinName"), PinDisplayName); Title = FText::Format(LOCTEXT("JumpToDescPin", "{NodeTitle} ({PinName})"), Args); } uint32& Count = LinkTitleCount.FindOrAdd(Title.ToString()); FText Description; FFormatNamedArguments Args; Args.Add(TEXT("NodeTitle"), Title); Args.Add(TEXT("NumberOfNodes"), Count); if (Count == 0) { Description = FText::Format(SingleDescFormat, Args); } else { Description = FText::Format(MultiDescFormat, Args); } ++Count; return FToolMenuEntry::InitMenuEntry( NAME_None, Description, Description, FSlateIcon(), Action); }; // Break all pin links { FToolUIAction BreakPinLinksAction; BreakPinLinksAction.ExecuteAction = FToolMenuExecuteAction::CreateSP(this, &SGraphEditorImpl::ExecuteBreakPinLinks); BreakPinLinksAction.IsActionVisibleDelegate = FToolMenuIsActionButtonVisible::CreateSP(this, &SGraphEditorImpl::IsBreakPinLinksVisible); TSharedPtr BreakPinLinksCommand = FGraphEditorCommands::Get().BreakPinLinks; Section.AddMenuEntry( BreakPinLinksCommand->GetCommandName(), BreakPinLinksCommand->GetLabel(), BreakPinLinksCommand->GetDescription(), BreakPinLinksCommand->GetIcon(), BreakPinLinksAction ); } // Break Specific Links { FToolUIAction BreakLinksMenuVisibility; BreakLinksMenuVisibility.IsActionVisibleDelegate = FToolMenuIsActionButtonVisible::CreateSP(this, &SGraphEditorImpl::IsBreakPinLinksVisible); Section.AddSubMenu("BreakLinkTo", LOCTEXT("BreakLinkTo", "Break Link To..."), LOCTEXT("BreakSpecificLinks", "Break a specific link..."), FNewToolMenuDelegate::CreateLambda([this, GetMenuEntryForPin](UToolMenu* InToolMenu) { UGraphNodeContextMenuContext* NodeContext = InToolMenu->FindContext(); if (NodeContext && NodeContext->Pin) { FText SingleDescFormat = LOCTEXT("BreakDesc", "Break Link to {NodeTitle}"); FText MultiDescFormat = LOCTEXT("BreakDescMulti", "Break Link to {NodeTitle} ({NumberOfNodes})"); TMap< FString, uint32 > LinkTitleCount; for (UEdGraphPin* TargetPin : NodeContext->Pin->LinkedTo) { FToolUIAction BreakSpecificLinkAction; BreakSpecificLinkAction.ExecuteAction = FToolMenuExecuteAction::CreateLambda([this, TargetPin](const FToolMenuContext& InMenuContext) { UGraphNodeContextMenuContext* NodeContext = InMenuContext.FindContext(); check(NodeContext && NodeContext->Pin); NodeContext->Pin->GetSchema()->BreakSinglePinLink(const_cast(NodeContext->Pin), TargetPin); }); InToolMenu->AddMenuEntry(NAME_None, GetMenuEntryForPin(TargetPin, BreakSpecificLinkAction, SingleDescFormat, MultiDescFormat, LinkTitleCount)); } } }), BreakLinksMenuVisibility, EUserInterfaceActionType::Button); // Break This Link { FToolUIAction BreakThisLinkAction; BreakThisLinkAction.ExecuteAction = FToolMenuExecuteAction::CreateSP(this, &SGraphEditorImpl::ExecuteBreakPinLinks); BreakThisLinkAction.IsActionVisibleDelegate = FToolMenuIsActionButtonVisible::CreateSP(this, &SGraphEditorImpl::IsBreakThisLinkVisible); TSharedPtr BreakThisLinkCommand = FGraphEditorCommands::Get().BreakThisLink; Section.AddMenuEntry( BreakThisLinkCommand->GetCommandName(), BreakThisLinkCommand->GetLabel(), BreakThisLinkCommand->GetDescription(), BreakThisLinkCommand->GetIcon(), BreakThisLinkAction ); } } FToolUIAction PinActionSubMenuVisibiliity; PinActionSubMenuVisibiliity.IsActionVisibleDelegate = FToolMenuIsActionButtonVisible::CreateSP(this, &SGraphEditorImpl::HasAnyLinkedPins); // Jump to specific connections { Section.AddSubMenu("JumpToConnection", LOCTEXT("JumpToConnection", "Jump to Connection..."), LOCTEXT("JumpToSpecificConnection", "Jump to specific connection..."), FNewToolMenuDelegate::CreateLambda([this, GetMenuEntryForPin](UToolMenu* InToolMenu) { UGraphNodeContextMenuContext* NodeContext = InToolMenu->FindContext(); check(NodeContext&& NodeContext->Pin); const SGraphEditor* ThisAsGraphEditor = this; FText SingleDescFormat = LOCTEXT("JumpDesc", "Jump to {NodeTitle}"); FText MultiDescFormat = LOCTEXT("JumpDescMulti", "Jump to {NodeTitle} ({NumberOfNodes})"); TMap< FString, uint32 > LinkTitleCount; for (UEdGraphPin* TargetPin : NodeContext->Pin->LinkedTo) { FToolUIAction JumpToConnectionAction; JumpToConnectionAction.ExecuteAction = FToolMenuExecuteAction::CreateLambda([ThisAsGraphEditor, TargetPin](const FToolMenuContext& InMenuContext) { const_cast(ThisAsGraphEditor)->JumpToPin(TargetPin); }); InToolMenu->AddMenuEntry(NAME_None, GetMenuEntryForPin(TargetPin, JumpToConnectionAction, SingleDescFormat, MultiDescFormat, LinkTitleCount)); } }), PinActionSubMenuVisibiliity, EUserInterfaceActionType::Button); } // Straighten Connections menu items { Section.AddSubMenu("StraightenPinConnection", LOCTEXT("StraightenConnection", "Straighten Connection..."), LOCTEXT("StraightenSpecificConnection", "Straighten specific connection..."), FNewToolMenuDelegate::CreateLambda([this, GetMenuEntryForPin](UToolMenu* InToolMenu) { const UGraphNodeContextMenuContext* NodeContext = InToolMenu->FindContext(); // Add straighten all connected pins { FToolUIAction StraightenConnectionsAction; StraightenConnectionsAction.ExecuteAction = FToolMenuExecuteAction::CreateLambda([this](const FToolMenuContext& Context) { UGraphNodeContextMenuContext* NodeContext = Context.FindContext(); StraightenConnections(const_cast(NodeContext->Pin), nullptr); }); StraightenConnectionsAction.IsActionVisibleDelegate = FToolMenuIsActionButtonVisible::CreateSP(this, &SGraphEditorImpl::HasAnyLinkedPins); InToolMenu->AddMenuEntry( NAME_None, FToolMenuEntry::InitMenuEntry(NAME_None, LOCTEXT("StraightenAllConnections", "Straighten All Pin Connections"), LOCTEXT("StraightenAllConnectionsTooltip", "Straightens all connected pins"), FGraphEditorCommands::Get().StraightenConnections->GetIcon(), StraightenConnectionsAction) ); } check(NodeContext && NodeContext->Pin); // Add individual pin connections FText SingleDescFormat = LOCTEXT("StraightenDesc", "Straighten Connection to {NodeTitle}"); FText MultiDescFormat = LOCTEXT("StraightenDescMulti", "Straighten Connection to {NodeTitle} ({NumberOfNodes})"); TMap< FString, uint32 > LinkTitleCount; for (UEdGraphPin* TargetPin : NodeContext->Pin->LinkedTo) { FToolUIAction StraightenConnectionAction; StraightenConnectionAction.ExecuteAction = FToolMenuExecuteAction::CreateLambda([this, TargetPin](const FToolMenuContext& InMenuContext) { UGraphNodeContextMenuContext* NodeContext = InMenuContext.FindContext(); if (NodeContext && NodeContext->Pin) { this->StraightenConnections(const_cast(NodeContext->Pin), const_cast(TargetPin)); } }); InToolMenu->AddMenuEntry(NAME_None, GetMenuEntryForPin(TargetPin, StraightenConnectionAction, SingleDescFormat, MultiDescFormat, LinkTitleCount)); } }), PinActionSubMenuVisibiliity, EUserInterfaceActionType::Button); } // Add any additional menu options from the asset toolkit that owns this graph editor UAssetEditorToolkitMenuContext* AssetToolkitContext = InMenu->FindContext(); if (AssetToolkitContext && AssetToolkitContext->Toolkit.IsValid()) { AssetToolkitContext->Toolkit.Pin()->AddGraphEditorPinActionsToContextMenu(Section); } } void SGraphEditorImpl::ExecuteBreakPinLinks(const FToolMenuContext& InContext) const { UGraphNodeContextMenuContext* NodeContext = InContext.FindContext(); if (NodeContext && NodeContext->Pin) { const UEdGraphSchema* Schema = NodeContext->Pin->GetSchema(); Schema->BreakPinLinks(const_cast(*(NodeContext->Pin)), true); } } bool SGraphEditorImpl::IsBreakPinLinksVisible(const FToolMenuContext& InContext) const { UGraphNodeContextMenuContext* NodeContext = InContext.FindContext(); if (NodeContext && NodeContext->Pin) { return !NodeContext->bIsDebugging && (NodeContext->Pin->LinkedTo.Num() > 1); } return false; } bool SGraphEditorImpl::IsBreakThisLinkVisible(const FToolMenuContext& InContext) const { UGraphNodeContextMenuContext* NodeContext = InContext.FindContext(); if (NodeContext && NodeContext->Pin) { return !NodeContext->bIsDebugging && (NodeContext->Pin->LinkedTo.Num() == 1); } return false; } bool SGraphEditorImpl::HasAnyLinkedPins(const FToolMenuContext& InContext) const { UGraphNodeContextMenuContext* NodeContext = InContext.FindContext(); if (NodeContext && NodeContext->Pin) { return (NodeContext->Pin->LinkedTo.Num() > 0); } return false; } void SGraphEditorImpl::ExecuteSelectConnectedNodesFromPin(const FToolMenuContext& InContext) const { UGraphNodeContextMenuContext* NodeContext = InContext.FindContext(); if (NodeContext && NodeContext->Pin) { SelectAllNodesInDirection(NodeContext->Pin); } } void SGraphEditorImpl::SelectAllNodesInDirection(const UEdGraphPin* InGraphPin) const { /** Traverses the node graph out from the specified pin, logging each node that it visits along the way. */ struct FDirectionalNodeVisitor { FDirectionalNodeVisitor(const UEdGraphPin* StartingPin, EEdGraphPinDirection TargetDirection) : Direction(TargetDirection) { TraversePin(StartingPin); } /** If the pin is the right direction, visits each of its attached nodes */ void TraversePin(const UEdGraphPin* Pin) { if (Pin->Direction == Direction) { for (UEdGraphPin* LinkedPin : Pin->LinkedTo) { VisitNode(LinkedPin->GetOwningNode()); } } } /** If the node has already been visited, does nothing. Otherwise it traverses each of its pins. */ void VisitNode(const UEdGraphNode* Node) { bool bAlreadyVisited = false; VisitedNodes.Add(Node, &bAlreadyVisited); if (!bAlreadyVisited) { for (UEdGraphPin* Pin : Node->Pins) { TraversePin(Pin); } } } EEdGraphPinDirection Direction; TSet VisitedNodes; }; FDirectionalNodeVisitor NodeVisitor(InGraphPin, InGraphPin->Direction); for (const UEdGraphNode* Node : NodeVisitor.VisitedNodes) { const_cast(this)->SetNodeSelection(const_cast(Node), true); } } bool SGraphEditorImpl::IsSelectConnectedNodesFromPinVisible(const FToolMenuContext& InContext, EEdGraphPinDirection DirectionToSelect) const { UGraphNodeContextMenuContext* NodeContext = InContext.FindContext(); if (NodeContext && NodeContext->Pin) { return (NodeContext->Pin->Direction == DirectionToSelect) && NodeContext->Pin->HasAnyConnections(); } return false; } //void SGraphEditorImpl::GraphEd_OnPanelUpdated() //{ // //} const TSet& SGraphEditorImpl::GetSelectedNodes() const { return GraphPanel->SelectionManager.GetSelectedNodes(); } void SGraphEditorImpl::ClearSelectionSet() { GraphPanel->SelectionManager.ClearSelectionSet(); } void SGraphEditorImpl::SetNodeSelection(UEdGraphNode* Node, bool bSelect) { GraphPanel->SelectionManager.SetNodeSelection(Node, bSelect); } void SGraphEditorImpl::SelectAllNodes() { FGraphPanelSelectionSet NewSet; for (int32 NodeIndex = 0; NodeIndex < EdGraphObj->Nodes.Num(); ++NodeIndex) { UEdGraphNode* Node = EdGraphObj->Nodes[NodeIndex]; if (Node) { ensureMsgf(Node->IsValidLowLevel(), TEXT("Node is invalid")); NewSet.Add(Node); } } GraphPanel->SelectionManager.SetSelectionSet(NewSet); } UEdGraphPin* SGraphEditorImpl::GetGraphPinForMenu() { return GraphPinForMenu.Get(); } UEdGraphNode* SGraphEditorImpl::GetGraphNodeForMenu() { return GraphNodeForMenu.Get(); } void SGraphEditorImpl::ZoomToFit(bool bOnlySelection) { GraphPanel->ZoomToFit(bOnlySelection); } bool SGraphEditorImpl::GetBoundsForSelectedNodes( class FSlateRect& Rect, float Padding ) { return GraphPanel->GetBoundsForSelectedNodes(Rect, Padding); } bool SGraphEditorImpl::GetBoundsForNode( const UEdGraphNode* InNode, class FSlateRect& Rect, float Padding) const { FVector2f TopLeft, BottomRight; if (GraphPanel->GetBoundsForNode(InNode, TopLeft, BottomRight, Padding)) { Rect.Left = TopLeft.X; Rect.Top = TopLeft.Y; Rect.Bottom = BottomRight.Y; Rect.Right = BottomRight.X; return true; } return false; } void SGraphEditorImpl::StraightenConnections() { const FScopedTransaction Transaction(FGraphEditorCommands::Get().StraightenConnections->GetLabel()); GraphPanel->StraightenConnections(); } void SGraphEditorImpl::StraightenConnections(UEdGraphPin* SourcePin, UEdGraphPin* PinToAlign) const { const FScopedTransaction Transaction(FGraphEditorCommands::Get().StraightenConnections->GetLabel()); GraphPanel->StraightenConnections(SourcePin, PinToAlign); } void SGraphEditorImpl::RefreshNode(UEdGraphNode& Node) { GraphPanel->RefreshNode(Node); } void SGraphEditorImpl::Construct( const FArguments& InArgs ) { Commands = MakeShareable( new FUICommandList() ); IsEditable = InArgs._IsEditable; DisplayAsReadOnly = InArgs._DisplayAsReadOnly; Appearance = InArgs._Appearance; TitleBar = InArgs._TitleBar; bAutoExpandActionMenu = InArgs._AutoExpandActionMenu; ShowGraphStateOverlay = InArgs._ShowGraphStateOverlay; OnNavigateHistoryBack = InArgs._OnNavigateHistoryBack; OnNavigateHistoryForward = InArgs._OnNavigateHistoryForward; OnNodeSpawnedByKeymap = InArgs._GraphEvents.OnNodeSpawnedByKeymap; NumNodesAddedSinceLastPointerPosition = 0; // Make sure that the editor knows about what kinds // of commands GraphEditor can do. FGraphEditorCommands::Register(); // Tell GraphEditor how to handle all the known commands { Commands->MapAction( FGraphEditorCommands::Get().ReconstructNodes, FExecuteAction::CreateSP( this, &SGraphEditorImpl::ReconstructNodes ), FCanExecuteAction::CreateSP( this, &SGraphEditorImpl::CanReconstructNodes ) ); Commands->MapAction( FGraphEditorCommands::Get().BreakNodeLinks, FExecuteAction::CreateSP( this, &SGraphEditorImpl::BreakNodeLinks ), FCanExecuteAction::CreateSP( this, &SGraphEditorImpl::CanBreakNodeLinks ) ); Commands->MapAction(FGraphEditorCommands::Get().SummonCreateNodeMenu, FExecuteAction::CreateSP(this, &SGraphEditorImpl::SummonCreateNodeMenu), FCanExecuteAction::CreateSP(this, &SGraphEditorImpl::CanSummonCreateNodeMenu) ); Commands->MapAction(FGraphEditorCommands::Get().StraightenConnections, FExecuteAction::CreateSP(this, &SGraphEditorImpl::StraightenConnections) ); // Append any additional commands that a consumer of GraphEditor wants us to be aware of. const TSharedPtr& AdditionalCommands = InArgs._AdditionalCommands; if ( AdditionalCommands.IsValid() ) { Commands->Append( AdditionalCommands.ToSharedRef() ); } } bResetMenuContext = false; GraphPinForMenu.SetPin(nullptr); EdGraphObj = InArgs._GraphToEdit; OnFocused = InArgs._GraphEvents.OnFocused; OnCreateActionMenuAtLocation = InArgs._GraphEvents.OnCreateActionMenuAtLocation; OnCreateNodeOrPinMenu = InArgs._GraphEvents.OnCreateNodeOrPinMenu; struct Local { static FText GetCornerText(TAttribute Appearance, FText DefaultText) { FText OverrideText = Appearance.Get().CornerText; return !OverrideText.IsEmpty() ? OverrideText : DefaultText; } static FText GetPIENotifyText(TAttribute Appearance, FText DefaultText) { FText OverrideText = Appearance.Get().PIENotifyText; return !OverrideText.IsEmpty() ? OverrideText : DefaultText; } static FText GetReadOnlyText(TAttributeAppearance, FText DefaultText) { FText OverrideText = Appearance.Get().ReadOnlyText; return !OverrideText.IsEmpty() ? OverrideText : DefaultText; } static FText GetWarningText(TAttribute Appearance, FText DefaultText) { FText OverrideText = Appearance.Get().WarningText; return !OverrideText.IsEmpty() ? OverrideText : DefaultText; } }; FText DefaultCornerText = Appearance.Get().CornerText; TAttribute CornerText = Appearance.IsBound() ? TAttribute::Create(TAttribute::FGetter::CreateStatic(&Local::GetCornerText, Appearance, DefaultCornerText)) : TAttribute(DefaultCornerText); FText DefaultPIENotify(LOCTEXT("GraphSimulatingText", "SIMULATING")); TAttribute PIENotifyText = Appearance.IsBound() ? TAttribute::Create(TAttribute::FGetter::CreateStatic(&Local::GetPIENotifyText, Appearance, DefaultPIENotify)) : TAttribute(DefaultPIENotify); FText DefaultReadOnlyText(LOCTEXT("GraphReadOnlyText", "READ-ONLY")); TAttribute ReadOnlyText = Appearance.IsBound() ? TAttribute::Create(TAttribute::FGetter::CreateStatic(&Local::GetReadOnlyText, Appearance, DefaultReadOnlyText)) : TAttribute(DefaultReadOnlyText); FText DefaultWarningText; TAttribute WarningText = Appearance.IsBound() ? TAttribute::Create(TAttribute::FGetter::CreateStatic(&Local::GetWarningText, Appearance, DefaultWarningText)) : TAttribute(DefaultWarningText); TSharedPtr OverlayWidget; this->ChildSlot [ SAssignNew(OverlayWidget, SOverlay) // The graph panel +SOverlay::Slot() .Expose(GraphPanelSlot) [ SAssignNew(GraphPanel, SGraphPanel) .GraphObj( EdGraphObj ) .DiffResults( InArgs._DiffResults) .FocusedDiffResult( InArgs._FocusedDiffResult) .OnGetContextMenuFor( this, &SGraphEditorImpl::GraphEd_OnGetContextMenuFor ) .OnSelectionChanged( InArgs._GraphEvents.OnSelectionChanged ) .OnNodeDoubleClicked( InArgs._GraphEvents.OnNodeDoubleClicked ) .IsEditable( this, &SGraphEditorImpl::IsGraphEditable ) .DisplayAsReadOnly( this, &SGraphEditorImpl::DisplayGraphAsReadOnly ) .OnDropActors(InArgs._GraphEvents.OnDropActors) .OnDropStreamingLevels( InArgs._GraphEvents.OnDropStreamingLevels ) .OnVerifyTextCommit( InArgs._GraphEvents.OnVerifyTextCommit ) .OnTextCommitted( InArgs._GraphEvents.OnTextCommitted ) .OnSpawnNodeByShortcutAtLocation( InArgs._GraphEvents.OnSpawnNodeByShortcutAtLocation) //.OnUpdateGraphPanel( this, &SGraphEditorImpl::GraphEd_OnPanelUpdated ) .OnDisallowedPinConnection( InArgs._GraphEvents.OnDisallowedPinConnection ) .ShowGraphStateOverlay(InArgs._ShowGraphStateOverlay) .OnDoubleClicked(InArgs._GraphEvents.OnDoubleClicked) .OnMouseButtonDown(InArgs._GraphEvents.OnMouseButtonDown) .OnNodeSingleClicked(InArgs._GraphEvents.OnNodeSingleClicked) ] // Indicator of current zoom level +SOverlay::Slot() .Padding(5) .VAlign(VAlign_Top) .HAlign(HAlign_Right) [ SNew(STextBlock) .TextStyle( FAppStyle::Get(), "Graph.ZoomText" ) .Text( this, &SGraphEditorImpl::GetZoomText ) .ColorAndOpacity( this, &SGraphEditorImpl::GetZoomTextColorAndOpacity ) ] // Title bar - optional +SOverlay::Slot() .VAlign(VAlign_Top) [ SNew(SVerticalBox) + SVerticalBox::Slot() [ InArgs._TitleBar.IsValid() ? InArgs._TitleBar.ToSharedRef() : (TSharedRef)SNullWidget::NullWidget ] + SVerticalBox::Slot() .Padding(20.f, 20.f, 20.f, 0.f) .VAlign(VAlign_Top) .HAlign(HAlign_Center) .AutoHeight() [ SNew(SBorder) .Padding(FMargin(10.f, 4.f)) .BorderImage(FAppStyle::GetBrush(TEXT("Graph.InstructionBackground"))) .BorderBackgroundColor(this, &SGraphEditorImpl::InstructionBorderColor) .HAlign(HAlign_Center) .ColorAndOpacity(this, &SGraphEditorImpl::InstructionTextTint) .Visibility(this, &SGraphEditorImpl::InstructionTextVisibility) [ SNew(STextBlock) .TextStyle(FAppStyle::Get(), "Graph.InstructionText") .Text(this, &SGraphEditorImpl::GetInstructionText) ] ] ] // Bottom-left corner text for Substrate +SOverlay::Slot() .Padding(10) .VAlign(VAlign_Bottom) .HAlign(HAlign_Left) [ SNew(STextBlock) .Visibility(EVisibility::Visible) .TextStyle(FAppStyle::Get(), "Graph.WarningText") .Text(WarningText) ] // Bottom-right corner text indicating the type of tool +SOverlay::Slot() .Padding(10) .VAlign(VAlign_Bottom) .HAlign(HAlign_Right) [ SNew(STextBlock) .Visibility( EVisibility::HitTestInvisible ) .TextStyle( FAppStyle::Get(), "Graph.CornerText" ) .Text(CornerText) ] // Top-right corner text indicating PIE is active +SOverlay::Slot() .Padding(20) .VAlign(VAlign_Top) .HAlign(HAlign_Right) [ SNew(STextBlock) .Visibility(this, &SGraphEditorImpl::PIENotification) .TextStyle( FAppStyle::Get(), "Graph.SimulatingText" ) .Text( PIENotifyText ) ] // Top-right corner text indicating read only when not simulating + SOverlay::Slot() .Padding(20) .VAlign(VAlign_Top) .HAlign(HAlign_Right) [ SNew(STextBlock) .Visibility(this, &SGraphEditorImpl::ReadOnlyVisibility) .TextStyle(FAppStyle::Get(), "Graph.CornerText") .Text(ReadOnlyText) ] // Bottom-right corner text for notification list position +SOverlay::Slot() .Padding(15.f) .VAlign(VAlign_Bottom) .HAlign(HAlign_Right) [ SAssignNew(NotificationListPtr, SNotificationList) .Visibility(EVisibility::Visible) ] ]; GraphPanel->RestoreViewSettings(FVector2f::ZeroVector, -1.0f); NotifyGraphChanged(); } EVisibility SGraphEditorImpl::PIENotification( ) const { if(ShowGraphStateOverlay.Get() && (GEditor->bIsSimulatingInEditor || GEditor->PlayWorld != NULL)) { return EVisibility::Visible; } return EVisibility::Hidden; } SGraphEditorImpl::~SGraphEditorImpl() { if (FocusEditorTimer.IsValid()) { UnRegisterActiveTimer(FocusEditorTimer.Pin().ToSharedRef()); } } void SGraphEditorImpl::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) { if (bResetMenuContext) { GraphPinForMenu.SetPin(nullptr); GraphNodeForMenu.Reset(); bResetMenuContext = false; } // If locked to another graph editor, and our panel has moved, synchronise the locked graph editor accordingly if ((EdGraphObj != NULL) && GraphPanel.IsValid()) { if(GraphPanel->HasMoved() && IsLocked()) { FocusLockedEditorHere(); } } } void SGraphEditorImpl::OnClosedActionMenu() { GraphPanel->OnStopMakingConnection(/*bForceStop=*/ true); } void SGraphEditorImpl::AddContextMenuCommentSection(UToolMenu* InMenu) { UGraphNodeContextMenuContext* Context = InMenu->FindContext(); if (!Context) { return; } const UEdGraphSchema* GraphSchema = Context->Graph->GetSchema(); if (!GraphSchema || GraphSchema->GetParentContextMenuName() == NAME_None) { return; } // Helper to do the node comment editing struct Local { // Called by the EditableText widget to get the current comment for the node static FString GetNodeComment(TWeakObjectPtr NodeWeakPtr) { if (UEdGraphNode* SelectedNode = NodeWeakPtr.Get()) { return SelectedNode->NodeComment; } return FString(); } // Called by the EditableText widget when the user types a new comment for the selected node static void OnNodeCommentTextCommitted(const FText& NewText, ETextCommit::Type CommitInfo, TWeakObjectPtr NodeWeakPtr) { // Apply the change to the selected actor UEdGraphNode* SelectedNode = NodeWeakPtr.Get(); FString NewString = NewText.ToString(); if (SelectedNode && !SelectedNode->NodeComment.Equals(NewString, ESearchCase::CaseSensitive)) { // send property changed events const FScopedTransaction Transaction(LOCTEXT("EditNodeComment", "Change Node Comment")); SelectedNode->Modify(); FProperty* NodeCommentProperty = FindFProperty(SelectedNode->GetClass(), "NodeComment"); if (NodeCommentProperty != nullptr) { SelectedNode->PreEditChange(NodeCommentProperty); SelectedNode->NodeComment = NewString; SelectedNode->SetMakeCommentBubbleVisible(true); FPropertyChangedEvent NodeCommentPropertyChangedEvent(NodeCommentProperty); SelectedNode->PostEditChangeProperty(NodeCommentPropertyChangedEvent); } } // Only dismiss all menus if the text was committed via some user action. // ETextCommit::Default implies that focus was switched by some other means. If this is because a submenu was opened, we don't want to close all the menus as a consequence. if (CommitInfo != ETextCommit::Default) { FSlateApplication::Get().DismissAllMenus(); } } }; if (!Context->Pin) { int32 SelectionCount = GraphSchema->GetNodeSelectionCount(Context->Graph); if (SelectionCount == 1) { // Node comment area TSharedRef NodeCommentBox = SNew(SHorizontalBox); { FToolMenuSection& Section = InMenu->AddSection("GraphNodeComment", LOCTEXT("NodeCommentMenuHeader", "Node Comment")); Section.AddEntry(FToolMenuEntry::InitWidget("NodeCommentBox", NodeCommentBox, FText::GetEmpty())); } TWeakObjectPtr SelectedNodeWeakPtr = MakeWeakObjectPtr(const_cast(ToRawPtr(Context->Node))); FText NodeCommentText; if (UEdGraphNode* SelectedNode = SelectedNodeWeakPtr.Get()) { NodeCommentText = FText::FromString(SelectedNode->NodeComment); } const FSlateBrush* NodeIcon = FCoreStyle::Get().GetDefaultBrush();//@TODO: FActorIconFinder::FindIconForActor(SelectedActors(0).Get()); // Comment label NodeCommentBox->AddSlot() .VAlign(VAlign_Center) .FillWidth(1.0f) .MaxWidth(250.0f) .Padding(FMargin(10.0f, 0.0f)) [ SNew(SMultiLineEditableTextBox) .Text(NodeCommentText) .ToolTipText(LOCTEXT("NodeComment_ToolTip", "Comment for this node")) .OnTextCommitted_Static(&Local::OnNodeCommentTextCommitted, SelectedNodeWeakPtr) .SelectAllTextWhenFocused(true) .RevertTextOnEscape(true) .AutoWrapText(true) .ModiferKeyForNewLine(EModifierKey::Control) ]; } else if (SelectionCount > 1) { struct SCommentUtility { static void CreateComment(const UEdGraphSchema* Schema, UEdGraph* Graph) { if (Schema && Graph) { TSharedPtr Action = Schema->GetCreateCommentAction(); if (Action.IsValid()) { Action->PerformAction(Graph, nullptr, FVector2f()); } } } }; FToolMenuSection& Section = InMenu->AddSection("SchemaActionComment", LOCTEXT("MultiCommentHeader", "Comment Group")); Section.AddMenuEntry( "MultiCommentDesc", LOCTEXT("MultiCommentDesc", "Create Comment from Selection"), LOCTEXT("CommentToolTip", "Create a resizable comment box around selection."), FSlateIcon(), FExecuteAction::CreateStatic(SCommentUtility::CreateComment, GraphSchema, const_cast(ToRawPtr(Context->Graph)) )); } } } FName SGraphEditorImpl::GetNodeParentContextMenuName(UClass* InClass) { if (InClass && InClass != UEdGraphNode::StaticClass()) { if (InClass->GetDefaultObject()->IncludeParentNodeContextMenu()) { if (UClass* SuperClass = InClass->GetSuperClass()) { return GetNodeContextMenuName(SuperClass); } } } return NAME_None; } FName SGraphEditorImpl::GetNodeContextMenuName(UClass* InClass) { return FName(*(FString(TEXT("GraphEditor.GraphNodeContextMenu.")) + InClass->GetName())); } void SGraphEditorImpl::RegisterContextMenu(const UEdGraphSchema* Schema, FToolMenuContext& MenuContext) const { UToolMenus* ToolMenus = UToolMenus::Get(); const FName SchemaMenuName = Schema->GetContextMenuName(); UGraphNodeContextMenuContext* Context = MenuContext.FindContext(); // Root menu // "GraphEditor.GraphContextMenu.Common" // contains: GraphSchema->GetContextMenuActions(Menu, Context) const FName CommonRootMenuName = "GraphEditor.GraphContextMenu.Common"; if (!ToolMenus->IsMenuRegistered(CommonRootMenuName)) { ToolMenus->RegisterMenu(CommonRootMenuName); } bool bDidRegisterGraphSchemaMenu = false; const FName EdGraphSchemaContextMenuName = UEdGraphSchema::GetContextMenuName(UEdGraphSchema::StaticClass()); if (!ToolMenus->IsMenuRegistered(EdGraphSchemaContextMenuName)) { ToolMenus->RegisterMenu(EdGraphSchemaContextMenuName); bDidRegisterGraphSchemaMenu = true; } // Menus for subclasses of EdGraphSchema for (UClass* CurrentClass = Schema->GetClass(); CurrentClass && CurrentClass->IsChildOf(UEdGraphSchema::StaticClass()); CurrentClass = CurrentClass->GetSuperClass()) { const UEdGraphSchema* CurrentSchema = CurrentClass->GetDefaultObject(); const FName CheckMenuName = CurrentSchema->GetContextMenuName(); // Some subclasses of UEdGraphSchema chose not to include UEdGraphSchema's menu // Note: menu "GraphEditor.GraphContextMenu.Common" calls GraphSchema->GetContextMenuActions() and adds entry for node comment const FName CheckParentName = CurrentSchema->GetParentContextMenuName(); if (!ToolMenus->IsMenuRegistered(CheckMenuName)) { FName ParentNameToUse = CheckParentName; // Connect final menu in chain to the common root if (ParentNameToUse == NAME_None) { ParentNameToUse = CommonRootMenuName; } ToolMenus->RegisterMenu(CheckMenuName, ParentNameToUse); } if (CheckParentName == NAME_None) { break; } } // Now register node menus, which will belong to their schemas bool bDidRegisterNodeMenu = false; if (Context->Node) { for (UClass* CurrentClass = Context->Node->GetClass(); CurrentClass && CurrentClass->IsChildOf(UEdGraphNode::StaticClass()); CurrentClass = CurrentClass->GetSuperClass()) { const FName CheckMenuName = GetNodeContextMenuName(CurrentClass); const FName CheckParentName = GetNodeParentContextMenuName(CurrentClass); if (!ToolMenus->IsMenuRegistered(CheckMenuName)) { FName ParentNameToUse = CheckParentName; // Connect final menu in chain to schema's chain of menus if (CheckParentName == NAME_None) { ParentNameToUse = EdGraphSchemaContextMenuName; } ToolMenus->RegisterMenu(CheckMenuName, ParentNameToUse); bDidRegisterNodeMenu = true; } if (CheckParentName == NAME_None) { break; } } } // Now that all the possible sections have been registered, we can add the dynamic section for the custom schema node actions to override if (bDidRegisterGraphSchemaMenu) { UToolMenu* Menu = ToolMenus->FindMenu(EdGraphSchemaContextMenuName); Menu->AddDynamicSection("EdGraphSchemaPinActions", FNewToolMenuDelegate::CreateLambda([](UToolMenu* InMenu) { UGraphNodeContextMenuContext* NodeContext = InMenu->FindContext(); if (NodeContext && NodeContext->Graph && NodeContext->Pin) { if (TSharedPtr GraphEditor = StaticCastSharedPtr(FindGraphEditorForGraph(NodeContext->Graph))) { GraphEditor->GetPinContextMenuActionsForSchema(InMenu); } } })); Menu->AddDynamicSection("GetContextMenuActions", FNewToolMenuDelegate::CreateLambda([](UToolMenu* InMenu) { if (UGraphNodeContextMenuContext* ContextObject = InMenu->FindContext()) { if (const UEdGraphSchema* GraphSchema = ContextObject->Graph->GetSchema()) { GraphSchema->GetContextMenuActions(InMenu, ContextObject); } } })); Menu->AddDynamicSection("EdGraphSchema", FNewToolMenuDelegate::CreateStatic(&SGraphEditorImpl::AddContextMenuCommentSection)); if(Substrate::IsSubstrateEnabled()) { Menu->AddDynamicSection("SubstrateRootActions", FNewToolMenuDelegate::CreateLambda([](UToolMenu* InMenu) { if (UGraphNodeContextMenuContext * ContextObject = InMenu->FindContext()) { if (const UEdGraphSchema* GraphSchema = ContextObject->Graph->GetSchema()) { if (ContextObject->Node->IsA() && !ContextObject->Pin) { const UMaterialGraphNode_Root* RootNode = Cast(ContextObject->Node); TSharedRef SubstrateConvertButton = SNew(SButton) .Text(LOCTEXT("SubstrateConvertButtonLabel", "\x26A0 Convert To Substrate \x26A0")) .ToolTipText(LOCTEXT("SubstrateConvertButtonToolTip", "Convert the root node to a Substrate graph. This material will then use Substrate more advanced features and topology processes.\nTo go back to the previous Non-Substrate material, you can use CTRL+Z or you will need to manually relink the graph.")) .ButtonStyle(FAppStyle::Get(), "Button") .OnClicked_Lambda([RootNode]() { if (RootNode && RootNode->Material) { RootNode->Material->UserRequestsConvertMaterialToSubstrateMaterial(); } return FReply::Handled(); }); FToolMenuSection& SubstrateSection = InMenu->AddSection("Substrate", LOCTEXT("SubstrateMenuHeader", "Substrate")); SubstrateSection.AddEntry(FToolMenuEntry::InitWidget("NodeCommentBox", SubstrateConvertButton, FText::GetEmpty())); } } } })); } } if (bDidRegisterNodeMenu) { UToolMenu* Menu = ToolMenus->FindMenu(GetNodeContextMenuName(Context->Node->GetClass())); Menu->AddDynamicSection("GetNodeContextMenuActions", FNewToolMenuDelegate::CreateLambda([](UToolMenu* InMenu) { UGraphNodeContextMenuContext* Context = InMenu->FindContext(); if (Context && Context->Node) { Context->Node->GetNodeContextMenuActions(InMenu, Context); } })); } } UToolMenu* SGraphEditorImpl::GenerateContextMenu(const UEdGraphSchema* Schema, FToolMenuContext& MenuContext) const { // Register all the menu's needed RegisterContextMenu(Schema, MenuContext); UToolMenus* ToolMenus = UToolMenus::Get(); UGraphNodeContextMenuContext* Context = MenuContext.FindContext(); FName MenuName = NAME_None; if (Context->Node) { MenuName = GetNodeContextMenuName(Context->Node->GetClass()); } else { MenuName = Schema->GetContextMenuName(); } return ToolMenus->GenerateMenu(MenuName, MenuContext); } FActionMenuContent SGraphEditorImpl::GraphEd_OnGetContextMenuFor(const FGraphContextMenuArguments& SpawnInfo) { FActionMenuContent Result; if (EdGraphObj != NULL) { Result = FActionMenuContent( SNew(STextBlock) .Text( NSLOCTEXT("GraphEditor", "NoNodes", "No Nodes") ) ); const UEdGraphSchema* Schema = EdGraphObj->GetSchema(); check(Schema); GraphPinForMenu.SetPin(SpawnInfo.GraphPin); GraphNodeForMenu = SpawnInfo.GraphNode; if ((SpawnInfo.GraphPin != NULL) || (SpawnInfo.GraphNode != NULL)) { // Get all menu extenders for this context menu from the graph editor module FGraphEditorModule& GraphEditorModule = FModuleManager::GetModuleChecked( TEXT("GraphEditor") ); TArray MenuExtenderDelegates = GraphEditorModule.GetAllGraphEditorContextMenuExtender(); TArray> Extenders; for (int32 i = 0; i < MenuExtenderDelegates.Num(); ++i) { if (MenuExtenderDelegates[i].IsBound()) { Extenders.Add(MenuExtenderDelegates[i].Execute(this->Commands.ToSharedRef(), EdGraphObj, SpawnInfo.GraphNode, SpawnInfo.GraphPin, !IsEditable.Get())); } } TSharedPtr MenuExtender = FExtender::Combine(Extenders); if (OnCreateNodeOrPinMenu.IsBound()) { // Show the menu for the pin or node under the cursor const bool bShouldCloseAfterAction = true; FMenuBuilder MenuBuilder( bShouldCloseAfterAction, this->Commands, MenuExtender ); Result = OnCreateNodeOrPinMenu.Execute(EdGraphObj, SpawnInfo.GraphNode, SpawnInfo.GraphPin, &MenuBuilder, !IsEditable.Get()); } else { UGraphNodeContextMenuContext* ContextObject = NewObject(); ContextObject->Init(EdGraphObj, SpawnInfo.GraphNode, SpawnInfo.GraphPin, !IsEditable.Get()); FToolMenuContext Context(this->Commands, MenuExtender, ContextObject); UAssetEditorToolkitMenuContext* ToolkitMenuContext = NewObject(); ToolkitMenuContext->Toolkit = AssetEditorToolkit; Context.AddObject(ToolkitMenuContext); if (TSharedPtr SharedToolKit = AssetEditorToolkit.Pin()) { SharedToolKit->InitToolMenuContext(Context); } // Need to additionally pass through the asset toolkit to hook up those commands? UToolMenus* ToolMenus = UToolMenus::Get(); UToolMenu* GeneratedMenu = GenerateContextMenu(Schema, Context); Result = FActionMenuContent(ToolMenus->GenerateWidget(GeneratedMenu)); } } else if (IsEditable.Get()) { if (EdGraphObj->GetSchema() != NULL ) { if(OnCreateActionMenuAtLocation.IsBound()) { Result = OnCreateActionMenuAtLocation.Execute( EdGraphObj, SpawnInfo.NodeAddPosition, SpawnInfo.DragFromPins, bAutoExpandActionMenu, SGraphEditor::FActionMenuClosed::CreateSP(this, &SGraphEditorImpl::OnClosedActionMenu) ); } else { TSharedRef Menu = SNew(SGraphEditorActionMenu) .GraphObj( EdGraphObj ) .NewNodePosition(SpawnInfo.NodeAddPosition) .DraggedFromPins(SpawnInfo.DragFromPins) .AutoExpandActionMenu(bAutoExpandActionMenu) .OnClosedCallback( SGraphEditor::FActionMenuClosed::CreateSP(this, &SGraphEditorImpl::OnClosedActionMenu) ); Result = FActionMenuContent( Menu, Menu->GetFilterTextBox() ); } if (SpawnInfo.DragFromPins.Num() > 0) { GraphPanel->PreservePinPreviewUntilForced(); } } } else { Result = FActionMenuContent( SNew(STextBlock) .Text( NSLOCTEXT("GraphEditor", "CannotCreateWhileDebugging", "Cannot create new nodes in a read only graph") ) ); } } else { Result = FActionMenuContent( SNew(STextBlock) .Text( NSLOCTEXT("GraphEditor", "GraphObjectIsNull", "Graph Object is Null") ) ); } Result.OnMenuDismissed.AddLambda([this]() { bResetMenuContext = true; }); return Result; } bool SGraphEditorImpl::CanReconstructNodes() const { return IsGraphEditable() && (GraphPanel->SelectionManager.AreAnyNodesSelected()); } bool SGraphEditorImpl::CanBreakNodeLinks() const { return IsGraphEditable() && (GraphPanel->SelectionManager.AreAnyNodesSelected()); } bool SGraphEditorImpl::CanSummonCreateNodeMenu() const { return IsGraphEditable() && GraphPanel->IsHovered() && GetDefault()->bOpenCreateMenuOnBlankGraphAreas; } void SGraphEditorImpl::ReconstructNodes() { const UEdGraphSchema* Schema = this->EdGraphObj->GetSchema(); { FScopedTransaction const Transaction(LOCTEXT("ReconstructNodeTransaction", "Refresh Node(s)")); for (FGraphPanelSelectionSet::TConstIterator NodeIt( GraphPanel->SelectionManager.GetSelectedNodes() ); NodeIt; ++NodeIt) { if (UEdGraphNode* Node = Cast(*NodeIt)) { const bool bCurDisableOrphanSaving = Node->bDisableOrphanPinSaving; Node->bDisableOrphanPinSaving = true; Schema->ReconstructNode(*Node); Node->ClearCompilerMessage(); Node->bDisableOrphanPinSaving = bCurDisableOrphanSaving; } } } NotifyGraphChanged(); } void SGraphEditorImpl::BreakNodeLinks() { const FScopedTransaction Transaction( NSLOCTEXT("UnrealEd", "GraphEd_BreakNodeLinks", "Break Node Links") ); for (FGraphPanelSelectionSet::TConstIterator NodeIt( GraphPanel->SelectionManager.GetSelectedNodes() ); NodeIt; ++NodeIt) { if (UEdGraphNode* Node = Cast(*NodeIt)) { const UEdGraphSchema* Schema = Node->GetSchema(); Schema->BreakNodeLinks(*Node); } } } void SGraphEditorImpl::SummonCreateNodeMenu() { GraphPanel->SummonCreateNodeMenuFromUICommand(NumNodesAddedSinceLastPointerPosition); } FText SGraphEditorImpl::GetZoomText() const { return GraphPanel->GetZoomText(); } FSlateColor SGraphEditorImpl::GetZoomTextColorAndOpacity() const { return GraphPanel->GetZoomTextColorAndOpacity(); } bool SGraphEditorImpl::IsGraphEditable() const { return (EdGraphObj != NULL) && IsEditable.Get(); } bool SGraphEditorImpl::DisplayGraphAsReadOnly() const { return (EdGraphObj != NULL) && DisplayAsReadOnly.Get(); } bool SGraphEditorImpl::IsLocked() const { for( auto LockedGraph : LockedGraphs ) { if( LockedGraph.IsValid() ) { return true; } } return false; } TSharedPtr SGraphEditorImpl::GetTitleBar() const { return TitleBar; } void SGraphEditorImpl::SetViewLocation( const FVector2f& Location, float ZoomAmount, const FGuid& BookmarkId ) { if( GraphPanel.IsValid() && EdGraphObj && (!IsLocked() || !GraphPanel->HasDeferredObjectFocus())) { GraphPanel->RestoreViewSettings(Location, ZoomAmount, BookmarkId); } } void SGraphEditorImpl::GetViewLocation( FVector2f& Location, float& ZoomAmount ) { if( GraphPanel.IsValid() && EdGraphObj && (!IsLocked() || !GraphPanel->HasDeferredObjectFocus())) { Location = GraphPanel->GetViewOffset(); ZoomAmount = GraphPanel->GetZoomAmount(); } } void SGraphEditorImpl::GetViewBookmark( FGuid& BookmarkId ) { if (GraphPanel.IsValid()) { BookmarkId = GraphPanel->GetViewBookmarkId(); } } void SGraphEditorImpl::LockToGraphEditor( TWeakPtr Other ) { if( !LockedGraphs.Contains(Other) ) { LockedGraphs.Push(Other); } if (GraphPanel.IsValid()) { FocusLockedEditorHere(); } } void SGraphEditorImpl::UnlockFromGraphEditor( TWeakPtr Other ) { check(Other.IsValid()); int idx = LockedGraphs.Find(Other); if (idx != INDEX_NONE) { LockedGraphs.RemoveAtSwap(idx); } } void SGraphEditorImpl::AddNotification( FNotificationInfo& Info, bool bSuccess ) { // set up common notification properties Info.bUseLargeFont = true; TSharedPtr Notification = NotificationListPtr->AddNotification(Info); if ( Notification.IsValid() ) { Notification->SetVisibility(EVisibility::HitTestInvisible); Notification->SetCompletionState( bSuccess ? SNotificationItem::CS_Success : SNotificationItem::CS_Fail ); } } TSharedPtr SGraphEditorImpl::AddNotification(FNotificationInfo& Info) { // set up common notification properties Info.bUseLargeFont = true; TSharedPtr Notification = NotificationListPtr->AddNotification(Info); if (Notification.IsValid()) { Notification->SetVisibility(EVisibility::HitTestInvisible); return Notification; } return nullptr; } EActiveTimerReturnType SGraphEditorImpl::HandleFocusEditorDeferred(double InCurrentTime, float InDeltaTime) { // If GraphPanel is going to pan to a target but hasn't yet, wait until it does so we don't miss it if (GraphPanel->HasDeferredZoomDestination()) { return EActiveTimerReturnType::Continue; } for( int i = 0; i < LockedGraphs.Num(); ++i ) { TSharedPtr LockedGraph = LockedGraphs[i].Pin(); if (LockedGraph != TSharedPtr()) { // If the locked graph is going to pan to a target but hasn't yet, wait until it does so we don't miss it if (LockedGraph->GetGraphPanel()->HasDeferredZoomDestination()) { return EActiveTimerReturnType::Continue; } FVector2f TopLeft, BottomRight; // If the locked graph was instructed to pan to a destination, let it ignore the lock to reach that destination. // this way we can support diffs of moved nodes. if (LockedGraph->GetGraphPanel()->GetZoomTargetRect(TopLeft, BottomRight)) { continue; } // Send the locked graph to the same place as this graph if (GraphPanel->GetZoomTargetRect(TopLeft, BottomRight)) { LockedGraph->GetGraphPanel()->JumpToRect(TopLeft, BottomRight); } else { LockedGraph->SetViewLocation(GraphPanel->GetViewOffset(), GraphPanel->GetZoomAmount()); } } else { LockedGraphs.RemoveAtSwap(i--); } } return EActiveTimerReturnType::Stop; } void SGraphEditorImpl::FocusLockedEditorHere() { if (!FocusEditorTimer.IsValid()) { FocusEditorTimer = RegisterActiveTimer( 0.f, FWidgetActiveTimerDelegate::CreateSP(this, &SGraphEditorImpl::HandleFocusEditorDeferred) ); } } void SGraphEditorImpl::SetPinVisibility( SGraphEditor::EPinVisibility InVisibility ) { if( GraphPanel.IsValid()) { SGraphEditor::EPinVisibility CachedVisibility = GraphPanel->GetPinVisibility(); GraphPanel->SetPinVisibility(InVisibility); if(CachedVisibility != InVisibility) { NotifyGraphChanged(); } } } TSharedRef SGraphEditorImpl::RegisterActiveTimer(float TickPeriod, FWidgetActiveTimerDelegate TickFunction) { return SWidget::RegisterActiveTimer(TickPeriod, TickFunction); } EVisibility SGraphEditorImpl::ReadOnlyVisibility() const { if(ShowGraphStateOverlay.Get() && PIENotification() == EVisibility::Hidden && !IsEditable.Get()) { return EVisibility::Visible; } return EVisibility::Hidden; } FText SGraphEditorImpl::GetInstructionText() const { if (Appearance.IsBound()) { return Appearance.Get().InstructionText; } return FText::GetEmpty(); } EVisibility SGraphEditorImpl::InstructionTextVisibility() const { if (!GetInstructionText().IsEmpty() && (GetInstructionTextFade() > 0.0f)) { return EVisibility::HitTestInvisible; } return EVisibility::Hidden; } float SGraphEditorImpl::GetInstructionTextFade() const { float InstructionOpacity = 1.0f; if (Appearance.IsBound()) { InstructionOpacity = Appearance.Get().InstructionFade.Get(); } return InstructionOpacity; } FLinearColor SGraphEditorImpl::InstructionTextTint() const { return FLinearColor(1.f, 1.f, 1.f, GetInstructionTextFade()); } FSlateColor SGraphEditorImpl::InstructionBorderColor() const { FLinearColor BorderColor(0.1f, 0.1f, 0.1f, 0.7f); BorderColor.A *= GetInstructionTextFade(); return BorderColor; } void SGraphEditorImpl::CaptureKeyboard() { FSlateApplication::Get().SetKeyboardFocus(GraphPanel); } void SGraphEditorImpl::SetNodeFactory(const TSharedRef& NewNodeFactory) { GraphPanel->SetNodeFactory(NewNodeFactory); } void SGraphEditorImpl::OnAlignTop() { const FScopedTransaction Transaction(FGraphEditorCommands::Get().AlignNodesTop->GetLabel()); FAlignmentHelper Helper(SharedThis(this), Orient_Vertical, EAlignType::Minimum); Helper.Align(); } void SGraphEditorImpl::OnAlignMiddle() { const FScopedTransaction Transaction(FGraphEditorCommands::Get().AlignNodesMiddle->GetLabel()); FAlignmentHelper Helper(SharedThis(this), Orient_Vertical, EAlignType::Middle); Helper.Align(); } void SGraphEditorImpl::OnAlignBottom() { const FScopedTransaction Transaction(FGraphEditorCommands::Get().AlignNodesBottom->GetLabel()); FAlignmentHelper Helper(SharedThis(this), Orient_Vertical, EAlignType::Maximum); Helper.Align(); } void SGraphEditorImpl::OnAlignLeft() { const FScopedTransaction Transaction(FGraphEditorCommands::Get().AlignNodesLeft->GetLabel()); FAlignmentHelper Helper(SharedThis(this), Orient_Horizontal, EAlignType::Minimum); Helper.Align(); } void SGraphEditorImpl::OnAlignCenter() { const FScopedTransaction Transaction(FGraphEditorCommands::Get().AlignNodesCenter->GetLabel()); FAlignmentHelper Helper(SharedThis(this), Orient_Horizontal, EAlignType::Middle); Helper.Align(); } void SGraphEditorImpl::OnAlignRight() { const FScopedTransaction Transaction(FGraphEditorCommands::Get().AlignNodesRight->GetLabel()); FAlignmentHelper Helper(SharedThis(this), Orient_Horizontal, EAlignType::Maximum); Helper.Align(); } void SGraphEditorImpl::OnStraightenConnections() { StraightenConnections(); } /** Distribute the specified array of node data evenly */ void DistributeNodes(TArray& InData, bool bIsHorizontal) { // Sort the data InData.Sort([](const FAlignmentData& A, const FAlignmentData& B) { return A.TargetProperty + FMath::RoundToInt(A.TargetOffset / 2) < B.TargetProperty + FMath::RoundToInt(B.TargetOffset / 2); }); // Measure the available space float TotalWidthOfNodes = 0.f; for (int32 Index = 1; Index < InData.Num() - 1; ++Index) { TotalWidthOfNodes += InData[Index].TargetOffset; } const float SpaceToDistributeIn = static_cast(InData.Last().TargetProperty) - InData[0].GetTarget(); const float PaddingAmount = ((SpaceToDistributeIn - TotalWidthOfNodes) / static_cast(InData.Num() - 1)); float TargetPosition = InData[0].GetTarget() + PaddingAmount; // Now set all the properties on the target if (InData.Num() > 1) { UEdGraph* Graph = InData[0].Node->GetGraph(); if (Graph) { const UEdGraphSchema* Schema = Graph->GetSchema(); // similar to FAlignmentHelper::Align(), first try using GraphSchema to move the nodes if applicable if (Schema) { for (int32 Index = 1; Index < InData.Num() - 1; ++Index) { FAlignmentData& Entry = InData[Index]; FVector2f Target2DPosition = Entry.Node->GetPosition(); if (bIsHorizontal) { Target2DPosition.X = TargetPosition; } else { Target2DPosition.Y = TargetPosition; } Schema->SetNodePosition(Entry.Node, Target2DPosition); TargetPosition = Entry.GetTarget() + PaddingAmount; } return; } } // fall back to the old approach if there isn't a schema for (int32 Index = 1; Index < InData.Num() - 1; ++Index) { FAlignmentData& Entry = InData[Index]; Entry.Node->Modify(); Entry.TargetProperty = UE::LWC::FloatToIntCastChecked(TargetPosition); TargetPosition = Entry.GetTarget() + PaddingAmount; } } } void SGraphEditorImpl::OnDistributeNodesH() { TArray AlignData; for (UObject* It : GetSelectedNodes()) { if (UEdGraphNode* Node = Cast(It)) { AlignData.Add(FAlignmentData(Node, Node->NodePosX, GetNodeSize(*this, Node).X)); } } if (AlignData.Num() > 2) { const FScopedTransaction Transaction(FGraphEditorCommands::Get().DistributeNodesHorizontally->GetLabel()); DistributeNodes(AlignData, true); } } void SGraphEditorImpl::OnDistributeNodesV() { TArray AlignData; for (UObject* It : GetSelectedNodes()) { if (UEdGraphNode* Node = Cast(It)) { AlignData.Add(FAlignmentData(Node, Node->NodePosY, GetNodeSize(*this, Node).Y)); } } if (AlignData.Num() > 2) { const FScopedTransaction Transaction(FGraphEditorCommands::Get().DistributeNodesVertically->GetLabel()); DistributeNodes(AlignData, false); } } /** Distribute the specified array of node data evenly and align left when aligning vertically, and align top when aligning horizontally */ void StackNodes(TArray& InData, bool bIsHorizontal) { // Sort the data InData.Sort([](const FAlignmentData& A, const FAlignmentData& B) { return A.TargetProperty < B.TargetProperty; }); float TargetPosition = static_cast(InData[0].TargetProperty); float StackAxisPosition = 0.0f; float PaddingAmount = static_cast(SNodePanel::GetSnapGridSize()); if (bIsHorizontal) { StackAxisPosition = InData[0].Node->GetNodePosY(); PaddingAmount *= 2.0f; } else { StackAxisPosition = InData[0].Node->GetNodePosX(); } // Now set all the properties on the target if (InData.Num() > 1) { UEdGraph* Graph = InData[0].Node->GetGraph(); if (Graph) { const UEdGraphSchema* Schema = Graph->GetSchema(); if (Schema) { for (int32 Index = 0; Index < InData.Num(); ++Index) { FAlignmentData& Entry = InData[Index]; FVector2f AdjustedTargetPosition = Entry.Node->GetPosition(); if (bIsHorizontal) { AdjustedTargetPosition.X = TargetPosition; AdjustedTargetPosition.Y = StackAxisPosition; } else { AdjustedTargetPosition.X = StackAxisPosition; AdjustedTargetPosition.Y = TargetPosition; } Schema->SetNodePosition(Entry.Node, AdjustedTargetPosition); TargetPosition = Entry.GetTarget() + PaddingAmount; } return; } } // fall back to the old approach if there isn't a schema for (int32 Index = 0; Index < InData.Num() - 1; ++Index) { FAlignmentData& Entry = InData[Index]; Entry.Node->Modify(); Entry.TargetProperty = UE::LWC::FloatToIntCastChecked(TargetPosition); TargetPosition = Entry.GetTarget() + PaddingAmount; } } } void SGraphEditorImpl::OnStackNodesH() { TArray AlignData; for (UObject* It : GetSelectedNodes()) { if (UEdGraphNode* Node = Cast(It)) { AlignData.Add(FAlignmentData(Node, Node->NodePosX, GetNodeSize(*this, Node).X)); } } if (AlignData.Num() > 1) { const FScopedTransaction Transaction(FGraphEditorCommands::Get().StackNodesHorizontally->GetLabel()); StackNodes(AlignData, true); } } void SGraphEditorImpl::OnStackNodesV() { TArray AlignData; for (UObject* It : GetSelectedNodes()) { if (UEdGraphNode* Node = Cast(It)) { AlignData.Add(FAlignmentData(Node, Node->NodePosY, GetNodeSize(*this, Node).Y)); } } if (AlignData.Num() > 1) { const FScopedTransaction Transaction(FGraphEditorCommands::Get().StackNodesVertically->GetLabel()); StackNodes(AlignData, false); } } int32 SGraphEditorImpl::GetNumberOfSelectedNodes() const { return GetSelectedNodes().Num(); } UEdGraphNode* SGraphEditorImpl::GetSingleSelectedNode() const { const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); return (SelectedNodes.Num() == 1) ? Cast(*SelectedNodes.CreateConstIterator()) : nullptr; } SGraphPanel* SGraphEditorImpl::GetGraphPanel() const { return GraphPanel.Get(); } ///////////////////////////////////////////////////// #undef LOCTEXT_NAMESPACE