// Copyright Epic Games, Inc. All Rights Reserved. #include "ControlRigGraphDetails.h" #include "Widgets/SWidget.h" #include "DetailLayoutBuilder.h" #include "DetailCategoryBuilder.h" #include "DetailWidgetRow.h" #include "IDetailChildrenBuilder.h" #include "EditorStyleSet.h" #include "SPinTypeSelector.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Colors/SColorPicker.h" #include "Widgets/Text/SInlineEditableTextBlock.h" #include "Widgets/Text/SMultiLineEditableText.h" #include "PropertyCustomizationHelpers.h" #include "NodeFactory.h" #include "Graph/ControlRigGraphNode.h" #include "ControlRig.h" #include "RigVMCore/RigVMExternalVariable.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Graph/ControlRigGraphSchema.h" #include "EditorCategoryUtils.h" #include "IPropertyUtilities.h" #include "Graph/SControlRigGraphPinVariableBinding.h" #define LOCTEXT_NAMESPACE "ControlRigGraphDetails" FControlRigArgumentGroupLayout::FControlRigArgumentGroupLayout( URigVMGraph* InGraph, UControlRigBlueprint* InBlueprint, TWeakPtr InEditor, bool bInputs) : GraphPtr(InGraph) , ControlRigBlueprintPtr(InBlueprint) , ControlRigEditorPtr(InEditor) , bIsInputGroup(bInputs) { if (ControlRigBlueprintPtr.IsValid()) { ControlRigBlueprintPtr.Get()->OnModified().AddRaw(this, &FControlRigArgumentGroupLayout::HandleModifiedEvent); } } FControlRigArgumentGroupLayout::~FControlRigArgumentGroupLayout() { if (ControlRigBlueprintPtr.IsValid()) { ControlRigBlueprintPtr.Get()->OnModified().RemoveAll(this); } } void FControlRigArgumentGroupLayout::GenerateChildContent(IDetailChildrenBuilder& ChildrenBuilder) { bool WasContentAdded = false; if (GraphPtr.IsValid()) { URigVMGraph* Graph = GraphPtr.Get(); if (URigVMLibraryNode* LibraryNode = Cast(Graph->GetOuter())) { for (URigVMPin* Pin : LibraryNode->GetPins()) { if ((bIsInputGroup && (Pin->GetDirection() == ERigVMPinDirection::Input || Pin->GetDirection() == ERigVMPinDirection::IO)) || (!bIsInputGroup && (Pin->GetDirection() == ERigVMPinDirection::Output || Pin->GetDirection() == ERigVMPinDirection::IO))) { TSharedRef ControlRigArgumentLayout = MakeShareable(new FControlRigArgumentLayout( Pin, Graph, ControlRigBlueprintPtr.Get(), ControlRigEditorPtr )); ChildrenBuilder.AddCustomBuilder(ControlRigArgumentLayout); WasContentAdded = true; } } } } if (!WasContentAdded) { // Add a text widget to let the user know to hit the + icon to add parameters. ChildrenBuilder.AddCustomRow(FText::GetEmpty()).WholeRowContent() .MaxDesiredWidth(980.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(0.0f, 0.0f, 4.0f, 0.0f) .AutoWidth() [ SNew(STextBlock) .Text(LOCTEXT("NoArgumentsAddedForControlRig", "Please press the + icon above to add parameters")) .Font(IDetailLayoutBuilder::GetDetailFont()) ] ]; } } void FControlRigArgumentGroupLayout::HandleModifiedEvent(ERigVMGraphNotifType InNotifType, URigVMGraph* InGraph, UObject* InSubject) { if (!GraphPtr.IsValid()) { return; } URigVMGraph* Graph = GraphPtr.Get(); URigVMLibraryNode* LibraryNode = Cast(Graph->GetOuter()); if (LibraryNode == nullptr) { return; } switch (InNotifType) { case ERigVMGraphNotifType::PinAdded: case ERigVMGraphNotifType::PinRemoved: case ERigVMGraphNotifType::PinIndexChanged: case ERigVMGraphNotifType::PinTypeChanged: { URigVMPin* Pin = CastChecked(InSubject); if (Pin->GetNode() == LibraryNode) { OnRebuildChildren.ExecuteIfBound(); } break; } default: { break; } } } class FControlRigArgumentPinTypeSelectorFilter : public IPinTypeSelectorFilter { public: FControlRigArgumentPinTypeSelectorFilter(TWeakPtr InControlRigEditor, TWeakObjectPtr InGraph) : ControlRigEditorPtr(InControlRigEditor), GraphPtr(InGraph) { } virtual bool ShouldShowPinTypeTreeItem(FPinTypeTreeItem InItem) const override { if (!InItem.IsValid()) { return false; } // Only allow an execute context pin if the graph doesnt have one already FString CPPType; UObject* CPPTypeObject = nullptr; RigVMTypeUtils::CPPTypeFromPinType(InItem.Get()->GetPinType(false), CPPType, &CPPTypeObject); if (UScriptStruct* ScriptStruct = Cast(CPPTypeObject)) { if (ScriptStruct->IsChildOf(FRigVMExecuteContext::StaticStruct())) { if (GraphPtr.IsValid()) { if (URigVMFunctionEntryNode* EntryNode = GraphPtr.Get()->GetEntryNode()) { for (URigVMPin* Pin : EntryNode->GetPins()) { if (Pin->IsExecuteContext()) { return false; } } } } } } if (ControlRigEditorPtr.IsValid()) { if (ControlRigEditorPtr.Pin()->GetImportedPinTypeSelectorFilter().IsValid()) { return ControlRigEditorPtr.Pin()->GetImportedPinTypeSelectorFilter()->ShouldShowPinTypeTreeItem(InItem); } } return false; } private: TWeakPtr ControlRigEditorPtr; TWeakObjectPtr GraphPtr; }; void FControlRigArgumentLayout::GenerateHeaderRowContent(FDetailWidgetRow& NodeRow) { const UEdGraphSchema* Schema = GetDefault(); ETypeTreeFilter TypeTreeFilter = ETypeTreeFilter::None; TypeTreeFilter |= ETypeTreeFilter::AllowExec; TSharedPtr CustomPinTypeFilter; if (ControlRigEditorPtr.IsValid()) { CustomPinTypeFilter = MakeShared(ControlRigEditorPtr, GraphPtr); } NodeRow .NameContent() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1) .VAlign(VAlign_Center) [ SAssignNew(ArgumentNameWidget, SEditableTextBox) .Text(this, &FControlRigArgumentLayout::OnGetArgNameText) .OnTextCommitted(this, &FControlRigArgumentLayout::OnArgNameTextCommitted) .ToolTipText(this, &FControlRigArgumentLayout::OnGetArgToolTipText) .Font(IDetailLayoutBuilder::GetDetailFont()) .IsEnabled(!ShouldPinBeReadOnly()) .OnVerifyTextChanged_Lambda([&](const FText& InNewText, FText& OutErrorMessage) -> bool { if (InNewText.IsEmpty()) { OutErrorMessage = LOCTEXT("ArgumentNameEmpty", "Cannot have an argument with an emtpy string name."); return false; } else if (InNewText.ToString().Len() >= NAME_SIZE) { OutErrorMessage = LOCTEXT("ArgumentNameTooLong", "Name of argument is too long."); return false; } EValidatorResult Result = NameValidator.IsValid(InNewText.ToString(), false); OutErrorMessage = INameValidatorInterface::GetErrorText(InNewText.ToString(), Result); return Result == EValidatorResult::Ok || Result == EValidatorResult::ExistingName; }) ] ] .ValueContent() .MaxDesiredWidth(980.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(0.0f, 0.0f, 4.0f, 0.0f) .AutoWidth() [ SNew(SPinTypeSelector, FGetPinTypeTree::CreateUObject(GetDefault(), &UEdGraphSchema_K2::GetVariableTypeTree)) .TargetPinType(this, &FControlRigArgumentLayout::OnGetPinInfo) .OnPinTypePreChanged(this, &FControlRigArgumentLayout::OnPrePinInfoChange) .OnPinTypeChanged(this, &FControlRigArgumentLayout::PinInfoChanged) .Schema(Schema) .TypeTreeFilter(TypeTreeFilter) .bAllowArrays(!ShouldPinBeReadOnly()) .IsEnabled(!ShouldPinBeReadOnly(true)) .CustomFilter(CustomPinTypeFilter) .Font(IDetailLayoutBuilder::GetDetailFont()) ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(SButton) .ButtonStyle(FAppStyle::Get(), TEXT("SimpleButton")) .ContentPadding(0) .IsEnabled(!IsPinEditingReadOnly()) .OnClicked(this, &FControlRigArgumentLayout::OnArgMoveUp) .ToolTipText(LOCTEXT("FunctionArgDetailsArgMoveUpTooltip", "Move this parameter up in the list.")) [ SNew(SImage) .Image(FEditorStyle::GetBrush("Icons.ChevronUp")) .ColorAndOpacity(FSlateColor::UseForeground()) ] ] + SHorizontalBox::Slot() .AutoWidth() .Padding(2, 0) [ SNew(SButton) .ButtonStyle(FAppStyle::Get(), TEXT("SimpleButton")) .ContentPadding(0) .IsEnabled(!IsPinEditingReadOnly()) .OnClicked(this, &FControlRigArgumentLayout::OnArgMoveDown) .ToolTipText(LOCTEXT("FunctionArgDetailsArgMoveDownTooltip", "Move this parameter down in the list.")) [ SNew(SImage) .Image(FEditorStyle::GetBrush("Icons.ChevronDown")) .ColorAndOpacity(FSlateColor::UseForeground()) ] ] + SHorizontalBox::Slot() .HAlign(HAlign_Right) .VAlign(VAlign_Center) .Padding(10, 0, 0, 0) .AutoWidth() [ PropertyCustomizationHelpers::MakeClearButton(FSimpleDelegate::CreateSP(this, &FControlRigArgumentLayout::OnRemoveClicked), LOCTEXT("FunctionArgDetailsClearTooltip", "Remove this parameter."), !IsPinEditingReadOnly()) ] ]; } void FControlRigArgumentLayout::GenerateChildContent(IDetailChildrenBuilder& ChildrenBuilder) { // we don't show defaults here - we rely on a SControlRigGraphNode widget in the top of the details } void FControlRigArgumentLayout::OnRemoveClicked() { if (PinPtr.IsValid() && ControlRigBlueprintPtr.IsValid()) { URigVMPin* Pin = PinPtr.Get(); UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); if (URigVMLibraryNode* LibraryNode = Cast(Pin->GetNode())) { if (URigVMController* Controller = Blueprint->GetController(LibraryNode->GetContainedGraph())) { Controller->RemoveExposedPin(Pin->GetFName(), true, true); } } } } FReply FControlRigArgumentLayout::OnArgMoveUp() { if (PinPtr.IsValid() && ControlRigBlueprintPtr.IsValid()) { URigVMPin* Pin = PinPtr.Get(); UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); if (URigVMLibraryNode* LibraryNode = Cast(Pin->GetNode())) { if (URigVMController* Controller = Blueprint->GetController(LibraryNode->GetContainedGraph())) { bool bIsInput = Pin->GetDirection() == ERigVMPinDirection::Input || Pin->GetDirection() == ERigVMPinDirection::IO; int32 NewPinIndex = Pin->GetPinIndex() - 1; while (NewPinIndex != INDEX_NONE) { URigVMPin* OtherPin = LibraryNode->GetPins()[NewPinIndex]; if (bIsInput) { if (OtherPin->GetDirection() == ERigVMPinDirection::Input || OtherPin->GetDirection() == ERigVMPinDirection::IO) { break; } } else { if (OtherPin->GetDirection() == ERigVMPinDirection::Output || OtherPin->GetDirection() == ERigVMPinDirection::IO) { break; } } --NewPinIndex; } if (NewPinIndex != INDEX_NONE) { Controller->SetExposedPinIndex(Pin->GetFName(), NewPinIndex, true, true); } return FReply::Handled(); } } } return FReply::Unhandled(); } FReply FControlRigArgumentLayout::OnArgMoveDown() { if (PinPtr.IsValid() && ControlRigBlueprintPtr.IsValid()) { URigVMPin* Pin = PinPtr.Get(); UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); if (URigVMLibraryNode* LibraryNode = Cast(Pin->GetNode())) { if (URigVMController* Controller = Blueprint->GetController(LibraryNode->GetContainedGraph())) { bool bIsInput = Pin->GetDirection() == ERigVMPinDirection::Input || Pin->GetDirection() == ERigVMPinDirection::IO; int32 NewPinIndex = Pin->GetPinIndex() + 1; while (NewPinIndex < LibraryNode->GetPins().Num()) { URigVMPin* OtherPin = LibraryNode->GetPins()[NewPinIndex]; if (bIsInput) { if (OtherPin->GetDirection() == ERigVMPinDirection::Input || OtherPin->GetDirection() == ERigVMPinDirection::IO) { break; } } else { if (OtherPin->GetDirection() == ERigVMPinDirection::Output || OtherPin->GetDirection() == ERigVMPinDirection::IO) { break; } } ++NewPinIndex; } if (NewPinIndex < LibraryNode->GetPins().Num()) { Controller->SetExposedPinIndex(Pin->GetFName(), NewPinIndex, true, true); } return FReply::Handled(); } } } return FReply::Unhandled(); } bool FControlRigArgumentLayout::ShouldPinBeReadOnly(bool bIsEditingPinType/* = false*/) const { return IsPinEditingReadOnly(bIsEditingPinType); } bool FControlRigArgumentLayout::IsPinEditingReadOnly(bool bIsEditingPinType/* = false*/) const { /* if (PinPtr.IsValid()) { return PinPtr.Get()->IsExecuteContext(); } */ return false; } FText FControlRigArgumentLayout::OnGetArgNameText() const { if (PinPtr.IsValid()) { return FText::FromName(PinPtr.Get()->GetFName()); } return FText(); } FText FControlRigArgumentLayout::OnGetArgToolTipText() const { return OnGetArgNameText(); // for now since we don't have tooltips } void FControlRigArgumentLayout::OnArgNameTextCommitted(const FText& NewText, ETextCommit::Type InTextCommit) { if (!NewText.IsEmpty() && PinPtr.IsValid() && ControlRigBlueprintPtr.IsValid() && !ShouldPinBeReadOnly()) { URigVMPin* Pin = PinPtr.Get(); UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); if (URigVMLibraryNode* LibraryNode = Cast(Pin->GetNode())) { if (URigVMController* Controller = Blueprint->GetController(LibraryNode->GetContainedGraph())) { const FString& NewName = NewText.ToString(); Controller->RenameExposedPin(Pin->GetFName(), *NewName, true, true); } } } } FEdGraphPinType FControlRigArgumentLayout::OnGetPinInfo() const { if (PinPtr.IsValid()) { return UControlRigGraphNode::GetPinTypeForModelPin(PinPtr.Get()); } return FEdGraphPinType(); } void FControlRigArgumentLayout::PinInfoChanged(const FEdGraphPinType& PinType) { if (PinPtr.IsValid() && ControlRigBlueprintPtr.IsValid() && FBlueprintEditorUtils::IsPinTypeValid(PinType)) { URigVMPin* Pin = PinPtr.Get(); UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); if (URigVMLibraryNode* LibraryNode = Cast(Pin->GetNode())) { if (URigVMController* Controller = Blueprint->GetController(LibraryNode->GetContainedGraph())) { FString CPPType; FName CPPTypeObjectName = NAME_None; RigVMTypeUtils::CPPTypeFromPinType(PinType, CPPType, CPPTypeObjectName); bool bSetupUndoRedo = true; Controller->ChangeExposedPinType(Pin->GetFName(), CPPType, CPPTypeObjectName, bSetupUndoRedo, false, true); // If the controller has identified this as a bulk change, it has not added the actions to the action stack // We need to disable the transaction from the UI as well to keep them synced if (!bSetupUndoRedo) { GEditor->CancelTransaction(0); } } } } } void FControlRigArgumentLayout::OnPrePinInfoChange(const FEdGraphPinType& PinType) { // not needed for Control Rig } FControlRigArgumentDefaultNode::FControlRigArgumentDefaultNode( URigVMGraph* InGraph, UControlRigBlueprint* InBlueprint ) : GraphPtr(InGraph) , ControlRigBlueprintPtr(InBlueprint) { if (GraphPtr.IsValid() && ControlRigBlueprintPtr.IsValid()) { ControlRigBlueprintPtr.Get()->OnModified().AddRaw(this, &FControlRigArgumentDefaultNode::HandleModifiedEvent); if (URigVMLibraryNode* LibraryNode = Cast(GraphPtr->GetOuter())) { if (UControlRigGraph* RigGraph = Cast(ControlRigBlueprintPtr->GetEdGraph(LibraryNode->GetGraph()))) { GraphChangedDelegateHandle = RigGraph->AddOnGraphChangedHandler( FOnGraphChanged::FDelegate::CreateRaw(this, &FControlRigArgumentDefaultNode::OnGraphChanged) ); } } } } FControlRigArgumentDefaultNode::~FControlRigArgumentDefaultNode() { if (GraphPtr.IsValid() && ControlRigBlueprintPtr.IsValid()) { ControlRigBlueprintPtr.Get()->OnModified().RemoveAll(this); if (URigVMLibraryNode* LibraryNode = Cast(GraphPtr->GetOuter())) { if (UControlRigGraph* RigGraph = Cast(ControlRigBlueprintPtr->GetEdGraph(LibraryNode->GetGraph()))) { if (GraphChangedDelegateHandle.IsValid()) { RigGraph->RemoveOnGraphChangedHandler(GraphChangedDelegateHandle); } } } } } void FControlRigArgumentDefaultNode::GenerateChildContent(IDetailChildrenBuilder& ChildrenBuilder) { if (!GraphPtr.IsValid() || !ControlRigBlueprintPtr.IsValid()) { return; } UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); URigVMGraph* Graph = GraphPtr.Get(); UControlRigGraphNode* ControlRigGraphNode = nullptr; if (URigVMLibraryNode* LibraryNode = Cast(Graph->GetOuter())) { if (UControlRigGraph* RigGraph = Cast(Blueprint->GetEdGraph(LibraryNode->GetGraph()))) { ControlRigGraphNode = Cast(RigGraph->FindNodeForModelNodeName(LibraryNode->GetFName())); } } if (ControlRigGraphNode == nullptr) { return; } ChildrenBuilder.AddCustomRow(FText::GetEmpty()) .WholeRowContent() .MaxDesiredWidth(980.f) [ SAssignNew(OwnedNodeWidget, SControlRigGraphNode).GraphNodeObj(ControlRigGraphNode) ]; OwnedNodeWidget->SetIsEditable(true); TArray< TSharedRef > Pins; OwnedNodeWidget->GetPins(Pins); for (TSharedRef Pin : Pins) { TSharedRef SPin = StaticCastSharedRef(Pin); SPin->GetPinObj()->bNotConnectable = true; } } void FControlRigArgumentDefaultNode::OnGraphChanged(const FEdGraphEditAction& InAction) { if (GraphPtr.IsValid() && ControlRigBlueprintPtr.IsValid()) { OnRebuildChildren.ExecuteIfBound(); } } void FControlRigArgumentDefaultNode::HandleModifiedEvent(ERigVMGraphNotifType InNotifType, URigVMGraph* InGraph, UObject* InSubject) { if (!GraphPtr.IsValid()) { return; } URigVMGraph* Graph = GraphPtr.Get(); URigVMLibraryNode* LibraryNode = Cast(Graph->GetOuter()); if (LibraryNode == nullptr) { return; } if (LibraryNode->GetGraph() != InGraph) { return; } switch (InNotifType) { case ERigVMGraphNotifType::PinAdded: case ERigVMGraphNotifType::PinRemoved: case ERigVMGraphNotifType::PinTypeChanged: case ERigVMGraphNotifType::PinIndexChanged: case ERigVMGraphNotifType::PinRenamed: { URigVMPin* Pin = CastChecked(InSubject); if (Pin->GetNode() == LibraryNode) { OnRebuildChildren.ExecuteIfBound(); } break; } case ERigVMGraphNotifType::NodeRenamed: case ERigVMGraphNotifType::NodeColorChanged: { URigVMNode* Node = CastChecked(InSubject); if (Node == LibraryNode) { OnRebuildChildren.ExecuteIfBound(); } break; } default: { break; } } } TSharedPtr FControlRigGraphDetails::MakeInstance(TSharedPtr InBlueprintEditor) { const TArray* Objects = (InBlueprintEditor.IsValid() ? InBlueprintEditor->GetObjectsCurrentlyBeingEdited() : nullptr); if (Objects && Objects->Num() == 1) { if (UControlRigBlueprint* ControlRigBlueprint = Cast((*Objects)[0])) { return MakeShareable(new FControlRigGraphDetails(StaticCastSharedPtr(InBlueprintEditor), ControlRigBlueprint)); } } return nullptr; } void FControlRigGraphDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) { bIsPickingColor = false; TArray> Objects; DetailLayout.GetObjectsBeingCustomized(Objects); GraphPtr = CastChecked(Objects[0].Get()); UControlRigGraph* Graph = GraphPtr.Get(); UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); URigVMGraph* Model = nullptr; URigVMController* Controller = nullptr; if (Blueprint) { Model = Blueprint->GetModel(Graph); Controller = Blueprint->GetController(Model); } if (Blueprint == nullptr || Model == nullptr || Controller == nullptr) { IDetailCategoryBuilder& Category = DetailLayout.EditCategory("Graph", LOCTEXT("FunctionDetailsGraph", "Graph")); Category.AddCustomRow(FText::GetEmpty()) [ SNew(STextBlock) .Text(LOCTEXT("GraphPresentButNotEditable", "Graph is not editable.")) ]; return; } if (Model->IsTopLevelGraph()) { IDetailCategoryBuilder& Category = DetailLayout.EditCategory("Graph", LOCTEXT("FunctionDetailsGraph", "Graph")); Category.AddCustomRow(FText::GetEmpty()) [ SNew(STextBlock) .Text(LOCTEXT("GraphIsTopLevelGraph", "Top-level Graphs are not editable.")) ]; return; } IDetailCategoryBuilder& InputsCategory = DetailLayout.EditCategory("Inputs", LOCTEXT("FunctionDetailsInputs", "Inputs")); TSharedRef InputArgumentGroup = MakeShareable(new FControlRigArgumentGroupLayout( Model, Blueprint, ControlRigEditorPtr, true)); InputsCategory.AddCustomBuilder(InputArgumentGroup); TSharedRef InputsHeaderContentWidget = SNew(SHorizontalBox); InputsHeaderContentWidget->AddSlot() .HAlign(HAlign_Right) [ SNew(SButton) .ButtonStyle(FEditorStyle::Get(), "SimpleButton") .ContentPadding(FMargin(1, 0)) .OnClicked(this, &FControlRigGraphDetails::OnAddNewInputClicked) .Visibility(this, &FControlRigGraphDetails::GetAddNewInputOutputVisibility) .HAlign(HAlign_Right) .ToolTipText(LOCTEXT("FunctionNewInputArgTooltip", "Create a new input argument")) .VAlign(VAlign_Center) .AddMetaData(FTagMetaData(TEXT("FunctionNewInputArg"))) .IsEnabled(this, &FControlRigGraphDetails::IsAddNewInputOutputEnabled) [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.PlusCircle")) .ColorAndOpacity(FSlateColor::UseForeground()) ] ]; InputsCategory.HeaderContent(InputsHeaderContentWidget); IDetailCategoryBuilder& OutputsCategory = DetailLayout.EditCategory("Outputs", LOCTEXT("FunctionDetailsOutputs", "Outputs")); TSharedRef OutputArgumentGroup = MakeShareable(new FControlRigArgumentGroupLayout( Model, Blueprint, ControlRigEditorPtr, false)); OutputsCategory.AddCustomBuilder(OutputArgumentGroup); TSharedRef OutputsHeaderContentWidget = SNew(SHorizontalBox); OutputsHeaderContentWidget->AddSlot() .HAlign(HAlign_Right) [ SNew(SButton) .ButtonStyle(FEditorStyle::Get(), "SimpleButton") .ContentPadding(FMargin(1, 0)) .OnClicked(this, &FControlRigGraphDetails::OnAddNewOutputClicked) .Visibility(this, &FControlRigGraphDetails::GetAddNewInputOutputVisibility) .HAlign(HAlign_Right) .ToolTipText(LOCTEXT("FunctionNewOutputArgTooltip", "Create a new output argument")) .VAlign(VAlign_Center) .AddMetaData(FTagMetaData(TEXT("FunctionNewOutputArg"))) .IsEnabled(this, &FControlRigGraphDetails::IsAddNewInputOutputEnabled) [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.PlusCircle")) .ColorAndOpacity(FSlateColor::UseForeground()) ] ]; OutputsCategory.HeaderContent(OutputsHeaderContentWidget); IDetailCategoryBuilder& SettingsCategory = DetailLayout.EditCategory("NodeSettings", LOCTEXT("FunctionDetailsNodeSettings", "Node Settings")); bool bIsFunction = false; if (Model) { if (URigVMLibraryNode* LibraryNode = Cast(Model->GetOuter())) { bIsFunction = LibraryNode->GetGraph()->IsA(); } } if(bIsFunction) { // node category SettingsCategory.AddCustomRow(FText::GetEmpty()) .NameContent() [ SNew(STextBlock) .Text(FText::FromString(TEXT("Category"))) .Font(IDetailLayoutBuilder::GetDetailFont()) ] .ValueContent() [ SNew(SEditableTextBox) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(this, &FControlRigGraphDetails::GetNodeCategory) .OnTextCommitted(this, &FControlRigGraphDetails::SetNodeCategory) .OnVerifyTextChanged_Lambda([&](const FText& InNewText, FText& OutErrorMessage) -> bool { const FText NewText = FEditorCategoryUtils::GetCategoryDisplayString(InNewText); if (NewText.ToString().Len() >= NAME_SIZE) { OutErrorMessage = LOCTEXT("CategoryTooLong", "Name of category is too long."); return false; } if (ControlRigBlueprintPtr.IsValid()) { if (NewText.EqualTo(FText::FromString(ControlRigBlueprintPtr.Get()->GetName()))) { OutErrorMessage = LOCTEXT("CategoryEqualsBlueprintName", "Cannot add a category with the same name as the blueprint."); return false; } } return true; }) ]; // node keywords SettingsCategory.AddCustomRow(FText::GetEmpty()) .NameContent() [ SNew(STextBlock) .Text(FText::FromString(TEXT("Keywords"))) .Font(IDetailLayoutBuilder::GetDetailFont()) ] .ValueContent() [ SNew(SEditableTextBox) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(this, &FControlRigGraphDetails::GetNodeKeywords) .OnTextCommitted(this, &FControlRigGraphDetails::SetNodeKeywords) ]; // description SettingsCategory.AddCustomRow(FText::GetEmpty()) .NameContent() [ SNew(STextBlock) .Text(FText::FromString(TEXT("Description"))) .Font(IDetailLayoutBuilder::GetDetailFont()) ] .ValueContent() [ SNew(SMultiLineEditableText) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(this, &FControlRigGraphDetails::GetNodeDescription) .OnTextCommitted(this, &FControlRigGraphDetails::SetNodeDescription) ]; if(AccessSpecifierStrings.IsEmpty()) { AccessSpecifierStrings.Add(TSharedPtr(new FString(TEXT("Public")))); AccessSpecifierStrings.Add(TSharedPtr(new FString(TEXT("Private")))); } // access specifier SettingsCategory.AddCustomRow( LOCTEXT( "AccessSpecifier", "Access Specifier" ) ) .NameContent() [ SNew(STextBlock) .Text( LOCTEXT( "AccessSpecifier", "Access Specifier" ) ) .Font( IDetailLayoutBuilder::GetDetailFont() ) ] .ValueContent() [ SNew(SComboButton) .ContentPadding(0) .ButtonContent() [ SNew(STextBlock) .Text(this, &FControlRigGraphDetails::GetCurrentAccessSpecifierName) .Font( IDetailLayoutBuilder::GetDetailFont() ) ] .MenuContent() [ SNew(SListView >) .ListItemsSource( &AccessSpecifierStrings ) .OnGenerateRow(this, &FControlRigGraphDetails::HandleGenerateRowAccessSpecifier) .OnSelectionChanged(this, &FControlRigGraphDetails::OnAccessSpecifierSelected) ] ]; } // node color SettingsCategory.AddCustomRow(FText::GetEmpty()) .NameContent() [ SNew(STextBlock) .Text(FText::FromString(TEXT("Color"))) .Font(IDetailLayoutBuilder::GetDetailFont()) ] .ValueContent() [ SNew(SButton) .ButtonStyle(FEditorStyle::Get(), "Menu.Button") .OnClicked(this, &FControlRigGraphDetails::OnNodeColorClicked) [ SAssignNew(ColorBlock, SColorBlock) .Color(this, &FControlRigGraphDetails::GetNodeColor) .Size(FVector2D(77, 16)) ] ]; IDetailCategoryBuilder& DefaultsCategory = DetailLayout.EditCategory("NodeDefaults", LOCTEXT("FunctionDetailsNodeDefaults", "Node Defaults")); TSharedRef DefaultsArgumentNode = MakeShareable(new FControlRigArgumentDefaultNode( Model, Blueprint)); DefaultsCategory.AddCustomBuilder(DefaultsArgumentNode); } bool FControlRigGraphDetails::IsAddNewInputOutputEnabled() const { return true; } EVisibility FControlRigGraphDetails::GetAddNewInputOutputVisibility() const { return EVisibility::Visible; } FReply FControlRigGraphDetails::OnAddNewInputClicked() { if (GraphPtr.IsValid() && ControlRigBlueprintPtr.IsValid()) { UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); URigVMGraph* Model = Blueprint->GetModel(GraphPtr.Get()); if (URigVMController* Controller = Blueprint->GetController(Model)) { FName ArgumentName = TEXT("Argument"); FString CPPType = TEXT("bool"); FName CPPTypeObjectPath = NAME_None; FString DefaultValue = TEXT("False"); if (URigVMLibraryNode* LibraryNode = Cast(Model->GetOuter())) { if (LibraryNode->GetPins().Num() > 0) { URigVMPin* LastPin = LibraryNode->GetPins().Last(); if (!LastPin->IsExecuteContext()) { ArgumentName = LastPin->GetFName(); CPPType = LastPin->GetCPPType(); if (LastPin->GetCPPTypeObject()) { CPPTypeObjectPath = *LastPin->GetCPPTypeObject()->GetPathName(); } DefaultValue = LastPin->GetDefaultValue(); } } } Controller->AddExposedPin(ArgumentName, ERigVMPinDirection::Input, CPPType, CPPTypeObjectPath, DefaultValue, true, true); } } return FReply::Unhandled(); } FReply FControlRigGraphDetails::OnAddNewOutputClicked() { if (GraphPtr.IsValid() && ControlRigBlueprintPtr.IsValid()) { UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); URigVMGraph* Model = Blueprint->GetModel(GraphPtr.Get()); if (URigVMController* Controller = Blueprint->GetController(Model)) { FName ArgumentName = TEXT("Argument"); FString CPPType = TEXT("bool"); FName CPPTypeObjectPath = NAME_None; FString DefaultValue = TEXT("False"); // todo: base decisions on types on last argument Controller->AddExposedPin(ArgumentName, ERigVMPinDirection::Output, CPPType, CPPTypeObjectPath, DefaultValue, true, true); } } return FReply::Unhandled(); } FText FControlRigGraphDetails::GetNodeCategory() const { if (GraphPtr.IsValid() && ControlRigBlueprintPtr.IsValid()) { UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); if (URigVMGraph* Model = Blueprint->GetModel(GraphPtr.Get())) { if (URigVMCollapseNode* OuterNode = Cast(Model->GetOuter())) { return FText::FromString(OuterNode->GetNodeCategory()); } } } return FText(); } void FControlRigGraphDetails::SetNodeCategory(const FText& InNewText, ETextCommit::Type InCommitType) { if(InCommitType == ETextCommit::OnCleared) { return; } if (GraphPtr.IsValid() && ControlRigBlueprintPtr.IsValid()) { UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); if (URigVMGraph* Model = Blueprint->GetModel(GraphPtr.Get())) { if (URigVMCollapseNode* OuterNode = Cast(Model->GetOuter())) { if (URigVMController* Controller = Blueprint->GetOrCreateController(OuterNode->GetGraph())) { Controller->SetNodeCategory(OuterNode, InNewText.ToString(), true, false, true); } } } } } FText FControlRigGraphDetails::GetNodeKeywords() const { if (GraphPtr.IsValid() && ControlRigBlueprintPtr.IsValid()) { UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); if (URigVMGraph* Model = Blueprint->GetModel(GraphPtr.Get())) { if (URigVMCollapseNode* OuterNode = Cast(Model->GetOuter())) { return FText::FromString(OuterNode->GetNodeKeywords()); } } } return FText(); } void FControlRigGraphDetails::SetNodeKeywords(const FText& InNewText, ETextCommit::Type InCommitType) { if(InCommitType == ETextCommit::OnCleared) { return; } if (GraphPtr.IsValid() && ControlRigBlueprintPtr.IsValid()) { UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); if (URigVMGraph* Model = Blueprint->GetModel(GraphPtr.Get())) { if (URigVMCollapseNode* OuterNode = Cast(Model->GetOuter())) { if (URigVMController* Controller = Blueprint->GetOrCreateController(OuterNode->GetGraph())) { Controller->SetNodeKeywords(OuterNode, InNewText.ToString(), true, false, true); } } } } } FText FControlRigGraphDetails::GetNodeDescription() const { if (GraphPtr.IsValid() && ControlRigBlueprintPtr.IsValid()) { UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); if (URigVMGraph* Model = Blueprint->GetModel(GraphPtr.Get())) { if (URigVMCollapseNode* OuterNode = Cast(Model->GetOuter())) { return FText::FromString(OuterNode->GetNodeDescription()); } } } return FText(); } void FControlRigGraphDetails::SetNodeDescription(const FText& InNewText, ETextCommit::Type InCommitType) { if(InCommitType == ETextCommit::OnCleared) { return; } if (GraphPtr.IsValid() && ControlRigBlueprintPtr.IsValid()) { UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); if (URigVMGraph* Model = Blueprint->GetModel(GraphPtr.Get())) { if (URigVMCollapseNode* OuterNode = Cast(Model->GetOuter())) { if (URigVMController* Controller = Blueprint->GetOrCreateController(OuterNode->GetGraph())) { Controller->SetNodeDescription(OuterNode, InNewText.ToString(), true, false, true); } } } } } FLinearColor FControlRigGraphDetails::GetNodeColor() const { if (GraphPtr.IsValid() && ControlRigBlueprintPtr.IsValid()) { UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); if (URigVMGraph* Model = Blueprint->GetModel(GraphPtr.Get())) { if (URigVMCollapseNode* OuterNode = Cast(Model->GetOuter())) { return OuterNode->GetNodeColor(); } } } return FLinearColor::White; } void FControlRigGraphDetails::SetNodeColor(FLinearColor InColor, bool bSetupUndoRedo) { TargetColor = InColor; if (GraphPtr.IsValid() && ControlRigBlueprintPtr.IsValid()) { UControlRigBlueprint* Blueprint = ControlRigBlueprintPtr.Get(); if (URigVMGraph* Model = Blueprint->GetModel(GraphPtr.Get())) { if (URigVMCollapseNode* OuterNode = Cast(Model->GetOuter())) { if (URigVMController* Controller = Blueprint->GetOrCreateController(OuterNode->GetGraph())) { Controller->SetNodeColor(OuterNode, TargetColor, bSetupUndoRedo, bIsPickingColor, true); } } } } } void FControlRigGraphDetails::OnNodeColorBegin() { bIsPickingColor = true; } void FControlRigGraphDetails::OnNodeColorEnd() { bIsPickingColor = false; } void FControlRigGraphDetails::OnNodeColorCancelled(FLinearColor OriginalColor) { SetNodeColor(OriginalColor, true); } FReply FControlRigGraphDetails::OnNodeColorClicked() { TargetColor = GetNodeColor(); TargetColors.Reset(); TargetColors.Add(&TargetColor); FColorPickerArgs PickerArgs; PickerArgs.ParentWidget = ColorBlock; PickerArgs.bUseAlpha = false; PickerArgs.DisplayGamma = false; PickerArgs.InitialColorOverride = TargetColor; PickerArgs.LinearColorArray = &TargetColors; PickerArgs.OnInteractivePickBegin = FSimpleDelegate::CreateSP(this, &FControlRigGraphDetails::OnNodeColorBegin); PickerArgs.OnInteractivePickEnd = FSimpleDelegate::CreateSP(this, &FControlRigGraphDetails::OnNodeColorEnd); PickerArgs.OnColorCommitted = FOnLinearColorValueChanged::CreateSP(this, &FControlRigGraphDetails::SetNodeColor, true); PickerArgs.OnColorPickerCancelled = FOnColorPickerCancelled::CreateSP(this, &FControlRigGraphDetails::OnNodeColorCancelled); OpenColorPicker(PickerArgs); return FReply::Handled(); } TArray> FControlRigGraphDetails::AccessSpecifierStrings; FText FControlRigGraphDetails::GetCurrentAccessSpecifierName() const { if(ControlRigBlueprintPtr.IsValid() && GraphPtr.IsValid()) { UControlRigGraph* Graph = GraphPtr.Get(); UControlRigBlueprint* ControlRigBlueprint = ControlRigBlueprintPtr.Get(); const FControlRigPublicFunctionData ExpectedFunctionData = Graph->GetPublicFunctionData(); if(ControlRigBlueprint->IsFunctionPublic(ExpectedFunctionData.Name)) { return FText::FromString(*AccessSpecifierStrings[0].Get()); // public } } return FText::FromString(*AccessSpecifierStrings[1].Get()); // private } void FControlRigGraphDetails::OnAccessSpecifierSelected( TSharedPtr SpecifierName, ESelectInfo::Type SelectInfo ) { if(ControlRigBlueprintPtr.IsValid() && GraphPtr.IsValid()) { UControlRigGraph* Graph = GraphPtr.Get(); UControlRigBlueprint* ControlRigBlueprint = ControlRigBlueprintPtr.Get(); const FControlRigPublicFunctionData ExpectedFunctionData = Graph->GetPublicFunctionData(); if(SpecifierName->Equals(TEXT("Private"))) { ControlRigBlueprint->MarkFunctionPublic(ExpectedFunctionData.Name, false); } else { ControlRigBlueprint->MarkFunctionPublic(ExpectedFunctionData.Name, true); } } } TSharedRef FControlRigGraphDetails::HandleGenerateRowAccessSpecifier( TSharedPtr SpecifierName, const TSharedRef& OwnerTable ) { return SNew(STableRow< TSharedPtr >, OwnerTable) .Content() [ SNew( STextBlock ) .Text(FText::FromString(*SpecifierName.Get()) ) ]; } #if !UE_RIGVM_UCLASS_BASED_STORAGE_DISABLED TSharedRef FControlRigWrappedNodeDetails::MakeInstance() { return MakeShareable(new FControlRigWrappedNodeDetails); } void FControlRigWrappedNodeDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) { TArray> Objects; DetailLayout.GetObjectsBeingCustomized(Objects); TArray Wrappers; TArray Nodes; for(const TWeakObjectPtr& Object : Objects) { UDetailsViewWrapperObject* Wrapper = CastChecked(Object); Wrappers.Add(Wrapper); URigVMNode* Node = Wrapper->GetTypedOuter(); if(Node == nullptr) { return; } Nodes.Add(Node); } UDetailsViewWrapperObject* FirstWrapper = Wrappers[0]; URigVMNode* FirstNode = Nodes[0]; TArray Categories; DetailLayout.GetCategoryNames(Categories); if(Categories.IsEmpty()) { return; } IDetailCategoryBuilder& DefaultsCategory = DetailLayout.EditCategory(Categories[0]); UControlRigBlueprint* Blueprint = FirstNode->GetTypedOuter(); if(Blueprint == nullptr) { return; } for(URigVMPin* Pin : FirstNode->GetPins()) { bool bHasAnyInputLink = false; for(URigVMNode* EachNode : Nodes) { if(URigVMPin* EachPin = EachNode->FindPin(Pin->GetName())) { if(EachPin->GetSourceLinks(true).Num() > 0) { bHasAnyInputLink = true; break; } } } TSharedPtr PinHandle = DetailLayout.GetProperty(Pin->GetFName()); if(PinHandle.IsValid()) { static const TCHAR DisabledFormat[] = TEXT("%s\n\nNote: Editing disabled since pin has an input link."); if (Pin->IsBoundToVariable()) { DefaultsCategory.AddCustomRow(FText::FromName(Pin->GetDisplayName())) .NameContent() [ PinHandle->CreatePropertyNameWidget() ] .ValueContent() [ SNew(SControlRigVariableBinding) .ModelPin(Pin) .Blueprint(Blueprint) ]; } else { DefaultsCategory.AddProperty(PinHandle) .DisplayName(FText::FromName(Pin->GetDisplayName())) .IsEnabled(!bHasAnyInputLink) .ToolTip(FText::FromString(FString::Printf(DisabledFormat, *Pin->GetToolTipText().ToString()))); } } } if(Objects.Num() > 1) { return; } UControlRig* DebuggedRig = Cast(Blueprint->GetObjectBeingDebugged()); if(DebuggedRig == nullptr) { return; } URigVM* VM = DebuggedRig->GetVM(); if(VM == nullptr) { return; } TSharedPtr AST = FirstNode->GetGraph()->GetRuntimeAST(Blueprint->VMCompileSettings.ASTSettings, false); if(!AST.IsValid()) { return; } FRigVMByteCode& ByteCode = VM->GetByteCode(); if(ByteCode.GetFirstInstructionIndexForSubject(FirstNode) == INDEX_NONE) { return; } IDetailCategoryBuilder& DebugCategory = DetailLayout.EditCategory("DebugLiveValues", LOCTEXT("DebugLiveValues", "Inspect Live Values"), ECategoryPriority::Uncommon); DebugCategory.InitiallyCollapsed(true); for(URigVMPin* Pin : FirstNode->GetPins()) { // only show hidden pins in debug mode if(Pin->GetDirection() == ERigVMPinDirection::Hidden) { if(!DebuggedRig->IsInDebugMode()) { continue; } } URigVMPin* SourcePin = Pin; if(Blueprint->VMCompileSettings.ASTSettings.bFoldAssignments) { do { TArray SourcePins = SourcePin->GetLinkedSourcePins(false); if(SourcePins.Num() > 0) { SourcePin = SourcePins[0]; } else { break; } } while(SourcePin->GetNode()->IsA()); } TArray Expressions = AST->GetExpressionsForSubject(SourcePin); if(Expressions.Num() == 0 && SourcePin != Pin) { SourcePin = Pin; Expressions = AST->GetExpressionsForSubject(Pin); } bool bHasVar = false; for(const FRigVMExprAST* Expression : Expressions) { if(Expression->IsA(FRigVMExprAST::EType::Literal)) { continue; } else if(Expression->IsA(FRigVMExprAST::EType::Var)) { bHasVar = true; break; } } TArray FilteredExpressions; for(const FRigVMExprAST* Expression : Expressions) { if(Expression->IsA(FRigVMExprAST::EType::Literal)) { if(bHasVar) { continue; } FilteredExpressions.Add(Expression); } else if(Expression->IsA(FRigVMExprAST::EType::Var)) { FilteredExpressions.Add(Expression); } else if(Expression->IsA(FRigVMExprAST::EType::CachedValue)) { const FRigVMCachedValueExprAST* CachedValueExpr = Expression->To(); FilteredExpressions.Add(CachedValueExpr->GetVarExpr()); } } bool bAddedProperty = false; int32 SuffixIndex = 1; FString NameSuffix; TArray KnownOperands; for(const FRigVMExprAST* Expression : FilteredExpressions) { const FRigVMVarExprAST* VarExpr = Expression->To(); FString PinHash = URigVMCompiler::GetPinHash(SourcePin, VarExpr, false); const FRigVMOperand* Operand = Blueprint->PinToOperandMap.Find(PinHash); if(Operand) { if(Operand->GetRegisterOffset() != INDEX_NONE) { continue; } if(KnownOperands.Contains(*Operand)) { continue; } const FProperty* Property = nullptr; TArray ExternalObjects; if(Operand->GetMemoryType() == ERigVMMemoryType::External) { if(!VM->GetExternalVariables().IsValidIndex(Operand->GetRegisterIndex())) { continue; } ExternalObjects.Add(DebuggedRig); Property = VM->GetExternalVariables()[Operand->GetRegisterIndex()].Property; } else { URigVMMemoryStorage* Memory = VM->GetMemoryByType(Operand->GetMemoryType()); if(Memory == nullptr) { continue; } if(Memory->GetOuter() == GetTransientPackage()) { continue; } // the UClass must be alive for the details view to access it // this ensure can fail if VM memory is not updated immediately after compile // because of deferred copy if(!ensure(IsValidChecked(Memory->GetClass()))) { continue; } if(Memory->GetClass()->GetOuter() == GetTransientPackage()) { continue; } if(!Memory->IsValidIndex(Operand->GetRegisterIndex())) { continue; } Property = Memory->GetProperty(Operand->GetRegisterIndex()); if(Property == nullptr) { continue; } ExternalObjects.Add(Memory); } check(ExternalObjects.Num() > 0); check(Property); IDetailPropertyRow* PropertyRow = DebugCategory.AddExternalObjectProperty(ExternalObjects, Property->GetFName(), EPropertyLocation::Default, FAddPropertyParams().ForceShowProperty()); if(PropertyRow) { PropertyRow->DisplayName(FText::FromString(FString::Printf(TEXT("%s%s"), *Pin->GetName(), *NameSuffix))); PropertyRow->IsEnabled(false); SuffixIndex++; bAddedProperty = true; NameSuffix = FString::Printf(TEXT("_%d"), SuffixIndex); } KnownOperands.Add(*Operand); } } if(!bAddedProperty) { TSharedPtr PinHandle = DetailLayout.GetProperty(Pin->GetFName()); if(PinHandle.IsValid()) { DebugCategory.AddProperty(PinHandle) .DisplayName(FText::FromName(Pin->GetDisplayName())) .IsEnabled(false); } } } } #endif #undef LOCTEXT_NAMESPACE