Files
UnrealEngineUWP/Engine/Source/Editor/SubobjectEditor/Private/SSubobjectInstanceEditor.cpp
ben hoffman 285a1503df Create the subobject Editor module that utilizes the new Subobject Data Interface instead of modifying subobjects directly in slate code.
#jira UE-114839
#rb phillip.kavan
#rnx
#preflight 609152aa90631e000199463e
#preflight 60919af859592e0001f41c7d

[CL 16198818 by ben hoffman in ue5-main branch]
2021-05-04 16:26:56 -04:00

477 lines
16 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SSubobjectInstanceEditor.h"
#include "ScopedTransaction.h"
#include "IDocumentation.h"
#include "GraphEditorActions.h"
#include "Editor/EditorEngine.h"
#include "ISCSEditorUICustomization.h" // #TODO_BH Rename this to subobject
#include "SCSEditorExtensionContext.h" // #TODO_BH Rename this to subobject
#include "Styling/SlateIconFinder.h"
#include "SlateOptMacros.h"
#include "Widgets/Input/SSearchBox.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "SComponentClassCombo.h"
#include "SEditorHeaderButton.h"
#include "Editor/UnrealEdEngine.h"
#include "Subsystems/PanelExtensionSubsystem.h" // SExtensionPanel
#include "Kismet2/ComponentEditorUtils.h"
#define LOCTEXT_NAMESPACE "SSubobjectInstanceEditor"
extern UNREALED_API UUnrealEdEngine* GUnrealEd;
////////////////////////////////////////////////
// SSubobjectInstanceEditor
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSubobjectInstanceEditor::Construct(const FArguments& InArgs)
{
ObjectContext = InArgs._ObjectContext;
OnSelectionUpdated = InArgs._OnSelectionUpdated;
OnItemDoubleClicked = InArgs._OnItemDoubleClicked;
AllowEditing = InArgs._AllowEditing;
OnObjectReplaced = InArgs._OnObjectReplaced;
bAllowTreeUpdates = true;
CreateCommandList();
// Build the tree widget
FSlateBrush const* MobilityHeaderBrush = FEditorStyle::GetBrush(TEXT("ClassIcon.ComponentMobilityHeaderIcon"));
ConstructTreeWidget();
// Should only be true when used in the blueprints details panel
const bool bInlineSearchBarWithButtons = ShowInlineSearchWithButtons();
TSharedPtr<SWidget> Contents;
TSharedPtr<SVerticalBox> HeaderBox;
TSharedPtr<SWidget> SearchBar = SAssignNew(FilterBox, SSearchBox)
.HintText(!bInlineSearchBarWithButtons ? LOCTEXT("SearchComponentsHint", "Search Components") : LOCTEXT("SearchHint", "Search"))
.OnTextChanged(this, &SSubobjectInstanceEditor::OnFilterTextChanged)
.Visibility(this, &SSubobjectInstanceEditor::GetComponentsFilterBoxVisibility);
FMenuBuilder EditBlueprintMenuBuilder = CreateMenuBuilder();
// Extension context for new boi
USCSEditorExtensionContext* ExtensionContext = NewObject<USCSEditorExtensionContext>();
ExtensionContext->SubobjectEditor = SharedThis(this);
ExtensionContext->AddToRoot();
ButtonBox = SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.Padding(0.0f, 0.0f, 4.0f, 0.0f)
.AutoWidth()
[
SNew(SComponentClassCombo)
.AddMetaData<FTagMetaData>(FTagMetaData(TEXT("Actor.AddComponent")))
.Visibility(HideComponentClassCombo.Get() ? EVisibility::Hidden : EVisibility::Visible)
.OnSubobjectClassSelected(this, &SSubobjectInstanceEditor::PerformComboAddClass)
.ToolTipText(LOCTEXT("AddComponent_Tooltip", "Adds a new component to this actor"))
.IsEnabled(true)
]
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.Padding(0,0,4,0)
.AutoWidth()
[
SAssignNew(ExtensionPanel, SExtensionPanel)
.ExtensionPanelID("SCSEditor.NextToAddComponentButton")
.ExtensionContext(ExtensionContext)
]
// horizontal slot index #2 => reserved for BP-editor search bar (see 'ButtonBox' and 'SearchBarHorizontalSlotIndex' usage below)
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.Padding(0,0,4,0)
.AutoWidth()
[
SNew(SEditorHeaderButton)
.AddMetaData<FTagMetaData>(FTagMetaData(TEXT("Actor.ConvertToBlueprint")))
.Visibility(this, &SSubobjectInstanceEditor::GetPromoteToBlueprintButtonVisibility)
.OnClicked(this, &SSubobjectInstanceEditor::OnPromoteToBlueprintClicked)
.Icon(FAppStyle::Get().GetBrush("Icons.Blueprints"))
.ToolTip(IDocumentation::Get()->CreateToolTip(
LOCTEXT("PromoteToBluerprintTooltip", "Converts this actor into a reusable Blueprint Class that can have script behavior" ),
nullptr,
TEXT("Shared/LevelEditor"),
TEXT("ConvertToBlueprint")))
]
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.AutoWidth()
[
SNew(SEditorHeaderButton)
.AddMetaData<FTagMetaData>(FTagMetaData(TEXT("Actor.EditBlueprint")))
.Visibility(this, &SSubobjectInstanceEditor::GetEditBlueprintButtonVisibility)
.ToolTipText(LOCTEXT("EditActorBlueprint_Tooltip", "Edit the Blueprint for this Actor"))
.Icon(FAppStyle::Get().GetBrush("Icons.Blueprints"))
.MenuContent()
[
EditBlueprintMenuBuilder.MakeWidget()
]
];
Contents = SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.VAlign(VAlign_Top)
.Padding(4.f, 0, 4.f, 4.f)
[
SAssignNew(HeaderBox, SVerticalBox)
]
+ SVerticalBox::Slot()
[
SNew(SBorder)
.BorderImage(FAppStyle::Get().GetBrush("SCSEditor.Background"))
.Padding(4.f)
.AddMetaData<FTagMetaData>(FTagMetaData(TEXT("ComponentsPanel")))
.Visibility(this, &SSubobjectInstanceEditor::GetComponentsTreeVisibility)
[
TreeWidget.ToSharedRef()
]
];
// Only insert the buttons and search bar in the Blueprints version
if (bInlineSearchBarWithButtons)
{
ButtonBox->AddSlot()
.FillWidth(1.0f)
.VAlign(VAlign_Center)
.Padding(3.0f, 3.0f)
[
SearchBar.ToSharedRef()
];
HeaderBox->AddSlot()
.VAlign(VAlign_Center)
.AutoHeight()
[
ButtonBox.ToSharedRef()
];
}
this->ChildSlot
[
Contents.ToSharedRef()
];
// Populate the tree with subobject data
UpdateTree();
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSubobjectInstanceEditor::OnDeleteNodes()
{
const FScopedTransaction Transaction( LOCTEXT("RemoveComponents", "Remove Components") );
// Invalidate any active component in the visualizer
GUnrealEd->ComponentVisManager.ClearActiveComponentVis();
// Gather the handles of the components that we want to delete
TArray<FSubobjectDataHandle> HandlesToDelete;
TArray<FSubobjectEditorTreeNodePtrType> SelectedNodes = GetSelectedNodes();
for(const FSubobjectEditorTreeNodePtrType& Node : SelectedNodes)
{
check(Node->IsValid());
const FSubobjectData* Data = Node->GetDataSource();
if(Data && Data->IsComponent())
{
HandlesToDelete.Add(Data->GetHandle());
}
}
if(USubobjectDataSubsystem* System = USubobjectDataSubsystem::Get())
{
FSubobjectDataHandle HandleToSelect;
int32 NumDeleted = System->DeleteSubobjects(RootNodes[0]->GetDataHandle(), HandlesToDelete, HandleToSelect);
if(NumDeleted > 0)
{
if(HandleToSelect.IsValid())
{
FSubobjectEditorTreeNodePtrType NodeToSelect = FindSlateNodeForHandle(HandleToSelect);
if(NodeToSelect.IsValid())
{
TreeWidget->SetSelection(NodeToSelect);
}
}
UpdateTree();
// Do this AFTER marking the Blueprint as modified
UpdateSelectionFromNodes(TreeWidget->GetSelectedItems());
}
}
}
void SSubobjectInstanceEditor::CopySelectedNodes()
{
TArray<FSubobjectDataHandle> SelectedHandles = GetSelectedHandles();
if(USubobjectDataSubsystem* System = USubobjectDataSubsystem::Get())
{
return System->CopySubobjects(SelectedHandles, /* BpContext = */ nullptr);
}
}
void SSubobjectInstanceEditor::OnDuplicateComponent()
{
TArray<FSubobjectDataHandle> SelectedNodes = GetSelectedHandles();
if(SelectedNodes.Num() > 0)
{
// Force the text box being edited (if any) to commit its text. The duplicate operation may trigger a regeneration of the tree view,
// releasing all row widgets. If one row was in edit mode (rename/rename on create), it was released before losing the focus and
// this would prevent the completion of the 'rename' or 'create + give initial name' transaction (occurring on focus lost).
FSlateApplication::Get().ClearKeyboardFocus();
const FScopedTransaction Transaction(SelectedNodes.Num() > 1 ? LOCTEXT("DuplicateComponents", "Duplicate Components") : LOCTEXT("DuplicateComponent", "Duplicate Component"));
if(USubobjectDataSubsystem* System = USubobjectDataSubsystem::Get())
{
System->DuplicateSubobjects(GetObjectContextHandle(), SelectedNodes, /* BpContext = */ nullptr);
UpdateTree();
}
}
}
void SSubobjectInstanceEditor::PasteNodes()
{
if(USubobjectDataSubsystem* System = USubobjectDataSubsystem::Get())
{
TArray<FSubobjectDataHandle> OutHandles;
System->PasteSubobjects(GetObjectContextHandle(), GetSelectedHandles(), nullptr, OutHandles);
if(OutHandles.Num() > 0)
{
// We only want the pasted node(s) to be selected
TreeWidget->ClearSelection();
UpdateTree();
for(const FSubobjectDataHandle& Handle : OutHandles)
{
if(FSubobjectEditorTreeNodePtrType SlateNode = FindSlateNodeForHandle(Handle))
{
TreeWidget->SetItemSelection(SlateNode, true);
}
}
}
}
}
void SSubobjectInstanceEditor::OnAttachToDropAction(FSubobjectEditorTreeNodePtrType DroppedOn, const TArray<FSubobjectEditorTreeNodePtrType>& DroppedNodePtrs)
{
// Ask the subsystem to attach the dropped nodes onto the dropped on node
check(DroppedOn.IsValid());
check(DroppedNodePtrs.Num() > 0);
USubobjectDataSubsystem* System = USubobjectDataSubsystem::Get();
check(System);
const FScopedTransaction TransactionContext(DroppedNodePtrs.Num() > 1 ? LOCTEXT("AttachComponents", "Attach Components") : LOCTEXT("AttachComponent", "Attach Component"));
TArray<FSubobjectDataHandle> HandlesToMove;
Utils::PopulateHandlesArray(DroppedNodePtrs, HandlesToMove);
FReparentSubobjectParams Params;
Params.NewParentHandle = DroppedOn->GetDataHandle();
System->ReparentSubobjects(Params, HandlesToMove);
check(TreeWidget.IsValid());
TreeWidget->SetItemExpansion(DroppedOn, true);
PostDragDropAction(true);
}
void SSubobjectInstanceEditor::OnDetachFromDropAction(const TArray<FSubobjectEditorTreeNodePtrType>& DroppedNodePtrs)
{
check(DroppedNodePtrs.Num() > 0);
const FScopedTransaction TransactionContext(DroppedNodePtrs.Num() > 1 ? LOCTEXT("DetachComponents", "Detach Components") : LOCTEXT("DetachComponent", "Detach Component"));
TArray<FSubobjectDataHandle> HandlesToMove;
Utils::PopulateHandlesArray(DroppedNodePtrs, HandlesToMove);
// Attach the dropped node to the current scene root node
FSubobjectEditorTreeNodePtrType SceneRootNodePtr = GetSceneRootNode();
check(SceneRootNodePtr.IsValid());
// Ask the subsystem to reparent this object to the scene root
if(USubobjectDataSubsystem* System = USubobjectDataSubsystem::Get())
{
FReparentSubobjectParams Params;
Params.NewParentHandle = SceneRootNodePtr->GetDataHandle();
System->ReparentSubobjects(Params, HandlesToMove);
}
PostDragDropAction(false);
}
void SSubobjectInstanceEditor::OnMakeNewRootDropAction(FSubobjectEditorTreeNodePtrType DroppedNodePtr)
{
// Get the current scene root node
FSubobjectEditorTreeNodePtrType SceneRootNodePtr = GetSceneRootNode();
// We cannot handle the drop action if any of these conditions fail on entry.
//if (!ensure(SceneRootNodePtr.IsValid()) || !ensure(DroppedNodePtr.IsValid()) || !ensure(DroppedNodePtr == SceneRootNodePtr))
//{
// return;
//}
// Create a transaction record
const FScopedTransaction TransactionContext(LOCTEXT("MakeNewSceneRoot", "Make New Scene Root"));
USubobjectDataSubsystem* Subsystem = USubobjectDataSubsystem::Get();
const bool bSuccess = Subsystem->MakeNewSceneRoot(
GetObjectContextHandle(),
DroppedNodePtr->GetDataHandle(),
nullptr);
PostDragDropAction(true);
}
void SSubobjectInstanceEditor::PostDragDropAction(bool bRegenerateTreeNodes)
{
GUnrealEd->ComponentVisManager.ClearActiveComponentVis();
UpdateTree(bRegenerateTreeNodes);
if(AActor* ActorInstance = Cast<AActor>(GetObjectContext()))
{
ActorInstance->RerunConstructionScripts();
}
}
TSharedPtr<SWidget> SSubobjectInstanceEditor::BuildSceneRootDropActionMenu(FSubobjectEditorTreeNodePtrType DroppedOntoNodePtr, FSubobjectEditorTreeNodePtrType DroppedNodePtr)
{
FMenuBuilder MenuBuilder(true, CommandList);
const FSubobjectData* DroppedNodeData = DroppedNodePtr->GetDataSource();
const FSubobjectData* DroppedOntoNodeData = DroppedOntoNodePtr->GetDataSource();
check(DroppedNodeData);
MenuBuilder.BeginSection("SceneRootNodeDropActions", LOCTEXT("SceneRootNodeDropActionContextMenu", "Drop Actions"));
{
const FText DroppedVariableNameText = FText::FromName( DroppedNodeData->GetVariableName() );
const FText NodeVariableNameText = FText::FromName( DroppedOntoNodeData->GetVariableName() );
bool bDroppedInSameBlueprint = true;
MenuBuilder.AddMenuEntry(
LOCTEXT("DropActionLabel_AttachToRootNode", "Attach"),
bDroppedInSameBlueprint
? FText::Format( LOCTEXT("DropActionToolTip_AttachToRootNode", "Attach {0} to {1}."), DroppedVariableNameText, NodeVariableNameText )
: FText::Format( LOCTEXT("DropActionToolTip_AttachToRootNodeFromCopy", "Copy {0} to a new variable and attach it to {1}."), DroppedVariableNameText, NodeVariableNameText ),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP(this, &SSubobjectEditor::OnAttachToDropAction, DroppedOntoNodePtr, DroppedNodePtr),
FCanExecuteAction()));
const bool bIsDefaultSceneRoot = DroppedNodeData->IsDefaultSceneRoot();
FText NewRootNodeText = bIsDefaultSceneRoot
? FText::Format(LOCTEXT("DropActionToolTip_MakeNewRootNodeAndDelete", "Make {0} the new root. The default root will be deleted."), DroppedVariableNameText)
: FText::Format(LOCTEXT("DropActionToolTip_MakeNewRootNode", "Make {0} the new root."), DroppedVariableNameText);
FText NewRootNodeFromCopyText = bIsDefaultSceneRoot
? FText::Format(LOCTEXT("DropActionToolTip_MakeNewRootNodeFromCopyAndDelete", "Copy {0} to a new variable and make it the new root. The default root will be deleted."), DroppedVariableNameText)
: FText::Format(LOCTEXT("DropActionToolTip_MakeNewRootNodeFromCopy", "Copy {0} to a new variable and make it the new root."), DroppedVariableNameText);
MenuBuilder.AddMenuEntry(
LOCTEXT("DropActionLabel_MakeNewRootNode", "Make New Root"),
bDroppedInSameBlueprint ? NewRootNodeText : NewRootNodeFromCopyText,
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP(this, &SSubobjectEditor::OnMakeNewRootDropAction, DroppedNodePtr),
FCanExecuteAction()));
}
MenuBuilder.EndSection();
return MenuBuilder.MakeWidget();
}
FSubobjectDataHandle SSubobjectInstanceEditor::AddNewSubobject(const FSubobjectDataHandle& ParentHandle, UClass* NewClass, UObject* AssetOverride, FText& OutFailReason, TUniquePtr<FScopedTransaction> InOngoingTransaction)
{
FAddNewSubobjectParams Params;
Params.ParentHandle = ParentHandle;
Params.NewClass = NewClass;
Params.AssetOverride = AssetOverride;
// This is an instance, so the blueprint context is null!
Params.BlueprintContext = nullptr;
USubobjectDataSubsystem* System = USubobjectDataSubsystem::Get();
check(System);
return System->AddNewSubobject(Params,OutFailReason);
}
void SSubobjectInstanceEditor::PopulateContextMenuImpl(UToolMenu* InMenu, TArray<FSubobjectEditorTreeNodePtrType>& InSelectedItems, bool bIsChildActorSubtreeNodeSelected)
{
TArray<UActorComponent*> SelectedComponents;
for(const FSubobjectEditorTreeNodePtrType& SelectedNodePtr : InSelectedItems)
{
check(SelectedNodePtr->IsValid());
// Get the component template associated with the selected node
const UActorComponent* ComponentTemplate = SelectedNodePtr->GetComponentTemplate();
if (ComponentTemplate)
{
// #TODO_BH Remove this const cast
SelectedComponents.Add(const_cast<UActorComponent*>(ComponentTemplate));
}
}
// Common menu options added for all component types
FComponentEditorUtils::FillComponentContextMenuOptions(InMenu, SelectedComponents);
}
FSlateColor SSubobjectInstanceEditor::GetColorTintForIcon(FSubobjectEditorTreeNodePtrType Node) const
{
static const FLinearColor IntroducedHereColor(FLinearColor::White);
// A blue-ish tint
static const FLinearColor InheritedBlueprintComponentColor(0.08f, 0.35f, 0.6f);
static const FLinearColor InstancedInheritedBlueprintComponentColor(0.08f, 0.35f, 0.6f);
// A green-ish tint
static const FLinearColor InheritedNativeComponentColor(0.7f, 0.9f, 0.7f);
const FSubobjectData* Data = Node ? Node->GetDataSource() : nullptr;
if(!Data)
{
return IntroducedHereColor;
}
if (Data->IsInheritedComponent())
{
// Native C++ components will be tinted green
if (Data->IsNativeComponent())
{
return InheritedNativeComponentColor;
}
else if (Data->IsInstancedComponent())
{
return InstancedInheritedBlueprintComponentColor;
}
else
{
return InheritedBlueprintComponentColor;
}
}
// If its not an actor, instanced, or native inherited component then it must be inherited from a BP
else if(!Data->IsActor() && !Data->IsInstancedComponent())
{
return InheritedBlueprintComponentColor;
}
else if(Data->IsActor())
{
return FSlateColor::UseForeground();
}
return IntroducedHereColor;
}
#undef LOCTEXT_NAMESPACE