Files
UnrealEngine/Engine/Source/Editor/Kismet/Private/SBlueprintSubPalette.cpp
2025-05-18 13:04:45 +08:00

535 lines
20 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SBlueprintSubPalette.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "BPDelegateDragDropAction.h"
#include "BPFunctionDragDropAction.h"
#include "BPVariableDragDropAction.h"
#include "BlueprintActionDatabase.h"
#include "BlueprintActionMenuUtils.h"
#include "BlueprintDragDropMenuItem.h"
#include "BlueprintEditor.h"
#include "CoreGlobals.h"
#include "Delegates/Delegate.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphSchema.h"
#include "EdGraphSchema_K2.h"
#include "EdGraphSchema_K2_Actions.h"
#include "Engine/Blueprint.h"
#include "Engine/LevelScriptBlueprint.h"
#include "Engine/MemberReference.h"
#include "Framework/Commands/Commands.h"
#include "Framework/Commands/InputChord.h"
#include "Framework/Commands/UIAction.h"
#include "Framework/Commands/UICommandInfo.h"
#include "Framework/Commands/UICommandList.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "HAL/PlatformMath.h"
#include "Internationalization/Internationalization.h"
#include "K2Node.h"
#include "K2Node_CallFunction.h"
#include "K2Node_Event.h"
#include "K2Node_SpawnActor.h"
#include "K2Node_SpawnActorFromClass.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Layout/Children.h"
#include "Layout/Margin.h"
#include "Layout/Visibility.h"
#include "Math/Color.h"
#include "Misc/AssertionMacros.h"
#include "SBlueprintActionMenu.h"
#include "SBlueprintPalette.h"
#include "SGraphActionMenu.h"
#include "SlotBase.h"
#include "Styling/AppStyle.h"
#include "Styling/CoreStyle.h"
#include "Styling/SlateTypes.h"
#include "Templates/Casts.h"
#include "Templates/SubclassOf.h"
#include "Types/WidgetActiveTimerDelegate.h"
#include "UObject/Class.h"
#include "UObject/NameTypes.h"
#include "UObject/Object.h"
#include "UObject/UnrealNames.h"
#include "UObject/UnrealType.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Layout/SBorder.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/SCompoundWidget.h"
#include "Widgets/SOverlay.h"
#include "Widgets/SToolTip.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Views/SExpanderArrow.h"
class FDragDropOperation;
class SWidget;
struct FPointerEvent;
struct FSlateBrush;
#define LOCTEXT_NAMESPACE "BlueprintSubPalette"
/*******************************************************************************
* Static File Helpers
*******************************************************************************/
/**
* An analytics hook, for tracking when a node was spawned from the palette
* (updates the "node creation stats" with a palette drag-placement flag).
*
* @param BlueprintEditorPtr A pointer to the blueprint editor currently being worked in.
*/
static void OnNodePlacement(TWeakPtr<FBlueprintEditor> BlueprintEditorPtr)
{
if( BlueprintEditorPtr.IsValid() )
{
BlueprintEditorPtr.Pin()->UpdateNodeCreationStats( ENodeCreateAction::PaletteDragPlacement );
}
}
/**
* Checks to see if the user can drop the currently dragged action to place its
* associated node in the graph.
*
* @param DropActionIn The action that will be executed when the user drops the dragged item.
* @param HoveredGraphIn A pointer to the graph that the user currently has the item dragged over.
* @param ImpededReasonOut If this returns false, this will be filled out with a reason to present the user with.
* @return True is the dragged palette item can be dropped where it currently is, false if not.
*/
static bool CanPaletteItemBePlaced(TSharedPtr<FEdGraphSchemaAction> DropActionIn, UEdGraph* HoveredGraphIn, FText& ImpededReasonOut)
{
bool bCanBePlaced = true;
if (!DropActionIn.IsValid())
{
bCanBePlaced = false;
ImpededReasonOut = LOCTEXT("InvalidDropAction", "Invalid action for placement");
}
else if (HoveredGraphIn == NULL)
{
bCanBePlaced = false;
ImpededReasonOut = LOCTEXT("DropOnlyInGraph", "Nodes can only be placed inside the blueprint graph");
}
else if (UK2Node const* NodeToBePlaced = FBlueprintActionMenuUtils::ExtractNodeTemplateFromAction(DropActionIn))
{
UEdGraphSchema const* const GraphSchema = HoveredGraphIn->GetSchema();
check(GraphSchema != nullptr);
bool bIsFunctionGraph = (GraphSchema->GetGraphType(HoveredGraphIn) == EGraphType::GT_Function);
if (UK2Node_CallFunction const* CallFuncNode = Cast<UK2Node_CallFunction const>(NodeToBePlaced))
{
FName const FuncName = CallFuncNode->FunctionReference.GetMemberName();
check(FuncName != NAME_None);
UClass const* const FuncOwner = CallFuncNode->FunctionReference.GetMemberParentClass(CallFuncNode->GetBlueprintClassFromNode());
check(FuncOwner != nullptr);
UFunction* const Function = FindUField<UFunction>(FuncOwner, FuncName);
UEdGraphSchema_K2 const* const K2Schema = Cast<UEdGraphSchema_K2 const>(GraphSchema);
if (Function == nullptr)
{
bCanBePlaced = false;
ImpededReasonOut = LOCTEXT("InvalidFuncAction", "Invalid function for placement");
}
else if (K2Schema == nullptr)
{
bCanBePlaced = false;
ImpededReasonOut = LOCTEXT("CannotCreateInThisSchema", "Cannot call functions in this type of graph");
}
else
{
// Note: We only check function context for UK2Node_CallFunction types specifically; derivatives are typically bound to specific functions that should be placeable but may not be explicitly callable (e.g. InternalUseOnly).
// @TODO - Consolidate this as a call to UK2Node::IsActionFilteredOut() here instead? Would need to add the ImpededReason as an 'out' param to that API first.
// We could then also skip the additonal 'CanPasteHere' check below in that case as it would be redundant for CallFunction node types specifically.
if(NodeToBePlaced->GetClass() == UK2Node_CallFunction::StaticClass())
{
uint32 AllowedFunctionTypes = UEdGraphSchema_K2::EFunctionType::FT_Pure | UEdGraphSchema_K2::EFunctionType::FT_Const | UEdGraphSchema_K2::EFunctionType::FT_Protected;
if(K2Schema->DoesGraphSupportImpureFunctions(HoveredGraphIn))
{
AllowedFunctionTypes |= UEdGraphSchema_K2::EFunctionType::FT_Imperative;
}
const UClass* GeneratedClass = FBlueprintEditorUtils::FindBlueprintForGraphChecked(HoveredGraphIn)->GeneratedClass;
bCanBePlaced = K2Schema->CanFunctionBeUsedInGraph(GeneratedClass, Function, HoveredGraphIn, AllowedFunctionTypes, false, &ImpededReasonOut);
}
}
}
else if (UK2Node_Event const* EventNode = Cast<UK2Node_Event const>(NodeToBePlaced))
{
// function graphs cannot have more than one entry point
if (bIsFunctionGraph)
{
bCanBePlaced = false;
ImpededReasonOut = LOCTEXT("NoSecondEntryPoint", "Function graphs can only have one entry point");
}
else if (GraphSchema->GetGraphType(HoveredGraphIn) != EGraphType::GT_Ubergraph)
{
bCanBePlaced = false;
ImpededReasonOut = LOCTEXT("NoEventsOnlyInUberGraphs", "Events can only be placed in event graphs");
}
}
else if (Cast<UK2Node_SpawnActor const>(NodeToBePlaced) || Cast<UK2Node_SpawnActorFromClass const>(NodeToBePlaced))
{
UEdGraphSchema_K2 const* const K2Schema = Cast<UEdGraphSchema_K2 const>(GraphSchema);
if (K2Schema && K2Schema->IsConstructionScript(HoveredGraphIn))
{
bCanBePlaced = false;
ImpededReasonOut = LOCTEXT("NoSpawnActorInConstruction", "Cannot spawn actors from a construction script");
}
}
bool bWillFocusOnExistingNode = (DropActionIn->GetTypeId() == FEdGraphSchemaAction_K2TargetNode::StaticGetTypeId());
if (!bWillFocusOnExistingNode && DropActionIn->GetTypeId() == FEdGraphSchemaAction_K2AddEvent::StaticGetTypeId())
{
FEdGraphSchemaAction_K2AddEvent* AddEventAction = (FEdGraphSchemaAction_K2AddEvent*)DropActionIn.Get();
bWillFocusOnExistingNode = AddEventAction->EventHasAlreadyBeenPlaced(FBlueprintEditorUtils::FindBlueprintForGraph(HoveredGraphIn));
}
// if this will instead focus on an existing node, reverse any previous decision... it is ok to drop!
if (bWillFocusOnExistingNode)
{
bCanBePlaced = true;
ImpededReasonOut = FText::GetEmpty();
}
// as a general catch-all, if a node cannot be pasted or placed in the graph, it probably can't be created there.
// Some nodes allow themselves to be pasted where they are generally not allowed, if either does not want the
// node placed, it should not be placeable
else if (bCanBePlaced && (!NodeToBePlaced->CanPasteHere(HoveredGraphIn) || !NodeToBePlaced->IsCompatibleWithGraph(HoveredGraphIn)) && !bWillFocusOnExistingNode)
{
bCanBePlaced = false;
ImpededReasonOut = LOCTEXT("CannotPaste", "Cannot place this node in this type of graph");
}
}
return bCanBePlaced;
}
/*******************************************************************************
* FBlueprintPaletteCommands
*******************************************************************************/
class FBlueprintPaletteCommands : public TCommands<FBlueprintPaletteCommands>
{
public:
FBlueprintPaletteCommands() : TCommands<FBlueprintPaletteCommands>
( "BlueprintPalette"
, LOCTEXT("PaletteContext", "Palette")
, NAME_None
, FAppStyle::GetAppStyleSetName() )
{
}
TSharedPtr<FUICommandInfo> RefreshPalette;
/**
* Registers context menu commands for the blueprint palette.
*/
virtual void RegisterCommands() override
{
UI_COMMAND(RefreshPalette, "Refresh List", "Refreshes the list of nodes.", EUserInterfaceActionType::Button, FInputChord());
}
};
/*******************************************************************************
* SBlueprintSubPalette Public Interface
*******************************************************************************/
//------------------------------------------------------------------------------
SBlueprintSubPalette::~SBlueprintSubPalette()
{
FBlueprintActionDatabase& ActionDatabase = FBlueprintActionDatabase::Get();
ActionDatabase.OnEntryRemoved().RemoveAll(this);
ActionDatabase.OnEntryUpdated().RemoveAll(this);
}
//------------------------------------------------------------------------------
void SBlueprintSubPalette::Construct(FArguments const& InArgs, TWeakPtr<FBlueprintEditor> InBlueprintEditor)
{
bIsActiveTimerRegistered = false;
BlueprintEditorPtr = InBlueprintEditor;
struct LocalUtils
{
static TSharedRef<SExpanderArrow> CreateCustomExpander(const FCustomExpanderData& ActionMenuData, bool bShowFavoriteToggle)
{
TSharedPtr<SExpanderArrow> CustomExpander;
if (bShowFavoriteToggle)
{
SAssignNew(CustomExpander, SBlueprintActionMenuExpander, ActionMenuData);
}
else
{
SAssignNew(CustomExpander, SExpanderArrow, ActionMenuData.TableRow);
}
return CustomExpander.ToSharedRef();
}
};
ChildSlot
[
SNew(SBorder)
.Padding(2.f)
.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
[
SNew( SVerticalBox )
+SVerticalBox::Slot()
.AutoHeight()
.Padding( 0.f, 2.f, 0.f, 0.f )
[
ConstructHeadingWidget(InArgs._Icon.Get(), InArgs._Title.Get(), InArgs._ToolTipText.Get())
]
+SVerticalBox::Slot()
[
SNew(SOverlay)
+SOverlay::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
SAssignNew(GraphActionMenu, SGraphActionMenu)
.OnCreateWidgetForAction(this, &SBlueprintSubPalette::OnCreateWidgetForAction)
.OnActionDragged(this, &SBlueprintSubPalette::OnActionDragged)
.OnCollectAllActions(this, &SBlueprintSubPalette::CollectAllActions)
.OnContextMenuOpening(this, &SBlueprintSubPalette::ConstructContextMenuWidget)
.OnCreateCustomRowExpander_Static(&LocalUtils::CreateCustomExpander, InArgs._ShowFavoriteToggles.Get())
]
]
]
];
CommandList = MakeShareable(new FUICommandList);
// has to come after GraphActionMenu has been set
BindCommands(CommandList);
FBlueprintActionDatabase& ActionDatabase = FBlueprintActionDatabase::Get();
ActionDatabase.OnEntryRemoved().AddSP(this, &SBlueprintSubPalette::OnDatabaseActionsRemoved);
ActionDatabase.OnEntryUpdated().AddSP(this, &SBlueprintSubPalette::OnDatabaseActionsUpdated);
}
//------------------------------------------------------------------------------
UBlueprint* SBlueprintSubPalette::GetBlueprint() const
{
UBlueprint* BlueprintBeingEdited = NULL;
if (BlueprintEditorPtr.IsValid())
{
BlueprintBeingEdited = BlueprintEditorPtr.Pin()->GetBlueprintObj();
}
return BlueprintBeingEdited;
}
//------------------------------------------------------------------------------
TSharedPtr<FEdGraphSchemaAction> SBlueprintSubPalette::GetSelectedAction() const
{
TArray< TSharedPtr<FEdGraphSchemaAction> > SelectedActions;
GraphActionMenu->GetSelectedActions(SelectedActions);
return TSharedPtr<FEdGraphSchemaAction>( (SelectedActions.Num() > 0) ? SelectedActions[0] : NULL );
}
/*******************************************************************************
* Protected SBlueprintSubPalette Methods
*******************************************************************************/
//------------------------------------------------------------------------------
void SBlueprintSubPalette::RefreshActionsList(bool bPreserveExpansion)
{
// Prevent refreshing the palette if we're in PIE
if( !GIsPlayInEditorWorld )
{
SGraphPalette::RefreshActionsList(bPreserveExpansion);
}
}
//------------------------------------------------------------------------------
TSharedRef<SWidget> SBlueprintSubPalette::OnCreateWidgetForAction(FCreateWidgetForActionData* const InCreateData)
{
return SNew(SBlueprintPaletteItem, InCreateData, BlueprintEditorPtr.Pin());
}
//------------------------------------------------------------------------------
FReply SBlueprintSubPalette::OnActionDragged( const TArray< TSharedPtr<FEdGraphSchemaAction> >& InActions, const FPointerEvent& MouseEvent )
{
if( InActions.Num() > 0 && InActions[0].IsValid() )
{
TSharedPtr<FEdGraphSchemaAction> InAction = InActions[0];
auto AnalyticsDelegate = FNodeCreationAnalytic::CreateStatic(&OnNodePlacement, BlueprintEditorPtr);
auto CanNodeBePlacedDelegate = FKismetDragDropAction::FCanBeDroppedDelegate::CreateStatic(&CanPaletteItemBePlaced);
if(InAction->GetTypeId() == FEdGraphSchemaAction_K2NewNode::StaticGetTypeId())
{
return FReply::Handled().BeginDragDrop(FKismetDragDropAction::New(InAction, AnalyticsDelegate, CanNodeBePlacedDelegate));
}
else if(InAction->GetTypeId() == FEdGraphSchemaAction_K2Var::StaticGetTypeId())
{
FEdGraphSchemaAction_K2Var* VarAction = (FEdGraphSchemaAction_K2Var*)InAction.Get();
if (UClass* VarClass = VarAction->GetVariableClass())
{
return FReply::Handled().BeginDragDrop(FKismetVariableDragDropAction::New(InAction, VarAction->GetVariableName(), VarClass, AnalyticsDelegate));
}
}
else if(InAction->GetTypeId() == FEdGraphSchemaAction_K2Delegate::StaticGetTypeId())
{
FEdGraphSchemaAction_K2Delegate* DelegateAction = (FEdGraphSchemaAction_K2Delegate*)InAction.Get();
if (UClass* VarClass = DelegateAction->GetDelegateClass())
{
return FReply::Handled().BeginDragDrop(FKismetDelegateDragDropAction::New(InAction, DelegateAction->GetDelegateName(), VarClass, AnalyticsDelegate));
}
}
else if (InAction->GetTypeId() == FBlueprintDragDropMenuItem::StaticGetTypeId())
{
FBlueprintDragDropMenuItem* BlueprintAction = (FBlueprintDragDropMenuItem*)InAction.Get();
TSharedPtr<FDragDropOperation> DragDropOp = BlueprintAction->OnDragged(AnalyticsDelegate);
if (DragDropOp.IsValid())
{
return FReply::Handled().BeginDragDrop(DragDropOp.ToSharedRef());
}
else
{
return FReply::Handled().BeginDragDrop(FKismetDragDropAction::New(InAction, AnalyticsDelegate, CanNodeBePlacedDelegate));
}
}
else
{
return FReply::Handled().BeginDragDrop(FKismetDragDropAction::New(InAction, AnalyticsDelegate, CanNodeBePlacedDelegate));
}
}
return FReply::Unhandled();
}
//------------------------------------------------------------------------------
void SBlueprintSubPalette::BindCommands(TSharedPtr<FUICommandList> CommandListIn) const
{
FBlueprintPaletteCommands::Register();
FBlueprintPaletteCommands const& PaletteCommands = FBlueprintPaletteCommands::Get();
CommandListIn->MapAction(
PaletteCommands.RefreshPalette,
FExecuteAction::CreateSP(const_cast<SBlueprintSubPalette*>(this), &SBlueprintSubPalette::RefreshActionsList, /*bPreserveExpansion =*/true)
);
}
//------------------------------------------------------------------------------
TSharedPtr<SWidget> SBlueprintSubPalette::ConstructContextMenuWidget() const
{
FMenuBuilder MenuBuilder(/* bInShouldCloseWindowAfterMenuSelection =*/true, CommandList);
GenerateContextMenuEntries(MenuBuilder);
return MenuBuilder.MakeWidget();
}
//------------------------------------------------------------------------------
void SBlueprintSubPalette::GenerateContextMenuEntries(FMenuBuilder& MenuBuilder) const
{
FBlueprintPaletteCommands const& PaletteCommands = FBlueprintPaletteCommands::Get();
MenuBuilder.AddMenuEntry(PaletteCommands.RefreshPalette);
}
//------------------------------------------------------------------------------
void SBlueprintSubPalette::RequestRefreshActionsList()
{
bIsActiveTimerRegistered = true;
}
//------------------------------------------------------------------------------
void SBlueprintSubPalette::OnDatabaseActionsUpdated(UObject* /*ActionsKey*/)
{
RequestRefreshActionsList();
}
//------------------------------------------------------------------------------
void SBlueprintSubPalette::OnDatabaseActionsRemoved(UObject* ActionsKey)
{
ULevelScriptBlueprint* RemovedLevelScript = Cast<ULevelScriptBlueprint>(ActionsKey);
bool const bAssumeDestroyingWorld = (RemovedLevelScript != nullptr);
if (bAssumeDestroyingWorld)
{
// have to update the action list immediatly (cannot wait until Tick(),
// because we have to handle level switching, which expects all references
// to be cleared immediately)
ForceRefreshActionList();
}
else
{
RequestRefreshActionsList();
}
}
void SBlueprintSubPalette::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
SGraphPalette::Tick(AllottedGeometry, InCurrentTime, InDeltaTime);
if(bIsActiveTimerRegistered)
{
static FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
if (!AssetRegistryModule.Get().IsLoadingAssets() && !AssetRegistryModule.Get().IsGathering() && !IsAsyncLoading())
{
bIsActiveTimerRegistered = false;
RefreshActionsList(true);
}
}
}
/*******************************************************************************
* Private SBlueprintSubPalette Methods
*******************************************************************************/
//------------------------------------------------------------------------------
void SBlueprintSubPalette::ForceRefreshActionList()
{
RefreshActionsList(/*bPreserveExpansion =*/true);
}
//------------------------------------------------------------------------------
TSharedRef<SVerticalBox> SBlueprintSubPalette::ConstructHeadingWidget(FSlateBrush const* const Icon, FText const& TitleText, FText const& ToolTipText)
{
TSharedPtr<SToolTip> ToolTipWidget;
SAssignNew(ToolTipWidget, SToolTip).Text(ToolTipText);
static FTextBlockStyle TitleStyle = FTextBlockStyle()
.SetFont(FCoreStyle::GetDefaultFontStyle("Bold", 10))
.SetColorAndOpacity(FLinearColor(0.4f, 0.4f, 0.4f));
return SNew(SVerticalBox)
.ToolTip(ToolTipWidget)
// so we still get tooltip text for an empty SHorizontalBox
.Visibility(EVisibility::Visible)
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(2.f, 2.f)
[
SNew(SImage).Image(Icon)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(2.f, 2.f)
[
SNew(STextBlock)
.Text(TitleText)
.TextStyle(&TitleStyle)
]
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.f, 2.f, 0.f, 5.f)
[
SNew(SBorder)
// use the border's padding to actually create the horizontal line
.Padding(1.f)
.BorderImage(FAppStyle::GetBrush(TEXT("Menu.Separator")))
];
}
#undef LOCTEXT_NAMESPACE