// Copyright Epic Games, Inc. All Rights Reserved. #include "Debugging/SKismetDebugTreeView.h" #include "Algo/Reverse.h" #include "AssetThumbnail.h" #include "BlueprintDebugger.h" #include "BlueprintEditor.h" #include "BlueprintEditorModule.h" #include "BlueprintEditorTabs.h" #include "CoreGlobals.h" #include "CoreTypes.h" #include "Debugging/SKismetDebuggingView.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" #include "EdGraph/EdGraphPin.h" #include "EdGraph/EdGraphSchema.h" #include "EdGraphSchema_K2.h" #include "Editor/EditorEngine.h" #include "Engine/Blueprint.h" #include "Engine/BlueprintGeneratedClass.h" #include "Engine/Engine.h" #include "Engine/LatentActionManager.h" #include "Engine/World.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Commands/UIAction.h" #include "Framework/Docking/TabManager.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "GameFramework/Actor.h" #include "GraphEditorSettings.h" #include "HAL/PlatformApplicationMisc.h" #include "Input/Reply.h" #include "Internationalization/Internationalization.h" #include "K2Node.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/Breakpoint.h" #include "Kismet2/KismetDebugUtilities.h" #include "Kismet2/KismetEditorUtilities.h" #include "Kismet2/WatchedPin.h" #include "Layout/Children.h" #include "Layout/Margin.h" #include "Layout/Visibility.h" #include "Logging/LogCategory.h" #include "Logging/LogMacros.h" #include "Math/Color.h" #include "Math/NumericLimits.h" #include "Math/Vector2D.h" #include "Misc/AssertionMacros.h" #include "Modules/ModuleManager.h" #include "PropertyInfoViewStyle.h" #include "SlotBase.h" #include "SourceCodeNavigation.h" #include "Styling/AppStyle.h" #include "Styling/ISlateStyle.h" #include "Styling/SlateColor.h" #include "Styling/SlateIconFinder.h" #include "Styling/StyleColors.h" #include "Subsystems/AssetEditorSubsystem.h" #include "Templates/Casts.h" #include "Templates/SubclassOf.h" #include "Templates/UniquePtr.h" #include "Templates/UnrealTypeTraits.h" #include "Textures/SlateIcon.h" #include "ThumbnailRendering/ThumbnailManager.h" #include "Trace/Detail/Channel.h" #include "Types/SlateEnums.h" #include "Types/SlateStructs.h" #include "UObject/Class.h" #include "UObject/Field.h" #include "UObject/FieldPath.h" #include "UObject/NameTypes.h" #include "UObject/Object.h" #include "UObject/ObjectMacros.h" #include "UObject/Package.h" #include "UObject/SoftObjectPtr.h" #include "UObject/UObjectGlobals.h" #include "UObject/UnrealType.h" #include "UObject/WeakObjectPtr.h" #include "UnrealEngine.h" #include "Widgets/Images/SImage.h" #include "Widgets/Images/SLayeredImage.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SHyperlink.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SSpacer.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SOverlay.h" #include "Widgets/SWidget.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Views/STableRow.h" class ITableRow; struct FGeometry; struct FSlateBrush; #define LOCTEXT_NAMESPACE "DebugViewUI" DEFINE_LOG_CATEGORY_STATIC(LogBlueprintDebugTreeView, Log, All); ////////////////////////////////////////////////////////////////////////// /** The editor object */ extern UNREALED_API class UEditorEngine* GEditor; static const FText ViewInDebuggerText = LOCTEXT("ViewInDebugger", "View in Blueprint Debugger"); static const FText ViewInDebuggerTooltipText = LOCTEXT("ViewInDebugger_Tooltip", "Opens the Blueprint Debugger and starts watching this variable if it isn't already watched"); static constexpr float ThumbnailIconSize = 16.0f; static constexpr uint32 ThumbnailIconResolution = 16; static int DebuggerMaxSearchDepth = 50; static FAutoConsoleVariableRef CVarDebuggerMaxDepth(TEXT("bp.DebuggerMaxSearchDepth"), DebuggerMaxSearchDepth, TEXT("The maximum search depth of Blueprint Debugger TreeView widgets (set to <= 0 for infinite depth)"), ECVF_Default); static bool bDebuggerEnableExternalSearch = false; static FAutoConsoleVariableRef CVarDebuggerEnableExternalSearch(TEXT("bp.DebuggerEnableExternalSearch"), bDebuggerEnableExternalSearch, TEXT("Allows the Blueprint Debugger TreeView widget to search external objects"), ECVF_Default); ////////////////////////////////////////////////////////////////////////// const FName SKismetDebugTreeView::ColumnId_Name("Name"); const FName SKismetDebugTreeView::ColumnId_Value("Value"); ////////////////////////////////////////////////////////////////////////// // SKismetDebugTreePropertyValueWidget namespace { class SKismetDebugTreePropertyValueWidget : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SKismetDebugTreePropertyValueWidget) : _PropertyInfo(nullptr) , _TreeItem(nullptr) {} SLATE_ATTRIBUTE(TSharedPtr, PropertyInfo) SLATE_ARGUMENT(FDebugTreeItemPtr, TreeItem) SLATE_END_ARGS() public: void Construct(const FArguments& InArgs, TSharedPtr InSearchString) { PropertyInfo = InArgs._PropertyInfo; TreeItem = InArgs._TreeItem; check(TreeItem.IsValid()); TSharedPtr Data = PropertyInfo.Get(); if (Data.IsValid()) { if (Data->Property->IsA() || Data->Property->IsA()) { ChildSlot [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(PropertyInfoViewStyle::STextHighlightOverlay) .FullText(this, &SKismetDebugTreePropertyValueWidget::GetObjectValueText) .HighlightText(this, &SKismetDebugTreePropertyValueWidget::GetHighlightText, InSearchString) [ SNew(STextBlock) .ToolTipText(this, &SKismetDebugTreePropertyValueWidget::GetValueTooltipText) .Text(this, &SKismetDebugTreePropertyValueWidget::GetObjectValueText) ] ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SSpacer) .Size(FVector2D(2.0f, 1.0f)) ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SHyperlink) .ToolTipText(this, &SKismetDebugTreePropertyValueWidget::GetClassLinkTooltipText) .Text(this, &SKismetDebugTreePropertyValueWidget::GetObjectClassText) .OnNavigate(this, &SKismetDebugTreePropertyValueWidget::OnNavigateToClass) ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SSpacer) .Size(FVector2D(2.0f, 1.0f)) ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(LOCTEXT("ObjectValueEnd", ")")) ] ]; } else { ChildSlot [ SNew(PropertyInfoViewStyle::STextHighlightOverlay) .FullText(this, &SKismetDebugTreePropertyValueWidget::GetDescription) .HighlightText(this, &SKismetDebugTreePropertyValueWidget::GetHighlightText, InSearchString) [ SNew(STextBlock) .ToolTipText(this, &SKismetDebugTreePropertyValueWidget::GetDescription) .Text(this, &SKismetDebugTreePropertyValueWidget::GetDescription) ] ]; } } } private: FText GetDescription() const { return TreeItem->GetDescription(); } FText GetHighlightText(TSharedPtr InSearchString) const { return TreeItem->GetHighlightText(InSearchString); } FText GetObjectValueText() const { TSharedPtr Data = PropertyInfo.Get(); if (Data.IsValid()) { if (const UObject* Object = Data->Object.Get()) { return FText::Format(LOCTEXT("ObjectValueBegin", "{0} (Class: "), FText::FromString(Object->GetName())); } } return LOCTEXT("UnknownObjectValueBegin", "[Unknown] (Class: "); } FText GetValueTooltipText() const { TSharedPtr Data = PropertyInfo.Get(); if (Data.IsValid()) { // if this is an Object property, tooltip text should include its full name if (const UObject* Object = Data->Object.Get()) { return FText::Format(LOCTEXT("ObjectValueTooltip", "{0}\nClass: {1}"), FText::FromString(Object->GetFullName()), FText::FromString(Object->GetClass()->GetFullName())); } } return GetDescription(); } FText GetClassLinkTooltipText() const { TSharedPtr Data = PropertyInfo.Get(); if (Data.IsValid()) { if (const UObject* Object = Data->Object.Get()) { if (UClass* Class = Object->GetClass()) { if (UBlueprint* Blueprint = Cast(Class->ClassGeneratedBy)) { return LOCTEXT("OpenBlueprintClass", "Opens this Class in the Blueprint Editor"); } else { // this is a native class return LOCTEXT("OpenNativeClass", "Navigates to this class' source file"); } } } } return LOCTEXT("UnknownClassName", "[Unknown]"); } FText GetObjectClassText() const { TSharedPtr Data = PropertyInfo.Get(); if (Data.IsValid()) { if (const UObject* Object = Data->Object.Get()) { return FText::FromString(Object->GetClass()->GetName()); } } return LOCTEXT("UnknownClassName", "[Unknown]"); } void OnNavigateToClass() const { TSharedPtr Data = PropertyInfo.Get(); if (Data.IsValid()) { if (const UObject* Object = Data->Object.Get()) { if (UClass* Class = Object->GetClass()) { if (UBlueprint* Blueprint = Cast(Class->ClassGeneratedBy)) { GEditor->GetEditorSubsystem()->OpenEditorForAsset(Blueprint); } else { // this is a native class FSourceCodeNavigation::NavigateToClass(Class); } } } } } FDebugTreeItemPtr TreeItem; TAttribute> PropertyInfo; }; } ////////////////////////////////////////////////////////////////////////// // FDebugLineItem uint16 FDebugLineItem::ActiveTypeBitset = TNumericLimits::Max(); // set all to active by default FText FDebugLineItem::GetName() const { return FText::GetEmpty(); } FText FDebugLineItem::GetDisplayName() const { return FText::GetEmpty(); } FText FDebugLineItem::GetDescription() const { return FText::GetEmpty(); } bool FDebugLineItem::HasName() const { return !GetDisplayName().IsEmpty(); } bool FDebugLineItem::HasValue() const { return !GetDescription().IsEmpty(); } void FDebugLineItem::CopyNameToClipboard() const { FPlatformApplicationMisc::ClipboardCopy(ToCStr(GetDisplayName().ToString())); } void FDebugLineItem::CopyValueToClipboard() const { FPlatformApplicationMisc::ClipboardCopy(ToCStr(GetDescription().ToString())); } TSharedRef FDebugLineItem::GenerateNameWidget(TSharedPtr InSearchString) { return SNew(PropertyInfoViewStyle::STextHighlightOverlay) .FullText(this, &FDebugLineItem::GetDisplayName) .HighlightText(this, &FDebugLineItem::GetHighlightText, InSearchString) [ SNew(STextBlock) .ToolTipText(this, &FDebugLineItem::GetDisplayName) .Text(this, &FDebugLineItem::GetDisplayName) ]; } TSharedRef FDebugLineItem::GenerateValueWidget(TSharedPtr InSearchString) { return SNew(PropertyInfoViewStyle::STextHighlightOverlay) .FullText(this, &FDebugLineItem::GetDescription) .HighlightText(this, &FDebugLineItem::GetHighlightText, InSearchString) [ SNew(STextBlock) .ToolTipText(this, &FDebugLineItem::GetDescription) .Text(this, &FDebugLineItem::GetDescription) ]; } void FDebugLineItem::MakeMenu(FMenuBuilder& MenuBuilder, bool bInDebuggerTab) { if (HasName()) { const FUIAction CopyName( FExecuteAction::CreateSP(this, &FDebugLineItem::CopyNameToClipboard), FCanExecuteAction::CreateSP(this, &FDebugLineItem::HasName) ); MenuBuilder.AddMenuEntry( LOCTEXT("CopyName", "Copy Name"), LOCTEXT("CopyName_ToolTip", "Copy name to clipboard"), FSlateIcon(), CopyName ); } if (HasValue()) { const FUIAction CopyValue( FExecuteAction::CreateSP(this, &FDebugLineItem::CopyValueToClipboard), FCanExecuteAction::CreateSP(this, &FDebugLineItem::HasValue) ); MenuBuilder.AddMenuEntry( LOCTEXT("CopyValue", "Copy Value"), LOCTEXT("CopyValue_ToolTip", "Copy value to clipboard"), FSlateIcon(), CopyValue ); } ExtendContextMenu(MenuBuilder, bInDebuggerTab); } void FDebugLineItem::ExtendContextMenu(class FMenuBuilder& MenuBuilder, bool bInDebuggerTab) { } void FDebugLineItem::UpdateSearch(const FString& InSearchString, FDebugLineItem::ESearchFlags SearchFlags) { const bool bIsRootNode = SearchFlags & SF_RootNode; const bool bIsContainerElement = SearchFlags & SF_ContainerElement; // Container elements share their parent's property name, so we shouldn't search them by name bVisible = (!bIsContainerElement && GetName().ToString().Contains(InSearchString)) || GetDisplayName().ToString().Contains(InSearchString) || GetDescription().ToString().Contains(InSearchString); // for root nodes, bParentsMatchSearch always matches bVisible if (bVisible || bIsRootNode) { bParentsMatchSearch = bVisible; } } bool FDebugLineItem::IsVisible() { return bVisible; } bool FDebugLineItem::DoParentsMatchSearch() { return bParentsMatchSearch; } bool FDebugLineItem::HasChildren() const { return false; } TSharedRef FDebugLineItem::GetNameIcon() { static const FSlateBrush* CachedBrush = FAppStyle::GetBrush(TEXT("NoBrush")); return SNew(SImage).Image(CachedBrush); } TSharedRef FDebugLineItem::GetValueIcon() { static const FSlateBrush* CachedBrush = FAppStyle::GetBrush(TEXT("NoBrush")); return SNew(SImage).Image(CachedBrush); } FText FDebugLineItem::GetHighlightText(const TSharedPtr InSearchString) const { return FText::FromString(*InSearchString); } UBlueprint* FDebugLineItem::GetBlueprintForObject(UObject* ParentObject) { if (ParentObject == nullptr) { return nullptr; } if (UBlueprint* ParentBlueprint = Cast(ParentObject)) { return ParentBlueprint; } if (UClass* ParentClass = ParentObject->GetClass()) { if (UBlueprint* ParentBlueprint = Cast(ParentClass->ClassGeneratedBy)) { return ParentBlueprint; } } // recursively walk up ownership hierarchy until we find the blueprint return GetBlueprintForObject(ParentObject->GetOuter()); } UBlueprintGeneratedClass* FDebugLineItem::GetClassForObject(UObject* ParentObject) { if (ParentObject != nullptr) { if (UBlueprint* Blueprint = Cast(ParentObject)) { return Cast(*Blueprint->GeneratedClass); } else if (UBlueprintGeneratedClass* Result = Cast(ParentObject)) { return Result; } else { return Cast(ParentObject->GetClass()); } } return nullptr; } bool FDebugLineItem::IsDebugLineTypeActive(EDebugLineType Type) { const uint16 Mask = 1 << Type; return ActiveTypeBitset & Mask; } void FDebugLineItem::OnDebugLineTypeActiveChanged(ECheckBoxState CheckState, EDebugLineType Type) { const uint16 Mask = 1 << Type; switch (CheckState) { case ECheckBoxState::Checked: ActiveTypeBitset |= Mask; break; default: ActiveTypeBitset &= ~Mask; break; } } ////////////////////////////////////////////////////////////////////////// // ILineItemWithChildren class FLineItemWithChildren : public FDebugLineItem { public: FLineItemWithChildren(EDebugLineType InType) : FDebugLineItem(InType) {} virtual ~FLineItemWithChildren() override = default; virtual bool HasChildren() const override { return !ChildrenMirrors.IsEmpty(); } virtual bool CanHaveChildren() override { return true; } /** Pilot for Recursive Search */ bool SearchRecursive(const FString& InSearchString, TSharedPtr> DebugTreeView) { TArray Parents; return SearchRecursive(InSearchString, DebugTreeView, Parents); } // ensures that ChildrenMirrors are set up for calls to EnsureChildIsAdded virtual void GatherChildrenBase(TArray& OutChildren, const FString& InSearchString, bool bRespectSearch) override { Swap(PrevChildrenMirrors, ChildrenMirrors); ChildrenMirrors.Empty(); GatherChildren(OutChildren, InSearchString, bRespectSearch); } // allows FDebugTreeItemPtr to be stored in TSets class FDebugTreeItemKeyFuncs { public: typedef FDebugTreeItemPtr ElementType; typedef TTypeTraits::ConstPointerType KeyInitType; typedef TCallTraits::ParamType ElementInitType; enum { bAllowDuplicateKeys = false }; /** * @return The key used to index the given element. */ static FORCEINLINE KeyInitType GetSetKey(ElementInitType Element) { return Element; } /** * @return True if the keys match. */ static FORCEINLINE bool Matches(KeyInitType A, KeyInitType B) { const FDebugLineItem* APtr = A.Get(); const FDebugLineItem* BPtr = B.Get(); if (APtr && BPtr) { return (APtr->Type == BPtr->Type) && APtr->Compare(BPtr); } return APtr == BPtr; } /** Calculates a hash index for a key. */ static FORCEINLINE uint32 GetKeyHash(KeyInitType Key) { if (const FDebugLineItem* KeyPtr = Key.Get()) { return KeyPtr->GetHash(); } return GetTypeHash(Key); } }; protected: // Last frames cached children TSet PrevChildrenMirrors; // This frames children TSet ChildrenMirrors; /** @returns whether this item represents a container property */ virtual bool IsContainer() const { return false; } virtual void GatherChildren(TArray& OutChildren, const FString& InSearchString, bool bRespectSearch) {} /** returns the object related to this line item if there is one, used to avoid searching external objects */ virtual const UObject* GetRelatedObject() const { return nullptr; }; /** returns whether a child is */ virtual bool IsExternalTo(const FLineItemWithChildren* Parent) const { if (const UObject* ChildObj = GetRelatedObject()) { if (const UObject* ParentObj = Parent->GetRelatedObject()) { if (!ChildObj->IsIn(ParentObj)) { return true; } } } return false; } /** * returns whether this node should be visible according to the users * search query * * O( number of recursive children ) */ bool SearchRecursive(const FString& InSearchString, TSharedPtr> DebugTreeView, TArray& Parents, ESearchFlags SearchFlags = SF_None) { bVisible = false; UpdateSearch(InSearchString, SearchFlags); bool bChildMatch = false; Parents.Push(this); ESearchFlags ChildSearchFlags = IsContainer() ? SF_ContainerElement : SF_None; TArray Children; GatherChildrenBase(Children, InSearchString,/*bRespectSearch =*/ false); for (const FDebugTreeItemPtr& ChildRef : Children) { if (ChildRef->CanHaveChildren()) { ChildRef->bParentsMatchSearch = bParentsMatchSearch; FLineItemWithChildren* Child = StaticCast(ChildRef.Get()); // check if the child has been seen already in parents. // if it has, skip it. (avoids stack overflows) if (Parents.FindByPredicate( [Child](const FLineItemWithChildren* Relative) { return (Relative->Type == Child->Type) && Relative->Compare(Child); } )) { continue; } // stop recursing if we reached the max depth if (DebuggerMaxSearchDepth > 0 && Parents.Num() > DebuggerMaxSearchDepth) { continue; } if (!bDebuggerEnableExternalSearch) { FLineItemWithChildren* ObjectParent = nullptr; for (FLineItemWithChildren* Parent : Parents) { if (Parent->GetRelatedObject()) { ObjectParent = Parent; break; } } // check if we need to stop searching due to external objects if (ObjectParent && Child->IsExternalTo(ObjectParent)) { // update this child, but don't recurse Child->UpdateSearch(InSearchString, ChildSearchFlags); // if any children need to expand, so should this if (ChildRef->IsVisible()) { bVisible = true; bChildMatch = true; } continue; } } // if any children need to expand, so should this if (Child->SearchRecursive(InSearchString, DebugTreeView, Parents, ChildSearchFlags)) { bVisible = true; bChildMatch = true; } } else { ChildRef->UpdateSearch(InSearchString, ChildSearchFlags); // if any children need to expand, so should this if (ChildRef->IsVisible()) { bVisible = true; bChildMatch = true; } } } Parents.Pop(EAllowShrinking::No); if (bChildMatch) { DebugTreeView->SetItemExpansion(SharedThis(this), true); } return bVisible; } /** * Adds either Item or an identical node that was previously * created (present in ChildrenMirrors) as a child to OutChildren. * * O( 1 ) */ void EnsureChildIsAdded(TArray& OutChildren, const FDebugLineItem& Item, const FString& InSearchString, bool bRespectSearch) { const FDebugTreeItemPtr Shareable = MakeShareable(Item.Duplicate()); if (FDebugTreeItemPtr* Found = PrevChildrenMirrors.Find(Shareable)) { FDebugTreeItemPtr FoundItem = *Found; FoundItem->UpdateData(Item); ChildrenMirrors.Add(FoundItem); // only add item if it matches search if (!bRespectSearch || InSearchString.IsEmpty() || FoundItem->IsVisible()) { OutChildren.Add(FoundItem); } } else { ChildrenMirrors.Add(Shareable); OutChildren.Add(Shareable); } } }; ////////////////////////////////////////////////////////////////////////// // FMessageLineItem struct FMessageLineItem : public FDebugLineItem { protected: FString Message; public: // Message line FMessageLineItem(const FString& InMessage) : FDebugLineItem(DLT_Message) , Message(InMessage) { } virtual FText GetDescription() const override { return FText::FromString(Message); } protected: virtual FDebugLineItem* Duplicate() const override { return new FMessageLineItem(Message); } virtual bool Compare(const FDebugLineItem* BaseOther) const override { FMessageLineItem* Other = (FMessageLineItem*)BaseOther; return Message == Other->Message; } virtual uint32 GetHash() const override { return GetTypeHash(Message); } }; ////////////////////////////////////////////////////////////////////////// // FLatentActionLineItem struct FLatentActionLineItem : public FDebugLineItem { protected: int32 UUID; TWeakObjectPtr< UObject > ParentObjectRef; public: FLatentActionLineItem(int32 InUUID, UObject* ParentObject) : FDebugLineItem(DLT_LatentAction) { UUID = InUUID; check(UUID != INDEX_NONE); ParentObjectRef = ParentObject; } virtual TSharedRef GenerateNameWidget(TSharedPtr InSearchString) override; virtual TSharedRef GetNameIcon() override; virtual FText GetDescription() const override; protected: virtual FDebugLineItem* Duplicate() const override { return new FLatentActionLineItem(UUID, ParentObjectRef.Get()); } virtual bool Compare(const FDebugLineItem* BaseOther) const override { FLatentActionLineItem* Other = (FLatentActionLineItem*)BaseOther; return (ParentObjectRef.Get() == Other->ParentObjectRef.Get()) && (UUID == Other->UUID); } virtual uint32 GetHash() const override { return HashCombine(GetTypeHash(UUID), GetTypeHash(ParentObjectRef)); } protected: virtual FText GetDisplayName() const override; void OnNavigateToLatentNode(); class UEdGraphNode* FindAssociatedNode() const; }; FText FLatentActionLineItem::GetDescription() const { if (UObject* ParentObject = ParentObjectRef.Get()) { if (UWorld* World = GEngine->GetWorldFromContextObject(ParentObject, EGetWorldErrorMode::ReturnNull)) { FLatentActionManager& LatentActionManager = World->GetLatentActionManager(); return FText::FromString(LatentActionManager.GetDescription(ParentObject, UUID)); } } return LOCTEXT("nullptrObject", "Object has been destroyed"); } TSharedRef FLatentActionLineItem::GenerateNameWidget(TSharedPtr InSearchString) { return SNew(PropertyInfoViewStyle::STextHighlightOverlay) .FullText(this, &FLatentActionLineItem::GetDisplayName) .HighlightText(this, &FLatentActionLineItem::GetHighlightText, InSearchString) [ SNew(SHyperlink) .Style(FAppStyle::Get(), "HoverOnlyHyperlink") .OnNavigate(this, &FLatentActionLineItem::OnNavigateToLatentNode) .Text(this, &FLatentActionLineItem::GetDisplayName) .ToolTipText(LOCTEXT("NavLatentActionLoc_Tooltip", "Navigate to the latent action location")) ]; } TSharedRef FLatentActionLineItem::GetNameIcon() { return SNew(SImage) .Image(FAppStyle::GetBrush(TEXT("Kismet.LatentActionIcon"))); } UEdGraphNode* FLatentActionLineItem::FindAssociatedNode() const { if (UBlueprintGeneratedClass* Class = GetClassForObject(ParentObjectRef.Get())) { return Class->GetDebugData().FindNodeFromUUID(UUID); } return nullptr; } FText FLatentActionLineItem::GetDisplayName() const { FFormatNamedArguments Args; Args.Add(TEXT("ID"), UUID); if (UK2Node* Node = Cast(FindAssociatedNode())) { Args.Add(TEXT("Title"), Node->GetCompactNodeTitle()); return FText::Format(LOCTEXT("ID", "{Title} (ID: {ID})"), Args); } else { return FText::Format(LOCTEXT("LatentAction", "Latent action # {ID}"), Args); } } void FLatentActionLineItem::OnNavigateToLatentNode() { if (UEdGraphNode* Node = FindAssociatedNode()) { FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(Node); } } ////////////////////////////////////////////////////////////////////////// // FWatchChildLineItem struct FWatchChildLineItem : public FLineItemWithChildren { protected: TSharedRef Data; TWeakPtr ParentTreeItem; private: bool bIconHovered = false; public: FWatchChildLineItem(TSharedRef Child, const FDebugTreeItemPtr& InParentTreeItem) : FLineItemWithChildren(DLT_WatchChild), Data(Child), ParentTreeItem(InParentTreeItem) {} virtual TSharedRef GenerateValueWidget(TSharedPtr InSearchString) override { return SNew(SKismetDebugTreePropertyValueWidget, InSearchString) .PropertyInfo(this, &FWatchChildLineItem::GetPropertyInfo) .TreeItem(AsShared()); } virtual void ExtendContextMenu(FMenuBuilder& MenuBuilder, bool bInDebuggerTab) override { //Navigate to Class source // Only add watch options if this has a pin in it's parent chain // (ok to discard the path, this only runs when a context menu is constructed) TArray PathToProperty; if (UEdGraphPin* PinParent = BuildPathToProperty(PathToProperty)) { //Add Watch FUIAction AddThisWatch( FExecuteAction::CreateSP(this, &FWatchChildLineItem::AddWatch), FCanExecuteAction::CreateSP(this, &FWatchChildLineItem::CanAddWatch) ); MenuBuilder.AddMenuEntry( LOCTEXT("AddPropertyWatch", "Start Watching"), LOCTEXT("AddPropertyWatchTooltip", "Start Watching This Variable"), FSlateIcon(), AddThisWatch ); //Add Watch FUIAction ClearThisWatch( FExecuteAction::CreateSP(this, &FWatchChildLineItem::ClearWatch), FCanExecuteAction::CreateSP(this, &FWatchChildLineItem::CanClearWatch) ); MenuBuilder.AddMenuEntry( LOCTEXT("ClearPropertyWatch", "Stop Watching"), LOCTEXT("ClearPropertyWatchTooltip", "Stop Watching This Variable"), FSlateIcon(), ClearThisWatch ); } if (!bInDebuggerTab) { //View in Debugger FUIAction ViewInDebugger( FExecuteAction::CreateSP(this, &FWatchChildLineItem::ViewInDebugger), FCanExecuteAction::CreateSP(this, &FWatchChildLineItem::CanViewInDebugger) ); MenuBuilder.AddMenuEntry( ViewInDebuggerText, ViewInDebuggerTooltipText, FSlateIcon(), ViewInDebugger ); } } // uses the icon and color associated with the property type virtual TSharedRef GetNameIcon() override { FSlateColor BaseColor; FSlateColor SecondaryColor; FSlateBrush const* SecondaryIcon = nullptr; const FSlateBrush* Icon = FBlueprintEditor::GetVarIconAndColorFromProperty( Data->Property.Get(), BaseColor, SecondaryIcon, SecondaryColor ); TSharedPtr LayeredImage; // make the icon a button so the user can open the asset in editor if there is one TSharedRef NameIcon = SNew(SButton) .OnClicked(this, &FWatchChildLineItem::OnFocusAsset) .ButtonStyle(FAppStyle::Get(), "NoBorder") .ContentPadding(0.0f) .OnHovered_Lambda( [&bIconHovered = bIconHovered]() {bIconHovered = true; } ) .OnUnhovered_Lambda( [&bIconHovered = bIconHovered]() {bIconHovered = false; } ) [ SNew(SOverlay) .ToolTipText(this, &FWatchChildLineItem::IconTooltipText) + SOverlay::Slot() .Padding(FMargin(10.f, 0.f, 0.f, 0.f)) [ SAssignNew(LayeredImage, SLayeredImage) .Image(Icon) .ColorAndOpacity(this, &FWatchChildLineItem::ModifiedIconColor, BaseColor) ] + SOverlay::Slot() .HAlign(HAlign_Left) [ SNew(SImage) .Image(FAppStyle::GetBrush(TEXT("Kismet.WatchIcon"))) .Visibility(this, &FWatchChildLineItem::GetWatchIconVisibility) ] ]; LayeredImage->AddLayer( SecondaryIcon, TAttribute::CreateSP(this, &FWatchChildLineItem::ModifiedIconColor, SecondaryColor) ); return NameIcon; } virtual TSharedRef GetValueIcon() override { if (UObject* Object = Data->Object.Get()) { if (Object->IsAsset()) { FAssetThumbnailConfig ThumbnailConfig; if (FSlateApplication::Get().InKismetDebuggingMode()) { ThumbnailConfig.bForceGenericThumbnail = true; } TSharedPtr Thumb = MakeShared(Object, ThumbnailIconResolution, ThumbnailIconResolution, UThumbnailManager::Get().GetSharedThumbnailPool()); return SNew(SButton) .OnClicked(this, &FWatchChildLineItem::OnFocusAsset) .ToolTipText(this, &FWatchChildLineItem::IconTooltipText) .ButtonStyle(FAppStyle::Get(), "NoBorder") [ SNew(SBox) .MaxDesiredHeight(ThumbnailIconSize) .MaxDesiredWidth(ThumbnailIconSize) [ Thumb->MakeThumbnailWidget(ThumbnailConfig) ] ]; } } return FDebugLineItem::GetValueIcon(); } virtual FText GetDescription() const override { const FString ValStr = Data->Value.ToString(); return FText::FromString(ValStr.Replace(TEXT("\n"), TEXT(" "))); } protected: virtual FDebugLineItem* Duplicate() const override { return new FWatchChildLineItem(Data, ParentTreeItem.Pin()); } virtual bool Compare(const FDebugLineItem* BaseOther) const override { FWatchChildLineItem* Other = (FWatchChildLineItem*)BaseOther; return Data->Property == Other->Data->Property && Data->DisplayName.CompareTo(Other->Data->DisplayName) == 0; } virtual uint32 GetHash() const override { return HashCombine(GetTypeHash(Data->Property), GetTypeHash(Data->DisplayName.ToString())); } virtual void UpdateData(const FDebugLineItem& NewerData) override { // Compare returns true even if the value or children of this node // is different. use this function to update the data without completely // replacing the node FWatchChildLineItem& Other = (FWatchChildLineItem&)NewerData; Data = Other.Data; } virtual FText GetName() const override { return Data->Name; } virtual FText GetDisplayName() const override { return Data->DisplayName; } // if data is pointing to an asset, get it's UPackage const UPackage* GetDataPackage() const { if (Data->Object.IsValid()) { if (const UBlueprintGeneratedClass* GeneratedClass = Cast(Data->Object->GetClass())) { if (const UPackage* Package = GeneratedClass->GetPackage()) { return Package; } } if (const UPackage* Package = Data->Object->GetPackage()) { return Package; } } return {}; } const UObject* GetRelatedObject() const override { return Data->Object.Get(); } bool CanOpenAsset() const { if (FSlateApplication::Get().InKismetDebuggingMode()) { if (const UPackage* Package = GetDataPackage()) { UObject* ReferencedAsset = Package->FindAssetInPackage(); // It is not safe to open asset editors for non-blueprint assets while stopped at a breakpoint return Cast(ReferencedAsset) != nullptr; } } return true; } // opens result of GetDataPackage in editor FReply OnFocusAsset() const { if (!CanOpenAsset()) { return FReply::Unhandled(); } const UPackage* Package = GetDataPackage(); if (!Package) { return FReply::Unhandled(); } const FString Path = Package->GetPathName(); if (Path.IsEmpty()) { return FReply::Unhandled(); } GEditor->GetEditorSubsystem()->OpenEditorForAsset(Path); return FReply::Handled(); } // returns the icon color given a precalculated color associated with this datatype. // the color changes slightly based on whether it's null or a hovered button FSlateColor ModifiedIconColor(FSlateColor BaseColor) const { FLinearColor LinearRGB = BaseColor.GetSpecifiedColor(); // check if Data is a UObject if (CastField(Data->Property.Get())) { FLinearColor LinearHSV = LinearRGB.LinearRGBToHSV(); // if it's a null object, darken the icon so it's clear that it's not a button if (Data->Object == nullptr) { LinearHSV.B *= 0.5f; // decrease value LinearHSV.A *= 0.5f; // decrease alpha LinearRGB = LinearHSV.HSVToLinearRGB(); } // if the icon is hovered, lighten the icon else if (bIconHovered) { LinearHSV.B *= 2.f; // increase value LinearHSV.G *= 0.8f; // decrease Saturation LinearRGB = LinearHSV.HSVToLinearRGB(); } } bool bPinWatched = false; TArray PathToProperty; if (UEdGraphPin* PinToWatch = BuildPathToProperty(PathToProperty)) { bPinWatched = FKismetDebugUtilities::IsPinBeingWatched(FBlueprintEditorUtils::FindBlueprintForNode(PinToWatch->GetOwningNode()), PinToWatch, PathToProperty); } if (bPinWatched) { LinearRGB.A *= 0.3f; } return LinearRGB; } FText IconTooltipText() const { const UPackage* Package = GetDataPackage(); if (Package) { if (CanOpenAsset()) { return FText::Format(LOCTEXT("OpenPackage", "Open: {0}"), FText::FromString(Package->GetName())); } } return Data->Type; } virtual void GatherChildren(TArray& OutChildren, const FString& InSearchString, bool bRespectSearch) override { for (const TSharedPtr& ChildData : Data->GetChildren()) { EnsureChildIsAdded(OutChildren, FWatchChildLineItem(ChildData.ToSharedRef(), AsShared()), InSearchString, bRespectSearch); } } virtual bool IsContainer() const override { return Data->Property->IsA() || Data->Property->IsA() || Data->Property->IsA(); } EVisibility GetWatchIconVisibility() const { bool bPinWatched = false; TArray PathToProperty; if (UEdGraphPin* PinToWatch = BuildPathToProperty(PathToProperty)) { bPinWatched = FKismetDebugUtilities::IsPinBeingWatched(FBlueprintEditorUtils::FindBlueprintForNode(PinToWatch->GetOwningNode()), PinToWatch, PathToProperty); } return bPinWatched ? EVisibility::Visible : EVisibility::Collapsed; } TSharedPtr GetPropertyInfo() const { return Data; } void AddWatch(); bool CanAddWatch() const; void ClearWatch(); bool CanClearWatch() const; void ViewInDebugger(); bool CanViewInDebugger() const; UEdGraphPin* BuildPathToProperty(TArray& OutPathToProperty) const; }; //////////////////////////////////////////////////////////////////////////\ // FSelfWatchLineItem struct FSelfWatchLineItem : public FLineItemWithChildren { protected: // watches a UObject instead of a pin TWeakObjectPtr ObjectToWatch; public: FSelfWatchLineItem(UObject* Object) : FLineItemWithChildren(DLT_SelfWatch), ObjectToWatch(Object) {} virtual TSharedRef GetNameIcon() override { return SNew(SImage) .Image(FAppStyle::GetBrush(TEXT("Kismet.WatchIcon"))); } protected: virtual FDebugLineItem* Duplicate() const override { return new FSelfWatchLineItem(ObjectToWatch.Get()); } virtual bool Compare(const FDebugLineItem* BaseOther) const override { FSelfWatchLineItem* Other = (FSelfWatchLineItem*)BaseOther; return (ObjectToWatch.Get() == Other->ObjectToWatch.Get()); } virtual uint32 GetHash() const override { return GetTypeHash(ObjectToWatch); } virtual FText GetDisplayName() const override { return LOCTEXT("SelfName", "Self"); } virtual void GatherChildren(TArray& OutChildren, const FString& InSearchString, bool bRespectSearch) override { if (UObject* Object = ObjectToWatch.Get()) { for (TFieldIterator It(Object->GetClass()); It; ++It) { TSharedPtr DebugInfo; FProperty* Property = *It; if (Property->HasAllPropertyFlags(CPF_BlueprintVisible)) { void* Value = Property->ContainerPtrToValuePtr(Object); FKismetDebugUtilities::GetDebugInfoInternal(DebugInfo, Property, Value); EnsureChildIsAdded(OutChildren, FWatchChildLineItem(DebugInfo.ToSharedRef(), AsShared()), InSearchString, bRespectSearch); } } } } }; ////////////////////////////////////////////////////////////////////////// // FWatchLineItem struct FWatchLineItem : public FLineItemWithChildren { protected: TWeakObjectPtr< UObject > ParentObjectRef; const FEdGraphPinReference ObjectRef; mutable TSharedPtr CachedPropertyInfo = nullptr; mutable FText CachedErrorString; const TArray PathToProperty; public: FWatchLineItem(const UEdGraphPin* PinToWatch, UObject* ParentObject) : FLineItemWithChildren(DLT_Watch) , ObjectRef(PinToWatch) , PathToProperty() { ParentObjectRef = ParentObject; } FWatchLineItem(const UEdGraphPin* PinToWatch, UObject* ParentObject, const TArray& InPathToProperty) : FLineItemWithChildren(DLT_Watch) , ObjectRef(PinToWatch) , PathToProperty(InPathToProperty) { ParentObjectRef = ParentObject; } virtual void ExtendContextMenu(class FMenuBuilder& MenuBuilder, bool bInDebuggerTab) override { if (UEdGraphPin* WatchedPin = ObjectRef.Get()) { FUIAction AddThisWatch( FExecuteAction::CreateSP(this, &FWatchLineItem::AddWatch), FCanExecuteAction::CreateSP(this, &FWatchLineItem::CanAddWatch) ); MenuBuilder.AddMenuEntry( LOCTEXT("AddWatch", "Start Watching"), LOCTEXT("AddWatch_ToolTip", "Start watching this variable"), FSlateIcon(), AddThisWatch); FUIAction ClearThisWatch( FExecuteAction::CreateSP(this, &FWatchLineItem::ClearWatch), FCanExecuteAction::CreateSP(this, &FWatchLineItem::CanClearWatch) ); MenuBuilder.AddMenuEntry( LOCTEXT("ClearWatch", "Stop watching"), LOCTEXT("ClearWatch_ToolTip", "Stop watching this variable"), FSlateIcon(), ClearThisWatch); if (bInDebuggerTab) { FUIAction ViewInDebugger( FExecuteAction::CreateSP(this, &FWatchLineItem::ViewInDebugger), FCanExecuteAction::CreateSP(this, &FWatchLineItem::CanViewInDebugger) ); MenuBuilder.AddMenuEntry( ViewInDebuggerText, ViewInDebuggerTooltipText, FSlateIcon(), ViewInDebugger ); } } } const FEdGraphPinReference& GetPin() const { return ObjectRef; } const TArray& GetPathToProperty() const { return PathToProperty; } virtual FText GetDescription() const override; virtual TSharedRef GenerateNameWidget(TSharedPtr InSearchString) override; virtual TSharedRef GenerateValueWidget(TSharedPtr InSearchString) override; virtual TSharedRef GetValueIcon() override; virtual TSharedRef GetNameIcon() override; /** Returns a SharedPtr to the debug info for the property being represented by this TreeItem */ TSharedPtr GetPropertyInfo() const; protected: virtual FDebugLineItem* Duplicate() const override { return new FWatchLineItem(ObjectRef.Get(), ParentObjectRef.Get(), PathToProperty); } virtual bool Compare(const FDebugLineItem* BaseOther) const override { FWatchLineItem* Other = (FWatchLineItem*)BaseOther; return (ParentObjectRef == Other->ParentObjectRef) && (ObjectRef == Other->ObjectRef) && (PathToProperty == Other->PathToProperty); } virtual uint32 GetHash() const override { return HashCombine(GetTypeHash(ParentObjectRef), GetTypeHash(ObjectRef)); } virtual void GatherChildren(TArray& OutChildren, const FString& InSearchString, bool bRespectSearch) override { TSharedPtr ThisDebugInfo = GetPropertyInfo(); if (ThisDebugInfo.IsValid()) { for (const TSharedPtr& ChildData : ThisDebugInfo->GetChildren()) { EnsureChildIsAdded(OutChildren, FWatchChildLineItem(ChildData.ToSharedRef(), AsShared()), InSearchString, bRespectSearch); } } } virtual const UObject* GetRelatedObject() const override { if (CachedPropertyInfo.Get()) { return CachedPropertyInfo->Object.Get(); } return nullptr; } virtual FText GetDisplayName() const override; const FSlateBrush* GetPinIcon() const; const FSlateBrush* GetSecondaryPinIcon() const; FSlateColor GetPinIconColor() const; FSlateColor GetSecondaryPinIconColor() const; EVisibility GetWatchIconVisibility() const; FText GetTypename() const; void OnNavigateToWatchLocation(); private: void AddWatch() const { if (UEdGraphPin* PinToWatch = ObjectRef.Get()) { UBlueprint* ParentBlueprint = GetBlueprintForObject(ParentObjectRef.Get()); FKismetDebugUtilities::AddPinWatch(ParentBlueprint, FBlueprintWatchedPin(PinToWatch, TArray(PathToProperty))); } } bool CanAddWatch() const { if (UEdGraphPin* PinToWatch = ObjectRef.Get()) { UBlueprint* ParentBlueprint = GetBlueprintForObject(ParentObjectRef.Get()); return FKismetDebugUtilities::CanWatchPin(ParentBlueprint, PinToWatch, PathToProperty); } return false; } void ClearWatch() const { if (UEdGraphPin* PinToWatch = ObjectRef.Get()) { UBlueprint* ParentBlueprint = GetBlueprintForObject(ParentObjectRef.Get()); TArray PathToPropertyCopy = PathToProperty; FKismetDebugUtilities::RemovePinWatch(ParentBlueprint, PinToWatch, MoveTemp(PathToPropertyCopy)); } } bool CanClearWatch() const { if (UEdGraphPin* PinToWatch = ObjectRef.Get()) { UBlueprint* ParentBlueprint = GetBlueprintForObject(ParentObjectRef.Get()); return FKismetDebugUtilities::IsPinBeingWatched(ParentBlueprint, PinToWatch, PathToProperty); } return false; } void ViewInDebugger() const { // If this isn't already watched, add it as a watch if (CanAddWatch()) { AddWatch(); } FGlobalTabmanager::Get()->TryInvokeTab(FBlueprintEditorTabs::BlueprintDebuggerID); FBlueprintEditorModule& BlueprintEditorModule = FModuleManager::Get().LoadModuleChecked(TEXT("Kismet")); BlueprintEditorModule.GetBlueprintDebugger()->SetDebuggedBlueprint(GetBlueprintForObject(ParentObjectRef.Get())); } bool CanViewInDebugger() const { if (UEdGraphPin* PinToWatch = ObjectRef.Get()) { UBlueprint* ParentBlueprint = GetBlueprintForObject(ParentObjectRef.Get()); return FKismetDebugUtilities::IsPinBeingWatched(ParentBlueprint, PinToWatch, PathToProperty) || FKismetDebugUtilities::CanWatchPin(ParentBlueprint, PinToWatch, PathToProperty); } return false; } bool CanOpenAsset(UObject* AssetObject) const { if (FSlateApplication::Get().InKismetDebuggingMode()) { // opening non-blueprint assets while stopped at a breakpoint is unsafe return Cast(AssetObject) != nullptr; } return false; } FReply OpenEditorForAsset() const { TSharedPtr PropertyInfo = GetPropertyInfo(); if (PropertyInfo.IsValid()) { if (UObject* Object = PropertyInfo->Object.Get()) { if (Object->IsAsset() && CanOpenAsset(Object)) { GEditor->GetEditorSubsystem()->OpenEditorForAsset(Object); return FReply::Handled(); } } } return FReply::Unhandled(); } FReply OpenEditorForType() const { TSharedPtr PropertyInfo = GetPropertyInfo(); if (PropertyInfo.IsValid()) { if (UObject* Object = PropertyInfo->Object.Get()) { if (UBlueprint* Blueprint = Cast(PropertyInfo->Object->GetClass()->ClassGeneratedBy)) { GEditor->GetEditorSubsystem()->OpenEditorForAsset(Blueprint); return FReply::Handled(); } } } return FReply::Unhandled(); } FText GetAssetThumbnailTooltip() const { TSharedPtr PropertyInfo = GetPropertyInfo(); if (PropertyInfo.IsValid()) { if (UObject* Object = PropertyInfo->Object.Get()) { if (CanOpenAsset(Object)) { return FText::Format(LOCTEXT("OpenPackage", "Open: {0}"), FText::FromString(Object->GetFullName())); } } return PropertyInfo->Type; } return FText::GetEmpty(); } FText GetIconTooltipText() const { TSharedPtr PropertyInfo = GetPropertyInfo(); if (PropertyInfo.IsValid()) { if (UObject* Object = PropertyInfo->Object.Get()) { if (UBlueprint* Blueprint = Cast(Object->GetClass()->ClassGeneratedBy)) { return FText::Format(LOCTEXT("OpenPackage", "Open: {0}"), FText::FromString(Blueprint->GetFullName())); } } return PropertyInfo->Type; } return FText::GetEmpty(); } }; FText FWatchLineItem::GetDisplayName() const { if (UEdGraphPin* PinToWatch = ObjectRef.Get()) { if (UBlueprint* Blueprint = GetBlueprintForObject(ParentObjectRef.Get())) { if (FProperty* Property = FKismetDebugUtilities::FindClassPropertyForPin(Blueprint, PinToWatch)) { FString PathString; for (FName PropertyName : PathToProperty) { PathString.Append(TEXT("/") + PropertyName.ToString()); } return FText::FromString(UEditorEngine::GetFriendlyName(Property) + PathString); } } FFormatNamedArguments Args; Args.Add(TEXT("PinWatchName"), PinToWatch->GetDisplayName()); return FText::Format(LOCTEXT("DisplayNameNoProperty", "{PinWatchName} (no prop)"), Args); } else { return FText::GetEmpty(); } } FText FWatchLineItem::GetDescription() const { if (UEdGraphPin* PinToWatch = ObjectRef.Get()) { // Try to determine the blueprint that generated the watch UBlueprint* ParentBlueprint = GetBlueprintForObject(ParentObjectRef.Get()); // Find a valid property mapping and display the current value UObject* ParentObject = ParentObjectRef.Get(); if ((ParentBlueprint != ParentObject) && (ParentBlueprint != nullptr)) { TSharedPtr DebugInfo = GetPropertyInfo(); if (DebugInfo.IsValid()) { const FString ValStr = DebugInfo->Value.ToString(); return FText::FromString(ValStr.Replace(TEXT("\n"), TEXT(" "))); } return CachedErrorString; } } return FText::GetEmpty(); } TSharedRef FWatchLineItem::GenerateNameWidget(TSharedPtr InSearchString) { FText NodeName = LOCTEXT("UnknownNode", "[Unknown]"); FText GraphName = LOCTEXT("UnknownGraph", "[Unknown]"); if (const UEdGraphPin* Pin = ObjectRef.Get()) { if (const UEdGraphNode* Node = Pin->GetOwningNode()) { NodeName = FText::FromString(Node->GetName()); if (const UEdGraph* Graph = Node->GetGraph()) { GraphName = FText::FromString(Graph->GetName()); } } } const FText ToolTipText = FText::FormatNamed(LOCTEXT("NavWatchLoc", "Navigate to the watch location\nGraph: {GraphName}\nNode: {NodeName}"), TEXT("GraphName"), GraphName, TEXT("NodeName"), NodeName ); return SNew(PropertyInfoViewStyle::STextHighlightOverlay) .FullText(this, &FWatchLineItem::GetDisplayName) .HighlightText(this, &FWatchLineItem::GetHighlightText, InSearchString) [ SNew(SHyperlink) .Style(FAppStyle::Get(), "HoverOnlyHyperlink") .OnNavigate(this, &FWatchLineItem::OnNavigateToWatchLocation) .Text(this, &FWatchLineItem::GetDisplayName) .ToolTipText(ToolTipText) ]; } TSharedRef FWatchLineItem::GenerateValueWidget(TSharedPtr InSearchString) { return SNew(SKismetDebugTreePropertyValueWidget, InSearchString) .PropertyInfo(this, &FWatchLineItem::GetPropertyInfo) .TreeItem(AsShared()); } TSharedRef FWatchLineItem::GetValueIcon() { TSharedPtr PropertyInfo = GetPropertyInfo(); if (PropertyInfo.IsValid()) { if (UObject* Object = PropertyInfo->Object.Get()) { if (Object->IsAsset()) { FAssetThumbnailConfig ThumbnailConfig; if (FSlateApplication::Get().InKismetDebuggingMode()) { ThumbnailConfig.bForceGenericThumbnail = true; } TSharedPtr Thumb = MakeShared(Object, ThumbnailIconResolution, ThumbnailIconResolution, UThumbnailManager::Get().GetSharedThumbnailPool()); return SNew(SButton) .OnClicked(this, &FWatchLineItem::OpenEditorForAsset) .ToolTipText(this, &FWatchLineItem::GetAssetThumbnailTooltip) .ButtonStyle(FAppStyle::Get(), "NoBorder") [ SNew(SBox) .MaxDesiredHeight(ThumbnailIconSize) .MaxDesiredWidth(ThumbnailIconSize) [ Thumb->MakeThumbnailWidget(ThumbnailConfig) ] ]; } } } return FDebugLineItem::GetValueIcon(); } // overlays the watch icon on top of a faded icon associated with the pin type TSharedRef FWatchLineItem::GetNameIcon() { TSharedPtr LayeredImage; TSharedRef NameIcon = SNew(SButton) .ButtonStyle(FAppStyle::Get(), "NoBorder") .ToolTipText(this, &FWatchLineItem::GetIconTooltipText) .OnClicked(this, &FWatchLineItem::OpenEditorForType) [ SNew(SOverlay) .ToolTipText(this, &FWatchLineItem::GetTypename) + SOverlay::Slot() .Padding(FMargin(10.f, 0.f, 0.f, 0.f)) [ SAssignNew(LayeredImage, SLayeredImage) .Image(this, &FWatchLineItem::GetPinIcon) .ColorAndOpacity(this, &FWatchLineItem::GetPinIconColor) ] + SOverlay::Slot() .HAlign(HAlign_Left) [ SNew(SImage) .Image(FAppStyle::GetBrush(TEXT("Kismet.WatchIcon"))) .Visibility(this, &FWatchLineItem::GetWatchIconVisibility) ] ]; LayeredImage->AddLayer( TAttribute(this, &FWatchLineItem::GetSecondaryPinIcon), TAttribute(this, &FWatchLineItem::GetSecondaryPinIconColor) ); return NameIcon; } const FSlateBrush* FWatchLineItem::GetPinIcon() const { TSharedPtr ThisDebugInfo = GetPropertyInfo(); if (ThisDebugInfo.IsValid()) { FSlateColor BaseColor; FSlateColor SecondaryColor; FSlateBrush const* SecondaryIcon; const FSlateBrush* Icon = FBlueprintEditor::GetVarIconAndColorFromProperty( ThisDebugInfo->Property.Get(), BaseColor, SecondaryIcon, SecondaryColor ); return Icon; } return FAppStyle::GetBrush(TEXT("NoBrush")); } const FSlateBrush* FWatchLineItem::GetSecondaryPinIcon() const { TSharedPtr ThisDebugInfo = GetPropertyInfo(); if (ThisDebugInfo.IsValid()) { FSlateColor BaseColor; FSlateColor SecondaryColor; FSlateBrush const* SecondaryIcon; const FSlateBrush* Icon = FBlueprintEditor::GetVarIconAndColorFromProperty( ThisDebugInfo->Property.Get(), BaseColor, SecondaryIcon, SecondaryColor ); return SecondaryIcon; } return FAppStyle::GetBrush(TEXT("NoBrush")); } FSlateColor FWatchLineItem::GetPinIconColor() const { FSlateColor PinIconColor = FLinearColor::White; if (UEdGraphPin* ObjectToFocus = ObjectRef.Get()) { TSharedPtr ThisDebugInfo = GetPropertyInfo(); if (ThisDebugInfo.IsValid()) { if (const UEdGraphSchema* Schema = ObjectToFocus->GetSchema()) { PinIconColor = Schema->GetPinTypeColor(ObjectToFocus->PinType); } } if (FKismetDebugUtilities::IsPinBeingWatched(FBlueprintEditorUtils::FindBlueprintForNode(ObjectToFocus->GetOwningNode()), ObjectToFocus, PathToProperty)) { FLinearColor Color = PinIconColor.GetSpecifiedColor(); Color.A = 0.3f; PinIconColor = Color; } } return PinIconColor; } FSlateColor FWatchLineItem::GetSecondaryPinIconColor() const { FSlateColor PinIconColor = FLinearColor::White; if (UEdGraphPin* ObjectToFocus = ObjectRef.Get()) { TSharedPtr ThisDebugInfo = GetPropertyInfo(); if (ThisDebugInfo.IsValid()) { if (const UEdGraphSchema* Schema = ObjectToFocus->GetSchema()) { PinIconColor = Schema->GetSecondaryPinTypeColor(ObjectToFocus->PinType); } } if (FKismetDebugUtilities::IsPinBeingWatched(FBlueprintEditorUtils::FindBlueprintForNode(ObjectToFocus->GetOwningNode()), ObjectToFocus, PathToProperty)) { FLinearColor Color = PinIconColor.GetSpecifiedColor(); Color.A = 0.3f; PinIconColor = Color; } } return PinIconColor; } EVisibility FWatchLineItem::GetWatchIconVisibility() const { if (const UEdGraphPin* Pin = ObjectRef.Get()) { return FKismetDebugUtilities::IsPinBeingWatched(FBlueprintEditorUtils::FindBlueprintForNode(Pin->GetOwningNode()), Pin, PathToProperty) ? EVisibility::Visible : EVisibility::Collapsed; } return EVisibility::Collapsed; } FText FWatchLineItem::GetTypename() const { TSharedPtr ThisDebugInfo = GetPropertyInfo(); if (ThisDebugInfo.IsValid()) { return UEdGraphSchema_K2::TypeToText(ThisDebugInfo->Property.Get()); } return FText::GetEmpty(); } void FWatchLineItem::OnNavigateToWatchLocation() { if (UEdGraphPin* ObjectToFocus = ObjectRef.Get()) { FKismetEditorUtilities::BringKismetToFocusAttentionOnPin(ObjectToFocus); } } TSharedPtr FWatchLineItem::GetPropertyInfo() const { if (CachedPropertyInfo.IsValid()) { return CachedPropertyInfo; } if (UEdGraphPin* ObjectToFocus = ObjectRef.Get()) { // Try to determine the blueprint that generated the watch UBlueprint* ParentBlueprint = GetBlueprintForObject(ParentObjectRef.Get()); // Find a valid property mapping and display the current value UObject* ParentObject = ParentObjectRef.Get(); if ((ParentBlueprint != ParentObject) && (ParentBlueprint != nullptr)) { TSharedPtr DebugInfo; const FKismetDebugUtilities::EWatchTextResult WatchStatus = FKismetDebugUtilities::GetDebugInfo(DebugInfo, ParentBlueprint, ParentObject, ObjectToFocus); switch (WatchStatus) { case FKismetDebugUtilities::EWTR_Valid: { check(DebugInfo); if (PathToProperty.IsEmpty()) { CachedPropertyInfo = DebugInfo; } else { CachedPropertyInfo = DebugInfo->ResolvePathToProperty(PathToProperty); } return CachedPropertyInfo; } case FKismetDebugUtilities::EWTR_NotInScope: CachedErrorString = LOCTEXT("NotInScope", "Not in scope"); case FKismetDebugUtilities::EWTR_NoProperty: CachedErrorString = LOCTEXT("UnknownProperty", "No debug data"); default: case FKismetDebugUtilities::EWTR_NoDebugObject: CachedErrorString = LOCTEXT("NoDebugObject", "No debug object"); } } } return nullptr; } ////////////////////////////////////////////////////////////////////////// // FWatchChildLineItem (some functions that need the definition of FWatchLineItem) void FWatchChildLineItem::AddWatch() { TArray PathToProperty; if (UEdGraphPin* PinToWatch = BuildPathToProperty(PathToProperty)) { FKismetDebugUtilities::AddPinWatch(FBlueprintEditorUtils::FindBlueprintForNode(PinToWatch->GetOwningNode()), FBlueprintWatchedPin(PinToWatch, MoveTemp(PathToProperty))); } } bool FWatchChildLineItem::CanAddWatch() const { TArray PathToProperty; if (UEdGraphPin* PinToWatch = BuildPathToProperty(PathToProperty)) { return FKismetDebugUtilities::CanWatchPin(FBlueprintEditorUtils::FindBlueprintForNode(PinToWatch->GetOwningNode()), PinToWatch, PathToProperty); } return false; } void FWatchChildLineItem::ClearWatch() { TArray PathToProperty; if (UEdGraphPin* PinToWatch = BuildPathToProperty(PathToProperty)) { FKismetDebugUtilities::RemovePinWatch(FBlueprintEditorUtils::FindBlueprintForNode(PinToWatch->GetOwningNode()), PinToWatch, PathToProperty); } } bool FWatchChildLineItem::CanClearWatch() const { TArray PathToProperty; if (UEdGraphPin* PinToWatch = BuildPathToProperty(PathToProperty)) { return FKismetDebugUtilities::IsPinBeingWatched(FBlueprintEditorUtils::FindBlueprintForNode(PinToWatch->GetOwningNode()), PinToWatch, PathToProperty); } return false; } void FWatchChildLineItem::ViewInDebugger() { // If this isn't already watched, add it as a watch if (CanAddWatch()) { AddWatch(); } TArray PathToProperty; if (UEdGraphPin* PinToWatch = BuildPathToProperty(PathToProperty)) { UBlueprint* Blueprint = FBlueprintEditorUtils::FindBlueprintForNode(PinToWatch->GetOwningNode()); FGlobalTabmanager::Get()->TryInvokeTab(FBlueprintEditorTabs::BlueprintDebuggerID); FBlueprintEditorModule& BlueprintEditorModule = FModuleManager::Get().LoadModuleChecked(TEXT("Kismet")); BlueprintEditorModule.GetBlueprintDebugger()->SetDebuggedBlueprint(Blueprint); } } bool FWatchChildLineItem::CanViewInDebugger() const { TArray PathToProperty; if (UEdGraphPin* PinToWatch = BuildPathToProperty(PathToProperty)) { UBlueprint* Blueprint = FBlueprintEditorUtils::FindBlueprintForNode(PinToWatch->GetOwningNode()); return FKismetDebugUtilities::IsPinBeingWatched(Blueprint, PinToWatch, PathToProperty) || FKismetDebugUtilities::CanWatchPin(Blueprint, PinToWatch, PathToProperty); } return false; } UEdGraphPin* FWatchChildLineItem::BuildPathToProperty(TArray& OutPathToProperty) const { UEdGraphPin* PinToWatch = nullptr; OutPathToProperty.Reset(); auto AddPropertyToPath = [&OutPathToProperty](const TSharedRef& InPropertyInfo) { if (InPropertyInfo->bIsInContainer) { // display name of map elements is their key exported to text OutPathToProperty.Emplace(InPropertyInfo->DisplayName.ToString()); } else { OutPathToProperty.Emplace(InPropertyInfo->Property->GetAuthoredName()); } }; AddPropertyToPath(Data); FDebugTreeItemPtr Parent = ParentTreeItem.Pin(); while (Parent.IsValid()) { if (Parent->GetType() == DLT_Watch) { // this is a FWatchLineItem TSharedPtr ParentAsWatch = StaticCastSharedPtr(Parent); PinToWatch = ParentAsWatch->GetPin().Get(); // Add the original path to the front of ours (we're still constructing it backwards) const TArray& ParentPath = ParentAsWatch->GetPathToProperty(); for (int32 PathIndex = ParentPath.Num() - 1; PathIndex >= 0; --PathIndex) { OutPathToProperty.Emplace(ParentPath[PathIndex]); } break; } if (Parent->GetType() == DLT_WatchChild) { // This is a FWatchChildLineItem TSharedPtr ParentAsChild = StaticCastSharedPtr(Parent); AddPropertyToPath(ParentAsChild->Data); Parent = ParentAsChild->ParentTreeItem.Pin(); } else { Parent.Reset(); } } // Reverse the array because we built it backwards Algo::Reverse(OutPathToProperty); return PinToWatch; } ////////////////////////////////////////////////////////////////////////// // FBreakpointLineItem struct FBreakpointLineItem : public FDebugLineItem { protected: TWeakObjectPtr ParentObjectRef; TSoftObjectPtr BreakpointNode; public: FBreakpointLineItem(TSoftObjectPtr BreakpointToWatch, UObject* ParentObject) : FDebugLineItem(DLT_Breakpoint) { BreakpointNode = BreakpointToWatch; ParentObjectRef = ParentObject; } virtual TSharedRef GenerateNameWidget(TSharedPtr InSearchString) override { return SNew(PropertyInfoViewStyle::STextHighlightOverlay) .FullText(this, &FBreakpointLineItem::GetDisplayName) .HighlightText(this, &FBreakpointLineItem::GetHighlightText, InSearchString) [ SNew(SHyperlink) .Style(FAppStyle::Get(), "HoverOnlyHyperlink") .Text(this, &FBreakpointLineItem::GetDisplayName) .ToolTipText(LOCTEXT("NavBreakpointLoc", "Navigate to the breakpoint location")) .OnNavigate(this, &FBreakpointLineItem::OnNavigateToBreakpointLocation) ]; } virtual void ExtendContextMenu(class FMenuBuilder& MenuBuilder, bool bInDebuggerTab) override { FBlueprintBreakpoint* Breakpoint = GetBreakpoint(); const UBlueprint* ParentBlueprint = GetBlueprintForObject(ParentObjectRef.Get()); // By default, we don't allow actions to execute when in debug mode. // Create an empty action to always allow execution for these commands (they are allowed in debug mode) FCanExecuteAction AlwaysAllowExecute; if (Breakpoint != nullptr) { const bool bNewEnabledState = !Breakpoint->IsEnabledByUser(); FUIAction ToggleThisBreakpoint( FExecuteAction::CreateSP(this, &FBreakpointLineItem::ToggleBreakpoint), AlwaysAllowExecute ); if (bNewEnabledState) { // Enable MenuBuilder.AddMenuEntry( LOCTEXT("EnableBreakpoint", "Enable breakpoint"), LOCTEXT("EnableBreakpoint_ToolTip", "Enable this breakpoint; the debugger will appear when this node is about to be executed."), FSlateIcon(), ToggleThisBreakpoint); } else { // Disable MenuBuilder.AddMenuEntry( LOCTEXT("DisableBreakpoint", "Disable breakpoint"), LOCTEXT("DisableBreakpoint_ToolTip", "Disable this breakpoint."), FSlateIcon(), ToggleThisBreakpoint); } } if ((Breakpoint != nullptr) && (ParentBlueprint != nullptr)) { FUIAction ClearThisBreakpoint( FExecuteAction::CreateSP(this, &FBreakpointLineItem::ClearBreakpoint), AlwaysAllowExecute ); MenuBuilder.AddMenuEntry( LOCTEXT("ClearBreakpoint", "Remove breakpoint"), LOCTEXT("ClearBreakpoint_ToolTip", "Remove the breakpoint from this node."), FSlateIcon(), ClearThisBreakpoint); } } virtual TSharedRef GetNameIcon() override { return SNew(SButton) .OnClicked(this, &FBreakpointLineItem::OnUserToggledEnabled) .ToolTipText(LOCTEXT("ToggleBreakpointButton_ToolTip", "Toggle this breakpoint")) .ButtonStyle(FAppStyle::Get(), "NoBorder") .ContentPadding(0.0f) [ SNew(SImage) .Image(this, &FBreakpointLineItem::GetStatusImage) .ToolTipText(this, &FBreakpointLineItem::GetStatusTooltip) ]; } protected: FBlueprintBreakpoint* GetBreakpoint() const { if (UEdGraphNode* Node = BreakpointNode.Get()) { if (const UBlueprint* Blueprint = GetBlueprintForObject(Node)) { return FKismetDebugUtilities::FindBreakpointForNode(Node, Blueprint); } } return nullptr; } virtual FDebugLineItem* Duplicate() const override { return new FBreakpointLineItem(BreakpointNode, ParentObjectRef.Get()); } virtual bool Compare(const FDebugLineItem* BaseOther) const override { FBreakpointLineItem* Other = (FBreakpointLineItem*)BaseOther; return (ParentObjectRef.Get() == Other->ParentObjectRef.Get()) && (BreakpointNode == Other->BreakpointNode); } virtual uint32 GetHash() const override { return HashCombine(GetTypeHash(ParentObjectRef), GetTypeHash(BreakpointNode)); } virtual FText GetDisplayName() const override; private: void ToggleBreakpoint() const { if (FBlueprintBreakpoint* Breakpoint = GetBreakpoint()) { FKismetDebugUtilities::SetBreakpointEnabled(*Breakpoint, !Breakpoint->IsEnabled()); } } void ClearBreakpoint() const { if (UEdGraphNode* Node = BreakpointNode.Get()) { if (UBlueprint* Blueprint = GetBlueprintForObject(Node)) { FKismetDebugUtilities::RemoveBreakpointFromNode(Node, Blueprint); } } } FReply OnUserToggledEnabled(); void OnNavigateToBreakpointLocation(); const FSlateBrush* GetStatusImage() const; FText GetStatusTooltip() const; }; FText FBreakpointLineItem::GetDisplayName() const { if (FBlueprintBreakpoint* MyBreakpoint = GetBreakpoint()) { return MyBreakpoint->GetLocationDescription(); } return FText::GetEmpty(); } FReply FBreakpointLineItem::OnUserToggledEnabled() { if (FBlueprintBreakpoint* MyBreakpoint = GetBreakpoint()) { FKismetDebugUtilities::SetBreakpointEnabled(*MyBreakpoint, !MyBreakpoint->IsEnabledByUser()); } return FReply::Handled(); } void FBreakpointLineItem::OnNavigateToBreakpointLocation() { if (FBlueprintBreakpoint* MyBreakpoint = GetBreakpoint()) { FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(MyBreakpoint->GetLocation()); } } const FSlateBrush* FBreakpointLineItem::GetStatusImage() const { if (FBlueprintBreakpoint* MyBreakpoint = GetBreakpoint()) { if (MyBreakpoint->IsEnabledByUser()) { return FAppStyle::GetBrush(FKismetDebugUtilities::IsBreakpointValid(*MyBreakpoint) ? TEXT("Kismet.Breakpoint.EnabledAndValid") : TEXT("Kismet.Breakpoint.EnabledAndInvalid")); } else { return FAppStyle::GetBrush(TEXT("Kismet.Breakpoint.Disabled")); } } return FAppStyle::GetDefaultBrush(); } FText FBreakpointLineItem::GetStatusTooltip() const { if (FBlueprintBreakpoint* MyBreakpoint = GetBreakpoint()) { if (!FKismetDebugUtilities::IsBreakpointValid(*MyBreakpoint)) { return LOCTEXT("Breakpoint_NoHit", "This breakpoint will not be hit because its node generated no code"); } else { return MyBreakpoint->IsEnabledByUser() ? LOCTEXT("ActiveBreakpoint", "Active breakpoint") : LOCTEXT("InactiveBreakpoint", "Inactive breakpoint"); } } else { return LOCTEXT("NoBreakpoint", "No Breakpoint"); } } ////////////////////////////////////////////////////////////////////////// // FTraceStackParentItem class FBreakpointParentItem : public FLineItemWithChildren { public: // The parent object TWeakObjectPtr Blueprint; FBreakpointParentItem(TWeakObjectPtr InBlueprint) : FLineItemWithChildren(DLT_BreakpointParent) , Blueprint(InBlueprint) { } virtual void ExtendContextMenu(FMenuBuilder& MenuBuilder, bool bInDebuggerTab) override { if (FKismetDebugUtilities::BlueprintHasBreakpoints(Blueprint.Get())) { const FUIAction ClearAllBreakpoints( FExecuteAction::CreateSP(this, &FBreakpointParentItem::ClearAllBreakpoints), FCanExecuteAction() // always allow ); MenuBuilder.AddMenuEntry( LOCTEXT("ClearBreakpoints", "Remove all breakpoints"), LOCTEXT("ClearBreakpoints_ToolTip", "Clear all breakpoints in this blueprint"), FSlateIcon(), ClearAllBreakpoints); const bool bEnabledBreakpointExists = FKismetDebugUtilities::FindBreakpointByPredicate( Blueprint.Get(), [](const FBlueprintBreakpoint& Breakpoint)->bool { return Breakpoint.IsEnabled(); } ) != nullptr; if (bEnabledBreakpointExists) { const FUIAction DisableAllBreakpoints( FExecuteAction::CreateSP(this, &FBreakpointParentItem::DisableAllBreakpoints), FCanExecuteAction() // always allow ); MenuBuilder.AddMenuEntry( LOCTEXT("DisableBreakpoints", "Disable all breakpoints"), LOCTEXT("DisableBreakpoints_ToolTip", "Disable all breakpoints in this blueprint"), FSlateIcon(), DisableAllBreakpoints); } const bool bDisabledBreakpointExists = FKismetDebugUtilities::FindBreakpointByPredicate( Blueprint.Get(), [](const FBlueprintBreakpoint& Breakpoint)->bool { return !Breakpoint.IsEnabled(); } ) != nullptr; if (bDisabledBreakpointExists) { const FUIAction EnableAllBreakpoints( FExecuteAction::CreateSP(this, &FBreakpointParentItem::EnableAllBreakpoints), FCanExecuteAction() // always allow ); MenuBuilder.AddMenuEntry( LOCTEXT("EnableBreakpoints", "Enable all breakpoints"), LOCTEXT("EnableBreakpoints_ToolTip", "Enable all breakpoints in this blueprint"), FSlateIcon(), EnableAllBreakpoints); } } } protected: virtual void GatherChildren(TArray& OutChildren, const FString& InSearchString, bool bRespectSearch) override { // update search flags to match that of a root node UpdateSearch(InSearchString, FDebugLineItem::SF_RootNode); if (!Blueprint.IsValid()) { return; } // Create children for each breakpoint FKismetDebugUtilities::ForeachBreakpoint( Blueprint.Get(), [this, &OutChildren, &InSearchString, bRespectSearch](FBlueprintBreakpoint& Breakpoint) { EnsureChildIsAdded(OutChildren, FBreakpointLineItem(Breakpoint.GetLocation(), Blueprint.Get()), InSearchString, bRespectSearch); } ); // Make sure there is something there, to let the user know if there is nothing if (OutChildren.Num() == 0) { EnsureChildIsAdded(OutChildren, FMessageLineItem(LOCTEXT("NoBreakpoints", "No breakpoints").ToString()), InSearchString, bRespectSearch); } } virtual FText GetDisplayName() const override { return LOCTEXT("Breakpoints", "Breakpoints"); } virtual FDebugLineItem* Duplicate() const override { check(false); return nullptr; } virtual bool Compare(const FDebugLineItem* BaseOther) const override { check(false); return false; } virtual uint32 GetHash() const override { check(false); return 0; } private: void DisableAllBreakpoints() const { FKismetDebugUtilities::ForeachBreakpoint(Blueprint.Get(), [](FBlueprintBreakpoint& Breakpoint) { FKismetDebugUtilities::SetBreakpointEnabled(Breakpoint, false); } ); } void EnableAllBreakpoints() const { FKismetDebugUtilities::ForeachBreakpoint(Blueprint.Get(), [](FBlueprintBreakpoint& Breakpoint) { FKismetDebugUtilities::SetBreakpointEnabled(Breakpoint, true); } ); } void ClearAllBreakpoints() const { FKismetDebugUtilities::ClearBreakpoints(Blueprint.Get()); } }; void FDebugLineItem::SetBreakpointParentItemBlueprint(FDebugTreeItemPtr InBreakpointParentItem, TWeakObjectPtr InBlueprint) { if (ensureMsgf(InBreakpointParentItem.IsValid() && InBreakpointParentItem->Type == DLT_BreakpointParent, TEXT("TreeItem is not Valid!"))) { TSharedPtr BreakpointItem = StaticCastSharedPtr(InBreakpointParentItem); BreakpointItem->Blueprint = InBlueprint; } } ////////////////////////////////////////////////////////////////////////// // FParentLineItem class FParentLineItem : public FLineItemWithChildren { protected: // The parent object TWeakObjectPtr ObjectRef; public: FParentLineItem(UObject* Object) : FLineItemWithChildren(DLT_Parent) { ObjectRef = Object; } virtual TSharedRef GenerateNameWidget(TSharedPtr InSearchString) override { return SNew(PropertyInfoViewStyle::STextHighlightOverlay) .FullText(this, &FParentLineItem::GetDisplayName) .HighlightText(this, &FParentLineItem::GetHighlightText, InSearchString) [ SNew(STextBlock) .ToolTipText(this, &FParentLineItem::GetTooltipText) .Text(this, &FParentLineItem::GetDisplayName) ]; } virtual UObject* GetParentObject() const override { return ObjectRef.Get(); } virtual void GatherChildren(TArray& OutChildren, const FString& InSearchString, bool bRespectSearch) override { // update search flags to match that of a root node UpdateSearch(InSearchString, SF_RootNode); if (UObject* ParentObject = ObjectRef.Get()) { // every instance should have an automatic watch for 'self' EnsureChildIsAdded(OutChildren, FSelfWatchLineItem(ParentObject), InSearchString, bRespectSearch); UBlueprint* ParentBP = FDebugLineItem::GetBlueprintForObject(ParentObject); if (ParentBP != nullptr) { // Create children for each watch if (IsDebugLineTypeActive(DLT_Watch)) { FKismetDebugUtilities::ForeachPinPropertyWatch(ParentBP, [this, &OutChildren, ParentObject, &InSearchString, bRespectSearch] (FBlueprintWatchedPin& WatchedPin) { EnsureChildIsAdded(OutChildren, FWatchLineItem(WatchedPin.Get(), ParentObject, WatchedPin.GetPathToProperty()), InSearchString, bRespectSearch); } ); } // It could also have active latent behaviors if (IsDebugLineTypeActive(DLT_LatentAction)) { if (UWorld* World = GEngine->GetWorldFromContextObject(ParentObject, EGetWorldErrorMode::ReturnNull)) { FLatentActionManager& LatentActionManager = World->GetLatentActionManager(); // Get the current list of action UUIDs TSet UUIDSet; LatentActionManager.GetActiveUUIDs(ParentObject, /*inout*/ UUIDSet); // Add the new ones for (TSet::TConstIterator RemainingIt(UUIDSet); RemainingIt; ++RemainingIt) { const int32 UUID = *RemainingIt; EnsureChildIsAdded(OutChildren, FLatentActionLineItem(UUID, ParentObject), InSearchString, bRespectSearch); } } } // Make sure there is something there, to let the user know if there is nothing if (OutChildren.Num() == 0) { EnsureChildIsAdded(OutChildren, FMessageLineItem(LOCTEXT("NoDebugInfo", "No debugging info").ToString()), InSearchString, bRespectSearch); } } //@TODO: try to get at TArray DebugProperties in UGameViewportClient, if available } } virtual TSharedRef GetNameIcon() override { return SNew(SImage) .Image(this, &FParentLineItem::GetStatusImage) .ColorAndOpacity_Raw(this, &FParentLineItem::GetStatusColor) .ToolTipText(this, &FParentLineItem::GetStatusTooltip); } const FSlateBrush* GetStatusImage() const { if (SKismetDebuggingView::CurrentActiveObject == ObjectRef) { return FAppStyle::GetBrush(TEXT("Kismet.Trace.CurrentIndex")); } if (ObjectRef.IsValid()) { return FSlateIconFinder::FindIconBrushForClass(ObjectRef->GetClass()); } return FAppStyle::GetBrush(TEXT("None")); } FSlateColor GetStatusColor() const { if (SKismetDebuggingView::CurrentActiveObject == ObjectRef) { return FSlateColor(EStyleColor::AccentYellow); } const UGraphEditorSettings* Settings = GetDefault(); return Settings->ObjectPinTypeColor; } FText GetStatusTooltip() const { if (SKismetDebuggingView::CurrentActiveObject == ObjectRef) { return LOCTEXT("BreakpointHIt", "Breakpoint Hit"); } return FText::GetEmpty(); } virtual void ExtendContextMenu(class FMenuBuilder& MenuBuilder, bool bInDebuggerTab) override { if (UBlueprint* BP = Cast(ObjectRef.Get())) { if (FKismetDebugUtilities::BlueprintHasPinWatches(BP)) { FUIAction ClearAllWatches( FExecuteAction::CreateSP(this, &FParentLineItem::ClearAllWatches), FCanExecuteAction() // always allow ); MenuBuilder.AddMenuEntry( LOCTEXT("ClearWatches", "Clear all watches"), LOCTEXT("ClearWatches_ToolTip", "Clear all watches in this blueprint"), FSlateIcon(), ClearAllWatches); } } } protected: virtual FDebugLineItem* Duplicate() const override { return new FParentLineItem(ObjectRef.Get()); } virtual bool Compare(const FDebugLineItem* BaseOther) const override { FParentLineItem* Other = (FParentLineItem*)BaseOther; return ObjectRef.Get() == Other->ObjectRef.Get(); } virtual uint32 GetHash() const override { return GetTypeHash(ObjectRef); } virtual FText GetDisplayName() const override { UObject* Object = ObjectRef.Get(); AActor* Actor = Cast(Object); if (Actor != nullptr) { return FText::FromString(Actor->GetActorLabel()); } else { return (Object != nullptr) ? FText::FromString(Object->GetName()) : LOCTEXT("nullptr", "(nullptr)"); } } private: void ClearAllWatches() const { if (UBlueprint* Blueprint = Cast(ObjectRef.Get())) { FKismetDebugUtilities::ClearPinWatches(Blueprint); } } FText GetTooltipText() const { if (const UObject* Object = ObjectRef.Get()) { if (UWorld* World = Object->GetTypedOuter()) { FText WorldName = FText::FromString(GetDebugStringForWorld(World)); return FText::FormatNamed(LOCTEXT("ParentLineTooltip", "{ObjectFullPath}\nWorld: {WorldFullPath}\nWorld Type: {WorldType}"), TEXT("ObjectFullPath"), FText::FromString(Object->GetPathName()), TEXT("WorldFullPath"), FText::FromString(World->GetPathName()), TEXT("WorldType"), WorldName); } } return GetDisplayName(); } }; ////////////////////////////////////////////////////////////////////////// // FTraceStackChildItem class FTraceStackChildItem : public FDebugLineItem { protected: int32 StackIndex; public: FTraceStackChildItem(int32 InStackIndex) : FDebugLineItem(DLT_TraceStackChild) { StackIndex = InStackIndex; } virtual TSharedRef GenerateNameWidget(TSharedPtr InSearchString) override { return SNew(PropertyInfoViewStyle::STextHighlightOverlay) .FullText(this, &FTraceStackChildItem::GetDisplayName) .HighlightText(this, &FTraceStackChildItem::GetHighlightText, InSearchString) [ SNew(SHyperlink) .Text(this, &FTraceStackChildItem::GetDisplayName) .Style(FAppStyle::Get(), "HoverOnlyHyperlink") .ToolTipText(LOCTEXT("NavigateToDebugTraceLocationHyperlink_ToolTip", "Navigate to the trace location")) .OnNavigate(this, &FTraceStackChildItem::OnNavigateToNode) ]; } // Visit time and actor name virtual TSharedRef GenerateValueWidget(TSharedPtr InSearchString) override { return SNew(PropertyInfoViewStyle::STextHighlightOverlay) .FullText(this, &FTraceStackChildItem::GetDescription) .HighlightText(this, &FTraceStackChildItem::GetHighlightText, InSearchString) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ SNew(SHyperlink) .Text(this, &FTraceStackChildItem::GetContextObjectName) .Style(FAppStyle::Get(), "HoverOnlyHyperlink") .ToolTipText(LOCTEXT("SelectActor_Tooltip", "Select this actor")) .OnNavigate(this, &FTraceStackChildItem::OnSelectContextObject) ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(STextBlock) .Text(this, &FTraceStackChildItem::GetVisitTime) ] ]; } virtual TSharedRef GetNameIcon() override { return SNew(SImage) .Image(FAppStyle::GetBrush( (StackIndex > 0) ? TEXT("Kismet.Trace.PreviousIndex") : TEXT("Kismet.Trace.CurrentIndex")) ); } virtual FText GetDescription() const override { return FText::FromString(GetContextObjectName().ToString() + GetVisitTime().ToString()); } protected: virtual FDebugLineItem* Duplicate() const override { check(false); return nullptr; } virtual bool Compare(const FDebugLineItem* BaseOther) const override { check(false); return false; } virtual uint32 GetHash() const override { check(false); return 0; } virtual FText GetDisplayName() const override { UEdGraphNode* Node = GetNode(); if (Node != nullptr) { return Node->GetNodeTitle(ENodeTitleType::ListView); } else { return LOCTEXT("Unknown", "(unknown)"); } } UEdGraphNode* GetNode() const { const TSimpleRingBuffer& TraceStack = FKismetDebugUtilities::GetTraceStack(); if (StackIndex < TraceStack.Num()) { const FKismetTraceSample& Sample = TraceStack(StackIndex); UObject* ObjectContext = Sample.Context.Get(); FString ContextName = (ObjectContext != nullptr) ? ObjectContext->GetName() : LOCTEXT("ObjectDoesNotExist", "(object no longer exists)").ToString(); FString NodeName = TEXT(" "); if (ObjectContext != nullptr) { // Try to find the node that got executed UEdGraphNode* Node = FKismetDebugUtilities::FindSourceNodeForCodeLocation(ObjectContext, Sample.Function.Get(), Sample.Offset); return Node; } } return nullptr; } FText GetVisitTime() const { const TSimpleRingBuffer& TraceStack = FKismetDebugUtilities::GetTraceStack(); if (StackIndex < TraceStack.Num()) { static const FNumberFormattingOptions TimeFormatOptions = FNumberFormattingOptions() .SetMinimumFractionalDigits(2) .SetMaximumFractionalDigits(2); return FText::Format(LOCTEXT("VisitTimeFmt", " @ {0} s"), FText::AsNumber(TraceStack(StackIndex).ObservationTime - GStartTime, &TimeFormatOptions)); } return FText::GetEmpty(); } FText GetContextObjectName() const { const TSimpleRingBuffer& TraceStack = FKismetDebugUtilities::GetTraceStack(); UObject* ObjectContext = (StackIndex < TraceStack.Num()) ? TraceStack(StackIndex).Context.Get() : nullptr; return (ObjectContext != nullptr) ? FText::FromString(ObjectContext->GetName()) : LOCTEXT("ObjectDoesNotExist", "(object no longer exists)"); } void OnNavigateToNode() { if (UEdGraphNode* Node = GetNode()) { FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(Node); } } void OnSelectContextObject() { const TSimpleRingBuffer& TraceStack = FKismetDebugUtilities::GetTraceStack(); UObject* ObjectContext = (StackIndex < TraceStack.Num()) ? TraceStack(StackIndex).Context.Get() : nullptr; // Add the object to the selection set if (AActor* Actor = Cast(ObjectContext)) { GEditor->SelectActor(Actor, true, true, true); } else { UE_LOG(LogBlueprintDebugTreeView, Warning, TEXT("Cannot select the non-actor object '%s'"), (ObjectContext != nullptr) ? *ObjectContext->GetName() : TEXT("(nullptr)")); } } }; ////////////////////////////////////////////////////////////////////////// // FTraceStackParentItem class FTraceStackParentItem : public FLineItemWithChildren { public: FTraceStackParentItem() : FLineItemWithChildren(DLT_TraceStackParent) { } virtual bool HasChildren() const override { return !ChildrenMirrorsArr.IsEmpty(); } protected: virtual FDebugLineItem* Duplicate() const override { check(false); return nullptr; } virtual bool Compare(const FDebugLineItem* BaseOther) const override { check(false); return false; } virtual uint32 GetHash() const override { check(false); return 0; } virtual FText GetDisplayName() const override { return LOCTEXT("ExecutionTrace", "Execution Trace"); } virtual void GatherChildren(TArray& OutChildren, const FString& InSearchString, bool bRespectSearch) override { // update search flags to match that of a root node UpdateSearch(InSearchString, SF_RootNode); const TSimpleRingBuffer& TraceStack = FKismetDebugUtilities::GetTraceStack(); const int32 NumVisible = TraceStack.Num(); // Create any new stack entries that are needed for (int32 i = ChildrenMirrorsArr.Num(); i < NumVisible; ++i) { ChildrenMirrorsArr.Add(MakeShareable(new FTraceStackChildItem(i))); } // Add the visible stack entries as children for (int32 i = 0; i < NumVisible; ++i) { OutChildren.Add(ChildrenMirrorsArr[i]); } } // use an array to store children mirrors instead of a set so it's ordered TArray ChildrenMirrorsArr; }; ////////////////////////////////////////////////////////////////////////// // SDebugLineItem class SDebugLineItem : public SMultiColumnTableRow< FDebugTreeItemPtr > { protected: FDebugTreeItemPtr ItemToEdit; TSharedPtr SearchString; public: SLATE_BEGIN_ARGS(SDebugLineItem) {} SLATE_END_ARGS() virtual TSharedRef GenerateWidgetForColumn(const FName& ColumnName) override { TSharedPtr ColumnContent = nullptr; if (ColumnName == SKismetDebugTreeView::ColumnId_Name) { SAssignNew(ColumnContent, SHorizontalBox) + SHorizontalBox::Slot() .HAlign(HAlign_Left) .VAlign(VAlign_Fill) .AutoWidth() [ SNew(PropertyInfoViewStyle::SIndent, SharedThis(this)) ] + SHorizontalBox::Slot() .HAlign(HAlign_Left) .VAlign(VAlign_Center) .AutoWidth() [ SNew(PropertyInfoViewStyle::SExpanderArrow, SharedThis(this)) .HasChildren_Lambda([ItemToEdit = ItemToEdit]() { const bool HasChildren = ItemToEdit->HasChildren(); return HasChildren; }) ] + SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Left) .VAlign(VAlign_Center) [ ItemToEdit->GetNameIcon() ] + SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Left) .VAlign(VAlign_Center) .Padding(5.f, 0.f, 0.f, 0.f) [ ItemToEdit->GenerateNameWidget(SearchString) ]; } else if (ColumnName == SKismetDebugTreeView::ColumnId_Value) { SAssignNew(ColumnContent, SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ ItemToEdit->GetValueIcon() ] + SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Left) .Padding(.5f, 1.f) [ ItemToEdit->GenerateValueWidget(SearchString) ]; } else { SAssignNew(ColumnContent, STextBlock) .Text(LOCTEXT("Error", "Error")); } return SNew(SBox) .Padding(FMargin(0.5f, 0.5f)) [ SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) .BorderBackgroundColor_Static( PropertyInfoViewStyle::GetRowBackgroundColor, static_cast(this) ) [ ColumnContent.ToSharedRef() ] ]; } void Construct(const FArguments& InArgs, TSharedRef OwnerTableView, FDebugTreeItemPtr InItemToEdit, TSharedPtr InSearchString) { ItemToEdit = InItemToEdit; SearchString = InSearchString; SMultiColumnTableRow::Construct(FSuperRowType::FArguments(), OwnerTableView); } protected: virtual FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override { const TSharedRef* NameWidget = GetWidgetFromColumnId(SKismetDebugTreeView::ColumnId_Name); const TSharedRef* ValWidget = GetWidgetFromColumnId(SKismetDebugTreeView::ColumnId_Value); if (NameWidget && ValWidget) { return FVector2D::Max((*NameWidget)->GetDesiredSize(), (*ValWidget)->GetDesiredSize()) * FVector2D(2.0f, 1.0f); } return STableRow::ComputeDesiredSize(LayoutScaleMultiplier); } }; ////////////////////////////////////////////////////////////////////////// // SKismetDebugTreeView void SKismetDebugTreeView::Construct(const FArguments& InArgs) { bFilteredItemsDirty = false; bInDebuggerTab = InArgs._InDebuggerTab; SearchString = MakeShared(); SearchMessageItem = MakeMessageItem(LOCTEXT("NoItemsMatchSearch", "No entries match the search text").ToString()); ChildSlot [ SAssignNew(TreeView, STreeView< FDebugTreeItemPtr >) .TreeItemsSource(&FilteredTreeRoots) .SelectionMode(InArgs._SelectionMode) .OnGetChildren(this, &SKismetDebugTreeView::OnGetChildren) .OnGenerateRow(this, &SKismetDebugTreeView::OnGenerateRow) .OnExpansionChanged(InArgs._OnExpansionChanged) .OnContextMenuOpening(this, &SKismetDebugTreeView::OnMakeContextMenu) .TreeViewStyle(&FAppStyle::Get().GetWidgetStyle("PropertyTable.InViewport.ListView")) .HeaderRow(InArgs._HeaderRow) ]; } void SKismetDebugTreeView::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { if (bFilteredItemsDirty) { UpdateFilteredItems(); bFilteredItemsDirty = false; } } void SKismetDebugTreeView::AddTreeItemUnique(const FDebugTreeItemPtr& Item) { RootTreeItems.AddUnique(Item); RequestUpdateFilteredItems(); } bool SKismetDebugTreeView::RemoveTreeItem(const FDebugTreeItemPtr& Item) { if (RootTreeItems.Remove(Item) != 0) { RequestUpdateFilteredItems(); return true; } return false; } void SKismetDebugTreeView::ClearTreeItems() { if (!RootTreeItems.IsEmpty()) { RootTreeItems.Empty(); RequestUpdateFilteredItems(); } } void SKismetDebugTreeView::SetSearchText(const FText& InSearchText) { *SearchString = InSearchText.ToString(); RequestUpdateFilteredItems(); } void SKismetDebugTreeView::RequestUpdateFilteredItems() { bFilteredItemsDirty = true; } const TArray& SKismetDebugTreeView::GetRootTreeItems() const { return RootTreeItems; } int32 SKismetDebugTreeView::GetSelectedItems(TArray& OutItems) { return TreeView->GetSelectedItems(OutItems); } void SKismetDebugTreeView::ClearExpandedItems() { TreeView->ClearExpandedItems(); } bool SKismetDebugTreeView::IsScrolling() const { return TreeView->IsScrolling(); } void SKismetDebugTreeView::SetItemExpansion(FDebugTreeItemPtr InItem, bool bInShouldExpandItem) { TreeView->SetItemExpansion(InItem, bInShouldExpandItem); } void SKismetDebugTreeView::UpdateFilteredItems() { FilteredTreeRoots.Empty(); for (FDebugTreeItemPtr Item : RootTreeItems) { if (Item.IsValid()) { if (Item->CanHaveChildren()) { FLineItemWithChildren* ItemWithChildren = StaticCast(Item.Get()); if (SearchString->IsEmpty() || ItemWithChildren->SearchRecursive(*SearchString, TreeView)) { FilteredTreeRoots.Add(Item); } } else { Item->UpdateSearch(*SearchString, FDebugLineItem::SF_RootNode); if (SearchString->IsEmpty() || Item->IsVisible()) { FilteredTreeRoots.Add(Item); } } } } if (FilteredTreeRoots.IsEmpty()) { FilteredTreeRoots.Add(SearchMessageItem); } TreeView->RequestTreeRefresh(); } TSharedRef SKismetDebugTreeView::OnGenerateRow(FDebugTreeItemPtr InItem, const TSharedRef& OwnerTable) { return SNew(SDebugLineItem, OwnerTable, InItem, SearchString); } void SKismetDebugTreeView::OnGetChildren(FDebugTreeItemPtr InParent, TArray& OutChildren) { InParent->GatherChildrenBase(OutChildren, *SearchString); } TSharedPtr SKismetDebugTreeView::OnMakeContextMenu() const { FMenuBuilder MenuBuilder(true, nullptr); MenuBuilder.BeginSection("DebugActions", LOCTEXT("DebugActionsMenuHeading", "Debug Actions")); { TArray SelectedItems; TreeView->GetSelectedItems(SelectedItems); for (FDebugTreeItemPtr& Item : SelectedItems) { Item->MakeMenu(MenuBuilder, bInDebuggerTab); } } MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } FDebugTreeItemPtr SKismetDebugTreeView::MakeTraceStackParentItem() { return MakeShared(); } FDebugTreeItemPtr SKismetDebugTreeView::MakeBreakpointParentItem(TWeakObjectPtr InBlueprint) { return MakeShared(InBlueprint); } FDebugTreeItemPtr SKismetDebugTreeView::MakeMessageItem(const FString& InMessage) { return MakeShared(InMessage); } FDebugTreeItemPtr SKismetDebugTreeView::MakeParentItem(UObject* InObject) { return MakeShared(InObject); } FDebugTreeItemPtr SKismetDebugTreeView::MakeWatchLineItem(const UEdGraphPin* InPinRef, UObject* InDebugObject) { return MakeShared(InPinRef, InDebugObject); } #undef LOCTEXT_NAMESPACE