// Copyright Epic Games, Inc. All Rights Reserved. #include "WatchPointViewer.h" #include "Blueprint/WidgetBlueprintGeneratedClass.h" #include "Styling/AppStyle.h" #include "Framework/Commands/GenericCommands.h" #include "Framework/Docking/TabManager.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "HAL/PlatformApplicationMisc.h" #include "K2Node_Event.h" #include "Kismet2/KismetEditorUtilities.h" #include "Kismet2/KismetDebugUtilities.h" #include "GraphEditorActions.h" #include "EdGraphSchema_K2.h" #include "UnrealEdGlobals.h" #include "Editor/UnrealEdEngine.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Editor.h" #include "ToolMenus.h" #include "Widgets/Docking/SDockTab.h" #include "Widgets/Images/SImage.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SSpacer.h" #include "Widgets/Views/STreeView.h" #include "Widgets/Views/STableRow.h" #include "Widgets/Input/SHyperlink.h" #include "WorkspaceMenuStructure.h" #include "WorkspaceMenuStructureModule.h" #include "Kismet2/DebuggerCommands.h" #include "KismetNodes/KismetNodeInfoContext.h" #include "Stats/Stats.h" #define LOCTEXT_NAMESPACE "WatchPointViewer" namespace { struct FWatchRow { FWatchRow( TWeakObjectPtr InBP, const UEdGraphNode* InNode, const UEdGraphPin* InPin, UObject* InObjectBeingDebugged, FText InBlueprintName, FText InGraphName, FText InNodeName, FText InDisplayName, FText InValue, FText InType ) : BP(InBP) , Node(InNode) , Pin(InPin) , ObjectBeingDebugged(InObjectBeingDebugged) , BlueprintName(MoveTemp(InBlueprintName)) , GraphName(MoveTemp(InGraphName)) , NodeName(MoveTemp(InNodeName)) , DisplayName(MoveTemp(InDisplayName)) , Value(MoveTemp(InValue)) , Type(MoveTemp(InType)) { SetObjectBeingDebuggedName(); UPackage* Package = Cast(BP.IsValid() ? BP->GetOuter() : nullptr); BlueprintPackageName = Package ? Package->GetFName() : FName(); } FWatchRow( TWeakObjectPtr InBP, const UEdGraphNode* InNode, const UEdGraphPin* InPin, UObject* InObjectBeingDebugged, FText InBlueprintName, FText InGraphName, FText InNodeName, FPropertyInstanceInfo& Info ) : BP(InBP) , Node(InNode) , Pin(InPin) , ObjectBeingDebugged(InObjectBeingDebugged) , BlueprintName(MoveTemp(InBlueprintName)) , GraphName(MoveTemp(InGraphName)) , NodeName(MoveTemp(InNodeName)) , DisplayName(Info.DisplayName) , Value(Info.Value) , Type(Info.Type) { SetObjectBeingDebuggedName(); UPackage* Package = Cast(BP.IsValid() ? BP->GetOuter() : nullptr); BlueprintPackageName = Package ? Package->GetFName() : FName(); for (const TSharedPtr& ChildInfo : Info.GetChildren()) { Children.Add(MakeShared(InBP, InNode, InPin, InObjectBeingDebugged, BlueprintName, GraphName, NodeName, *ChildInfo)); } } // this can't be const because we store watches in the blueprint TWeakObjectPtr BP; const UEdGraphNode* Node; const UEdGraphPin* Pin; // this can't be const because SelectActor takes a non-const actor UObject* ObjectBeingDebugged; FText BlueprintName; FText ObjectBeingDebuggedName; FText GraphName; FText NodeName; FText DisplayName; FText Value; FText Type; FName BlueprintPackageName; TArray> Children; // used for copying entries in the watch viewer FText GetTextForEntry() const { FFormatNamedArguments Args; Args.Add(TEXT("ObjectName"), FText::FromString(ObjectBeingDebugged ? ObjectBeingDebugged->GetName() : TEXT(""))); Args.Add(TEXT("BlueprintName"), BlueprintName); Args.Add(TEXT("GraphName"), GraphName); Args.Add(TEXT("NodeName"), NodeName); Args.Add(TEXT("DisplayName"), DisplayName); Args.Add(TEXT("Type"), Type); Args.Add(TEXT("Value"), Value); return FText::Format(LOCTEXT("WatchEntry", "{ObjectName}({BlueprintName}) {GraphName} {NodeName} {DisplayName}({Type}): {Value}"), Args); } private: void SetObjectBeingDebuggedName() { if (ObjectBeingDebugged != nullptr) { AActor* ActorBeingDebugged = Cast(ObjectBeingDebugged); if (ActorBeingDebugged) { ObjectBeingDebuggedName = FText::AsCultureInvariant(ActorBeingDebugged->GetActorLabel()); } else { ObjectBeingDebuggedName = FText::FromName(ObjectBeingDebugged->GetFName()); } } else { ObjectBeingDebuggedName = BlueprintName; } } }; DECLARE_MULTICAST_DELEGATE_OneParam(FOnDisplayedWatchWindowChanged, TArray>*); FOnDisplayedWatchWindowChanged WatchListSubscribers; // Proxy array of the watches. This allows us to manually refresh UI state when changes are made: TArray> Private_WatchSource; TArray> Private_InstanceWatchSource; TArray> WatchedBlueprints; // Returns true if the blueprint execution is currently paused; false otherwise bool IsPaused() { return GUnrealEd && GUnrealEd->PlayWorld && GUnrealEd->PlayWorld->bDebugPauseExecution; } void UpdateNonInstancedWatchDisplay() { Private_WatchSource.Reset(); for (TWeakObjectPtr BlueprintObj : WatchedBlueprints) { if (!BlueprintObj.IsValid()) { continue; } FText BlueprintName = FText::FromString(BlueprintObj->GetName()); FKismetDebugUtilities::ForeachPinWatch( BlueprintObj.Get(), [BlueprintObj, BlueprintName](UEdGraphPin* Pin) { FText GraphName = FText::FromString(Pin->GetOwningNode()->GetGraph()->GetName()); FText NodeName = Pin->GetOwningNode()->GetNodeTitle(ENodeTitleType::ListView); const UEdGraphSchema* Schema = Pin->GetOwningNode()->GetSchema(); TSharedPtr DebugInfo; DebugInfo->DisplayName = Schema->GetPinDisplayName(Pin); DebugInfo->Type = UEdGraphSchema_K2::TypeToText(Pin->PinType); DebugInfo->Value = LOCTEXT("ExecutionNotPaused", "(execution not paused)"); Private_WatchSource.Add( MakeShared( BlueprintObj, Pin->GetOwningNode(), Pin, nullptr, BlueprintName, MoveTemp(GraphName), MoveTemp(NodeName), *DebugInfo ) ); } ); } } void UpdateWatchListFromBlueprintImpl(TWeakObjectPtr BlueprintObj, const bool bShouldWatch) { if (bShouldWatch) { // make sure the blueprint is in our list WatchedBlueprints.AddUnique(BlueprintObj); } else { // if this blueprint shouldn't be watched and we aren't watching it already then there is nothing to do int32 FoundIdx = WatchedBlueprints.Find(BlueprintObj); if (FoundIdx == INDEX_NONE) { // if we didn't find it, it could be because BlueprintObj is no longer valid // in this case the pointer in WatchedBlueprints would also be invalid bool bRemovedBP = false; for (int32 Idx = 0; Idx < WatchedBlueprints.Num(); ++Idx) { if (!WatchedBlueprints[Idx].IsValid()) { bRemovedBP = true; WatchedBlueprints.RemoveAt(Idx); --Idx; } } if (!bRemovedBP) { return; } } else { // since we're not watching the blueprint anymore we should remove it from the watched list WatchedBlueprints.RemoveAt(FoundIdx); } } // something changed so we need to update the lists shown in the UI UpdateNonInstancedWatchDisplay(); if (IsPaused()) { #ifndef WATCH_VIEWER_DEPRECATED WatchViewer::UpdateInstancedWatchDisplay(); #endif } // Notify subscribers: WatchListSubscribers.Broadcast(&Private_WatchSource); } // Updates all of the watches from the currently watched blueprints void UpdateAllBlueprintWatches() { for (TWeakObjectPtr Blueprint : WatchedBlueprints) { UpdateWatchListFromBlueprintImpl(Blueprint, true); } } }; /** * Widget that visualizes the contents of a FWatchRow. */ class SWatchTreeWidgetItem : public SMultiColumnTableRow> { public: SLATE_BEGIN_ARGS(SWatchTreeWidgetItem) : _WatchToVisualize() { } SLATE_ARGUMENT(TSharedPtr, WatchToVisualize) SLATE_END_ARGS() public: /** * Construct child widgets that comprise this widget. * * @param InArgs Declaration from which to construct this widget. */ void Construct(const FArguments& InArgs, class SWatchViewer* InOwner, const TSharedRef& InOwnerTableView); public: // SMultiColumnTableRow overrides virtual TSharedRef GenerateWidgetForColumn(const FName& ColumnName) override; protected: FText GetDebuggedObjectName() const { return WatchRow->ObjectBeingDebuggedName; } FText GetBlueprintName() const { return WatchRow->BlueprintName; } FText GetGraphName() const { return WatchRow->GraphName; } FText GetNodeName() const { return WatchRow->NodeName; } FText GetVariableName() const { return WatchRow->DisplayName; } FText GetValue() const { return WatchRow->Value; } FText GetType() const { return WatchRow->Type; } void HandleHyperlinkDebuggedObjectNavigate() const { if (AActor* Actor = Cast(WatchRow.IsValid() ? WatchRow->ObjectBeingDebugged : nullptr)) { // unselect whatever was selected GEditor->SelectNone(false, false, false); // select the actor we care about GEditor->SelectActor(Actor, true, true, true); } } EVisibility DisplayDebuggedObjectAsHyperlink() const { if ( AActor* Actor = Cast(WatchRow.IsValid() ? WatchRow->ObjectBeingDebugged : nullptr)) { return EVisibility::Visible; } return EVisibility::Collapsed; } EVisibility DisplayDebuggedObjectAsText() const { if (AActor* Actor = Cast(WatchRow.IsValid() ? WatchRow->ObjectBeingDebugged : nullptr)) { return EVisibility::Collapsed; } return EVisibility::Visible; } void HandleHyperlinkNodeNavigate() const { if (WatchRow.IsValid() && WatchRow->Node) { FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(WatchRow->Node); } } private: /** The info about the widget that we are visualizing. */ TSharedPtr WatchRow; SWatchViewer* Owner; }; typedef STreeView> SWatchTree; class SWatchViewer : public SCompoundWidget { friend class FBlueprintEditor; public: SLATE_BEGIN_ARGS(SWatchViewer){} SLATE_END_ARGS() SWatchViewer() { // make sure we have the latest information about the watches on loaded blueprints UpdateAllBlueprintWatches(); FKismetDebugUtilities::WatchedPinsListChangedEvent.AddRaw(this, &SWatchViewer::HandleWatchedPinsChanged); FEditorDelegates::ResumePIE.AddRaw(this, &SWatchViewer::HandleResumePIE); FEditorDelegates::EndPIE.AddRaw(this, &SWatchViewer::HandleEndPIE); FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); AssetRegistryModule.Get().OnAssetRemoved().AddRaw(this, &SWatchViewer::HandleAssetRemoved); AssetRegistryModule.Get().OnAssetRenamed().AddRaw(this, &SWatchViewer::HandleAssetRenamed); } ~SWatchViewer() { FKismetDebugUtilities::WatchedPinsListChangedEvent.RemoveAll(this); FEditorDelegates::ResumePIE.RemoveAll(this); FEditorDelegates::EndPIE.RemoveAll(this); if (FModuleManager::Get().IsModuleLoaded(TEXT("AssetRegistry"))) { IAssetRegistry* AssetRegistry = FModuleManager::GetModuleChecked(TEXT("AssetRegistry")).TryGet(); if (AssetRegistry) { AssetRegistry->OnAssetRemoved().RemoveAll(this); AssetRegistry->OnAssetRenamed().RemoveAll(this); } } } void Construct(const FArguments& InArgs, TArray>* InWatchSource); TSharedRef HandleGenerateRow(TSharedRef InWatchRow, const TSharedRef& OwnerTable); void HandleGetChildren(TSharedRef InWatchRow, TArray>& OutChildren); void HandleWatchedPinsChanged(UBlueprint* BlueprintObj); void HandleResumePIE(bool); void HandleEndPIE(bool); void HandleAssetRemoved(const FAssetData& InAssetData); void HandleAssetRenamed(const FAssetData& InAssetData, const FString& InOldName); void UpdateWatches(TArray>* WatchValues); void CopySelectedRows() const; void StopWatchingPin() const; TSharedPtr WatchTreeWidget; TArray>* WatchSource; TSharedPtr< FUICommandList > CommandList; private: void CopySelectedRowsHelper(const TArray>& RowSource, FString& StringToCopy) const; }; void SWatchViewer::Construct(const FArguments& InArgs, TArray>* InWatchSource) { CommandList = MakeShareable( new FUICommandList ); CommandList->MapAction( FGenericCommands::Get().Copy, FExecuteAction::CreateSP( this, &SWatchViewer::CopySelectedRows ), // we need to override the default 'can execute' because we want to be available during debugging: FCanExecuteAction::CreateStatic( [](){ return true; } ) ); CommandList->MapAction( FGraphEditorCommands::Get().StopWatchingPin, FExecuteAction::CreateSP(this, &SWatchViewer::StopWatchingPin), FCanExecuteAction::CreateStatic([]() { return true; }) ); WatchSource = InWatchSource; const auto ContextMenuOpened = [](TWeakPtr InCommandList, TWeakPtr ControlOwnerWeak) -> TSharedPtr { const bool CloseAfterSelection = true; FMenuBuilder MenuBuilder( CloseAfterSelection, InCommandList.Pin() ); MenuBuilder.AddMenuEntry(FGraphEditorCommands::Get().StopWatchingPin); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Copy); return MenuBuilder.MakeWidget(); }; const auto EmptyWarningVisibility = [](TWeakPtr ControlOwnerWeak) -> EVisibility { TSharedPtr ControlOwner = ControlOwnerWeak.Pin(); if (ControlOwner.IsValid() && ControlOwner->WatchSource && ControlOwner->WatchSource->Num() > 0) { return EVisibility::Hidden; } return EVisibility::Visible; }; const auto WatchViewIsEnabled = [](TWeakPtr ControlOwnerWeak) -> bool { TSharedPtr ControlOwner = ControlOwnerWeak.Pin(); if (ControlOwner.IsValid() && ControlOwner->WatchSource && ControlOwner->WatchSource->Num() > 0) { return true; } return false; }; // Cast due to TSharedFromThis inheritance issues: TSharedRef SelfTyped = StaticCastSharedRef(AsShared()); TWeakPtr SelfWeak = SelfTyped; TWeakPtr CommandListWeak = CommandList; ChildSlot [ SNew(SBorder) .Padding(4.0f) .BorderImage( FAppStyle::GetBrush("ToolPanel.GroupBorder") ) [ SNew(SOverlay) +SOverlay::Slot() [ SAssignNew(WatchTreeWidget, SWatchTree) .TreeItemsSource(WatchSource) .OnGenerateRow(this, &SWatchViewer::HandleGenerateRow) .OnGetChildren(this, &SWatchViewer::HandleGetChildren) .OnContextMenuOpening(FOnContextMenuOpening::CreateStatic(ContextMenuOpened, CommandListWeak, SelfWeak)) .IsEnabled( TAttribute::Create( TAttribute::FGetter::CreateStatic(WatchViewIsEnabled, SelfWeak) ) ) .HeaderRow ( SNew(SHeaderRow) +SHeaderRow::Column(TEXT("ObjectName")) .FillWidth(.2f) .VAlignHeader(VAlign_Center) .DefaultLabel(LOCTEXT("ObjectName", "Object Name")) .DefaultTooltip(LOCTEXT("ObjectNameTooltip", "Name of the object instance being debugged or the blueprint if there is no object being debugged")) + SHeaderRow::Column(TEXT("GraphName")) .FillWidth(.2f) .VAlignHeader(VAlign_Center) .DefaultLabel(LOCTEXT("GraphName", "Graph Name")) .DefaultTooltip(LOCTEXT("GraphNameTooltip", "Name of the source blueprint graph for this variable")) + SHeaderRow::Column(TEXT("NodeName")) .FillWidth(.3f) .VAlignHeader(VAlign_Center) .DefaultLabel(LOCTEXT("NodeName", "Node Name")) .DefaultTooltip(LOCTEXT("NodeNameTooltip", "Name of the source blueprint graph node for this variable")) + SHeaderRow::Column(TEXT("VariableName")) .FillWidth(.3f) .VAlignHeader(VAlign_Center) .DefaultLabel(LOCTEXT("VariableName", "Variable Name")) .DefaultTooltip(LOCTEXT("VariabelNameTooltip", "Name of the variable")) + SHeaderRow::Column(TEXT("Value")) .FillWidth(.8f) .VAlignHeader(VAlign_Center) .DefaultLabel(LOCTEXT("Value", "Value")) .DefaultTooltip(LOCTEXT("ValueTooltip", "Current value of this variable")) ) ] +SOverlay::Slot() .Padding(32.f) [ SNew(STextBlock) .Text( LOCTEXT("NoWatches", "No watches to display") ) .Justification(ETextJustify::Center) .Visibility( TAttribute::Create( TAttribute::FGetter::CreateStatic(EmptyWarningVisibility, SelfWeak) ) ) ] ] ]; WatchListSubscribers.AddSP(StaticCastSharedRef(AsShared()), &SWatchViewer::UpdateWatches); } TSharedRef SWatchViewer::HandleGenerateRow(TSharedRef InWatchRow, const TSharedRef& OwnerTable) { return SNew(SWatchTreeWidgetItem, this, OwnerTable) .WatchToVisualize(InWatchRow); } void SWatchViewer::HandleGetChildren(TSharedRef InWatchRow, TArray>& OutChildren) { OutChildren = InWatchRow->Children; } void SWatchViewer::HandleWatchedPinsChanged(UBlueprint* BlueprintObj) { #ifndef WATCH_VIEWER_DEPRECATED WatchViewer::UpdateWatchListFromBlueprint(BlueprintObj); #endif } void SWatchViewer::HandleResumePIE(bool) { #ifndef WATCH_VIEWER_DEPRECATED // swap to displaying the unpaused watches WatchViewer::ContinueExecution(); #endif } void SWatchViewer::HandleEndPIE(bool) { #ifndef WATCH_VIEWER_DEPRECATED // show the unpaused watches in case we stopped PIE while at a breakpoint WatchViewer::ContinueExecution(); #endif } void SWatchViewer::HandleAssetRemoved(const FAssetData& InAssetData) { #ifndef WATCH_VIEWER_DEPRECATED WatchViewer::RemoveWatchesForAsset(InAssetData); #endif } void SWatchViewer::HandleAssetRenamed(const FAssetData& InAssetData, const FString& InOldName) { #ifndef WATCH_VIEWER_DEPRECATED WatchViewer::OnRenameAsset(InAssetData, InOldName); #endif } void SWatchViewer::UpdateWatches(TArray>* Watches) { WatchSource = Watches; WatchTreeWidget->SetTreeItemsSource(Watches); } void SWatchViewer::CopySelectedRowsHelper(const TArray>& RowSource, FString& StringToCopy) const { for (const TSharedRef& Item : RowSource) { if (WatchTreeWidget->IsItemSelected(Item)) { StringToCopy.Append(Item->GetTextForEntry().ToString()); StringToCopy.Append(LINE_TERMINATOR); } CopySelectedRowsHelper(Item->Children, StringToCopy); } } void SWatchViewer::CopySelectedRows() const { FString StringToCopy; // We want to copy in the order displayed, not the order selected, so iterate the list and build up the string: if (WatchSource) { CopySelectedRowsHelper(*WatchSource, StringToCopy); } if (!StringToCopy.IsEmpty()) { FPlatformApplicationMisc::ClipboardCopy(*StringToCopy); } } void SWatchViewer::StopWatchingPin() const { TArray> SelectedRows = WatchTreeWidget->GetSelectedItems(); for (TSharedRef& Row : SelectedRows) { FKismetDebugUtilities::TogglePinWatch(Row->BP.Get(), Row->Pin); } } void SWatchTreeWidgetItem::Construct(const FArguments& InArgs, SWatchViewer* InOwner, const TSharedRef& InOwnerTableView) { this->WatchRow = InArgs._WatchToVisualize; Owner = InOwner; SMultiColumnTableRow>::Construct(SMultiColumnTableRow>::FArguments().Padding(1.0f), InOwnerTableView); } TSharedRef SWatchTreeWidgetItem::GenerateWidgetForColumn(const FName& ColumnName) { const static FName NAME_ObjectName(TEXT("ObjectName")); const static FName NAME_GraphName(TEXT("GraphName")); const static FName NAME_NodeName(TEXT("NodeName")); const static FName NAME_VariableName(TEXT("VariableName")); const static FName NAME_Value(TEXT("Value")); if (ColumnName == NAME_ObjectName) { return SNew(SBox) .HAlign(HAlign_Left) .VAlign(VAlign_Center) .Padding(FMargin(2.0f, 1.0f)) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ SNew(SHyperlink) .Text(this, &SWatchTreeWidgetItem::GetDebuggedObjectName) .ToolTipText(this, &SWatchTreeWidgetItem::GetBlueprintName) .OnNavigate(this, &SWatchTreeWidgetItem::HandleHyperlinkDebuggedObjectNavigate) .Visibility(this, &SWatchTreeWidgetItem::DisplayDebuggedObjectAsHyperlink) ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(STextBlock) .Text(this, &SWatchTreeWidgetItem::GetDebuggedObjectName) .ToolTipText(this, &SWatchTreeWidgetItem::GetBlueprintName) .Visibility(this, &SWatchTreeWidgetItem::DisplayDebuggedObjectAsText) ] ]; } else if (ColumnName == NAME_GraphName) { return SNew(SBox) .HAlign(HAlign_Left) .VAlign(VAlign_Center) .Padding(FMargin(2.0f, 1.0f)) [ SNew(STextBlock) .Text(this, &SWatchTreeWidgetItem::GetGraphName) ]; } else if (ColumnName == NAME_NodeName) { FString Comment; if (WatchRow->Node->NodeComment.Len() > 0) { Comment = TEXT("\n\n"); Comment.Append(WatchRow->Node->NodeComment); } FText TooltipText = FText::Format(LOCTEXT("NodeTooltip", "Find the {0} node in the blueprint graph.{1}"), GetNodeName(), FText::FromString(Comment)); return SNew(SBox) .HAlign(HAlign_Left) .VAlign(VAlign_Center) .Padding(FMargin(2.0f, 1.0f)) [ SNew(SHyperlink) .Text(this, &SWatchTreeWidgetItem::GetNodeName) .ToolTipText(TooltipText) .OnNavigate(this, &SWatchTreeWidgetItem::HandleHyperlinkNodeNavigate) ]; } else if (ColumnName == NAME_VariableName) { return SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SExpanderArrow, SharedThis(this)) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(2.0f, 1.0f) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(this, &SWatchTreeWidgetItem::GetVariableName) .ToolTipText(this, &SWatchTreeWidgetItem::GetType) ]; } else if (ColumnName == NAME_Value) { return SNew(SBox) .HAlign(HAlign_Left) .VAlign(VAlign_Center) .Padding(FMargin(2.0f, 1.0f)) [ SNew(STextBlock) .Text(this, &SWatchTreeWidgetItem::GetValue) ]; } else { return SNullWidget::NullWidget; } } void WatchViewer::UpdateInstancedWatchDisplay() { #if DO_BLUEPRINT_GUARD { Private_InstanceWatchSource.Reset(); TArrayView ScriptStack = FBlueprintContextTracker::Get().GetCurrentScriptStack(); TSet SeenBlueprints; for (const FFrame* ScriptFrame : ScriptStack) { UObject* BlueprintInstance = ScriptFrame ? ScriptFrame->Object : nullptr; UClass* Class = BlueprintInstance ? BlueprintInstance->GetClass() : nullptr; UBlueprint* BlueprintObj = (Class ? Cast(Class->ClassGeneratedBy) : nullptr); if (BlueprintObj == nullptr) { continue; } // Only add watchpoints from each blueprint once if (SeenBlueprints.Contains(BlueprintObj)) { continue; } SeenBlueprints.Add(BlueprintObj); FText BlueprintName = FText::FromString(BlueprintObj->GetName()); // Don't show info for the CDO if (BlueprintInstance->IsTemplate()) { continue; } // Don't show info if this instance is pending kill if (!IsValid(BlueprintInstance)) { continue; } // Don't show info if this instance isn't in the current world UObject* ObjOuter = BlueprintInstance; UWorld* ObjWorld = nullptr; static bool bUseNewWorldCode = false; do // Run through at least once in case the TestObject is a UGameInstance { UGameInstance* ObjGameInstance = Cast(ObjOuter); ObjOuter = ObjOuter->GetOuter(); ObjWorld = ObjGameInstance ? ObjGameInstance->GetWorld() : Cast(ObjOuter); } while (ObjWorld == nullptr && ObjOuter != nullptr); if (ObjWorld) { // Make check on owning level (not streaming level) if (ObjWorld->PersistentLevel && ObjWorld->PersistentLevel->OwningWorld) { ObjWorld = ObjWorld->PersistentLevel->OwningWorld; } if (ObjWorld->WorldType != EWorldType::PIE && !((ObjWorld->WorldType == EWorldType::Editor) && (GUnrealEd->GetPIEViewport() == nullptr))) { continue; } } // We have a valid instance, iterate over all the watched pins and create rows for them FKismetDebugUtilities::ForeachPinWatch( BlueprintObj, [BlueprintObj, BlueprintInstance, BlueprintName](UEdGraphPin* Pin) { FText GraphName = FText::FromString(Pin->GetOwningNode()->GetGraph()->GetName()); FText NodeName = Pin->GetOwningNode()->GetNodeTitle(ENodeTitleType::ListView); TSharedPtr DebugInfo; const FKismetDebugUtilities::EWatchTextResult WatchStatus = FKismetDebugUtilities::GetDebugInfo(DebugInfo, BlueprintObj, BlueprintInstance, Pin); if (WatchStatus != FKismetDebugUtilities::EWTR_Valid) { const UEdGraphSchema* Schema = Pin->GetOwningNode()->GetSchema(); DebugInfo->DisplayName = Schema->GetPinDisplayName(Pin); DebugInfo->Type = UEdGraphSchema_K2::TypeToText(Pin->PinType); switch (WatchStatus) { case FKismetDebugUtilities::EWTR_NotInScope: DebugInfo->Value = LOCTEXT("NotInScope", "(not in scope)"); break; case FKismetDebugUtilities::EWTR_NoProperty: DebugInfo->Value = LOCTEXT("NoDebugData", "(no debug data)"); break; case FKismetDebugUtilities::EWTR_NoDebugObject: DebugInfo->Value = LOCTEXT("NoDebugObject", "(no debug object)"); break; default: // do nothing break; } } Private_InstanceWatchSource.Add( MakeShared( BlueprintObj, Pin->GetOwningNode(), Pin, BlueprintInstance, BlueprintName, GraphName, NodeName, *DebugInfo ) ); } ); } // Notify subscribers: WatchListSubscribers.Broadcast(&Private_InstanceWatchSource); } #endif } void WatchViewer::ContinueExecution() { // Notify subscribers: WatchListSubscribers.Broadcast(&Private_WatchSource); } FName WatchViewer::GetTabName() { const FName TabName = TEXT("WatchViewer"); return TabName; } void WatchViewer::RemoveWatchesForBlueprint(TWeakObjectPtr BlueprintObj) { if (!ensure(BlueprintObj.IsValid())) { return; } int32 FoundIdx = WatchedBlueprints.Find(BlueprintObj); if (FoundIdx == INDEX_NONE) { return; } // since we're not watching any pins anymore we should remove it from the watched list WatchedBlueprints.RemoveAt(FoundIdx); // something changed so we need to update the lists shown in the UI UpdateNonInstancedWatchDisplay(); if (IsPaused()) { WatchViewer::UpdateInstancedWatchDisplay(); } // Notify subscribers WatchListSubscribers.Broadcast(&Private_WatchSource); } void WatchViewer::RemoveWatchesForAsset(const struct FAssetData& AssetData) { for (TSharedRef WatchRow : Private_WatchSource) { if (AssetData.PackageName == WatchRow->BlueprintPackageName && FText::FromName(AssetData.AssetName).EqualTo(WatchRow->BlueprintName)) { RemoveWatchesForBlueprint(WatchRow->BP); break; } } } void WatchViewer::OnRenameAsset(const struct FAssetData& AssetData, const FString& OldAssetName) { FString OldPackageName; FString OldBPName; if (OldAssetName.Split(".", &OldPackageName, &OldBPName)) { bool bUpdated = false; for (TSharedRef WatchRow : Private_WatchSource) { if (OldPackageName == WatchRow->BlueprintPackageName.ToString() && FText::FromString(OldBPName).EqualTo(WatchRow->BlueprintName)) { WatchRow->BlueprintName = FText::FromName(AssetData.AssetName); bUpdated = true; } } if (bUpdated) { // something changed so we need to update the lists shown in the UI UpdateNonInstancedWatchDisplay(); if (IsPaused()) { WatchViewer::UpdateInstancedWatchDisplay(); } // Notify subscribers if necessary WatchListSubscribers.Broadcast(&Private_WatchSource); } } } void WatchViewer::UpdateWatchListFromBlueprint(TWeakObjectPtr BlueprintObj) { UpdateWatchListFromBlueprintImpl(BlueprintObj, true); } void WatchViewer::ClearWatchListFromBlueprint(TWeakObjectPtr BlueprintObj) { UpdateWatchListFromBlueprintImpl(BlueprintObj, false); } void WatchViewer::RegisterTabSpawner(FTabManager& TabManager) { const auto SpawnWatchViewTab = []( const FSpawnTabArgs& Args ) { TArray>* Source = &Private_WatchSource; if (IsPaused()) { Source = &Private_InstanceWatchSource; } static const FName ToolbarName = TEXT("Kismet.DebuggingViewToolBar"); FToolMenuContext MenuContext(FPlayWorldCommands::GlobalPlayWorldActions); TSharedRef ToolbarWidget = UToolMenus::Get()->GenerateWidget(ToolbarName, MenuContext); return SNew(SDockTab) .TabRole( ETabRole::PanelTab ) .Label( LOCTEXT("TabTitle", "Watches") ) [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() [ SNew(SBorder) .BorderImage( FAppStyle::GetBrush( TEXT("NoBorder") ) ) [ ToolbarWidget ] ] + SVerticalBox::Slot() .AutoHeight() [ SNew(SBorder) .BorderImage( FAppStyle::GetBrush("Docking.Tab.ContentAreaBrush") ) [ SNew(SWatchViewer, Source) ] ] ]; }; TabManager.RegisterTabSpawner( WatchViewer::GetTabName(), FOnSpawnTab::CreateStatic(SpawnWatchViewTab) ) .SetDisplayName( LOCTEXT("SpawnerTitle", "Watch Window") ) .SetTooltipText( LOCTEXT("SpawnerTooltipText", "Open the watch window tab") ); } #undef LOCTEXT_NAMESPACE