Files
UnrealEngine/Engine/Plugins/PCG/Source/PCGEditor/Private/Details/PCGComponentDetails.cpp
2025-05-18 13:04:45 +08:00

375 lines
10 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Details/PCGComponentDetails.h"
#include "PCGComponent.h"
#include "PCGEditorStyle.h"
#include "PCGSubsystem.h"
#include "Framework/Application/SlateApplication.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Input/SButton.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"
#include "Modules/ModuleManager.h"
#include "LevelEditor.h"
#define LOCTEXT_NAMESPACE "PCGComponentDetails"
static TAutoConsoleVariable<bool> CVarBroadcastComponentChanged(
TEXT("pcg.ComponentDetails.BroadcastComponentChanged"),
false,
TEXT("Controls whether we should broadcast component changes to the level editor"));
TSharedRef<IDetailCustomization> FPCGComponentDetails::MakeInstance()
{
return MakeShareable(new FPCGComponentDetails());
}
void FPCGComponentDetails::GatherPCGComponentsFromSelection(const TArray<TWeakObjectPtr<UObject>>& InObjectSelected)
{
for (const TWeakObjectPtr<UObject>& Object : InObjectSelected)
{
UPCGComponent* Component = Cast<UPCGComponent>(Object.Get());
if (ensure(Component))
{
SelectedComponents.Add(Component);
}
}
}
void FPCGComponentDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
const FName PCGCategoryName("PCG");
IDetailCategoryBuilder& PCGCategory = DetailBuilder.EditCategory(PCGCategoryName);
TArray<TWeakObjectPtr<UObject>> ObjectsBeingCustomized;
DetailBuilder.GetObjectsBeingCustomized(ObjectsBeingCustomized);
GatherPCGComponentsFromSelection(ObjectsBeingCustomized);
if (AddDefaultProperties())
{
TArray<TSharedRef<IPropertyHandle>> AllProperties;
bool bSimpleProperties = true;
bool bAdvancedProperties = false;
// Add all properties in the category in order
PCGCategory.GetDefaultProperties(AllProperties, bSimpleProperties, bAdvancedProperties);
for (auto& Property : AllProperties)
{
PCGCategory.AddProperty(Property);
}
}
for (TWeakObjectPtr<UPCGComponent>& ComponentPtr : SelectedComponents)
{
if (UPCGComponent* Component = ComponentPtr.Get())
{
AActor* ComponentOwner = Component->GetOwner();
if (!ComponentOwner || (ComponentOwner->IsInLevelInstance() && !ComponentOwner->IsInEditLevelInstance()))
{
// Do not customize for non editing Level Instance
return;
}
}
}
FDetailWidgetRow& NewRow = PCGCategory.AddCustomRow(FText::GetEmpty());
NewRow.ValueContent()
.MaxDesiredWidth(120.f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(2.0f, 0.0f)
.VAlign(VAlign_Fill)
[
SNew(SButton)
.OnClicked(this, &FPCGComponentDetails::OnGenerateClicked)
.ToolTipText(FText::FromString("Generates graph data. \nCtrl + Click flushes the cache and force generates."))
.Visibility(this, &FPCGComponentDetails::GenerateButtonVisible)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.Padding(0.0f, 0.0f, 6.0f, 0.0f)
.VAlign(VAlign_Center)
.AutoWidth()
[
SNew(SImage)
.DesiredSizeOverride(FVector2D(16,16))
.Image_Lambda([]() { return FSlateApplication::Get().GetModifierKeys().IsControlDown() ? FPCGEditorStyle::Get().GetBrush("PCG.Command.ForceRegenClearCache") : FPCGEditorStyle::Get().GetBrush("PCG.Command.ForceRegen"); })
.ColorAndOpacity(FSlateColor::UseForeground())
]
+SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.AutoWidth()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text_Lambda([]() { return FSlateApplication::Get().GetModifierKeys().IsControlDown() ? LOCTEXT("ForceRegenerateButton", "Force Generate") : LOCTEXT("GenerateButton", "Generate"); })
]
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(2.0f, 0.0f)
.VAlign(VAlign_Fill)
[
SNew(SButton)
.OnClicked(this, &FPCGComponentDetails::OnCancelClicked)
.Visibility(this, &FPCGComponentDetails::CancelButtonVisible)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.Padding(0.0f, 0.0f, 6.0f, 0.0f)
.VAlign(VAlign_Center)
.AutoWidth()
[
SNew(SImage)
.DesiredSizeOverride(FVector2D(16, 16))
.Image(FPCGEditorStyle::Get().GetBrush("PCG.Command.StopRegen"))
.ColorAndOpacity(FSlateColor::UseForeground())
]
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.AutoWidth()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LOCTEXT("CancelButton", "Cancel"))
]
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Fill)
.Padding(2.0f, 0.0f)
[
SNew(SButton)
.VAlign(VAlign_Center)
.OnClicked(this, &FPCGComponentDetails::OnCleanupClicked)
.ToolTipText(FText::FromString("Cleans up graph data. \nCtrl + Click purges all components and attached actors tagged as created by PCG."))
.Visibility(this, &FPCGComponentDetails::CleanupButtonVisible)
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LOCTEXT("CleanupButton", "Cleanup"))
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Fill)
.Padding(2.0f, 0.0f)
[
SNew(SButton)
.VAlign(VAlign_Center)
.OnClicked(this, &FPCGComponentDetails::OnRefreshClicked)
.Visibility(this, &FPCGComponentDetails::RefreshButtonVisible)
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LOCTEXT("RefreshButton", "Refresh"))
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Fill)
.Padding(2.0f, 0.0f)
[
SNew(SButton)
.VAlign(VAlign_Center)
.OnClicked(this, &FPCGComponentDetails::OnClearPCGLinkClicked)
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LOCTEXT("ClearPCGLinkButton", "Clear PCG Link"))
]
]
];
// Attach to generated delegate on the selected components
for (TWeakObjectPtr<UPCGComponent>& Component : SelectedComponents)
{
if (Component.IsValid())
{
Component.Get()->OnPCGGraphGeneratedDelegate.AddRaw(this, &FPCGComponentDetails::OnGraphChanged);
Component.Get()->OnPCGGraphCleanedDelegate.AddRaw(this, &FPCGComponentDetails::OnGraphChanged);
}
}
}
void FPCGComponentDetails::PendingDelete()
{
// Detach from the generated delegate
for (TWeakObjectPtr<UPCGComponent>& Component : SelectedComponents)
{
if (Component.IsValid())
{
Component.Get()->OnPCGGraphGeneratedDelegate.RemoveAll(this);
Component.Get()->OnPCGGraphCleanedDelegate.RemoveAll(this);
}
}
}
EVisibility FPCGComponentDetails::GenerateButtonVisible() const
{
for (const TWeakObjectPtr<UPCGComponent>& Component : SelectedComponents)
{
// If component is runtime generated then generate/cleanup is managed by the scheduler.
if (Component.IsValid() && !Component->IsGenerating() && !Component->IsManagedByRuntimeGenSystem())
{
return EVisibility::Visible;
}
}
return EVisibility::Collapsed;
}
EVisibility FPCGComponentDetails::CancelButtonVisible() const
{
for (const TWeakObjectPtr<UPCGComponent>& Component : SelectedComponents)
{
// If component is runtime generated then generate/cleanup is managed by the scheduler.
if (Component.IsValid() && Component->IsGenerating() && !Component->IsManagedByRuntimeGenSystem())
{
return EVisibility::Visible;
}
}
return EVisibility::Collapsed;
}
EVisibility FPCGComponentDetails::CleanupButtonVisible() const
{
for (const TWeakObjectPtr<UPCGComponent>& Component : SelectedComponents)
{
// If component is runtime generated then generate/cleanup is managed by the scheduler.
if (Component.IsValid() && !Component->IsManagedByRuntimeGenSystem())
{
return EVisibility::Visible;
}
}
return EVisibility::Collapsed;
}
EVisibility FPCGComponentDetails::RefreshButtonVisible() const
{
for (const TWeakObjectPtr<UPCGComponent>& Component : SelectedComponents)
{
if (Component.IsValid() && Component->IsManagedByRuntimeGenSystem())
{
return EVisibility::Visible;
}
}
return EVisibility::Collapsed;
}
FReply FPCGComponentDetails::OnGenerateClicked()
{
const FModifierKeysState ModifierKeys = FSlateApplication::Get().GetModifierKeys();
const bool bIsControlDown = ModifierKeys.IsControlDown();
if (bIsControlDown)
{
for (TWeakObjectPtr<UPCGComponent>& Component : SelectedComponents)
{
if (Component.IsValid())
{
Component->GetSubsystem()->FlushCache();
break;
}
}
}
for (TWeakObjectPtr<UPCGComponent>& Component : SelectedComponents)
{
if (Component.IsValid())
{
Component.Get()->Generate(/*bForce=*/bIsControlDown);
}
}
return FReply::Handled();
}
FReply FPCGComponentDetails::OnCancelClicked()
{
for (TWeakObjectPtr<UPCGComponent>& Component : SelectedComponents)
{
if (Component.IsValid())
{
Component.Get()->CancelGeneration();
}
}
return FReply::Handled();
}
FReply FPCGComponentDetails::OnRefreshClicked()
{
for (TWeakObjectPtr<UPCGComponent>& Component : SelectedComponents)
{
if (Component.IsValid())
{
// Trigger the deepest refresh - re-initialize the PAs.
Component.Get()->Refresh(EPCGChangeType::Structural | EPCGChangeType::GenerationGrid, /*bCancelExistingRefresh=*/true);
}
}
return FReply::Handled();
}
FReply FPCGComponentDetails::OnClearPCGLinkClicked()
{
for (TWeakObjectPtr<UPCGComponent>& Component : SelectedComponents)
{
if (Component.IsValid())
{
Component.Get()->ClearPCGLink();
}
}
return FReply::Handled();
}
void FPCGComponentDetails::OnGraphChanged(UPCGComponent* InComponent)
{
if (!InComponent || !InComponent->GetOwner())
{
return;
}
// Notify editor that some components might have changed
if (CVarBroadcastComponentChanged.GetValueOnGameThread())
{
FLevelEditorModule& LevelEditor = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
LevelEditor.BroadcastComponentsEdited();
}
}
FReply FPCGComponentDetails::OnCleanupClicked()
{
for (TWeakObjectPtr<UPCGComponent>& Component : SelectedComponents)
{
if (Component.IsValid())
{
FModifierKeysState ModifierKeys = FSlateApplication::Get().GetModifierKeys();
if (ModifierKeys.IsControlDown())
{
Component.Get()->CleanupLocalDeleteAllGeneratedObjects({});
}
else
{
Component.Get()->Cleanup();
}
}
}
return FReply::Handled();
}
#undef LOCTEXT_NAMESPACE