// Copyright Epic Games, Inc. All Rights Reserved. #include "SPacketView.h" #include "Fonts/FontMeasure.h" #include "Fonts/SlateFontInfo.h" #include "Framework/Application/SlateApplication.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "HAL/PlatformTime.h" #include "Rendering/DrawElements.h" #include "TraceServices/Model/NetProfiler.h" #include "Styling/CoreStyle.h" #include "Widgets/Layout/SScrollBar.h" // Insights #include "Insights/Common/PaintUtils.h" #include "Insights/Common/Stopwatch.h" #include "Insights/Common/TimeUtils.h" #include "Insights/InsightsManager.h" #include "Insights/InsightsStyle.h" #include "Insights/NetworkingProfiler/NetworkingProfilerManager.h" #include "Insights/NetworkingProfiler/Widgets/SNetworkingProfilerWindow.h" #include "Insights/NetworkingProfiler/Widgets/SPacketContentView.h" #include //////////////////////////////////////////////////////////////////////////////////////////////////// #define LOCTEXT_NAMESPACE "SPacketView" //////////////////////////////////////////////////////////////////////////////////////////////////// SPacketView::SPacketView() : ProfilerWindow(), PacketSeries(MakeShared()) { Reset(); } //////////////////////////////////////////////////////////////////////////////////////////////////// SPacketView::~SPacketView() { } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::Reset() { // ProfilerWindow GameInstanceIndex = 0; ConnectionIndex = 0; ConnectionMode = Trace::ENetProfilerConnectionMode::Outgoing; Viewport.Reset(); FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); ViewportX.SetScaleLimits(0.0001f, 16.0f); // 10000 [sample/px] to 16 [px/sample] ViewportX.SetScale(5.0f); FAxisViewportDouble& ViewportY = Viewport.GetVerticalAxisViewport(); ViewportY.SetScaleLimits(0.0001, 50.0); ViewportY.SetScale(0.02); bIsViewportDirty = true; PacketSeries->Reset(); bIsStateDirty = true; bIsAutoZoomEnabled = true; AutoZoomViewportPos = ViewportX.GetPos(); AutoZoomViewportScale = ViewportX.GetScale(); AutoZoomViewportSize = 0.0f; AnalysisSyncNextTimestamp = 0; ConnectionChangeCount = 0; MousePosition = FVector2D::ZeroVector; MousePositionOnButtonDown = FVector2D::ZeroVector; ViewportPosXOnButtonDown = 0.0f; MousePositionOnButtonUp = FVector2D::ZeroVector; bIsLMB_Pressed = false; bIsRMB_Pressed = false; bIsScrolling = false; SelectionStartPacketIndex = 0; SelectionEndPacketIndex = 0; LastSelectedPacketIndex = 0; SelectedSample.Reset(); HoveredSample.Reset(); TooltipDesiredOpacity = 0.9f; TooltipOpacity = 0.0f; // ThisGeometry CursorType = ECursorType::Default; NumUpdatedPackets = 0; UpdateDurationHistory.Reset(); DrawDurationHistory.Reset(); OnPaintDurationHistory.Reset(); LastOnPaintTime = FPlatformTime::Cycles64(); if (ProfilerWindow.IsValid()) { SetConnection(ProfilerWindow->GetSelectedGameInstanceIndex(), ProfilerWindow->GetSelectedConnectionIndex(), ProfilerWindow->GetSelectedConnectionMode()); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::SetConnection(uint32 InGameInstanceIndex, uint32 InConnectionIndex, Trace::ENetProfilerConnectionMode InConnectionMode) { GameInstanceIndex = InGameInstanceIndex; ConnectionIndex = InConnectionIndex; ConnectionMode = InConnectionMode; HoveredSample.Reset(); SelectedSample.Reset(); SelectionStartPacketIndex = 0; SelectionEndPacketIndex = 0; LastSelectedPacketIndex = 0; OnSelectionChanged(); bIsStateDirty = true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::Construct(const FArguments& InArgs, TSharedPtr InProfilerWindow) { ProfilerWindow = InProfilerWindow; ChildSlot [SNew(SOverlay) .Visibility(EVisibility::SelfHitTestInvisible) + SOverlay::Slot() .VAlign(VAlign_Top) .Padding(FMargin(0, 0, 0, 0)) [SAssignNew(HorizontalScrollBar, SScrollBar) .Orientation(Orient_Horizontal) .AlwaysShowScrollbar(false) .Visibility(EVisibility::Visible) .Thickness(FVector2D(5.0f, 5.0f)) .RenderOpacity(0.75) .OnUserScrolled(this, &SPacketView::HorizontalScrollBar_OnUserScrolled)]]; UpdateHorizontalScrollBar(); BindCommands(); Reset(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { if (ThisGeometry != AllottedGeometry || bIsViewportDirty) { bIsViewportDirty = false; const float ViewWidth = AllottedGeometry.GetLocalSize().X; const float ViewHeight = AllottedGeometry.GetLocalSize().Y; Viewport.SetSize(ViewWidth, ViewHeight); bIsStateDirty = true; } ThisGeometry = AllottedGeometry; FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); if (!bIsScrolling) { // Elastic snap to horizontal limits. if (ViewportX.UpdatePosWithinLimits()) { bIsStateDirty = true; } } // Disable auto-zoom if viewport's position or scale has changed. if (AutoZoomViewportPos != ViewportX.GetPos() || AutoZoomViewportScale != ViewportX.GetScale()) { bIsAutoZoomEnabled = false; } // Update auto-zoom if viewport size has changed. bool bAutoZoom = bIsAutoZoomEnabled && AutoZoomViewportSize != ViewportX.GetSize(); const uint64 Time = FPlatformTime::Cycles64(); if (Time > AnalysisSyncNextTimestamp) { const uint64 WaitTime = static_cast(0.1 / FPlatformTime::GetSecondsPerCycle64()); // 100ms AnalysisSyncNextTimestamp = Time + WaitTime; TSharedPtr Session = FInsightsManager::Get()->GetSession(); if (Session.IsValid()) { Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const Trace::INetProfilerProvider& NetProfilerProvider = Trace::ReadNetProfilerProvider(*Session.Get()); const uint32 NewConnectionChangeCount = NetProfilerProvider.GetConnectionChangeCount(); if (NewConnectionChangeCount != ConnectionChangeCount) { ConnectionChangeCount = NewConnectionChangeCount; bIsStateDirty = true; if (bIsAutoZoomEnabled) { bAutoZoom = true; } } } } if (bAutoZoom) { AutoZoom(); } if (bIsStateDirty) { bIsStateDirty = false; UpdateState(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SPacketView::IsConnectionValid(const Trace::INetProfilerProvider& NetProfilerProvider, const uint32 InGameInstanceIndex, const uint32 InConnectionIndex, const Trace::ENetProfilerConnectionMode InConnectionMode) { // TODO: return NetProfilerProvider.IsConnectionValid(GameInstanceIndex, ConnectionIndex, ConnectionMode); const uint32 GameInstanceCount = NetProfilerProvider.GetGameInstanceCount(); if (InGameInstanceIndex >= GameInstanceCount) { return false; } bool bIsValidConnection = false; NetProfilerProvider.ReadConnections(InGameInstanceIndex, [InConnectionIndex, InConnectionMode, &bIsValidConnection](const Trace::FNetProfilerConnection& Connection) { if (InConnectionIndex == Connection.ConnectionIndex) { if ((InConnectionMode == Trace::ENetProfilerConnectionMode::Outgoing && Connection.bHasOutgoingData) || (InConnectionMode == Trace::ENetProfilerConnectionMode::Incoming && Connection.bHasIncomingData)) { bIsValidConnection = true; } } }); return bIsValidConnection; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::UpdateState() { FStopwatch Stopwatch; Stopwatch.Start(); ////////////////////////////////////////////////// struct FPacketFilter { bool bByNetId = false; uint32 NetId = 0; bool bByEventType = false; uint32 EventTypeIndex = 0; }; FPacketFilter Filter; if (ProfilerWindow) { TSharedPtr PacketContentView = ProfilerWindow->GetPacketContentView(); if (PacketContentView) { Filter.bByNetId = PacketContentView->IsFilterByNetIdEnabled(); Filter.NetId = PacketContentView->GetFilterNetId(); Filter.bByEventType = PacketContentView->IsFilterByEventTypeEnabled(); Filter.EventTypeIndex = PacketContentView->GetFilterEventTypeIndex(); } } ////////////////////////////////////////////////// FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); const int32 PreviousMaxValue = ViewportX.GetMaxValue(); ViewportX.SetMinMaxInterval(0, 0); // Reset stats. PacketSeries->NumAggregatedPackets = 0; NumUpdatedPackets = 0; FNetworkPacketSeriesBuilder Builder(*PacketSeries, Viewport); TSharedPtr Session = FInsightsManager::Get()->GetSession(); if (Session.IsValid()) { Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const Trace::INetProfilerProvider& NetProfilerProvider = Trace::ReadNetProfilerProvider(*Session.Get()); if (IsConnectionValid(NetProfilerProvider, GameInstanceIndex, ConnectionIndex, ConnectionMode)) { const uint32 NumPackets = NetProfilerProvider.GetPacketCount(ConnectionIndex, ConnectionMode); ViewportX.SetMinMaxInterval(0, NumPackets); if (NumPackets > 0) { const int32 MinIndex = ViewportX.GetValueAtOffset(0.0f); const int32 MaxIndex = ViewportX.GetValueAtOffset(ViewportX.GetSize()); const uint32 PacketStartIndex = static_cast(FMath::Max(0, MinIndex)); const uint32 PacketEndIndex = FMath::Min(NumPackets - 1, static_cast(FMath::Max(0, MaxIndex))); if (PacketStartIndex <= PacketEndIndex) { int32 PacketIndex = PacketStartIndex; NetProfilerProvider.EnumeratePackets(ConnectionIndex, ConnectionMode, PacketStartIndex, PacketEndIndex, [this, &Builder, &PacketIndex, &Filter, &NetProfilerProvider](const Trace::FNetProfilerPacket& Packet) { FNetworkPacketAggregatedSample* SamplePtr = Builder.AddPacket(PacketIndex++, Packet); if (SamplePtr) { if (SamplePtr->NumPackets == 1) { SamplePtr->bAtLeastOnePacketMatchesFilter = !Filter.bByNetId && !Filter.bByEventType; } if (!SamplePtr->bAtLeastOnePacketMatchesFilter && (Filter.bByNetId || Filter.bByEventType)) { bool bFilterMatch = false; const uint32 StartPos = 0; const uint32 EndPos = Packet.ContentSizeInBits; NetProfilerProvider.EnumeratePacketContentEventsByPosition(ConnectionIndex, ConnectionMode, PacketIndex - 1, StartPos, EndPos, [this, &bFilterMatch, &Filter, &NetProfilerProvider](const Trace::FNetProfilerContentEvent& Event) { if (!bFilterMatch) { bool bEventMatchesFilter = true; if (Filter.bByEventType && Filter.EventTypeIndex != Event.EventTypeIndex) { bEventMatchesFilter = false; } if (bEventMatchesFilter && Filter.bByNetId) { uint32 NetId = uint32(-1); if (Event.ObjectInstanceIndex != 0) { NetProfilerProvider.ReadObject(GameInstanceIndex, Event.ObjectInstanceIndex, [&NetId](const Trace::FNetProfilerObjectInstance& ObjectInstance) { NetId = ObjectInstance.NetId; }); } if (Filter.NetId != NetId) { bEventMatchesFilter = false; } } if (bEventMatchesFilter) { bFilterMatch = true; } } }); if (bFilterMatch) { SamplePtr->bAtLeastOnePacketMatchesFilter = true; } } } }); } } } } // Init series with mock data. if (false) { constexpr int32 NumPackets = 100000; ViewportX.SetMinMaxInterval(0, NumPackets); const int32 StartIndex = FMath::Max(0, ViewportX.GetValueAtOffset(0.0f)); const int32 EndIndex = FMath::Min(NumPackets, ViewportX.GetValueAtOffset(ViewportX.GetSize())); for (int32 PacketIndex = StartIndex; PacketIndex < EndIndex; ++PacketIndex) { FRandomStream RandomStream((PacketIndex * PacketIndex * PacketIndex) ^ 0x2c2c57ed); int64 Size = RandomStream.RandRange(0, 2000); Trace::ENetProfilerDeliveryStatus Status = Trace::ENetProfilerDeliveryStatus::Unknown; const float Fraction = RandomStream.GetFraction(); if (Fraction < 0.01) // 1% { Status = Trace::ENetProfilerDeliveryStatus::Dropped; } else if (Fraction < 0.05) // 4% { Status = Trace::ENetProfilerDeliveryStatus::Delivered; } const double Timestamp = ((double)PacketIndex * 100.0) / (double)NumPackets + RandomStream.GetFraction() * 0.1; Trace::FNetProfilerPacket Packet; Packet.TimeStamp = static_cast(Timestamp); Packet.SequenceNumber = PacketIndex; Packet.ContentSizeInBits = Size; Packet.TotalPacketSizeInBytes = (Size + 7) / 8; Packet.DeliveryStatus = Status; Builder.AddPacket(PacketIndex, Packet); } } NumUpdatedPackets += Builder.GetNumAddedPackets(); if (bIsAutoZoomEnabled && ViewportX.GetMaxValue() != PreviousMaxValue) { // Forces auto-zoom to be updated in the next Tick(), like after a viewport resize. AutoZoomViewportSize = 0.0f; } Stopwatch.Stop(); UpdateDurationHistory.AddValue(Stopwatch.AccumulatedTime); } //////////////////////////////////////////////////////////////////////////////////////////////////// FNetworkPacketSampleRef SPacketView::GetSample(const int32 InPacketIndex) { FNetworkPacketSampleRef SampleRef; SampleRef.Series = PacketSeries; TSharedPtr Session = FInsightsManager::Get()->GetSession(); if (Session.IsValid()) { Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const Trace::INetProfilerProvider& NetProfilerProvider = Trace::ReadNetProfilerProvider(*Session.Get()); if (IsConnectionValid(NetProfilerProvider, GameInstanceIndex, ConnectionIndex, ConnectionMode)) { const uint32 NumPackets = NetProfilerProvider.GetPacketCount(ConnectionIndex, ConnectionMode); if (InPacketIndex >= 0 && InPacketIndex < static_cast(NumPackets)) { NetProfilerProvider.EnumeratePackets(ConnectionIndex, ConnectionMode, InPacketIndex, InPacketIndex, [InPacketIndex, &SampleRef](const Trace::FNetProfilerPacket& Packet) { SampleRef.Sample = MakeShared(); SampleRef.Sample->AddPacket(InPacketIndex, Packet); }); } } } return SampleRef; } //////////////////////////////////////////////////////////////////////////////////////////////////// FNetworkPacketSampleRef SPacketView::GetSampleAtMousePosition(float X, float Y) { if (!bIsStateDirty) { float SampleW = Viewport.GetSampleWidth(); int32 SampleIndex = FMath::FloorToInt(X / SampleW); if (SampleIndex >= 0) { if (PacketSeries->NumAggregatedPackets > 0 && SampleIndex < PacketSeries->Samples.Num()) { const FNetworkPacketAggregatedSample& Sample = PacketSeries->Samples[SampleIndex]; if (Sample.NumPackets > 0) { const FAxisViewportDouble& ViewportY = Viewport.GetVerticalAxisViewport(); const float ViewHeight = FMath::RoundToFloat(Viewport.GetHeight()); const float BaselineY = FMath::RoundToFloat(ViewportY.GetOffsetForValue(0.0)); // const float ValueY = FMath::RoundToFloat(ViewportY.GetOffsetForValue(static_cast(Sample.LargestPacket.ContentSizeInBits))); const float ValueY = FMath::RoundToFloat(ViewportY.GetOffsetForValue(static_cast(Sample.LargestPacket.TotalSizeInBytes * 8))); constexpr float ToleranceY = 3.0f; // [pixels] const float BottomY = FMath::Min(ViewHeight, ViewHeight - BaselineY + ToleranceY); const float TopY = FMath::Max(0.0f, ViewHeight - ValueY - ToleranceY); if (Y >= TopY && Y < BottomY) { return FNetworkPacketSampleRef(PacketSeries, MakeShared(Sample)); } } } } } return FNetworkPacketSampleRef(nullptr, nullptr); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::SelectSampleAtMousePosition(float X, float Y, const FPointerEvent& MouseEvent) { FNetworkPacketSampleRef SampleRef = GetSampleAtMousePosition(X, Y); if (!SampleRef.IsValid()) { SampleRef = GetSampleAtMousePosition(X - 1.0f, Y); } if (!SampleRef.IsValid()) { SampleRef = GetSampleAtMousePosition(X + 1.0f, Y); } bool bRaiseSelectionChanged = false; if (SampleRef.IsValid()) { const int32 SelectedPacketIndex = SampleRef.Sample->LargestPacket.Index; if (MouseEvent.GetModifierKeys().IsShiftDown()) { if (SelectedPacketIndex >= LastSelectedPacketIndex) { // Extend selection toward right. if (SelectionStartPacketIndex != LastSelectedPacketIndex || SelectionEndPacketIndex != SelectedPacketIndex + 1) { SelectionStartPacketIndex = LastSelectedPacketIndex; SelectionEndPacketIndex = SelectedPacketIndex + 1; bRaiseSelectionChanged = true; } LastSelectedPacketIndex = SelectionStartPacketIndex; } else { // Extend selection toward left. if (SelectionEndPacketIndex != SelectedPacketIndex + 1) { SelectionStartPacketIndex = SelectedPacketIndex; SelectionEndPacketIndex = LastSelectedPacketIndex + 1; bRaiseSelectionChanged = true; } LastSelectedPacketIndex = SelectionEndPacketIndex - 1; } } else { if (SelectionStartPacketIndex != SelectedPacketIndex || SelectionEndPacketIndex != SelectedPacketIndex + 1) { SelectionStartPacketIndex = SelectedPacketIndex; SelectionEndPacketIndex = SelectedPacketIndex + 1; bRaiseSelectionChanged = true; } LastSelectedPacketIndex = SelectedPacketIndex; } } else { if (SelectionStartPacketIndex != 0 || SelectionEndPacketIndex != 0) { SelectionStartPacketIndex = 0; SelectionEndPacketIndex = 0; LastSelectedPacketIndex = 0; bRaiseSelectionChanged = true; } } if (SelectionEndPacketIndex == SelectionStartPacketIndex + 1) { if (!SelectedSample.Equals(SampleRef)) { SelectedSample = SampleRef; } } else { SelectedSample.Reset(); } if (bRaiseSelectionChanged) { OnSelectionChanged(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::OnSelectionChanged() { if (ProfilerWindow.IsValid()) { if (SelectedSample.IsValid()) { const uint32 BitSize = SelectedSample.Sample->LargestPacket.TotalSizeInBytes * 8; ProfilerWindow->SetSelectedPacket(SelectedSample.Sample->LargestPacket.Index, SelectedSample.Sample->LargestPacket.Index + 1, BitSize); ProfilerWindow->SetSelectedBitRange(0, BitSize); } else { ProfilerWindow->SetSelectedPacket(SelectionStartPacketIndex, SelectionEndPacketIndex); ProfilerWindow->SetSelectedBitRange(0, 0); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// const TCHAR* StatusToString(Trace::ENetProfilerDeliveryStatus Status) { switch (Status) { case Trace::ENetProfilerDeliveryStatus::Delivered: return TEXT("Delivered"); case Trace::ENetProfilerDeliveryStatus::Dropped: return TEXT("Dropped"); case Trace::ENetProfilerDeliveryStatus::Unknown: default: return TEXT("Unknown"); }; } const TCHAR* AggregatedStatusToString(Trace::ENetProfilerDeliveryStatus Status) { switch (Status) { case Trace::ENetProfilerDeliveryStatus::Delivered: return TEXT("all packets are Delivered"); case Trace::ENetProfilerDeliveryStatus::Dropped: return TEXT("at least one Dropped packet"); case Trace::ENetProfilerDeliveryStatus::Unknown: default: return TEXT("Unknown"); } } //////////////////////////////////////////////////////////////////////////////////////////////////// int32 SPacketView::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const { const bool bEnabled = ShouldBeEnabled(bParentEnabled); const ESlateDrawEffect DrawEffects = bEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect; FDrawContext DrawContext(AllottedGeometry, MyCullingRect, InWidgetStyle, DrawEffects, OutDrawElements, LayerId); const TSharedRef FontMeasureService = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); FSlateFontInfo SummaryFont = FCoreStyle::GetDefaultFontStyle("Regular", 8); const FSlateBrush* WhiteBrush = FInsightsStyle::Get().GetBrush("WhiteBrush"); const float ViewWidth = AllottedGeometry.Size.X; const float ViewHeight = AllottedGeometry.Size.Y; int32 NumDrawSamples = 0; ////////////////////////////////////////////////// { FStopwatch Stopwatch; Stopwatch.Start(); FPacketViewDrawHelper Helper(DrawContext, Viewport); Helper.DrawBackground(); // Draw the horizontal axis grid. DrawHorizontalAxisGrid(DrawContext, WhiteBrush, SummaryFont); Helper.DrawCached(*PacketSeries); NumDrawSamples = Helper.GetNumDrawSamples(); // Highlight the selected and/or hovered sample. bool bIsSelectedAndHovered = SelectedSample.Equals(HoveredSample); if (SelectedSample.IsValid()) { Helper.DrawSampleHighlight(*SelectedSample.Sample, bIsSelectedAndHovered ? FPacketViewDrawHelper::EHighlightMode::SelectedAndHovered : FPacketViewDrawHelper::EHighlightMode::Selected); } if (HoveredSample.IsValid() && !bIsSelectedAndHovered) { Helper.DrawSampleHighlight(*HoveredSample.Sample, FPacketViewDrawHelper::EHighlightMode::Hovered); } if (SelectionEndPacketIndex > SelectionStartPacketIndex + 1) { Helper.DrawSelection(SelectionStartPacketIndex, SelectionEndPacketIndex); } // Draw the vertical axis grid. DrawVerticalAxisGrid(DrawContext, WhiteBrush, SummaryFont); // Draw tooltip for hovered sample. if (HoveredSample.IsValid()) { if (TooltipOpacity < TooltipDesiredOpacity) { TooltipOpacity = TooltipOpacity * 0.9f + TooltipDesiredOpacity * 0.1f; } else { TooltipOpacity = TooltipDesiredOpacity; } const double Precision = 0.01; // 10ms int NumLines; FString Text; uint32 UnusedBits = HoveredSample.Sample->LargestPacket.TotalSizeInBytes * 8 - HoveredSample.Sample->LargestPacket.ContentSizeInBits; if (HoveredSample.Sample->NumPackets == 1) { NumLines = 6; Text = FString::Format(TEXT("Packet Index: {0}\n" "Sequence Number: {1}\n" "Content Size: {2} bits\n" "Total Size: {3} bytes ({4} unused bits)\n" "Timestamp: {5}\n" "Status: {6}"), {FText::AsNumber(HoveredSample.Sample->LargestPacket.Index).ToString(), FText::AsNumber(HoveredSample.Sample->LargestPacket.SequenceNumber).ToString(), FText::AsNumber(HoveredSample.Sample->LargestPacket.ContentSizeInBits).ToString(), FText::AsNumber(HoveredSample.Sample->LargestPacket.TotalSizeInBytes).ToString(), FText::AsNumber(UnusedBits).ToString(), TimeUtils::FormatTimeHMS(HoveredSample.Sample->LargestPacket.TimeStamp, Precision), ::StatusToString(HoveredSample.Sample->LargestPacket.Status)}); } else { NumLines = 9; Text = FString::Format(TEXT("{0} network packets\n" "({1})\n" "Largest Packet\n" " Index: {2}\n" " Sequance Number: {3}\n" " Content Size: {4} bits\n" " Total Size: {5} bytes ({6} unused bits)\n" " Timestamp: {7}\n" " Status: {8}"), {HoveredSample.Sample->NumPackets, ::AggregatedStatusToString(HoveredSample.Sample->AggregatedStatus), FText::AsNumber(HoveredSample.Sample->LargestPacket.Index).ToString(), FText::AsNumber(HoveredSample.Sample->LargestPacket.SequenceNumber).ToString(), FText::AsNumber(HoveredSample.Sample->LargestPacket.ContentSizeInBits).ToString(), FText::AsNumber(HoveredSample.Sample->LargestPacket.TotalSizeInBytes).ToString(), FText::AsNumber(UnusedBits).ToString(), TimeUtils::FormatTimeHMS(HoveredSample.Sample->LargestPacket.TimeStamp, Precision), ::StatusToString(HoveredSample.Sample->LargestPacket.Status)}); } FVector2D TextSize = FontMeasureService->Measure(Text, SummaryFont); const float DX = 2.0f; const float W2 = TextSize.X / 2 + DX; const FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); float X1 = ViewportX.GetOffsetForValue(HoveredSample.Sample->LargestPacket.Index); float CX = X1 + FMath::RoundToFloat(Viewport.GetSampleWidth() / 2); if (CX + W2 > ViewportX.GetSize()) { CX = FMath::RoundToFloat(ViewportX.GetSize() - W2); } if (CX - W2 < 0) { CX = W2; } const float Y = 10.0f; const float H = 2.0f + 13.0f * NumLines; DrawContext.DrawBox(CX - W2, Y, 2 * W2, H, WhiteBrush, FLinearColor(0.7, 0.7, 0.7, TooltipOpacity)); DrawContext.LayerId++; DrawContext.DrawText(CX - W2 + DX, Y + 1.0f, Text, SummaryFont, FLinearColor(0.0, 0.0, 0.0, TooltipOpacity)); DrawContext.LayerId++; } Stopwatch.Stop(); DrawDurationHistory.AddValue(Stopwatch.AccumulatedTime); } ////////////////////////////////////////////////// const bool bShouldDisplayDebugInfo = FInsightsManager::Get()->IsDebugInfoEnabled(); if (bShouldDisplayDebugInfo) { const float MaxFontCharHeight = FontMeasureService->Measure(TEXT("!"), SummaryFont).Y; const float DbgDY = MaxFontCharHeight; const float DbgW = 280.0f; const float DbgH = DbgDY * 5 + 3.0f; const float DbgX = ViewWidth - DbgW - 20.0f; float DbgY = 7.0f; DrawContext.LayerId++; DrawContext.DrawBox(DbgX - 2.0f, DbgY - 2.0f, DbgW, DbgH, WhiteBrush, FLinearColor(1.0, 1.0, 1.0, 0.9)); DrawContext.LayerId++; FLinearColor DbgTextColor(0.0, 0.0, 0.0, 0.9); // Time interval since last OnPaint call. const uint64 CurrentTime = FPlatformTime::Cycles64(); const uint64 OnPaintDuration = CurrentTime - LastOnPaintTime; LastOnPaintTime = CurrentTime; OnPaintDurationHistory.AddValue(OnPaintDuration); // saved for last 32 OnPaint calls const uint64 AvgOnPaintDuration = OnPaintDurationHistory.ComputeAverage(); const uint64 AvgOnPaintDurationMs = FStopwatch::Cycles64ToMilliseconds(AvgOnPaintDuration); const double AvgOnPaintFps = AvgOnPaintDurationMs != 0 ? 1.0 / FStopwatch::Cycles64ToSeconds(AvgOnPaintDuration) : 0.0; const uint64 AvgUpdateDurationMs = FStopwatch::Cycles64ToMilliseconds(UpdateDurationHistory.ComputeAverage()); const uint64 AvgDrawDurationMs = FStopwatch::Cycles64ToMilliseconds(DrawDurationHistory.ComputeAverage()); // Draw performance info. DrawContext.DrawText( DbgX, DbgY, FString::Printf(TEXT("U: %llu ms, D: %llu ms + %llu ms = %llu ms (%d fps)"), AvgUpdateDurationMs, // caching time AvgDrawDurationMs, // drawing time AvgOnPaintDurationMs - AvgDrawDurationMs, // other overhead to OnPaint calls AvgOnPaintDurationMs, // average time between two OnPaint calls FMath::RoundToInt(AvgOnPaintFps)), // framerate of OnPaint calls SummaryFont, DbgTextColor); DbgY += DbgDY; // Draw number of draw calls. DrawContext.DrawText( DbgX, DbgY, FString::Printf(TEXT("U: %s packets, D: %s samples"), *FText::AsNumber(NumUpdatedPackets).ToString(), *FText::AsNumber(NumDrawSamples).ToString()), SummaryFont, DbgTextColor); DbgY += DbgDY; // Draw viewport's horizontal info. DrawContext.DrawText( DbgX, DbgY, Viewport.GetHorizontalAxisViewport().ToDebugString(TEXT("X"), TEXT("packet")), SummaryFont, DbgTextColor); DbgY += DbgDY; // Draw viewport's vertical info. DrawContext.DrawText( DbgX, DbgY, Viewport.GetVerticalAxisViewport().ToDebugString(TEXT("Y")), SummaryFont, DbgTextColor); DbgY += DbgDY; // Draw connection info. DrawContext.DrawText( DbgX, DbgY, FString::Printf(TEXT("Game Instance %d, Connection %d (%s)"), GameInstanceIndex, ConnectionIndex, (ConnectionMode == Trace::ENetProfilerConnectionMode::Outgoing) ? TEXT("Outgoing") : TEXT("Incoming")), SummaryFont, DbgTextColor); DbgY += DbgDY; } return SCompoundWidget::OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled && IsEnabled()); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::DrawVerticalAxisGrid(FDrawContext& DrawContext, const FSlateBrush* Brush, const FSlateFontInfo& Font) const { const FAxisViewportDouble& ViewportY = Viewport.GetVerticalAxisViewport(); const float RoundedViewHeight = FMath::RoundToFloat(ViewportY.GetSize()); constexpr float MinDY = 32.0f; // min vertical distance between horizontal grid lines const double TopValue = ViewportY.GetValueAtOffset(RoundedViewHeight); const double GridValue = ViewportY.GetValueAtOffset(MinDY); const double BottomValue = ViewportY.GetValueAtOffset(0.0f); const double Delta = GridValue - BottomValue; if (Delta > 0.0) { int64 DeltaBits = static_cast(Delta); if (DeltaBits <= 0) { DeltaBits = 1; } // Compute rounding based on magnitude of visible range of values (Delta). int64 Power10 = 1; int64 Delta10 = DeltaBits; while (Delta10 > 0) { Delta10 /= 10; Power10 *= 10; } if (Power10 >= 100) { Power10 /= 100; } else { Power10 = 1; } const double Grid = static_cast(((DeltaBits + Power10 - 1) / Power10) * Power10); // next value divisible with a multiple of 10 const double StartValue = FMath::GridSnap(BottomValue, Grid); const float ViewWidth = Viewport.GetWidth(); const FLinearColor GridColor(0.0f, 0.0f, 0.0f, 0.1f); const FLinearColor TextBgColor(0.05f, 0.05f, 0.05f, 1.0f); const FLinearColor TextColor(1.0f, 1.0f, 1.0f, 1.0f); const TSharedRef FontMeasureService = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); for (double Value = StartValue; Value < TopValue; Value += Grid) { const float Y = RoundedViewHeight - FMath::RoundToFloat(ViewportY.GetOffsetForValue(Value)); // Draw horizontal grid line. DrawContext.DrawBox(0, Y, ViewWidth, 1, Brush, GridColor); const int64 ValueBits = static_cast(Value); const FString Text = (ValueBits == 0) ? TEXT("0") : FString::Format(TEXT("{0} bits"), {FText::AsNumber(ValueBits).ToString()}); const FVector2D TextSize = FontMeasureService->Measure(Text, Font); constexpr float TextH = 14.0f; // Draw background for value text. DrawContext.DrawBox(ViewWidth - TextSize.X - 4.0f, Y - TextH, TextSize.X + 4.0f, TextH, Brush, TextBgColor); // Draw value text. DrawContext.DrawText(ViewWidth - TextSize.X - 2.0f, Y - TextH + 1.0f, Text, Font, TextColor); } DrawContext.LayerId++; } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::DrawHorizontalAxisGrid(FDrawContext& DrawContext, const FSlateBrush* Brush, const FSlateFontInfo& Font) const { const FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); const float RoundedViewWidth = FMath::RoundToFloat(ViewportX.GetSize()); constexpr float MinDX = 100.0f; // min horizontal distance between vertical grid lines const int32 LeftIndex = ViewportX.GetValueAtOffset(0.0f); const int32 GridIndex = ViewportX.GetValueAtOffset(MinDX); const int32 RightIndex = ViewportX.GetValueAtOffset(RoundedViewWidth); const int32 Delta = GridIndex - LeftIndex; if (Delta > 0) { // Compute rounding based on magnitude of visible range of samples (Delta). int32 Power10 = 1; int32 Delta10 = Delta; while (Delta10 > 0) { Delta10 /= 10; Power10 *= 10; } if (Power10 >= 100) { Power10 /= 100; } else { Power10 = 1; } const int32 Grid = ((Delta + Power10 - 1) / Power10) * Power10; // next value divisible with a multiple of 10 // Skip grid lines for negative indices. double StartIndex = ((LeftIndex + Grid - 1) / Grid) * Grid; while (StartIndex < 0) { StartIndex += Grid; } const float ViewHeight = Viewport.GetHeight(); const FLinearColor GridColor(0.0f, 0.0f, 0.0f, 0.1f); // const FLinearColor TextBgColor(0.05f, 0.05f, 0.05f, 1.0f); // const FLinearColor TextColor(1.0f, 1.0f, 1.0f, 1.0f); const FLinearColor TopTextColor(1.0f, 1.0f, 1.0f, 0.7f); // const TSharedRef FontMeasureService = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); for (int32 Index = StartIndex; Index < RightIndex; Index += Grid) { const float X = FMath::RoundToFloat(ViewportX.GetOffsetForValue(Index)); // Draw vertical grid line. DrawContext.DrawBox(X, 0, 1, ViewHeight, Brush, GridColor); const FString Text = FText::AsNumber(Index).ToString(); // const FVector2D TextSize = FontMeasureService->Measure(Text, Font); // constexpr float TextH = 14.0f; // Draw background for index text. // DrawContext.DrawBox(X, ViewHeight - TextH, TextSize.X + 4.0f, TextH, Brush, TextBgColor); // Draw index text. // DrawContext.DrawText(X + 2.0f, ViewHeight - TextH + 1.0f, Text, Font, TextColor); DrawContext.DrawText(X + 2.0f, 10.0f, Text, Font, TopTextColor); } DrawContext.LayerId++; } } //////////////////////////////////////////////////////////////////////////////////////////////////// FReply SPacketView::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { FReply Reply = FReply::Unhandled(); MousePositionOnButtonDown = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()); ViewportPosXOnButtonDown = Viewport.GetHorizontalAxisViewport().GetPos(); if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) { bIsLMB_Pressed = true; // Capture mouse. Reply = FReply::Handled().CaptureMouse(SharedThis(this)); } else if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton) { bIsRMB_Pressed = true; // Capture mouse, so we can scroll outside this widget. Reply = FReply::Handled().CaptureMouse(SharedThis(this)); } return Reply; } //////////////////////////////////////////////////////////////////////////////////////////////////// FReply SPacketView::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { FReply Reply = FReply::Unhandled(); MousePositionOnButtonUp = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()); const bool bIsValidForMouseClick = MousePositionOnButtonUp.Equals(MousePositionOnButtonDown, MOUSE_SNAP_DISTANCE); if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) { if (bIsLMB_Pressed) { if (bIsScrolling) { bIsScrolling = false; CursorType = ECursorType::Default; } else if (bIsValidForMouseClick) { SelectSampleAtMousePosition(MousePositionOnButtonUp.X, MousePositionOnButtonUp.Y, MouseEvent); } bIsLMB_Pressed = false; // Release the mouse. Reply = FReply::Handled().ReleaseMouseCapture(); } } else if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton) { if (bIsRMB_Pressed) { if (bIsScrolling) { bIsScrolling = false; CursorType = ECursorType::Default; } else if (bIsValidForMouseClick) { ShowContextMenu(MouseEvent); } bIsRMB_Pressed = false; // Release mouse as we no longer scroll. Reply = FReply::Handled().ReleaseMouseCapture(); } } return Reply; } //////////////////////////////////////////////////////////////////////////////////////////////////// FReply SPacketView::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { FReply Reply = FReply::Unhandled(); MousePosition = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()); if (!MouseEvent.GetCursorDelta().IsZero()) { if (MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton) || MouseEvent.IsMouseButtonDown(EKeys::RightMouseButton)) { if (HasMouseCapture()) { if (!bIsScrolling) { bIsScrolling = true; CursorType = ECursorType::Hand; HoveredSample.Reset(); } FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); const float PosX = ViewportPosXOnButtonDown + (MousePositionOnButtonDown.X - MousePosition.X); ViewportX.ScrollAtValue(ViewportX.GetValueAtPos(PosX)); // align viewport position with sample UpdateHorizontalScrollBar(); bIsStateDirty = true; } } else { if (!HoveredSample.IsValid()) { TooltipOpacity = 0.0f; } HoveredSample = GetSampleAtMousePosition(MousePosition.X, MousePosition.Y); if (!HoveredSample.IsValid()) { HoveredSample = GetSampleAtMousePosition(MousePosition.X - 1.0f, MousePosition.Y); } if (!HoveredSample.IsValid()) { HoveredSample = GetSampleAtMousePosition(MousePosition.X + 1.0f, MousePosition.Y); } } Reply = FReply::Handled(); } return Reply; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::OnMouseLeave(const FPointerEvent& MouseEvent) { if (!HasMouseCapture()) { bIsLMB_Pressed = false; bIsRMB_Pressed = false; HoveredSample.Reset(); CursorType = ECursorType::Default; } } //////////////////////////////////////////////////////////////////////////////////////////////////// FReply SPacketView::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { MousePosition = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()); if (MouseEvent.GetModifierKeys().IsShiftDown()) { FAxisViewportDouble& ViewportY = Viewport.GetVerticalAxisViewport(); // Zoom in/out vertically. const float Delta = MouseEvent.GetWheelDelta(); constexpr float ZoomStep = 0.25f; // as percent float ScaleY; if (Delta > 0) { ScaleY = ViewportY.GetScale() * FMath::Pow(1.0f + ZoomStep, Delta); } else { ScaleY = ViewportY.GetScale() * FMath::Pow(1.0f / (1.0f + ZoomStep), -Delta); } ViewportY.SetScale(ScaleY); // UpdateVerticalScrollBar(); } else // if (MouseEvent.GetModifierKeys().IsControlDown()) { // Zoom in/out horizontally. const float Delta = MouseEvent.GetWheelDelta(); ZoomHorizontally(Delta, MousePosition.X); } return FReply::Handled(); } //////////////////////////////////////////////////////////////////////////////////////////////////// FReply SPacketView::OnMouseButtonDoubleClick(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { return FReply::Unhandled(); } //////////////////////////////////////////////////////////////////////////////////////////////////// FCursorReply SPacketView::OnCursorQuery(const FGeometry& MyGeometry, const FPointerEvent& CursorEvent) const { FCursorReply CursorReply = FCursorReply::Unhandled(); if (CursorType == ECursorType::Arrow) { CursorReply = FCursorReply::Cursor(EMouseCursor::ResizeLeftRight); } else if (CursorType == ECursorType::Hand) { CursorReply = FCursorReply::Cursor(EMouseCursor::GrabHand); } return CursorReply; } //////////////////////////////////////////////////////////////////////////////////////////////////// FReply SPacketView::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) { if (InKeyEvent.GetKey() == EKeys::A) { if (InKeyEvent.GetModifierKeys().IsControlDown()) { const FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); const int32 LastPacketIndex = ViewportX.GetMaxValue(); // Select all packets. SelectionStartPacketIndex = 0; SelectionEndPacketIndex = LastPacketIndex; LastSelectedPacketIndex = 0; SelectedSample.Reset(); if (SelectionEndPacketIndex == SelectionStartPacketIndex + 1) { SelectedSample = GetSample(SelectionStartPacketIndex); } OnSelectionChanged(); return FReply::Handled(); } } else if (InKeyEvent.GetKey() == EKeys::Left) { if (InKeyEvent.GetModifierKeys().IsShiftDown()) { if (InKeyEvent.GetModifierKeys().IsControlDown()) { ShrinkRightSideOfSelectedInterval(); } else { ExtendLeftSideOfSelectedInterval(); } } else { SelectPreviousPacket(); } return FReply::Handled(); } else if (InKeyEvent.GetKey() == EKeys::Right) { if (InKeyEvent.GetModifierKeys().IsShiftDown()) { if (InKeyEvent.GetModifierKeys().IsControlDown()) { ShrinkLeftSideOfSelectedInterval(); } else { ExtendRightSideOfSelectedInterval(); } } else { SelectNextPacket(); } return FReply::Handled(); } return SCompoundWidget::OnKeyDown(MyGeometry, InKeyEvent); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::EnsurePacketIsVisible(const int InPacketIndex) { FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); const float LeftX = ViewportX.GetPosForValue(InPacketIndex); const float RightX = LeftX + ViewportX.GetSampleSize(); if (RightX > ViewportX.GetPos() + ViewportX.GetSize()) { const int32 VisibleSampleCount = FMath::FloorToInt(ViewportX.GetSize() / ViewportX.GetSampleSize()); ViewportX.ScrollAtValue(InPacketIndex - VisibleSampleCount + 1); UpdateHorizontalScrollBar(); bIsStateDirty = true; } if (LeftX < ViewportX.GetPos()) { ViewportX.ScrollAtValue(InPacketIndex); UpdateHorizontalScrollBar(); bIsStateDirty = true; } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::SetSelectedPacket(const int32 InPacketIndex) { const FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); const int32 PacketCount = ViewportX.GetMaxValue(); if (PacketCount > 0) { const int32 PacketIndex = FMath::Min(FMath::Max(0, InPacketIndex), PacketCount - 1); SelectionStartPacketIndex = PacketIndex; SelectionEndPacketIndex = PacketIndex + 1; LastSelectedPacketIndex = PacketIndex; EnsurePacketIsVisible(LastSelectedPacketIndex); UpdateSelectedSample(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::SelectPacketBySequenceNumber(const uint32 InSequenceNumber) { // Find the PacketIndex from sequence number TSharedPtr Session = FInsightsManager::Get()->GetSession(); if (Session.IsValid()) { Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const Trace::INetProfilerProvider& NetProfilerProvider = Trace::ReadNetProfilerProvider(*Session.Get()); const int32 PacketId = NetProfilerProvider.FindPacketIndexFromPacketSequence(ConnectionIndex, ConnectionMode, InSequenceNumber); if (PacketId != -1) { SetSelectedPacket(PacketId); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::SelectPreviousPacket() { const FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); const int32 PacketCount = ViewportX.GetMaxValue(); if (PacketCount > 0) { if (SelectionStartPacketIndex >= SelectionEndPacketIndex) // no selection? { // Select the first packet. SelectionStartPacketIndex = 0; SelectionEndPacketIndex = 1; } else { if (SelectionStartPacketIndex > 0) { // Select the previous packet. SelectionStartPacketIndex--; } SelectionEndPacketIndex = SelectionStartPacketIndex + 1; } LastSelectedPacketIndex = SelectionStartPacketIndex; EnsurePacketIsVisible(LastSelectedPacketIndex); UpdateSelectedSample(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::SelectNextPacket() { const FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); const int32 PacketCount = ViewportX.GetMaxValue(); if (PacketCount > 0) { if (SelectionStartPacketIndex >= SelectionEndPacketIndex) // no selection? { // Select the last packet. SelectionStartPacketIndex = PacketCount - 1; SelectionEndPacketIndex = PacketCount; } else { if (SelectionEndPacketIndex < PacketCount) { // Select the next packet. SelectionEndPacketIndex++; } SelectionStartPacketIndex = SelectionEndPacketIndex - 1; } LastSelectedPacketIndex = SelectionStartPacketIndex; EnsurePacketIsVisible(LastSelectedPacketIndex); UpdateSelectedSample(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::ExtendLeftSideOfSelectedInterval() { const FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); const int32 PacketCount = ViewportX.GetMaxValue(); if (PacketCount > 0) { bool bSelectionChanged = false; if (SelectionStartPacketIndex >= SelectionEndPacketIndex) // no selection? { // Select the first packet. SelectionStartPacketIndex = 0; SelectionEndPacketIndex = 1; bSelectionChanged = true; } else { if (SelectionStartPacketIndex > 0) { // Extend left side of selected interval. SelectionStartPacketIndex--; bSelectionChanged = true; } } if (bSelectionChanged) { LastSelectedPacketIndex = SelectionStartPacketIndex; EnsurePacketIsVisible(LastSelectedPacketIndex); UpdateSelectedSample(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::ShrinkLeftSideOfSelectedInterval() { const FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); const int32 PacketCount = ViewportX.GetMaxValue(); if (PacketCount > 0) { if (SelectionStartPacketIndex + 1 < SelectionEndPacketIndex) { // Shrink left side of selected interval. SelectionStartPacketIndex++; LastSelectedPacketIndex = SelectionStartPacketIndex; EnsurePacketIsVisible(LastSelectedPacketIndex); UpdateSelectedSample(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::ExtendRightSideOfSelectedInterval() { const FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); const int32 PacketCount = ViewportX.GetMaxValue(); if (PacketCount > 0) { bool bSelectionChanged = false; if (SelectionStartPacketIndex >= SelectionEndPacketIndex) // no selection? { // Select the last packet. SelectionStartPacketIndex = PacketCount - 1; SelectionEndPacketIndex = PacketCount; bSelectionChanged = true; } else { if (SelectionEndPacketIndex < PacketCount) { // Extend right side of selected interval. SelectionEndPacketIndex++; bSelectionChanged = true; } } if (bSelectionChanged) { LastSelectedPacketIndex = SelectionEndPacketIndex - 1; EnsurePacketIsVisible(LastSelectedPacketIndex); UpdateSelectedSample(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::ShrinkRightSideOfSelectedInterval() { const FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); const int32 PacketCount = ViewportX.GetMaxValue(); if (PacketCount > 0) { if (SelectionEndPacketIndex - 1 > SelectionStartPacketIndex) { // Shrink right side of selected interval. SelectionEndPacketIndex--; LastSelectedPacketIndex = SelectionEndPacketIndex - 1; EnsurePacketIsVisible(LastSelectedPacketIndex); UpdateSelectedSample(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::UpdateSelectedSample() { SelectedSample.Reset(); if (SelectionEndPacketIndex == SelectionStartPacketIndex + 1) { SelectedSample = GetSample(SelectionStartPacketIndex); } OnSelectionChanged(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::ShowContextMenu(const FPointerEvent& MouseEvent) { const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, NULL); MenuBuilder.BeginSection("Misc"); { FUIAction Action_AutoZoom( FExecuteAction::CreateSP(this, &SPacketView::ContextMenu_AutoZoom_Execute), FCanExecuteAction::CreateSP(this, &SPacketView::ContextMenu_AutoZoom_CanExecute), FIsActionChecked::CreateSP(this, &SPacketView::ContextMenu_AutoZoom_IsChecked)); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_AutoZoom", "Auto Zoom"), LOCTEXT("ContextMenu_AutoZoom_Desc", "Enable auto zoom. Makes entire graph series to fit into view."), FSlateIcon(), Action_AutoZoom, NAME_None, EUserInterfaceActionType::ToggleButton); } MenuBuilder.EndSection(); TSharedRef MenuWidget = MenuBuilder.MakeWidget(); FWidgetPath EventPath = MouseEvent.GetEventPath() != nullptr ? *MouseEvent.GetEventPath() : FWidgetPath(); const FVector2D ScreenSpacePosition = MouseEvent.GetScreenSpacePosition(); FSlateApplication::Get().PushMenu(SharedThis(this), EventPath, MenuWidget, ScreenSpacePosition, FPopupTransitionEffect::ContextMenu); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::ContextMenu_AutoZoom_Execute() { bIsAutoZoomEnabled = !bIsAutoZoomEnabled; if (bIsAutoZoomEnabled) { AutoZoom(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SPacketView::ContextMenu_AutoZoom_CanExecute() { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SPacketView::ContextMenu_AutoZoom_IsChecked() { return bIsAutoZoomEnabled; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::AutoZoom() { FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); AutoZoomViewportPos = ViewportX.GetMinPos(); ViewportX.ScrollAtPos(AutoZoomViewportPos); AutoZoomViewportSize = ViewportX.GetSize(); if (AutoZoomViewportSize > 0.0f && ViewportX.GetMaxValue() - ViewportX.GetMinValue() > 0) { float DX = ViewportX.GetMaxPos() - ViewportX.GetMinPos(); // Auto zoom in. while (DX < AutoZoomViewportSize) { const float OldScale = ViewportX.GetScale(); ViewportX.RelativeZoomWithFixedOffset(+0.1f, 0.0f); ViewportX.ScrollAtPos(AutoZoomViewportPos); DX = ViewportX.GetMaxPos() - ViewportX.GetMinPos(); if (OldScale == ViewportX.GetScale()) { break; } } // Auto zoom out (until entire session frame range fits into view). while (DX > AutoZoomViewportSize) { const float OldScale = ViewportX.GetScale(); ViewportX.RelativeZoomWithFixedOffset(-0.1f, 0.0f); ViewportX.ScrollAtPos(AutoZoomViewportPos); DX = ViewportX.GetMaxPos() - ViewportX.GetMinPos(); if (OldScale == ViewportX.GetScale()) { break; } } } AutoZoomViewportScale = ViewportX.GetScale(); UpdateHorizontalScrollBar(); bIsStateDirty = true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::BindCommands() { } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::HorizontalScrollBar_OnUserScrolled(float ScrollOffset) { FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); ViewportX.OnUserScrolled(HorizontalScrollBar, ScrollOffset); bIsStateDirty = true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::UpdateHorizontalScrollBar() { FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); ViewportX.UpdateScrollBar(HorizontalScrollBar); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SPacketView::ZoomHorizontally(const float Delta, const float X) { FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); ViewportX.RelativeZoomWithFixedOffset(Delta, X); ViewportX.ScrollAtValue(ViewportX.GetValueAtPos(ViewportX.GetPos())); // align viewport position with sample UpdateHorizontalScrollBar(); bIsStateDirty = true; } //////////////////////////////////////////////////////////////////////////////////////////////////// #undef LOCTEXT_NAMESPACE