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

548 lines
16 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Debugging/SKismetDebuggingView.h"
#include "ClassViewerFilter.h"
#include "ClassViewerModule.h"
#include "Containers/Array.h"
#include "Containers/EnumAsByte.h"
#include "Containers/Set.h"
#include "Debugging/SKismetDebugTreeView.h"
#include "Delegates/Delegate.h"
#include "Editor.h"
#include "Editor/EditorEngine.h"
#include "Engine/Blueprint.h"
#include "Engine/EngineTypes.h"
#include "Engine/World.h"
#include "Framework/MultiBox/MultiBoxDefs.h"
#include "HAL/PlatformCrt.h"
#include "Internationalization/Internationalization.h"
#include "Kismet2/Breakpoint.h"
#include "Kismet2/DebuggerCommands.h"
#include "Kismet2/KismetDebugUtilities.h"
#include "Layout/Children.h"
#include "Logging/LogMacros.h"
#include "Misc/Attribute.h"
#include "Modules/ModuleManager.h"
#include "SlotBase.h"
#include "Styling/AppStyle.h"
#include "Styling/SlateTypes.h"
#include "Templates/Casts.h"
#include "Templates/SubclassOf.h"
#include "ToolMenu.h"
#include "ToolMenuContext.h"
#include "ToolMenus.h"
#include "Types/SlateEnums.h"
#include "Types/SlateStructs.h"
#include "UObject/Class.h"
#include "UObject/NameTypes.h"
#include "UObject/Object.h"
#include "UObject/ObjectMacros.h"
#include "UObject/ObjectPtr.h"
#include "UObject/Script.h"
#include "UObject/UObjectIterator.h"
#include "UObject/UnrealNames.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SCheckBox.h"
#include "Widgets/Input/SComboButton.h"
#include "Widgets/Input/SSearchBox.h"
#include "Widgets/Layout/SBorder.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/Layout/SSplitter.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Views/SHeaderRow.h"
class SWidget;
struct FGeometry;
struct FToolMenuSection;
#define LOCTEXT_NAMESPACE "DebugViewUI"
DEFINE_LOG_CATEGORY_STATIC(LogBlueprintDebuggingView, Log, All);
//////////////////////////////////////////////////////////////////////////
namespace KismetDebugViewConstants
{
const FText ColumnText_Name( NSLOCTEXT("DebugViewUI", "Name", "Name") );
const FText ColumnText_Value( NSLOCTEXT("DebugViewUI", "Value", "Value") );
const FText ColumnText_DebugKey( FText::GetEmpty() );
const FText ColumnText_Info( NSLOCTEXT("DebugViewUI", "Info", "Info") );
}
//////////////////////////////////////////////////////////////////////////
// SKismetDebuggingView
TWeakObjectPtr<const UObject> SKismetDebuggingView::CurrentActiveObject = nullptr;
TSharedRef<SHorizontalBox> SKismetDebuggingView::GetDebugLineTypeToggle(FDebugLineItem::EDebugLineType Type, const FText& Text)
{
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SCheckBox)
.IsChecked(ECheckBoxState::Checked)
.OnCheckStateChanged_Static(&FDebugLineItem::OnDebugLineTypeActiveChanged, Type)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(4.0f, 0.0f, 10.0f, 0.0f)
.VAlign(VAlign_Center)
[
SNew( STextBlock )
.Text(Text)
];
}
void SKismetDebuggingView::OnSearchTextChanged(const FText& Text)
{
DebugTreeView->ClearExpandedItems();
OtherTreeView->ClearExpandedItems();
DebugTreeView->SetSearchText(Text);
OtherTreeView->SetSearchText(Text);
}
FText SKismetDebuggingView::GetTabLabel() const
{
return BlueprintToWatchPtr.IsValid() ?
FText::FromString(BlueprintToWatchPtr->GetName()) :
NSLOCTEXT("BlueprintExecutionFlow", "TabTitle", "Data Flow");
}
void SKismetDebuggingView::TryRegisterDebugToolbar()
{
static const FName ToolbarName = "Kismet.DebuggingViewToolBar";
if (!UToolMenus::Get()->IsMenuRegistered(ToolbarName))
{
UToolMenu* ToolBar = UToolMenus::Get()->RegisterMenu(ToolbarName, NAME_None, EMultiBoxType::SlimHorizontalToolBar);
FToolMenuSection& Section = ToolBar->AddSection("Debug");
FPlayWorldCommands::BuildToolbar(Section);
}
}
FText SKismetDebuggingView::GetTopText() const
{
return LOCTEXT("ShowDebugForActors", "Showing debug info for instances of the blueprint:");
}
FText SKismetDebuggingView::GetToggleAllBreakpointsText() const
{
const FBlueprintBreakpoint* EnabledBreakpoint = FKismetDebugUtilities::FindBreakpointByPredicate(BlueprintToWatchPtr.Get(), [](const FBlueprintBreakpoint& Breakpoint)
{
return Breakpoint.IsEnabled();
});
if (EnabledBreakpoint)
{
return LOCTEXT("DisableAllBreakPoints", "Disable All Breakpoints");
}
else
{
return LOCTEXT("EnableAllBreakPoints", "Enable All Breakpoints");
}
}
bool SKismetDebuggingView::CanToggleAllBreakpoints() const
{
if(BlueprintToWatchPtr.IsValid())
{
return FKismetDebugUtilities::BlueprintHasBreakpoints(BlueprintToWatchPtr.Get());
}
return false;
}
FReply SKismetDebuggingView::OnToggleAllBreakpointsClicked()
{
if (UBlueprint* Blueprint = BlueprintToWatchPtr.Get())
{
const FBlueprintBreakpoint* EnabledBreakpoint = FKismetDebugUtilities::FindBreakpointByPredicate(BlueprintToWatchPtr.Get(), [](const FBlueprintBreakpoint& Breakpoint)
{
return Breakpoint.IsEnabled();
});
bool bHasAnyEnabledBreakpoint = EnabledBreakpoint != nullptr;
if (BlueprintToWatchPtr.IsValid())
{
FKismetDebugUtilities::ForeachBreakpoint(BlueprintToWatchPtr.Get(),
[bHasAnyEnabledBreakpoint](FBlueprintBreakpoint& Breakpoint)
{
FKismetDebugUtilities::SetBreakpointEnabled(Breakpoint, !bHasAnyEnabledBreakpoint);
}
);
}
return FReply::Handled();
}
return FReply::Unhandled();
}
class FBlueprintFilter : public IClassViewerFilter
{
public:
FBlueprintFilter() = default;
virtual bool IsClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const UClass* InClass, TSharedRef< FClassViewerFilterFuncs > InFilterFuncs ) override
{
return InClass && !InClass->HasAnyClassFlags(CLASS_Deprecated) &&
InClass->HasAllClassFlags(CLASS_CompiledFromBlueprint);
}
virtual bool IsUnloadedClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const TSharedRef< const IUnloadedBlueprintData > InUnloadedClassData, TSharedRef< FClassViewerFilterFuncs > InFilterFuncs) override
{
return !InUnloadedClassData->HasAnyClassFlags(CLASS_Deprecated) &&
InUnloadedClassData->HasAllClassFlags(CLASS_CompiledFromBlueprint);
}
};
void SKismetDebuggingView::OnBlueprintClassPicked(UClass* PickedClass)
{
if (PickedClass)
{
BlueprintToWatchPtr = Cast<UBlueprint>(PickedClass->ClassGeneratedBy);
}
else
{
// User selected None Option
BlueprintToWatchPtr.Reset();
}
FDebugLineItem::SetBreakpointParentItemBlueprint(BreakpointParentItem, BlueprintToWatchPtr);
DebugClassComboButton->SetIsOpen(false);
}
TSharedRef<SWidget> SKismetDebuggingView::ConstructBlueprintClassPicker()
{
FClassViewerInitializationOptions Options;
Options.Mode = EClassViewerMode::ClassPicker;
Options.bShowBackgroundBorder = false;
Options.ClassFilters.Add(MakeShared<FBlueprintFilter>());
Options.bIsBlueprintBaseOnly = true;
Options.bShowUnloadedBlueprints = false;
Options.bShowNoneOption = true;
FClassViewerModule& ClassViewerModule = FModuleManager::LoadModuleChecked<FClassViewerModule>("ClassViewer");
FOnClassPicked OnClassPicked;
OnClassPicked.BindRaw(this, &SKismetDebuggingView::OnBlueprintClassPicked);
return SNew(SBox)
.HeightOverride(500.f)
[
ClassViewerModule.CreateClassViewer(Options, OnClassPicked)
];
}
void SKismetDebuggingView::Construct(const FArguments& InArgs)
{
BlueprintToWatchPtr = InArgs._BlueprintToWatch;
// Build the debug toolbar
static const FName ToolbarName = "Kismet.DebuggingViewToolBar";
TryRegisterDebugToolbar();
FToolMenuContext MenuContext(FPlayWorldCommands::GlobalPlayWorldActions);
TSharedRef<SWidget> ToolbarWidget = UToolMenus::Get()->GenerateWidget(ToolbarName, MenuContext);
DebugClassComboButton =
SNew(SComboButton)
.OnGetMenuContent_Raw(this, &SKismetDebuggingView::ConstructBlueprintClassPicker)
.ButtonContent()
[
SNew(STextBlock)
.Text_Lambda([&BlueprintToWatchPtr = BlueprintToWatchPtr]()
{
return BlueprintToWatchPtr.IsValid()?
FText::FromString(BlueprintToWatchPtr->GetName()) :
LOCTEXT("SelectBlueprint", "Select Blueprint");
})
];
FBlueprintContextTracker::OnEnterScriptContext.AddLambda(
[](const FBlueprintContextTracker& ContextTracker, const UObject* ContextObject, const UFunction* ContextFunction)
{
CurrentActiveObject = ContextObject;
}
);
FBlueprintContextTracker::OnExitScriptContext.AddLambda(
[](const FBlueprintContextTracker& ContextTracker)
{
CurrentActiveObject = nullptr;
}
);
this->ChildSlot
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SBorder)
.BorderImage( FAppStyle::GetBrush( TEXT("NoBorder") ) )
[
ToolbarWidget
]
]
+SVerticalBox::Slot()
.AutoHeight()
[
SNew( SVerticalBox )
+SVerticalBox::Slot()
.AutoHeight()
[
SNew( STextBlock )
.Text( this, &SKismetDebuggingView::GetTopText )
]
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew( SHorizontalBox )
+ SHorizontalBox::Slot()
.HAlign( HAlign_Left )
[
SNew(SBox)
.WidthOverride(400.f)
[
DebugClassComboButton.ToSharedRef()
]
]
+ SHorizontalBox::Slot()
.HAlign( HAlign_Right )
[
SNew( SButton )
.IsEnabled( this, &SKismetDebuggingView::CanToggleAllBreakpoints )
.Text( this, &SKismetDebuggingView::GetToggleAllBreakpointsText )
.OnClicked( this, &SKismetDebuggingView::OnToggleAllBreakpointsClicked )
]
]
+ SVerticalBox::Slot()
.AutoHeight()
[
SAssignNew(SearchBox, SSearchBox)
.OnTextChanged(this, &SKismetDebuggingView::OnSearchTextChanged)
]
]
+SVerticalBox::Slot()
[
SNew(SSplitter)
.Orientation(Orient_Vertical)
+SSplitter::Slot()
[
SAssignNew( DebugTreeView, SKismetDebugTreeView )
.InDebuggerTab(true)
.HeaderRow
(
SNew(SHeaderRow)
+ SHeaderRow::Column(SKismetDebugTreeView::ColumnId_Name)
.DefaultLabel(KismetDebugViewConstants::ColumnText_Name)
+ SHeaderRow::Column(SKismetDebugTreeView::ColumnId_Value)
.DefaultLabel(KismetDebugViewConstants::ColumnText_Value)
)
]
+SSplitter::Slot()
[
SAssignNew( OtherTreeView, SKismetDebugTreeView )
.InDebuggerTab(true)
.HeaderRow
(
SNew(SHeaderRow)
+ SHeaderRow::Column(SKismetDebugTreeView::ColumnId_Name)
.DefaultLabel(KismetDebugViewConstants::ColumnText_DebugKey)
+ SHeaderRow::Column(SKismetDebugTreeView::ColumnId_Value)
.DefaultLabel(KismetDebugViewConstants::ColumnText_Info)
)
]
]
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
[
GetDebugLineTypeToggle(FDebugLineItem::DLT_Watch, LOCTEXT("Watchpoints", "Watchpoints"))
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
GetDebugLineTypeToggle(FDebugLineItem::DLT_LatentAction, LOCTEXT("LatentActions", "Latent Actions"))
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
GetDebugLineTypeToggle(FDebugLineItem::DLT_BreakpointParent, LOCTEXT("Breakpoints", "Breakpoints"))
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
GetDebugLineTypeToggle(FDebugLineItem::DLT_TraceStackParent, LOCTEXT("ExecutionTrace", "Execution Trace"))
]
]
];
TraceStackItem = SKismetDebugTreeView::MakeTraceStackParentItem();
BreakpointParentItem = SKismetDebugTreeView::MakeBreakpointParentItem(BlueprintToWatchPtr);
}
void SKismetDebuggingView::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
// don't update during scroll. this will help make the scroll smoother
if (DebugTreeView->IsScrolling() || OtherTreeView->IsScrolling())
{
return;
}
// If there is no play world, immediately clear the list. this avoids showing a phantom 'nullptr' item when ending PIE
const bool bIsDebugging = GEditor->PlayWorld != nullptr;
if (!bIsDebugging && DebugTreeView->GetRootTreeItems().Num() > 0)
{
DebugTreeView->ClearTreeItems();
return;
}
// update less often to avoid lag
TreeUpdateTimer += InDeltaTime;
if (TreeUpdateTimer < UpdateInterval)
{
return;
}
TreeUpdateTimer = 0.f;
// Gather the old root set
TSet<UObject*> OldRootSet;
for (const FDebugTreeItemPtr& Item : DebugTreeView->GetRootTreeItems())
{
if (UObject* OldObject = Item->GetParentObject())
{
OldRootSet.Add(OldObject);
}
}
// Gather what we'd like to be the new root set
TSet<UObject*> NewRootSet;
const auto TryAddBlueprintToNewRootSet = [&NewRootSet](UBlueprint* InBlueprint)
{
for (FThreadSafeObjectIterator Iter(InBlueprint->GeneratedClass, /*bOnlyGCedObjects =*/ false, /*AdditionalExclusionFlags =*/ RF_ArchetypeObject | RF_ClassDefaultObject); Iter; ++Iter)
{
UObject* Instance = *Iter;
if (!Instance)
{
continue;
}
// only include instances of objects in a PIE world
if (UWorld* World = Instance->GetTypedOuter<UWorld>())
{
if (!World || World->WorldType != EWorldType::PIE)
{
continue;
}
}
NewRootSet.Add(Instance);
}
};
if(bIsDebugging)
{
if (BlueprintToWatchPtr.IsValid())
{
// Show blueprint objects of the selected class
TryAddBlueprintToNewRootSet(BlueprintToWatchPtr.Get());
}
else
{
// Show all blueprint objects with watches
for (FThreadSafeObjectIterator BlueprintIter(UBlueprint::StaticClass(), /*bOnlyGCedObjects =*/ false, /*AdditionalExclusionFlags =*/ RF_ArchetypeObject | RF_ClassDefaultObject); BlueprintIter; ++BlueprintIter)
{
UBlueprint* Blueprint = Cast<UBlueprint>(*BlueprintIter);
if (Blueprint && FKismetDebugUtilities::BlueprintHasPinWatches(Blueprint))
{
TryAddBlueprintToNewRootSet(Blueprint);
}
}
}
}
// This will pull anything out of Old that is also New (sticking around), so afterwards Old is a list of things to remove
DebugTreeView->ClearTreeItems();
for (UObject* ObjectToAdd : NewRootSet)
{
TWeakObjectPtr<UObject> WeakObject = ObjectToAdd;
// destroyed objects can still appear if they haven't ben GCed yet.
// weak object pointers will detect it and return nullptr
if(!WeakObject.Get())
{
continue;
}
if (OldRootSet.Contains(ObjectToAdd))
{
OldRootSet.Remove(ObjectToAdd);
const TSharedPtr<FDebugLineItem>& Item = ObjectToTreeItemMap.FindChecked(ObjectToAdd);
DebugTreeView->AddTreeItemUnique(Item);
}
else
{
FDebugTreeItemPtr NewPtr = SKismetDebugTreeView::MakeParentItem(ObjectToAdd);
ObjectToTreeItemMap.Add(ObjectToAdd, NewPtr);
DebugTreeView->AddTreeItemUnique(NewPtr);
}
}
// Remove the old root set items that didn't get used again
for (UObject* ObjectToRemove : OldRootSet)
{
ObjectToTreeItemMap.Remove(ObjectToRemove);
}
// Add a message if there are no active instances of DebugClass
if (DebugTreeView->GetRootTreeItems().Num() == 0)
{
DebugTreeView->AddTreeItemUnique(SKismetDebugTreeView::MakeMessageItem(
bIsDebugging ?
LOCTEXT("NoInstances", "No instances of this blueprint in existence").ToString() :
LOCTEXT("NoPIEorSIE", "run PIE or SIE to see instance debug info").ToString()
));
}
// Show Breakpoints
if(FDebugLineItem::IsDebugLineTypeActive(FDebugLineItem::DLT_BreakpointParent))
{
OtherTreeView->AddTreeItemUnique(BreakpointParentItem);
}
else
{
OtherTreeView->RemoveTreeItem(BreakpointParentItem);
}
// Show the trace stack when debugging
if (bIsDebugging && FDebugLineItem::IsDebugLineTypeActive(FDebugLineItem::DLT_TraceStackParent))
{
OtherTreeView->AddTreeItemUnique(TraceStackItem);
}
else
{
OtherTreeView->RemoveTreeItem(TraceStackItem);
}
OtherTreeView->RequestUpdateFilteredItems();
}
void SKismetDebuggingView::SetBlueprintToWatch(TWeakObjectPtr<UBlueprint> InBlueprintToWatch)
{
BlueprintToWatchPtr = InBlueprintToWatch;
FDebugLineItem::SetBreakpointParentItemBlueprint(BreakpointParentItem, BlueprintToWatchPtr);
}
#undef LOCTEXT_NAMESPACE