// Copyright Epic Games, Inc. All Rights Reserved. #include "SMassBreakpointsView.h" #include "MassDebugger.h" #include "MassDebuggerModel.h" #include "MassProcessor.h" #include "HAL/PlatformApplicationMisc.h" #include "Widgets/Input/SButton.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/SBoxPanel.h" #include "SSearchableComboBox.h" #include "Serialization/JsonSerializer.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonWriter.h" #include "Dom/JsonObject.h" #define LOCTEXT_NAMESPACE "SMassDebugger" void SMassBreakpointsView::Construct(const FArguments& InArgs, TSharedRef InModel) { Initialize(InModel); #if WITH_MASSENTITY_DEBUG InModel->OnEditorBreakpointsChangedDelegate.AddSP(this, &SMassBreakpointsView::RefreshBreakpoints); InModel->OnQueriesChangedDelegate.AddSP(this, &SMassBreakpointsView::RefreshBreakpoints); TriggerTypeOptions = { MakeShared(UE::Mass::Debug::FBreakpoint::TriggerTypeToString( UE::Mass::Debug::FBreakpoint::ETriggerType::None)), MakeShared(UE::Mass::Debug::FBreakpoint::TriggerTypeToString( UE::Mass::Debug::FBreakpoint::ETriggerType::ProcessorExecute)), MakeShared(UE::Mass::Debug::FBreakpoint::TriggerTypeToString( UE::Mass::Debug::FBreakpoint::ETriggerType::FragmentWrite)), MakeShared(UE::Mass::Debug::FBreakpoint::TriggerTypeToString( UE::Mass::Debug::FBreakpoint::ETriggerType::EntityCreate)), MakeShared(UE::Mass::Debug::FBreakpoint::TriggerTypeToString( UE::Mass::Debug::FBreakpoint::ETriggerType::EntityDestroy)), MakeShared(UE::Mass::Debug::FBreakpoint::TriggerTypeToString( UE::Mass::Debug::FBreakpoint::ETriggerType::FragmentAdd)), MakeShared(UE::Mass::Debug::FBreakpoint::TriggerTypeToString( UE::Mass::Debug::FBreakpoint::ETriggerType::FragmentRemove)), MakeShared(UE::Mass::Debug::FBreakpoint::TriggerTypeToString( UE::Mass::Debug::FBreakpoint::ETriggerType::TagAdd)), MakeShared(UE::Mass::Debug::FBreakpoint::TriggerTypeToString( UE::Mass::Debug::FBreakpoint::ETriggerType::TagRemove)) }; check(TriggerTypeOptions.Num() == static_cast(UE::Mass::Debug::FBreakpoint::ETriggerType::MAX)); for (TSharedPtr& TypeStringPtr : TriggerTypeOptions) { UE::Mass::Debug::FBreakpoint::ETriggerType Trigger; if (TypeStringPtr && UE::Mass::Debug::FBreakpoint::StringToTriggerType(*TypeStringPtr, Trigger)) { TriggerMap.Add(Trigger, TypeStringPtr); } } FilterTypeOptions = { MakeShared(UE::Mass::Debug::FBreakpoint::FilterTypeToString( UE::Mass::Debug::FBreakpoint::EFilterType::None)), MakeShared(UE::Mass::Debug::FBreakpoint::FilterTypeToString( UE::Mass::Debug::FBreakpoint::EFilterType::SpecificEntity)), MakeShared(UE::Mass::Debug::FBreakpoint::FilterTypeToString( UE::Mass::Debug::FBreakpoint::EFilterType::SelectedEntity)), MakeShared(UE::Mass::Debug::FBreakpoint::FilterTypeToString( UE::Mass::Debug::FBreakpoint::EFilterType::Query)) }; for (TSharedPtr& TypeStringPtr : FilterTypeOptions) { UE::Mass::Debug::FBreakpoint::EFilterType Filter; if (TypeStringPtr && UE::Mass::Debug::FBreakpoint::StringToFilterType(*TypeStringPtr, Filter)) { FilterMap.Add(Filter, TypeStringPtr); } } BreakpointsListView = SNew(SListView>) .ListItemsSource(&InModel->CachedBreakpoints) .OnGenerateRow(this, &SMassBreakpointsView::OnGenerateBreakpointRow) .HeaderRow( SNew(SHeaderRow) + SHeaderRow::Column("Enabled") .DefaultLabel(FText::GetEmpty()) .FixedWidth(128) + SHeaderRow::Column("TriggerType") .DefaultLabel(LOCTEXT("TriggerTypeColumn", "Trigger Type")) .FillWidth(0.2f) + SHeaderRow::Column("Trigger") .DefaultLabel(LOCTEXT("TriggerColumn", "Trigger")) .FillWidth(0.2f) + SHeaderRow::Column("FilterType") .DefaultLabel(LOCTEXT("FilterTypeColumn", "Filter Type")) .FillWidth(0.2f) + SHeaderRow::Column("Filter") .DefaultLabel(LOCTEXT("FilterColumn", "Filter")) .FillWidth(0.2f) + SHeaderRow::Column(TEXT("HitCount")) .DefaultLabel(LOCTEXT("HitCount", "Hit Count")) .FillWidth(0.2f) ); ChildSlot [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(4) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth() [ SNew(SButton) .ToolTipText(LOCTEXT("NewBreakpointTooltip", "New Breakpoint")) .OnClicked(this, &SMassBreakpointsView::NewBreakpointClicked) [ SNew(SImage).Image(FAppStyle::GetBrush("Icons.PlusCircle")) ] ] + SHorizontalBox::Slot().AutoWidth() [ SNew(SButton) .ToolTipText(LOCTEXT("PasteBreakpointTooltip", "Paste Breakpoint")) .OnClicked(this, &SMassBreakpointsView::PasteBreakpointClicked) [ SNew(SImage).Image(FAppStyle::GetBrush("GenericCommands.Paste")) ] ] + SHorizontalBox::Slot().AutoWidth().Padding(0,0,4,0) [ SNew(SButton) .ToolTipText(LOCTEXT("SaveBreakpointsTooltip", "Save Breakpoints")) .OnClicked(this, &SMassBreakpointsView::SaveBreakpointsClicked) [ SNew(SImage).Image(FAppStyle::GetBrush("Icons.SaveModified")) ] ] + SHorizontalBox::Slot().AutoWidth().Padding(0,0,4,0) [ SNew(SButton) .ToolTipText(LOCTEXT("ClearAllBreakpoints", "Clear All Breakpoints")) .OnClicked(this, &SMassBreakpointsView::ClearBreakpointsClicked) [ SNew(SImage).Image(FAppStyle::GetBrush("Icons.Delete")) ] ] ] + SVerticalBox::Slot().FillHeight(1.f).Padding(4) [ BreakpointsListView.ToSharedRef() ] ]; #else ChildSlot [ SNew(STextBlock).Text(LOCTEXT("DebugNotEnabled", "Mass Entity Debugging Not Enabled")) ]; #endif } void SMassBreakpointsView::RefreshBreakpoints() { #if WITH_MASSENTITY_DEBUG if (BreakpointsListView.IsValid()) { BreakpointsListView->RebuildList(); } #endif } FReply SMassBreakpointsView::ClearBreakpointsClicked() { #if WITH_MASSENTITY_DEBUG if (DebuggerModel) { DebuggerModel->RemoveAllBreakpoints(); } #endif return FReply::Handled(); } FReply SMassBreakpointsView::SaveBreakpointsClicked() { #if WITH_MASSENTITY_DEBUG if (DebuggerModel) { DebuggerModel->SaveBreakpointsToDisk(); } #endif return FReply::Handled(); } FReply SMassBreakpointsView::PasteBreakpointClicked() { #if WITH_MASSENTITY_DEBUG if (DebuggerModel) { FString ClipboardText; FPlatformApplicationMisc::ClipboardPaste(ClipboardText); DebuggerModel->CreateBreakpointFromString(ClipboardText); } #endif return FReply::Handled(); } FReply SMassBreakpointsView::NewBreakpointClicked() { #if WITH_MASSENTITY_DEBUG if (DebuggerModel) { DebuggerModel->CreateBreakpoint(); } #endif return FReply::Handled(); } TSharedRef SMassBreakpointsView::OnGenerateBreakpointRow( TSharedPtr InItem, const TSharedRef& OwnerTable) { return SNew(SMassBreakpointsView::SBreakpointTableRow, OwnerTable, SharedThis(this), InItem.ToSharedRef(), DebuggerModel.ToSharedRef()); } void SMassBreakpointsView::SBreakpointTableRow::Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView, TSharedRef InParentView, TSharedRef InBreakpointData, TSharedRef InModel) { BreakpointData = InBreakpointData; DebuggerModel = InModel; ParentView = InParentView; SMultiColumnTableRow>::Construct(SMultiColumnTableRow>::FArguments(), InOwnerTableView); } TSharedRef SMassBreakpointsView::SBreakpointTableRow::GenerateWidgetForColumn(const FName& ColumnId) { #if WITH_MASSENTITY_DEBUG TSharedPtr Data = BreakpointData.Pin(); TSharedPtr View = ParentView.Pin(); if (!Data || !View) { return SNullWidget::NullWidget; } if (ColumnId == "Enabled") { return SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .Padding(2) [ SNew(SCheckBox) .IsChecked_Lambda([WeakItem = BreakpointData]() { if (TSharedPtr Item = WeakItem.Pin()) { return Item->BreakpointInstance.bEnabled ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } return ECheckBoxState::Unchecked; }) .OnCheckStateChanged_Lambda([WeakItem = BreakpointData, WeakModel = DebuggerModel](ECheckBoxState NewState) { if (TSharedPtr Item = WeakItem.Pin()) { Item->BreakpointInstance.bEnabled = (NewState == ECheckBoxState::Checked); if (TSharedPtr Model = WeakModel.Pin()) { Model->ApplyBreakpointsToCurrentEnvironment(); } } }) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(2) [ SNew(SButton) .ToolTipText(LOCTEXT("DeleteBreakpointTooltip", "Delete Breakpoint")) .OnClicked_Lambda([WeakItem = BreakpointData, WeakModel = DebuggerModel]() { if (TSharedPtr Item = WeakItem.Pin()) { if (TSharedPtr Model = WeakModel.Pin()) { Model->RemoveBreakpoint(Item->BreakpointInstance.Handle); } } return FReply::Handled(); }) [ SNew(SImage).Image(FAppStyle::GetBrush("Icons.Delete")) ] ] + SHorizontalBox::Slot() .AutoWidth() .Padding(2) [ SNew(SButton) .ToolTipText(LOCTEXT("CopyBreakpointTooltip", "Copy Breakpoint")) .OnClicked_Lambda([WeakItem = BreakpointData]() { if (TSharedPtr Item = WeakItem.Pin()) { FString JsonString; const TSharedRef> Writer = TJsonWriterFactory<>::Create(&JsonString); FJsonSerializer::Serialize(Item->SerializeToJson().ToSharedRef(), Writer); FPlatformApplicationMisc::ClipboardCopy(*JsonString); } return FReply::Handled(); }) [ SNew(SImage) .Image(FAppStyle::GetBrush("GenericCommands.Copy")) ] ]; } else if (ColumnId == "TriggerType") { return SNew(SComboBox>) .OptionsSource(&View->TriggerTypeOptions) .OnGenerateWidget_Lambda([](TSharedPtr Str) { return SNew(STextBlock).Text(FText::FromString(*Str)); }) .OnSelectionChanged_Lambda([WeakData = BreakpointData, WeakModel = DebuggerModel](TSharedPtr Str, ESelectInfo::Type) { UE::Mass::Debug::FBreakpoint::ETriggerType NewType; TSharedPtr Data = WeakData.Pin(); if (Data && Str && UE::Mass::Debug::FBreakpoint::StringToTriggerType(*Str, NewType)) { Data->BreakpointInstance.TriggerType = NewType; if (TSharedPtr Model = WeakModel.Pin()) { Model->ApplyBreakpointsToCurrentEnvironment(); } } }) .InitiallySelectedItem(View->TriggerMap[Data->BreakpointInstance.TriggerType]) [ SNew(STextBlock) .Text_Lambda([WeakData = BreakpointData]() { if (TSharedPtr Data = WeakData.Pin()) { return FText::FromString(UE::Mass::Debug::FBreakpoint::TriggerTypeToString(Data->BreakpointInstance.TriggerType)); } return LOCTEXT("BreakpointInvalid", "Invalid Breakpoint"); }) ]; } else if (ColumnId == "Trigger") { if (Data->BreakpointInstance.TriggerType == UE::Mass::Debug::FBreakpoint::ETriggerType::ProcessorExecute) { return SNew(SSearchableComboBox) .OptionsSource(&DebuggerModel.Pin()->ProcessorNames) .OnGenerateWidget_Lambda([](TSharedPtr Str) { if (Str) { return SNew(STextBlock) .Text(FText::FromString(*Str)); } return SNew(STextBlock) .Text(LOCTEXT("ProcessorNone", "None")); }) .OnSelectionChanged_Lambda([WeakData = BreakpointData, WeakModel = DebuggerModel](TSharedPtr Str, ESelectInfo::Type) { if (TSharedPtr Data = WeakData.Pin()) { if (Str) { Data->TriggerName = *Str; } else { Data->TriggerName.Empty(); } if (TSharedPtr Model = WeakModel.Pin()) { Data->ReconcileDataFromNames(*Model); Model->ApplyBreakpointsToCurrentEnvironment(); } } }) [ SNew(STextBlock).Text_Lambda([WeakData = BreakpointData]() { if (TSharedPtr Data = WeakData.Pin()) { return FText::FromString(Data->TriggerName); } return LOCTEXT("SelectProcessor","Select Processor"); }) ]; } else if (Data->BreakpointInstance.TriggerType == UE::Mass::Debug::FBreakpoint::ETriggerType::FragmentWrite || Data->BreakpointInstance.TriggerType == UE::Mass::Debug::FBreakpoint::ETriggerType::FragmentAdd || Data->BreakpointInstance.TriggerType == UE::Mass::Debug::FBreakpoint::ETriggerType::FragmentRemove) { return SNew(SSearchableComboBox) .OptionsSource(&DebuggerModel.Pin()->FragmentNames) .OnGenerateWidget_Lambda([](TSharedPtr Str) { if (Str) { return SNew(STextBlock) .Text(FText::FromString(*Str)); } return SNew(STextBlock) .Text(LOCTEXT("FragmentNone", "None")); }) .OnSelectionChanged_Lambda([WeakData = BreakpointData, WeakModel = DebuggerModel](TSharedPtr Str, ESelectInfo::Type) { if (TSharedPtr Data = WeakData.Pin()) { if (Str) { Data->TriggerName = *Str; } else { Data->TriggerName.Empty(); } if (TSharedPtr Model = WeakModel.Pin(); !Model->IsStale()) { Data->ReconcileDataFromNames(*Model); Data->ApplyToEngine(*Model); } } }) [ SNew(STextBlock).Text_Lambda([WeakData = BreakpointData]() { if (TSharedPtr Data = WeakData.Pin()) { return FText::FromString(Data->TriggerName); } return LOCTEXT("SelectFragment", "Select Fragment"); }) ]; } else if (Data->BreakpointInstance.TriggerType == UE::Mass::Debug::FBreakpoint::ETriggerType::TagAdd || Data->BreakpointInstance.TriggerType == UE::Mass::Debug::FBreakpoint::ETriggerType::TagRemove) { return SNew(SSearchableComboBox) .OptionsSource(&DebuggerModel.Pin()->TagNames) .OnGenerateWidget_Lambda([](TSharedPtr Str) { if (Str) { return SNew(STextBlock) .Text(FText::FromString(*Str)); } return SNew(STextBlock) .Text(LOCTEXT("TagNone", "None")); }) .OnSelectionChanged_Lambda([WeakData = BreakpointData, WeakModel = DebuggerModel](TSharedPtr Str, ESelectInfo::Type) { if (TSharedPtr Data = WeakData.Pin()) { if (Str) { Data->TriggerName = *Str; } else { Data->TriggerName.Empty(); } if (TSharedPtr Model = WeakModel.Pin(); !Model->IsStale()) { Data->ReconcileDataFromNames(*Model); Data->ApplyToEngine(*Model); } } }) [ SNew(STextBlock).Text_Lambda([WeakData = BreakpointData]() { if (TSharedPtr Data = WeakData.Pin()) { return FText::FromString(Data->TriggerName); } return LOCTEXT("SelectTag", "Select Tag"); }) ]; } return SNullWidget::NullWidget; } else if (ColumnId == "FilterType") { return SNew(SComboBox>) .OptionsSource(&View->FilterTypeOptions) .OnGenerateWidget_Lambda([](TSharedPtr Str) { return SNew(STextBlock).Text(FText::FromString(*Str)); }) .OnSelectionChanged_Lambda([WeakData = BreakpointData, WeakModel = DebuggerModel](TSharedPtr Str, ESelectInfo::Type) { if (TSharedPtr Data = WeakData.Pin()) { UE::Mass::Debug::FBreakpoint::EFilterType NewType; if (UE::Mass::Debug::FBreakpoint::StringToFilterType(*Str, NewType)) { Data->BreakpointInstance.FilterType = NewType; if (TSharedPtr Model = WeakModel.Pin()) { Model->ApplyBreakpointsToCurrentEnvironment(); } } } }) .InitiallySelectedItem(View->FilterMap[Data->BreakpointInstance.FilterType]) [ SNew(STextBlock) .Text_Lambda([WeakData = BreakpointData]() { if (TSharedPtr Data = WeakData.Pin()) { return FText::FromString(UE::Mass::Debug::FBreakpoint::FilterTypeToString(Data->BreakpointInstance.FilterType)); } return LOCTEXT("BreakpointInvalid", "Invalid Breakpoint"); }) ]; } else if (ColumnId == "Filter") { if (Data->BreakpointInstance.FilterType == UE::Mass::Debug::FBreakpoint::EFilterType::Query) { return SNew(SSearchableComboBox) .OptionsSource(&DebuggerModel.Pin()->QueryNames) .OnGenerateWidget_Lambda([](TSharedPtr Str) { if (Str) { return SNew(STextBlock) .Text(FText::FromString(*Str)); } return SNew(STextBlock) .Text(LOCTEXT("QueryNone", "None")); }) .OnSelectionChanged_Lambda([WeakData = BreakpointData, WeakModel = DebuggerModel](TSharedPtr Str, ESelectInfo::Type) { if (TSharedPtr Data = WeakData.Pin()) { if (Str) { Data->FilterName = *Str; } else { Data->FilterName.Empty(); } if (TSharedPtr Model = WeakModel.Pin()) { Data->ReconcileDataFromNames(*Model); Model->ApplyBreakpointsToCurrentEnvironment(); } } }) [ SNew(STextBlock) .Text_Lambda([WeakData = BreakpointData]() { if (TSharedPtr Data = WeakData.Pin()) { if (!Data->FilterName.IsEmpty()) { return FText::FromString(Data->FilterName); } } return LOCTEXT("SelectQuery","Select Query"); }) ]; } } else if (ColumnId == "HitCount") { return SAssignNew(HitCount, STextBlock) .Text_Lambda([WeakData = BreakpointData, WeakModel = DebuggerModel]() { if (TSharedPtr Data = WeakData.Pin()) { if (TSharedPtr Model = WeakModel.Pin(); !Model->IsStale()) { UE::Mass::Debug::FBreakpoint* EngineBreak = FMassDebugger::FindBreakpoint(*Model->Environment->GetEntityManager(), Data->BreakpointInstance.Handle); if (EngineBreak) { return FText::AsNumber(EngineBreak->HitCount); } } } return FText::AsNumber(0); }); } #endif //WITH_MASSENTITY_DEBUG return SNullWidget::NullWidget; } void SMassBreakpointsView::SBreakpointTableRow::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { SMultiColumnTableRow>::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); if (HitCount.IsValid()) { HitCount->Invalidate(EInvalidateWidgetReason::Paint); } } #undef LOCTEXT_NAMESPACE