498 lines
15 KiB
C++
498 lines
15 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
|
|
#include "SGraphNodeKnot.h"
|
|
|
|
#include "Containers/Array.h"
|
|
#include "Containers/EnumAsByte.h"
|
|
#include "Delegates/Delegate.h"
|
|
#include "DragConnection.h"
|
|
#include "EdGraph/EdGraph.h"
|
|
#include "EdGraph/EdGraphNode.h"
|
|
#include "EdGraph/EdGraphPin.h"
|
|
#include "EdGraph/EdGraphSchema.h"
|
|
#include "Framework/Commands/GenericCommands.h"
|
|
#include "Framework/Commands/UICommandInfo.h"
|
|
#include "GenericPlatform/ICursor.h"
|
|
#include "GraphEditorSettings.h"
|
|
#include "HAL/Platform.h"
|
|
#include "HAL/PlatformCrt.h"
|
|
#include "Input/DragAndDrop.h"
|
|
#include "Input/Events.h"
|
|
#include "Input/Reply.h"
|
|
#include "InputCoreTypes.h"
|
|
#include "Layout/Visibility.h"
|
|
#include "Math/Color.h"
|
|
#include "Misc/AssertionMacros.h"
|
|
#include "Misc/Attribute.h"
|
|
#include "Misc/Optional.h"
|
|
#include "SCommentBubble.h"
|
|
#include "SGraphNode.h"
|
|
#include "SGraphPanel.h"
|
|
#include "SGraphPin.h"
|
|
#include "SNodePanel.h"
|
|
#include "ScopedTransaction.h"
|
|
#include "SlotBase.h"
|
|
#include "Styling/AppStyle.h"
|
|
#include "Styling/SlateColor.h"
|
|
#include "UObject/UObjectGlobals.h"
|
|
#include "UObject/WeakObjectPtr.h"
|
|
#include "UObject/WeakObjectPtrTemplates.h"
|
|
#include "Widgets/Layout/SBorder.h"
|
|
#include "Widgets/Layout/SSpacer.h"
|
|
#include "Widgets/SBoxPanel.h"
|
|
#include "Widgets/SNullWidget.h"
|
|
#include "Widgets/SOverlay.h"
|
|
#include "Widgets/Text/SInlineEditableTextBlock.h"
|
|
|
|
class FText;
|
|
class SWidget;
|
|
struct FGeometry;
|
|
struct FSlateBrush;
|
|
|
|
namespace SKnotNodeDefinitions
|
|
{
|
|
/** Offset from the left edge to display comment toggle button at */
|
|
static const float KnotCenterButtonAdjust = 3.f;
|
|
|
|
/** Offset from the left edge to display comment bubbles at */
|
|
static const float KnotCenterBubbleAdjust = 20.f;
|
|
|
|
/** Knot node spacer sizes */
|
|
static const FVector2f NodeSpacerSize(42.0f, 24.0f);
|
|
}
|
|
|
|
class FAmbivalentDirectionDragConnection : public FDragConnection
|
|
{
|
|
public:
|
|
// FDragDropOperation interface
|
|
virtual void OnDragged(const class FDragDropEvent& DragDropEvent) override;
|
|
// End of FDragDropOperation interface
|
|
|
|
UEdGraphPin* GetBestPin() const;
|
|
|
|
// FDragConnection interface
|
|
virtual void ValidateGraphPinList(TArray<UEdGraphPin*>& OutValidPins) override;
|
|
// End of FDragConnection interface
|
|
|
|
|
|
static TSharedRef<FAmbivalentDirectionDragConnection> New(UEdGraphNode* InKnot, const TSharedRef<SGraphPanel>& InGraphPanel, const FDraggedPinTable& InStartingPins)
|
|
{
|
|
TSharedRef<FAmbivalentDirectionDragConnection> Operation = MakeShareable(new FAmbivalentDirectionDragConnection(InKnot, InGraphPanel, InStartingPins));
|
|
Operation->Construct();
|
|
|
|
return Operation;
|
|
}
|
|
|
|
protected:
|
|
FAmbivalentDirectionDragConnection(UEdGraphNode* InKnot, const TSharedRef<SGraphPanel>& InGraphPanel, const FDraggedPinTable& InStartingPins)
|
|
: FDragConnection(InGraphPanel, InStartingPins)
|
|
, KnotPtr(InKnot)
|
|
, StartScreenPos(FVector2f::ZeroVector)
|
|
, MostRecentScreenPos(FVector2f::ZeroVector)
|
|
, bLatchedStartScreenPos(false)
|
|
{
|
|
}
|
|
|
|
protected:
|
|
TWeakObjectPtr<UEdGraphNode> KnotPtr;
|
|
|
|
FVector2f StartScreenPos;
|
|
FVector2f MostRecentScreenPos;
|
|
bool bLatchedStartScreenPos;
|
|
};
|
|
|
|
UEdGraphPin* FAmbivalentDirectionDragConnection::GetBestPin() const
|
|
{
|
|
if (bLatchedStartScreenPos)
|
|
{
|
|
int32 InputPinIndex = -1;
|
|
int32 OutputPinIndex = -1;
|
|
|
|
if (KnotPtr.Get() && KnotPtr->ShouldDrawNodeAsControlPointOnly(InputPinIndex, OutputPinIndex))
|
|
{
|
|
const bool bIsRight = MostRecentScreenPos.X >= StartScreenPos.X;
|
|
return bIsRight ? KnotPtr->GetPinAt(OutputPinIndex) : KnotPtr->GetPinAt(InputPinIndex);
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void FAmbivalentDirectionDragConnection::OnDragged(const class FDragDropEvent& DragDropEvent)
|
|
{
|
|
if (bLatchedStartScreenPos)
|
|
{
|
|
const FVector2f LastScreenPos = MostRecentScreenPos;
|
|
MostRecentScreenPos = DragDropEvent.GetScreenSpacePosition();
|
|
|
|
// Switch directions on the preview connector as we cross from left to right of the starting drag point (or vis versa)
|
|
const bool bWasRight = LastScreenPos.X >= StartScreenPos.X;
|
|
const bool bIsRight = MostRecentScreenPos.X >= StartScreenPos.X;
|
|
|
|
if (bWasRight ^ bIsRight)
|
|
{
|
|
GraphPanel->OnStopMakingConnection(/*bForceStop=*/ true);
|
|
GraphPanel->OnBeginMakingConnection(GetBestPin());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
StartScreenPos = DragDropEvent.GetScreenSpacePosition();
|
|
MostRecentScreenPos = StartScreenPos;
|
|
bLatchedStartScreenPos = true;
|
|
}
|
|
|
|
FDragConnection::OnDragged(DragDropEvent);
|
|
}
|
|
|
|
void FAmbivalentDirectionDragConnection::ValidateGraphPinList(TArray<UEdGraphPin*>& OutValidPins)
|
|
{
|
|
OutValidPins.Empty(DraggingPins.Num());
|
|
|
|
if (KnotPtr.Get() != nullptr)
|
|
{
|
|
bool bUseOutput = true;
|
|
|
|
// Pick output or input based on if the drag op is currently to the left or to the right of the starting drag point
|
|
if (bLatchedStartScreenPos)
|
|
{
|
|
bUseOutput = (StartScreenPos.X < MostRecentScreenPos.X);
|
|
}
|
|
|
|
if (UEdGraphPin* TargetPinObj = GetHoveredPin())
|
|
{
|
|
// Dragging to another pin, pick the opposite direction as a source to maximize connection chances
|
|
if (TargetPinObj->Direction == EGPD_Input)
|
|
{
|
|
bUseOutput = true;
|
|
}
|
|
else
|
|
{
|
|
bUseOutput = false;
|
|
}
|
|
}
|
|
|
|
int32 InputPinIndex = -1;
|
|
int32 OutputPinIndex = -1;
|
|
|
|
if (KnotPtr->ShouldDrawNodeAsControlPointOnly(InputPinIndex, OutputPinIndex))
|
|
{
|
|
// Switch the effective valid pin so it makes sense for the current drag context
|
|
if (bUseOutput)
|
|
{
|
|
OutValidPins.Add(KnotPtr->GetPinAt(OutputPinIndex));
|
|
}
|
|
else
|
|
{
|
|
OutValidPins.Add(KnotPtr->GetPinAt(InputPinIndex));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Fall back to the default behavior
|
|
FDragConnection::ValidateGraphPinList(OutValidPins);
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////////
|
|
// SGraphPinKnot
|
|
|
|
void SGraphPinKnot::Construct(const FArguments& InArgs, UEdGraphPin* InPin)
|
|
{
|
|
SGraphPin::Construct(SGraphPin::FArguments().SideToSideMargin(0.0f), InPin);
|
|
}
|
|
|
|
void SGraphPinKnot::OnDragEnter(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent)
|
|
{
|
|
TSharedPtr<FDragDropOperation> Operation = DragDropEvent.GetOperation();
|
|
if (Operation.IsValid() && Operation->IsOfType<FDragConnection>())
|
|
{
|
|
TSharedPtr<FDragConnection> DragConnectionOp = StaticCastSharedPtr<FDragConnection>(Operation);
|
|
|
|
TArray<UEdGraphPin*> ValidPins;
|
|
DragConnectionOp->ValidateGraphPinList(/*out*/ ValidPins);
|
|
|
|
if (ValidPins.Num() > 0)
|
|
{
|
|
UEdGraphPin* PinToHoverOver = nullptr;
|
|
UEdGraphNode* Knot = GraphPinObj->GetOwningNode();
|
|
int32 InputPinIndex = -1;
|
|
int32 OutputPinIndex = -1;
|
|
|
|
if (Knot != nullptr && Knot->ShouldDrawNodeAsControlPointOnly(InputPinIndex, OutputPinIndex))
|
|
{
|
|
// Dragging to another pin, pick the opposite direction as a source to maximize connection chances
|
|
PinToHoverOver = (ValidPins[0]->Direction == EGPD_Input) ? Knot->GetPinAt(OutputPinIndex) : Knot->GetPinAt(InputPinIndex);
|
|
check(PinToHoverOver);
|
|
}
|
|
|
|
if (PinToHoverOver != nullptr)
|
|
{
|
|
DragConnectionOp->SetHoveredPin(PinToHoverOver);
|
|
|
|
// Pins treat being dragged over the same as being hovered outside of drag and drop if they know how to respond to the drag action.
|
|
SBorder::OnMouseEnter(MyGeometry, DragDropEvent);
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
SGraphPin::OnDragEnter(MyGeometry, DragDropEvent);
|
|
}
|
|
|
|
FSlateColor SGraphPinKnot::GetPinColor() const
|
|
{
|
|
// Make ourselves transparent if we're the input, since we are underneath the output pin and would double-blend looking ugly
|
|
if (UEdGraphPin* PinObj = GetPinObj())
|
|
{
|
|
if (PinObj->Direction == EEdGraphPinDirection::EGPD_Input)
|
|
{
|
|
return FLinearColor::Transparent;
|
|
}
|
|
}
|
|
return SGraphPin::GetPinColor();
|
|
}
|
|
|
|
TSharedRef<SWidget> SGraphPinKnot::GetDefaultValueWidget()
|
|
{
|
|
return SNullWidget::NullWidget;
|
|
}
|
|
|
|
TSharedRef<FDragDropOperation> SGraphPinKnot::SpawnPinDragEvent(const TSharedRef<SGraphPanel>& InGraphPanel, const TArray< TSharedRef<SGraphPin> >& InStartingPins)
|
|
{
|
|
FAmbivalentDirectionDragConnection::FDraggedPinTable PinHandles;
|
|
PinHandles.Reserve(InStartingPins.Num());
|
|
// since the graph can be refreshed and pins can be reconstructed/replaced
|
|
// behind the scenes, the DragDropOperation holds onto FGraphPinHandles
|
|
// instead of direct widgets/graph-pins
|
|
for (const TSharedRef<SGraphPin>& PinWidget : InStartingPins)
|
|
{
|
|
PinHandles.Add(PinWidget->GetPinObj());
|
|
}
|
|
|
|
TSharedRef<FAmbivalentDirectionDragConnection> Operation = FAmbivalentDirectionDragConnection::New(GetPinObj()->GetOwningNode(), InGraphPanel, PinHandles);
|
|
return Operation;
|
|
}
|
|
|
|
FReply SGraphPinKnot::OnPinMouseDown(const FGeometry& SenderGeometry, const FPointerEvent& MouseEvent)
|
|
{
|
|
if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton)
|
|
{
|
|
if (!GraphPinObj->bNotConnectable && IsEditingEnabled())
|
|
{
|
|
if (MouseEvent.IsAltDown())
|
|
{
|
|
// Normally break connections, but overloaded here to delete the node entirely
|
|
const FScopedTransaction Transaction(FGenericCommands::Get().Delete->GetDescription());
|
|
|
|
UEdGraphNode* NodeToDelete = GetPinObj()->GetOwningNode();
|
|
if (NodeToDelete != nullptr)
|
|
{
|
|
UEdGraph* Graph = NodeToDelete->GetGraph();
|
|
if (Graph != nullptr)
|
|
{
|
|
const UEdGraphSchema* Schema = Graph->GetSchema();
|
|
if (Schema != nullptr && Schema->SafeDeleteNodeFromGraph(Graph, NodeToDelete))
|
|
{
|
|
return FReply::Handled();
|
|
}
|
|
}
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
else if (MouseEvent.IsControlDown())
|
|
{
|
|
// Normally moves the connections from one pin to another, but moves the node instead since it's really representing a set of connections
|
|
// Returning unhandled will cause the node behind us to catch it and move us
|
|
return FReply::Unhandled();
|
|
}
|
|
}
|
|
}
|
|
|
|
return SGraphPin::OnPinMouseDown(SenderGeometry, MouseEvent);
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// SGraphNodeKnot
|
|
|
|
void SGraphNodeKnot::Construct(const FArguments& InArgs, UEdGraphNode* InKnot)
|
|
{
|
|
int32 InputPinIndex = -1;
|
|
int32 OutputPinIndex = -1;
|
|
verify(InKnot->ShouldDrawNodeAsControlPointOnly(InputPinIndex, OutputPinIndex) == true &&
|
|
InputPinIndex >= 0 && OutputPinIndex >= 0);
|
|
SGraphNodeDefault::Construct(SGraphNodeDefault::FArguments().GraphNodeObj(InKnot));
|
|
}
|
|
|
|
|
|
void SGraphNodeKnot::UpdateGraphNode()
|
|
{
|
|
InputPins.Empty();
|
|
OutputPins.Empty();
|
|
|
|
// Reset variables that are going to be exposed, in case we are refreshing an already setup node.
|
|
RightNodeBox.Reset();
|
|
LeftNodeBox.Reset();
|
|
|
|
//@TODO: Keyboard focus on edit doesn't work unless the node is visible, but the text is just the comment and it's already shown in a bubble, so Transparent black it is...
|
|
InlineEditableText = SNew(SInlineEditableTextBlock)
|
|
.ColorAndOpacity(FLinearColor::Transparent)
|
|
.Style(FAppStyle::Get(), "Graph.Node.NodeTitleInlineEditableText")
|
|
.Text(this, &SGraphNodeKnot::GetEditableNodeTitleAsText)
|
|
.OnVerifyTextChanged(this, &SGraphNodeKnot::OnVerifyNameTextChanged)
|
|
.OnTextCommitted(this, &SGraphNodeKnot::OnNameTextCommited)
|
|
.IsReadOnly(this, &SGraphNodeKnot::IsNameReadOnly)
|
|
.IsSelected(this, &SGraphNodeKnot::IsSelectedExclusively);
|
|
|
|
this->ContentScale.Bind( this, &SGraphNode::GetContentScale );
|
|
|
|
SetupErrorReporting();
|
|
|
|
this->GetOrAddSlot( ENodeZone::Center )
|
|
.HAlign(HAlign_Center)
|
|
.VAlign(VAlign_Center)
|
|
[
|
|
SNew(SOverlay)
|
|
+SOverlay::Slot()
|
|
[
|
|
// Grab handle to be able to move the node
|
|
SNew(SSpacer)
|
|
.Size(SKnotNodeDefinitions::NodeSpacerSize)
|
|
.Visibility(EVisibility::Visible)
|
|
.Cursor(EMouseCursor::CardinalCross)
|
|
]
|
|
|
|
+SOverlay::Slot()
|
|
// .VAlign(VAlign_Center)
|
|
// .HAlign(HAlign_Center)
|
|
[
|
|
SNew(SVerticalBox)
|
|
+SVerticalBox::Slot()
|
|
.VAlign(VAlign_Top)
|
|
.HAlign(HAlign_Center)
|
|
[
|
|
SNew(SHorizontalBox)
|
|
+ SHorizontalBox::Slot()
|
|
.AutoWidth()
|
|
[
|
|
SNew(SOverlay)
|
|
+SOverlay::Slot()
|
|
[
|
|
// LEFT
|
|
SAssignNew(LeftNodeBox, SVerticalBox)
|
|
]
|
|
+SOverlay::Slot()
|
|
[
|
|
// RIGHT
|
|
SAssignNew(RightNodeBox, SVerticalBox)
|
|
]
|
|
]
|
|
]
|
|
+SVerticalBox::Slot()
|
|
.VAlign(VAlign_Bottom)
|
|
.HAlign(HAlign_Center)
|
|
.AutoHeight()
|
|
[
|
|
ErrorReporting->AsWidget()
|
|
]
|
|
]
|
|
];
|
|
// Create comment bubble
|
|
const FSlateColor CommentColor = GetDefault<UGraphEditorSettings>()->DefaultCommentNodeTitleColor;
|
|
|
|
SAssignNew(CommentBubble, SCommentBubble)
|
|
.GraphNode(GraphNode)
|
|
.Text(this, &SGraphNode::GetNodeComment)
|
|
.OnTextCommitted(this, &SGraphNode::OnCommentTextCommitted)
|
|
.EnableTitleBarBubble(true)
|
|
.EnableBubbleCtrls(true)
|
|
.AllowPinning(true)
|
|
.ColorAndOpacity(CommentColor)
|
|
.GraphLOD(this, &SGraphNode::GetCurrentLOD)
|
|
.IsGraphNodeHovered(this, &SGraphNode::IsHovered)
|
|
.OnToggled(this, &SGraphNode::OnCommentBubbleToggled);
|
|
|
|
GetOrAddSlot(ENodeZone::TopCenter)
|
|
.SlotOffset2f(TAttribute<FVector2f>(this, &SGraphNodeKnot::GetCommentOffset))
|
|
.SlotSize2f( TAttribute<FVector2f>( CommentBubble.Get(), &SCommentBubble::GetSize2f))
|
|
.AllowScaling( TAttribute<bool>(CommentBubble.Get(), &SCommentBubble::IsScalingAllowed))
|
|
.VAlign(VAlign_Top)
|
|
[
|
|
CommentBubble.ToSharedRef()
|
|
];
|
|
CreatePinWidgets();
|
|
}
|
|
|
|
const FSlateBrush* SGraphNodeKnot::GetShadowBrush(bool bSelected) const
|
|
{
|
|
return bSelected ? FAppStyle::GetBrush(TEXT("Graph.Node.ShadowSelected")) : FAppStyle::GetNoBrush();
|
|
}
|
|
|
|
TSharedPtr<SGraphPin> SGraphNodeKnot::CreatePinWidget(UEdGraphPin* Pin) const
|
|
{
|
|
return SNew(SGraphPinKnot, Pin);
|
|
}
|
|
|
|
void SGraphNodeKnot::AddPin(const TSharedRef<SGraphPin>& PinToAdd)
|
|
{
|
|
PinToAdd->SetOwner(SharedThis(this));
|
|
|
|
const UEdGraphPin* PinObj = PinToAdd->GetPinObj();
|
|
PinToAdd->SetShowLabel(false);
|
|
|
|
if (PinToAdd->GetDirection() == EEdGraphPinDirection::EGPD_Input)
|
|
{
|
|
LeftNodeBox->AddSlot()
|
|
.AutoHeight()
|
|
.HAlign(HAlign_Left)
|
|
.VAlign(VAlign_Center)
|
|
//.Padding(10, 4)
|
|
[
|
|
PinToAdd
|
|
];
|
|
InputPins.Add(PinToAdd);
|
|
}
|
|
else
|
|
{
|
|
RightNodeBox->AddSlot()
|
|
.AutoHeight()
|
|
.HAlign(HAlign_Right)
|
|
.VAlign(VAlign_Center)
|
|
//.Padding(10, 4)
|
|
[
|
|
PinToAdd
|
|
];
|
|
OutputPins.Add(PinToAdd);
|
|
}
|
|
}
|
|
|
|
FVector2f SGraphNodeKnot::GetCommentOffset() const
|
|
{
|
|
const bool bBubbleVisible = GraphNode->bCommentBubbleVisible || bAlwaysShowCommentBubble;
|
|
const float ZoomAmount = GraphNode->bCommentBubblePinned && OwnerGraphPanelPtr.IsValid() ? OwnerGraphPanelPtr.Pin()->GetZoomAmount() : 1.f;
|
|
const float NodeWidthOffset = bBubbleVisible ? SKnotNodeDefinitions::KnotCenterBubbleAdjust * ZoomAmount :
|
|
SKnotNodeDefinitions::KnotCenterButtonAdjust * ZoomAmount;
|
|
|
|
return FVector2f(NodeWidthOffset - CommentBubble->GetArrowCenterOffset(), -CommentBubble->GetDesiredSize().Y);
|
|
}
|
|
|
|
void SGraphNodeKnot::OnCommentBubbleToggled(bool bInCommentBubbleVisible)
|
|
{
|
|
SGraphNode::OnCommentBubbleToggled(bInCommentBubbleVisible);
|
|
bAlwaysShowCommentBubble = bInCommentBubbleVisible;
|
|
}
|
|
|
|
void SGraphNodeKnot::OnCommentTextCommitted(const FText& NewComment, ETextCommit::Type CommitInfo)
|
|
{
|
|
SGraphNode::OnCommentTextCommitted(NewComment, CommitInfo);
|
|
if (!bAlwaysShowCommentBubble && !CommentBubble->TextBlockHasKeyboardFocus() && !CommentBubble->IsHovered())
|
|
{
|
|
// Hide the comment bubble if visibility hasn't changed
|
|
CommentBubble->SetCommentBubbleVisibility(/*bVisible =*/false);
|
|
}
|
|
}
|