// Copyright Epic Games, Inc. All Rights Reserved. #include "Widgets/SWidgetReflector.h" #include "ISlateReflectorModule.h" #include "SlateReflectorModule.h" #include "Rendering/DrawElements.h" #include "Misc/App.h" #include "Misc/ConfigCacheIni.h" #include "Misc/Parse.h" #include "Modules/ModuleManager.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Docking/TabManager.h" #include "Framework/Commands/UICommandList.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/Notifications/NotificationManager.h" #include "InputEventVisualizer.h" #include "Styling/WidgetReflectorStyle.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SSpacer.h" #include "Widgets/Images/SImage.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SSeparator.h" #include "Widgets/Input/SButton.h" #include "Widgets/SToolTip.h" #include "Widgets/Layout/SSplitter.h" #include "Widgets/Views/SHeaderRow.h" #include "Widgets/Views/STableViewBase.h" #include "Widgets/Views/STableRow.h" #include "Widgets/Views/SListView.h" #include "Widgets/Views/STreeView.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Input/SSpinBox.h" #include "Widgets/SSlateOptions.h" #include "Widgets/SWidgetReflectorTreeWidgetItem.h" #include "Widgets/SWidgetReflectorToolTipWidget.h" #include "Widgets/SWidgetEventLog.h" #include "Widgets/SWidgetHittestGrid.h" #include "Widgets/SWidgetList.h" #include "Widgets/SInvalidationPanel.h" #include "Widgets/Docking/SDockTab.h" #include "Widgets/Notifications/SNotificationList.h" #include "Widgets/Navigation/SBreadcrumbTrail.h" #include "Widgets/Input/SComboBox.h" #include "Widgets/SWidgetSnapshotVisualizer.h" #include "WidgetSnapshotService.h" #include "Types/ReflectionMetadata.h" #include "Debugging/SlateDebugging.h" #include "VisualTreeCapture.h" #include "Models/WidgetReflectorNode.h" #include "Fonts/FontCache.h" #include "Fonts/FontTypes.h" #include "IImageWrapperModule.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "Textures/SlateShaderResource.h" #if SLATE_REFLECTOR_HAS_DESKTOP_PLATFORM #include "DesktopPlatformModule.h" #endif // SLATE_REFLECTOR_HAS_DESKTOP_PLATFORM #if SLATE_REFLECTOR_HAS_SESSION_SERVICES #include "ISessionManager.h" #include "ISessionServicesModule.h" #endif // SLATE_REFLECTOR_HAS_SESSION_SERVICES #if WITH_EDITOR #include "Framework/Docking/LayoutService.h" #include "PropertyEditorModule.h" #endif #define LOCTEXT_NAMESPACE "SWidgetReflector" /** * Widget reflector user widget. */ namespace WidgetReflectorCVars { static bool bEnableFocusOnPick = true; static FAutoConsoleVariableRef EnableFocusOnPick( TEXT("Slate.EnableFocusOnPick"), bEnableFocusOnPick, TEXT("If true, Widget Reflector window will be automatically focused when a pick is made.") ); } /* Local helpers *****************************************************************************/ namespace WidgetReflectorImpl { /** Command to take a snapshot. */ void TakeSnapshotCommand(const TArray&); void DumpFontAtlasesCommand(const TArray&); FAutoConsoleCommand CCommandWidgetReflectorTakeSnapshot( TEXT("WidgetReflector.TakeSnapshot") , TEXT("Take a snapshot and save the result on the local drive. ie. WidgetReflector.TakeSnapshot [Delay=1.0] [Navigation=false]") , FConsoleCommandWithArgsDelegate::CreateStatic(&TakeSnapshotCommand)); FAutoConsoleCommand CCommandDumpFontAtlases( TEXT("WidgetReflector.DumpFontAtlases") , TEXT("Save all of the font atlases currently in the font cache on the local drive. A timestamp will be automatically appended to the directory where the textures will be saved. The default location is YourProject/Saved/WidgetReflector/FontAtlases_Timestamp ie. WidgetReflector.DumpFontAtlases[SaveDirectory=D:\\My\\Path] [SaveFilePrefix=ExamplePrefix]") , FConsoleCommandWithArgsDelegate::CreateStatic(&DumpFontAtlasesCommand)); /** Information about a potential widget snapshot target */ struct FWidgetSnapshotTarget { /** Display name of the target (used in the UI) */ FText DisplayName; /** Instance ID of the target */ FGuid InstanceId; }; /** Different UI modes the widget reflector can be in */ enum class EWidgetReflectorUIMode : uint8 { Live, Snapshot, }; namespace WidgetReflectorTabID { static const FName WidgetHierarchy = "WidgetReflector.WidgetHierarchyTab"; static const FName SnapshotWidgetPicker = "WidgetReflector.SnapshotWidgetPickerTab"; static const FName WidgetDetails = "WidgetReflector.WidgetDetailsTab"; static const FName SlateOptions = "WidgetReflector.SlateOptionsTab"; static const FName WidgetEvents = "WidgetReflector.WidgetEventsTab"; static const FName HittestGrid = "WidgetReflector.HittestGridTab"; static const FName WidgetList = "WidgetReflector.WidgetList"; } namespace WidgetReflectorText { static const FText HitTestPicking = LOCTEXT("PickHitTestable", "Pick Hit-Testable Widgets"); static const FText VisualPicking = LOCTEXT("PickVisual", "Pick Painted Widgets"); static const FText Focus = LOCTEXT("ShowFocus", "Show Focus"); static const FText Focusing = LOCTEXT("ShowingFocus", "Showing Focus (Esc to Stop)"); static const FText Picking = LOCTEXT("PickingWidget", "Picking (Esc to Stop)"); } namespace WidgetReflectorIcon { static const FName FocusPicking = "Icon.FocusPicking"; static const FName HitTestPicking = "Icon.HitTestPicking"; static const FName VisualPicking = "Icon.VisualPicking"; static const FName Ellipsis = "Icon.Ellipsis"; static const FName Filter = "Icon.Filter"; static const FName LoadSnapshot = "Icon.LoadSnapshot"; static const FName TakeSnapshot = "Icon.TakeSnapshot"; } enum class EWidgetPickingMode : uint8 { None = 0, Focus, HitTesting, Drawable }; EWidgetPickingMode ConvertToWidgetPickingMode(int32 Number) { if (Number < 0 || Number > static_cast(EWidgetPickingMode::Drawable)) { return EWidgetPickingMode::None; } return static_cast(Number); } /** * Widget reflector implementation */ class SWidgetReflector : public ::SWidgetReflector { // The reflector uses a tree that observes FWidgetReflectorNodeBase objects. typedef STreeView> SReflectorTree; private: //~ Begin ::SWidgetReflector implementation virtual void Construct( const FArguments& InArgs) override; //~ End ::SWidgetReflector implementation void HandlePullDownAtlasesMenu(FMenuBuilder& MenuBuilder); void HandlePullDownWindowMenu(FMenuBuilder& MenuBuilder); TSharedRef SpawnSlateOptionWidgetTab(const FSpawnTabArgs& Args); TSharedRef SpawnWidgetHierarchyTab(const FSpawnTabArgs& Args); TSharedRef SpawnSnapshotWidgetPicker(const FSpawnTabArgs& Args); #if WITH_EDITOR TSharedRef SpawnWidgetDetails(const FSpawnTabArgs& Args); #endif #if WITH_SLATE_DEBUGGING TSharedRef SpawnWidgetEvents(const FSpawnTabArgs& Args); TSharedRef SpawnWidgetHittestGrid(const FSpawnTabArgs& Args); #endif TSharedRef SpawnWidgetList(const FSpawnTabArgs& Args); public: void HandleTabManagerPersistLayout(const TSharedRef& LayoutToSave); void OnTabSpawned(const FName& TabIdentifier, const TSharedRef& SpawnedTab); void CloseTab(const FName& TabIdentifier); void SaveSettings(); void LoadSettings(); void SetUIMode(const EWidgetReflectorUIMode InNewMode); //~ Begin SCompoundWidget overrides virtual void Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) override; //~ End SCompoundWidget overrides //~ Begin IWidgetReflector interface virtual bool IsInPickingMode() const override { return PickingMode != EWidgetPickingMode::None; } virtual bool IsShowingFocus() const override { return PickingMode == EWidgetPickingMode::Focus; } virtual bool IsVisualizingLayoutUnderCursor() const override { return PickingMode == EWidgetPickingMode::HitTesting || PickingMode == EWidgetPickingMode::Drawable; } virtual void OnWidgetPicked() override { SetPickingMode(EWidgetPickingMode::None); } virtual bool ReflectorNeedsToDrawIn( TSharedRef ThisWindow ) const override; virtual void SetSourceAccessDelegate( FAccessSourceCode InDelegate ) override { SourceAccessDelegate = InDelegate; } virtual void SetAssetAccessDelegate(FAccessAsset InDelegate) override { AsseetAccessDelegate = InDelegate; } virtual void SetWidgetsToVisualize( const FWidgetPath& InWidgetsToVisualize ) override; virtual int32 Visualize( const FWidgetPath& InWidgetsToVisualize, FSlateWindowElementList& OutDrawElements, int32 LayerId ) override; //~ End IWidgetReflector interface /** * Generates a tool tip for the given reflector tree node. * * @param InReflectorNode The node to generate the tool tip for. * @return The tool tip widget. */ TSharedPtr GenerateToolTipForReflectorNode( TWeakPtr InReflectorNode ) const; /** * Mark the provided reflector nodes such that they stand out in the tree and are visible. * * @param WidgetPathToObserve The nodes to mark. */ void VisualizeAsTree( const TArray< TSharedRef >& WidgetPathToVisualize ); /** * Draw the widget path to the picked widget as the widgets' outlines. * * @param InWidgetsToVisualize A widget path whose widgets' outlines to draw. * @param OutDrawElements A list of draw elements; we will add the output outlines into it. * @param LayerId The maximum layer achieved in OutDrawElements so far. * @return The maximum layer ID we achieved while painting. */ int32 VisualizePickAsRectangles( const FWidgetPath& InWidgetsToVisualize, FSlateWindowElementList& OutDrawElements, int32 LayerId ); /** * Draw an outline for the specified nodes. * * @param InNodesToDraw A widget path whose widgets' outlines to draw. * @param WindowGeometry The geometry of the window in which to draw. * @param OutDrawElements A list of draw elements; we will add the output outlines into it. * @param LayerId the maximum layer achieved in OutDrawElements so far. * @return The maximum layer ID we achieved while painting. */ int32 VisualizeSelectedNodesAsRectangles( const TArray>& InNodesToDraw, const TSharedRef& VisualizeInWindow, FSlateWindowElementList& OutDrawElements, int32 LayerId ); /** Draw the actual highlight */ void DrawWidgetVisualization(const FPaintGeometry& WidgetGeometry, FLinearColor Color, FSlateWindowElementList& OutDrawElements, int32& LayerId); /** Clear previous selection and set the selection to the live widget. */ void SelectLiveWidget( TSharedPtr InWidget ); /** Set the given nodes as the root of the tree. */ void SetNodesAsReflectorTreeRoot(TArray> RootNodes); /** Filter the selected nodes before setting them as root. */ TArray> FilterSelectedToSetAsReflectorTreeRoot(TArray> RootNodes); /** Is there any selected node in the reflector tree. */ bool DoesReflectorTreeHasSelectedItem() const { return SelectedNodes.Num() > 0; } /** Apply the requested filter to the reflected tree root. */ void UpdateFilteredTreeRoot(); void HandleDisplayTextureAtlases(); void HandleDisplayFontAtlases(); //~ Handle for the picking button ECheckBoxState HandleGetPickingButtonChecked() const; void HandlePickingModeStateChanged(); FSlateIcon HandleGetPickingModeImage() const; FText HandleGetPickingModeText() const; TSharedRef HandlePickingModeContextMenu(); void HandlePickButtonClicked(EWidgetPickingMode InPickingMode); void SetPickingMode(EWidgetPickingMode InMode) { if (PickingMode != InMode) { // Disable visual picking, and re-enable widget caching. VisualCapture.Disable(); // Enable the picking mode. PickingMode = InMode; // If we're enabling hit test, reset the visual capture entirely, we don't want to use the visual tree. if (PickingMode == EWidgetPickingMode::HitTesting) { VisualCapture.Reset(); } // If we're using the drawing picking mode enable it! else if (PickingMode == EWidgetPickingMode::Drawable) { VisualCapture.Enable(); } } } /** Callback to see whether the "Snapshot Target" combo should be enabled */ bool IsSnapshotTargetComboEnabled() const; /** Callback to see whether the "Take Snapshot" button should be enabled */ bool IsTakeSnapshotButtonEnabled() const; /** Callback for clicking the "Take Snapshot" button. */ void HandleTakeSnapshotButtonClicked(); /** Build option menu for snaphot. */ TSharedRef HandleSnapshotOptionsTreeContextMenu(); /** Takes a snapshot of the current state of the snapshot target. */ void TakeSnapshot(); /** Used as a callback for the "snapshot pending" notification item buttons, called when we should give up on a snapshot request */ void OnCancelPendingRemoteSnapshot(); /** Callback for when a remote widget snapshot is available */ void HandleRemoteSnapshotReceived(const TArray& InSnapshotData); #if SLATE_REFLECTOR_HAS_DESKTOP_PLATFORM /** Callback for clicking the "Load Snapshot" button. */ void HandleLoadSnapshotButtonClicked(); #endif // SLATE_REFLECTOR_HAS_DESKTOP_PLATFORM /** Called to update the list of available snapshot targets */ void UpdateAvailableSnapshotTargets(); /** Called to update the currently selected snapshot target (after the list has been refreshed) */ void UpdateSelectedSnapshotTarget(); /** Called when the list of available snapshot targets changes */ void OnAvailableSnapshotTargetsChanged(); /** Called when a node is set as root to create the breadcrum trail */ void CreateCrumbTrailForNode(TSharedRef InReflectorNode); /** Get the display name of the currently selected snapshot target */ FText GetSelectedSnapshotTargetDisplayName() const; /** Generate a row widget for the available targets combo box */ TSharedRef HandleGenerateAvailableSnapshotComboItemWidget(TSharedPtr InItem) const; /** Update the selected target when the combo box selection is changed */ void HandleAvailableSnapshotComboSelectionChanged(TSharedPtr InItem, ESelectInfo::Type InSeletionInfo); /** Callback for generating a row in the reflector tree view. */ TSharedRef HandleReflectorTreeGenerateRow( TSharedRef InReflectorNode, const TSharedRef& OwnerTable ); /** Callback for getting the child items of the given reflector tree node. */ void HandleReflectorTreeGetChildren( TSharedRef InReflectorNode, TArray>& OutChildren ); /** Callback for when the selection in the reflector tree has changed. */ void HandleReflectorTreeSelectionChanged( TSharedPtr, ESelectInfo::Type /*SelectInfo*/ ); /** Callback for when the context menu in the reflector tree is requested. */ TSharedRef HandleReflectorTreeContextMenu(); TSharedPtr HandleReflectorTreeContextMenuPtr(); /** Callback for when an item in the reflector tree is clicked on. */ void HandleReflectorTreeOnMouseClick(TSharedRef InReflectorNode); /** Callback for when a crumb is clicked on. */ void HandleBreadcrumbOnClick(const TSharedRef& InReflectorNode); /** Callback for when the menu of a crumb delimiter is requested. */ TSharedRef< SWidget > HandleBreadcrumbDelimiterMenu(const TSharedRef& InReflectorNode); /** Callback for when the reflector tree header list changed. */ void HandleReflectorTreeHiddenColumnsListChanged(); /** Reset the filtered tree root. */ void HandleResetFilteredTreeRoot(); /** Show the start of the UMG tree. */ void HandleStartTreeWithUMG(); /** Should we display the breadcrumb trail. */ EVisibility HandleIsBreadcrumbVisible() const; /** Should we show only the UMG tree. */ bool HandleIsStartTreeWithUMGEnabled() const { return bFilterReflectorTreeRootWithUMG; } bool HandleHasPendingActions() const { return !bIsPendingDelayedSnapshot; } /** Determine the text of Take Snapshot button */ FText HandleGetTakeSnapshotText() const { return bIsPendingDelayedSnapshot ? LOCTEXT("CancelSnapshotButtonText", "Cancel Snapshot") : LOCTEXT("TakeSnapshotButtonText", "Take Snapshot"); } private: TSharedPtr TabManager; TMap> SpawnedTabs; TSharedPtr ReflectorTree; TArray HiddenReflectorTreeColumns; TSharedPtr> > BreadCrumb; /** Node that are currently selected */ TArray> SelectedNodes; /** The original path of the widget picked. It may include node that are now hidden by the filter */ TArray> PickedWidgetPath; /** Root of the tree before filtering */ TArray> ReflectorTreeRoot; /** Root of the tree after filtering */ TArray> FilteredTreeRoot; /** When working with a snapshotted tree, this will contain the snapshot hierarchy and screenshot info */ FWidgetSnapshotData SnapshotData; TSharedPtr WidgetSnapshotVisualizer; /** List of available snapshot targets, as well as the one we currently have selected */ TSharedPtr>> AvailableSnapshotTargetsComboBox; TArray> AvailableSnapshotTargets; FGuid SelectedSnapshotTargetInstanceId; TSharedPtr WidgetSnapshotService; TWeakPtr WidgetSnapshotNotificationPtr; FGuid RemoteSnapshotRequestId; FAccessSourceCode SourceAccessDelegate; FAccessAsset AsseetAccessDelegate; EWidgetReflectorUIMode CurrentUIMode; EWidgetPickingMode PickingMode; EWidgetPickingMode LastPickingMode; bool bFilterReflectorTreeRootWithUMG; #if WITH_EDITOR TSharedPtr PropertyViewPtr; #endif #if WITH_SLATE_DEBUGGING TWeakPtr WidgetHittestGrid; #endif FVisualTreeCapture VisualCapture; private: float SnapshotDelay; bool bIsPendingDelayedSnapshot; bool bRequestNavigationSimulation; double TimeOfScheduledSnapshot; }; void SWidgetReflector::Construct( const FArguments& InArgs ) { // If saved, LoadSettings will override these variables. LastPickingMode = EWidgetPickingMode::HitTesting; HiddenReflectorTreeColumns.Add(SReflectorTreeWidgetItem::NAME_Enabled.ToString()); HiddenReflectorTreeColumns.Add(SReflectorTreeWidgetItem::NAME_Volatile.ToString()); HiddenReflectorTreeColumns.Add(SReflectorTreeWidgetItem::NAME_HasActiveTimer.ToString()); HiddenReflectorTreeColumns.Add(SReflectorTreeWidgetItem::NAME_ActualSize.ToString()); HiddenReflectorTreeColumns.Add(SReflectorTreeWidgetItem::NAME_LayerId.ToString()); LoadSettings(); CurrentUIMode = EWidgetReflectorUIMode::Live; PickingMode = EWidgetPickingMode::None; bFilterReflectorTreeRootWithUMG = false; SnapshotDelay = 0.0f; bIsPendingDelayedSnapshot = false; bRequestNavigationSimulation = false; TimeOfScheduledSnapshot = -1.0; WidgetSnapshotService = InArgs._WidgetSnapshotService; #if SLATE_REFLECTOR_HAS_SESSION_SERVICES { TSharedPtr SessionManager = FModuleManager::LoadModuleChecked("SessionServices").GetSessionManager(); if (SessionManager.IsValid()) { SessionManager->OnSessionsUpdated().AddSP(this, &SWidgetReflector::OnAvailableSnapshotTargetsChanged); } } #endif // SLATE_REFLECTOR_HAS_SESSION_SERVICES SelectedSnapshotTargetInstanceId = FApp::GetInstanceId(); UpdateAvailableSnapshotTargets(); const FName TabLayoutName = "WidgetReflector_Layout_NoStats_v2"; TSharedRef Layout = FTabManager::NewLayout(TabLayoutName) ->AddArea ( FTabManager::NewPrimaryArea() ->SetOrientation(Orient_Vertical) ->Split ( FTabManager::NewStack() ->SetHideTabWell(true) ->AddTab(WidgetReflectorTabID::SlateOptions, ETabState::OpenedTab) ) ->Split ( // Main application area FTabManager::NewSplitter() ->SetOrientation(Orient_Horizontal) ->Split ( // Main application area FTabManager::NewSplitter() ->SetOrientation(Orient_Vertical) ->Split ( FTabManager::NewStack() ->SetHideTabWell(true) ->SetSizeCoefficient(0.7f) ->AddTab(WidgetReflectorTabID::WidgetHierarchy, ETabState::OpenedTab) ) ->Split ( FTabManager::NewStack() ->SetHideTabWell(true) ->SetSizeCoefficient(0.3f) ->AddTab(WidgetReflectorTabID::SnapshotWidgetPicker, ETabState::ClosedTab) #if WITH_SLATE_DEBUGGING ->AddTab(WidgetReflectorTabID::WidgetEvents, ETabState::ClosedTab) ->AddTab(WidgetReflectorTabID::HittestGrid, ETabState::ClosedTab) #endif ->AddTab(WidgetReflectorTabID::WidgetList, ETabState::ClosedTab) ) ) #if WITH_EDITOR ->Split ( FTabManager::NewStack() ->SetHideTabWell(true) ->SetSizeCoefficient(0.3f) ->AddTab(WidgetReflectorTabID::WidgetDetails, ETabState::ClosedTab) ) #endif ) ); auto RegisterTrackedTabSpawner = [this](const FName& TabId, const FOnSpawnTab& OnSpawnTab) -> FTabSpawnerEntry& { return TabManager->RegisterTabSpawner(TabId, FOnSpawnTab::CreateLambda([this, OnSpawnTab](const FSpawnTabArgs& Args) -> TSharedRef { TSharedRef SpawnedTab = OnSpawnTab.Execute(Args); OnTabSpawned(Args.GetTabId().TabType, SpawnedTab); return SpawnedTab; })); }; check(InArgs._ParentTab.IsValid()); TabManager = FGlobalTabmanager::Get()->NewTabManager(InArgs._ParentTab.ToSharedRef()); TabManager->SetOnPersistLayout(FTabManager::FOnPersistLayout::CreateRaw(this, &SWidgetReflector::HandleTabManagerPersistLayout)); RegisterTrackedTabSpawner(WidgetReflectorTabID::SlateOptions, FOnSpawnTab::CreateSP(this, &SWidgetReflector::SpawnSlateOptionWidgetTab)) .SetDisplayName(LOCTEXT("OptionsTab", "Toolbar")); RegisterTrackedTabSpawner(WidgetReflectorTabID::WidgetHierarchy, FOnSpawnTab::CreateSP(this, &SWidgetReflector::SpawnWidgetHierarchyTab)) .SetDisplayName(LOCTEXT("WidgetHierarchyTab", "Widget Hierarchy")); RegisterTrackedTabSpawner(WidgetReflectorTabID::SnapshotWidgetPicker, FOnSpawnTab::CreateSP(this, &SWidgetReflector::SpawnSnapshotWidgetPicker)) .SetDisplayName(LOCTEXT("SnapshotWidgetPickerTab", "Snapshot Widget Picker")); #if WITH_EDITOR if (GIsEditor) { RegisterTrackedTabSpawner(WidgetReflectorTabID::WidgetDetails, FOnSpawnTab::CreateSP(this, &SWidgetReflector::SpawnWidgetDetails)) .SetDisplayName(LOCTEXT("WidgetDetailsTab", "Widget Details")); } #endif //WITH_EDITOR #if WITH_SLATE_DEBUGGING RegisterTrackedTabSpawner(WidgetReflectorTabID::WidgetEvents, FOnSpawnTab::CreateSP(this, &SWidgetReflector::SpawnWidgetEvents)) .SetDisplayName(LOCTEXT("WidgetEventsTab", "Widget Events")); RegisterTrackedTabSpawner(WidgetReflectorTabID::HittestGrid, FOnSpawnTab::CreateSP(this, &SWidgetReflector::SpawnWidgetHittestGrid)) .SetDisplayName(LOCTEXT("HitTestGridTab", "Hit Test Grid")); #endif RegisterTrackedTabSpawner(WidgetReflectorTabID::WidgetList, FOnSpawnTab::CreateSP(this, &SWidgetReflector::SpawnWidgetList)) .SetDisplayName(LOCTEXT("WidgetListTab", "Widget List")); #if WITH_EDITOR if (GIsEditor) { Layout = FLayoutSaveRestore::LoadFromConfig(GEditorLayoutIni, Layout); } #endif FMenuBarBuilder MenuBarBuilder = FMenuBarBuilder(TSharedPtr()); #if WITH_SLATE_DEBUGGING MenuBarBuilder.AddPullDownMenu( LOCTEXT("DemoModeLabel", "Demo Mode"), FText::GetEmpty(), FNewMenuDelegate::CreateRaw(FSlateReflectorModule::GetModulePtr()->GetInputEventVisualizer(), &FInputEventVisualizer::PopulateMenu), "DemoMode" ); #endif MenuBarBuilder.AddPullDownMenu( LOCTEXT("AtlasesMenuLabel", "Atlases"), FText::GetEmpty(), FNewMenuDelegate::CreateSP(this, &SWidgetReflector::HandlePullDownAtlasesMenu), "Atlases" ); MenuBarBuilder.AddPullDownMenu( LOCTEXT("WindowMenuLabel", "Window"), FText::GetEmpty(), FNewMenuDelegate::CreateSP(this, &SWidgetReflector::HandlePullDownWindowMenu), "Window" ); const TSharedRef MenuWidget = MenuBarBuilder.MakeWidget(); TabManager->SetMenuMultiBox(MenuBarBuilder.GetMultiBox(), MenuWidget); this->ChildSlot [ SNew(SBorder) .BorderImage(FCoreStyle::Get().GetBrush("ToolPanel.GroupBorder")) .BorderBackgroundColor(FLinearColor::Gray) // Darken the outer border [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .Padding(FMargin(0.0f, 4.0f, 0.0f, 0.0f)) [ MenuWidget ] + SVerticalBox::Slot() .Padding(FMargin(0.0f, 4.0f, 0.0f, 0.0f)) [ TabManager->RestoreFrom(Layout, nullptr).ToSharedRef() ] ] ]; } void SWidgetReflector::HandlePullDownAtlasesMenu(FMenuBuilder& MenuBuilder) { MenuBuilder.AddMenuEntry( LOCTEXT("DisplayTextureAtlases", "Display Texture Atlases"), FText::GetEmpty(), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SWidgetReflector::HandleDisplayTextureAtlases) )); MenuBuilder.AddMenuEntry( LOCTEXT("DisplayFontAtlases", "Display Font Atlases"), FText::GetEmpty(), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SWidgetReflector::HandleDisplayFontAtlases) )); } void SWidgetReflector::HandlePullDownWindowMenu(FMenuBuilder& MenuBuilder) { if (!TabManager.IsValid()) { return; } TabManager->PopulateLocalTabSpawnerMenu(MenuBuilder); } TSharedRef SWidgetReflector::SpawnSlateOptionWidgetTab(const FSpawnTabArgs& Args) { TSharedRef SpawnedTab = SNew(SDockTab) .Label(LOCTEXT("ToolbarTab", "Toolbar")) .ShouldAutosize(true) [ SNew(SSlateOptions) ]; return SpawnedTab; } TSharedRef SWidgetReflector::SpawnWidgetHierarchyTab(const FSpawnTabArgs& Args) { FSlimHorizontalToolBarBuilder ToolbarBuilderGlobal(TSharedPtr(), FMultiBoxCustomization::None); TArray HiddenColumnsList; HiddenColumnsList.Reserve(HiddenReflectorTreeColumns.Num()); for (const FString& Item : HiddenReflectorTreeColumns) { HiddenColumnsList.Add(*Item); } // Button that controls the target for the snapshot operation AvailableSnapshotTargetsComboBox = SNew(SComboBox>) .IsEnabled(this, &SWidgetReflector::IsSnapshotTargetComboEnabled) .ToolTipText(LOCTEXT("ChooseSnapshotTargetToolTipText", "Choose Snapshot Target")) .OptionsSource(&AvailableSnapshotTargets) .OnGenerateWidget(this, &SWidgetReflector::HandleGenerateAvailableSnapshotComboItemWidget) .OnSelectionChanged(this, &SWidgetReflector::HandleAvailableSnapshotComboSelectionChanged) [ SNew(STextBlock) .Text(this, &SWidgetReflector::GetSelectedSnapshotTargetDisplayName) ]; FSlateIcon EmptyIcon(FWidgetReflectorStyle::GetStyleSetName(), "Icon.Empty"); ToolbarBuilderGlobal.BeginSection("Picking"); { FTextBuilder TooltipText; ToolbarBuilderGlobal.AddToolBarButton( FUIAction( FExecuteAction::CreateSP(this, &SWidgetReflector::HandlePickingModeStateChanged), FCanExecuteAction::CreateSP(this,&SWidgetReflector::HandleHasPendingActions), FGetActionCheckState::CreateSP(this, &SWidgetReflector::HandleGetPickingButtonChecked) ), NAME_None, MakeAttributeSP(this,&SWidgetReflector::HandleGetPickingModeText), TooltipText.ToText(), MakeAttributeSP(this, &SWidgetReflector::HandleGetPickingModeImage), EUserInterfaceActionType::ToggleButton ); ToolbarBuilderGlobal.AddComboButton( FUIAction( FExecuteAction(), FCanExecuteAction::CreateSP(this, &SWidgetReflector::HandleHasPendingActions), FGetActionCheckState() ), FOnGetContent::CreateSP(this, &SWidgetReflector::HandlePickingModeContextMenu), FText::GetEmpty(), TooltipText.ToText(), EmptyIcon, true ); } ToolbarBuilderGlobal.EndSection(); ToolbarBuilderGlobal.BeginSection("Filter"); { FTextBuilder TooltipText; ToolbarBuilderGlobal.AddComboButton( FUIAction( FExecuteAction(), FCanExecuteAction::CreateSP(this, &SWidgetReflector::HandleHasPendingActions), FGetActionCheckState() ), FOnGetContent::CreateSP(this, &SWidgetReflector::HandleReflectorTreeContextMenu), LOCTEXT("FilterLabel", "Filter"), TooltipText.ToText(), FSlateIcon(FWidgetReflectorStyle::GetStyleSetName(), WidgetReflectorIcon::Filter), false ); } ToolbarBuilderGlobal.EndSection(); ToolbarBuilderGlobal.AddWidget(SNew(SSpacer),NAME_None,true,HAlign_Right); ToolbarBuilderGlobal.BeginSection("Option"); { FTextBuilder TooltipText; ToolbarBuilderGlobal.AddToolBarButton( FUIAction( FExecuteAction::CreateSP(this, &SWidgetReflector::HandleTakeSnapshotButtonClicked), FCanExecuteAction::CreateSP(this, &SWidgetReflector::IsTakeSnapshotButtonEnabled), FGetActionCheckState() ), NAME_None, MakeAttributeSP(this, &SWidgetReflector::HandleGetTakeSnapshotText), TooltipText.ToText(), FSlateIcon(FWidgetReflectorStyle::GetStyleSetName(), WidgetReflectorIcon::TakeSnapshot), EUserInterfaceActionType::Button ); ToolbarBuilderGlobal.AddComboButton( FUIAction( FExecuteAction(), FCanExecuteAction::CreateSP(this, &SWidgetReflector::HandleHasPendingActions), FGetActionCheckState() ), FOnGetContent::CreateSP(this, &SWidgetReflector::HandleSnapshotOptionsTreeContextMenu), LOCTEXT("OptionsLabel", "Options"), TooltipText.ToText(), FSlateIcon(FWidgetReflectorStyle::GetStyleSetName(), WidgetReflectorIcon::Ellipsis), false ); #if SLATE_REFLECTOR_HAS_DESKTOP_PLATFORM ToolbarBuilderGlobal.AddToolBarButton( FUIAction( FExecuteAction::CreateSP(this, &SWidgetReflector::HandleLoadSnapshotButtonClicked), FCanExecuteAction::CreateSP(this, &SWidgetReflector::HandleHasPendingActions), FGetActionCheckState() ), NAME_None, LOCTEXT("LoadSnapshotButtonText", "Load Snapshot"), TooltipText.ToText(), FSlateIcon(FWidgetReflectorStyle::GetStyleSetName(), WidgetReflectorIcon::LoadSnapshot), EUserInterfaceActionType::Button ); #endif } ToolbarBuilderGlobal.EndSection(); ToolbarBuilderGlobal.SetStyle(&FAppStyle::Get(), "SlimToolBar"); TSharedRef SpawnedTab = SNew(SDockTab) .Label(LOCTEXT("WidgetHierarchyTab", "Widget Hierarchy")) //.OnCanCloseTab_Lambda([]() { return false; }) // Can't prevent this as it stops the editor from being able to close while the widget reflector is open [ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() .Padding(FMargin(0.0f, 2.0f)) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() [ ToolbarBuilderGlobal.MakeWidget() ] ] + SVerticalBox::Slot() .AutoHeight() .Padding(FMargin(2.0f, 2.0f)) [ SNew(SVerticalBox) .Visibility(this, &SWidgetReflector::HandleIsBreadcrumbVisible) + SVerticalBox::Slot() .AutoHeight() [ SNew(SSeparator).Thickness(5.f) ] + SVerticalBox::Slot() [ SAssignNew(BreadCrumb, SBreadcrumbTrail>) .ButtonStyle(FAppStyle::Get(), "SimpleButton") .DelimiterImage(FAppStyle::Get().GetBrush("Icons.ChevronRight")) .TextStyle(FAppStyle::Get(), "NormalText") .OnCrumbClicked(this, &SWidgetReflector::HandleBreadcrumbOnClick) .GetCrumbMenuContent(this, &SWidgetReflector::HandleBreadcrumbDelimiterMenu) ] ] +SVerticalBox::Slot() .FillHeight(1.0f) [ SNew(SBorder) .Padding(0.f) .BorderImage(FCoreStyle::Get().GetBrush("ToolPanel.GroupBorder")) [ // The tree view that shows all the info that we capture. SAssignNew(ReflectorTree, SReflectorTree) .TreeItemsSource(&FilteredTreeRoot) .OnGenerateRow(this, &SWidgetReflector::HandleReflectorTreeGenerateRow) .OnGetChildren(this, &SWidgetReflector::HandleReflectorTreeGetChildren) .OnSelectionChanged(this, &SWidgetReflector::HandleReflectorTreeSelectionChanged) .OnContextMenuOpening(this, &SWidgetReflector::HandleReflectorTreeContextMenuPtr) .OnMouseButtonClick(this, &SWidgetReflector::HandleReflectorTreeOnMouseClick) .HighlightParentNodesForSelection(true) .HeaderRow ( SNew(SHeaderRow) .CanSelectGeneratedColumn(true) .HiddenColumnsList(HiddenColumnsList) .OnHiddenColumnsListChanged(this, &SWidgetReflector::HandleReflectorTreeHiddenColumnsListChanged) +SHeaderRow::Column(SReflectorTreeWidgetItem::NAME_WidgetName) .DefaultLabel(LOCTEXT("WidgetName", "Widget Name")) .FillWidth(1.f) .ShouldGenerateWidget(true) +SHeaderRow::Column(SReflectorTreeWidgetItem::NAME_ForegroundColor) .DefaultLabel(LOCTEXT("ForegroundColor", "FG")) .DefaultTooltip(LOCTEXT("ForegroundColorToolTip", "Foreground Color")) .FixedWidth(24.0f) +SHeaderRow::Column(SReflectorTreeWidgetItem::NAME_Visibility) .DefaultLabel(LOCTEXT("Visibility", "Visibility")) .DefaultTooltip(LOCTEXT("VisibilityTooltip", "Visibility")) .FillSized(125.0f) + SHeaderRow::Column(SReflectorTreeWidgetItem::NAME_Enabled) .DefaultLabel(LOCTEXT("Enabled", "Enabled")) .DefaultTooltip(LOCTEXT("EnabledToolTip", "Enabled")) .FillSized(60.0f) + SHeaderRow::Column(SReflectorTreeWidgetItem::NAME_Focusable) .DefaultLabel(LOCTEXT("Focus", "Focus")) .DefaultTooltip(LOCTEXT("FocusableTooltip", "Focusability (Note that for hit-test directional navigation to work it must be Focusable and \"Visible\"!)")) .FillSized(60.0f) + SHeaderRow::Column(SReflectorTreeWidgetItem::NAME_HasActiveTimer) .DefaultLabel(LOCTEXT("HasActiveTimer", "Timer")) .DefaultTooltip(LOCTEXT("HasActiveTimerTooltip", "Has Active Timer")) .FillSized(60.0f) +SHeaderRow::Column(SReflectorTreeWidgetItem::NAME_Clipping) .DefaultLabel(LOCTEXT("Clipping", "Clipping" )) .FillSized(100.0f) + SHeaderRow::Column(SReflectorTreeWidgetItem::NAME_LayerId) .DefaultLabel(LOCTEXT("LayerId", "LayerId")) .FillSized(35.f) + SHeaderRow::Column(SReflectorTreeWidgetItem::NAME_ActualSize) .DefaultLabel(LOCTEXT("ActualSize", "Size")) .FillSized(100.0f) +SHeaderRow::Column(SReflectorTreeWidgetItem::NAME_WidgetInfo) .DefaultLabel(LOCTEXT("Source", "Source" )) .FillSized(200.f) +SHeaderRow::Column(SReflectorTreeWidgetItem::NAME_Address) .DefaultLabel( LOCTEXT("Address", "Address") ) .FillSized(170.0f) ) ] ] ]; UpdateSelectedSnapshotTarget(); return SpawnedTab; } TSharedRef SWidgetReflector::SpawnSnapshotWidgetPicker(const FSpawnTabArgs& Args) { TWeakPtr WeakSelf = SharedThis(this); auto OnTabClosed = [WeakSelf](TSharedRef) { // Tab closed - leave snapshot mode if (TSharedPtr SelfPinned = WeakSelf.Pin()) { SelfPinned->SetUIMode(EWidgetReflectorUIMode::Live); } }; auto OnWidgetPathPicked = [WeakSelf](const TArray>& InPickedWidgetPath) { if (TSharedPtr SelfPinned = WeakSelf.Pin()) { SelfPinned->SelectedNodes.Reset(); SelfPinned->PickedWidgetPath = InPickedWidgetPath; SelfPinned->UpdateFilteredTreeRoot(); } }; auto OnSnapshotWidgetPicked = [WeakSelf](FWidgetReflectorNodeBase::TPointerAsInt InSnapshotWidget) { if (TSharedPtr SelfPinned = WeakSelf.Pin()) { SelfPinned->SelectedNodes.Reset(); FWidgetReflectorNodeUtils::FindSnaphotWidget(SelfPinned->ReflectorTreeRoot, InSnapshotWidget, SelfPinned->PickedWidgetPath); SelfPinned->UpdateFilteredTreeRoot(); } }; return SNew(SDockTab) .Label(LOCTEXT("SnapshotWidgetPickerTab", "Snapshot Widget Picker")) .OnTabClosed_Lambda(OnTabClosed) [ SAssignNew(WidgetSnapshotVisualizer, SWidgetSnapshotVisualizer) .SnapshotData(&SnapshotData) .OnWidgetPathPicked_Lambda(OnWidgetPathPicked) .OnSnapshotWidgetSelected_Lambda(OnSnapshotWidgetPicked) ]; } #if WITH_EDITOR TSharedRef SWidgetReflector::SpawnWidgetDetails(const FSpawnTabArgs& Args) { FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked("PropertyEditor"); FDetailsViewArgs DetailsViewArgs; { DetailsViewArgs.bAllowSearch = true; DetailsViewArgs.bShowOptions = true; DetailsViewArgs.bAllowMultipleTopLevelObjects = false; DetailsViewArgs.bAllowFavoriteSystem = true; DetailsViewArgs.bShowObjectLabel = false; DetailsViewArgs.bHideSelectionTip = true; } TSharedRef PropertyView = PropertyEditorModule.CreateDetailView(DetailsViewArgs); PropertyViewPtr = PropertyView; auto OnTabClosed = [this](TSharedRef) { }; return SNew(SDockTab) .Label(LOCTEXT("WidgetDetailsTab", "Widget Details")) .OnTabClosed_Lambda(OnTabClosed) [ PropertyView ]; } #endif //WITH_EDITOR #if WITH_SLATE_DEBUGGING TSharedRef SWidgetReflector::SpawnWidgetEvents(const FSpawnTabArgs& Args) { return SNew(SDockTab) .Label(LOCTEXT("WidgetEventsTab", "Widget Events")) [ SNew(SWidgetEventLog, AsShared()) .OnWidgetTokenActivated(this, &SWidgetReflector::SelectLiveWidget) ]; } TSharedRef SWidgetReflector::SpawnWidgetHittestGrid(const FSpawnTabArgs& Args) { return SNew(SDockTab) .Label(LOCTEXT("HitTestGridTab", "Hit Test Grid")) [ SAssignNew(WidgetHittestGrid, SWidgetHittestGrid, AsShared()) .OnWidgetSelected(this, &SWidgetReflector::SelectLiveWidget) .OnVisualizeWidget(this, &SWidgetReflector::SetWidgetsToVisualize) ]; } #endif //WITH_SLATE_DEBUGGING TSharedRef SWidgetReflector::SpawnWidgetList(const FSpawnTabArgs& Args) { return SNew(SDockTab) .Label(LOCTEXT("WidgetListTab", "Widget List")) [ SNew(SWidgetList) .OnAccessSource(SourceAccessDelegate) .OnAccessAsset(AsseetAccessDelegate) ]; } void SWidgetReflector::HandleTabManagerPersistLayout(const TSharedRef& LayoutToSave) { #if WITH_EDITOR FLayoutSaveRestore::SaveToConfig(GEditorLayoutIni, LayoutToSave); #endif //WITH_EDITOR } void SWidgetReflector::SaveSettings() { GConfig->SetArray(TEXT("WidgetReflector"), TEXT("HiddenReflectorTreeColumns"), HiddenReflectorTreeColumns, *GEditorPerProjectIni); GConfig->SetInt(TEXT("WidgetReflector"), TEXT("LastPickingMode"), static_cast(LastPickingMode), *GEditorPerProjectIni); } void SWidgetReflector::LoadSettings() { if (GConfig->DoesSectionExist(TEXT("WidgetReflector"), *GEditorPerProjectIni)) { int32 LastPickingModeAsInt = static_cast(EWidgetPickingMode::HitTesting); GConfig->GetInt(TEXT("WidgetReflector"), TEXT("LastPickingMode"), LastPickingModeAsInt, *GEditorPerProjectIni); LastPickingMode = ConvertToWidgetPickingMode(LastPickingModeAsInt); if (LastPickingMode == EWidgetPickingMode::None) { LastPickingMode = EWidgetPickingMode::HitTesting; } GConfig->GetArray(TEXT("WidgetReflector"), TEXT("HiddenReflectorTreeColumns"), HiddenReflectorTreeColumns, *GEditorPerProjectIni); } } void SWidgetReflector::OnTabSpawned(const FName& TabIdentifier, const TSharedRef& SpawnedTab) { TWeakPtr* const ExistingTab = SpawnedTabs.Find(TabIdentifier); if (!ExistingTab) { SpawnedTabs.Add(TabIdentifier, SpawnedTab); } else { check(!ExistingTab->IsValid()); *ExistingTab = SpawnedTab; } } void SWidgetReflector::CloseTab(const FName& TabIdentifier) { TWeakPtr* const ExistingTab = SpawnedTabs.Find(TabIdentifier); if (ExistingTab) { TSharedPtr ExistingTabPin = ExistingTab->Pin(); if (ExistingTabPin.IsValid()) { ExistingTabPin->RequestCloseTab(); } } } void SWidgetReflector::SetUIMode(const EWidgetReflectorUIMode InNewMode) { if (CurrentUIMode != InNewMode) { CurrentUIMode = InNewMode; SelectedNodes.Reset(); PickedWidgetPath.Reset(); ReflectorTreeRoot.Reset(); FilteredTreeRoot.Reset(); ReflectorTree->RequestTreeRefresh(); if (CurrentUIMode == EWidgetReflectorUIMode::Snapshot) { TabManager->TryInvokeTab(WidgetReflectorTabID::SnapshotWidgetPicker); } else { SnapshotData.ClearSnapshot(); if (WidgetSnapshotVisualizer.IsValid()) { WidgetSnapshotVisualizer->SnapshotDataUpdated(); } CloseTab(WidgetReflectorTabID::SnapshotWidgetPicker); } } BreadCrumb->ClearCrumbs(); } /* SCompoundWidget overrides *****************************************************************************/ void SWidgetReflector::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) { if (bIsPendingDelayedSnapshot && FSlateApplication::Get().GetCurrentTime() > TimeOfScheduledSnapshot) { // TakeSnapshot leads to the widget being ticked indirectly recursively, // so the recursion of this tick mustn't trigger a recursive snapshot. // Immediately clear the pending snapshot flag. bIsPendingDelayedSnapshot = false; TimeOfScheduledSnapshot = -1.0; TakeSnapshot(); } } /* IWidgetReflector overrides *****************************************************************************/ bool SWidgetReflector::ReflectorNeedsToDrawIn( TSharedRef ThisWindow ) const { return ((SelectedNodes.Num() > 0) && (ReflectorTreeRoot.Num() > 0) && (ReflectorTreeRoot[0]->GetLiveWidget() == ThisWindow)); } void SWidgetReflector::SetWidgetsToVisualize( const FWidgetPath& InWidgetsToVisualize ) { ReflectorTreeRoot.Reset(); FilteredTreeRoot.Reset(); PickedWidgetPath.Reset(); SelectedNodes.Reset(); if (InWidgetsToVisualize.IsValid()) { ReflectorTreeRoot.Add(FWidgetReflectorNodeUtils::NewLiveNodeTreeFrom(InWidgetsToVisualize.Widgets[0])); FWidgetReflectorNodeUtils::FindLiveWidgetPath(ReflectorTreeRoot, InWidgetsToVisualize, PickedWidgetPath); UpdateFilteredTreeRoot(); } ReflectorTree->RequestTreeRefresh(); } int32 SWidgetReflector::Visualize( const FWidgetPath& InWidgetsToVisualize, FSlateWindowElementList& OutDrawElements, int32 LayerId ) { if (!InWidgetsToVisualize.IsValid() && SelectedNodes.Num() > 0 && ReflectorTreeRoot.Num() > 0) { TSharedPtr WindowWidget = ReflectorTreeRoot[0]->GetLiveWidget(); if (WindowWidget.IsValid()) { TSharedPtr Window = StaticCastSharedPtr(WindowWidget); return VisualizeSelectedNodesAsRectangles(SelectedNodes, Window.ToSharedRef(), OutDrawElements, LayerId); } } const bool bAttemptingToVisualizeReflector = InWidgetsToVisualize.ContainsWidget(ReflectorTree.Get()); if (PickingMode == EWidgetPickingMode::Drawable) { TSharedPtr Tree = VisualCapture.GetVisualTreeForWindow(OutDrawElements.GetPaintWindow()); if (Tree.IsValid()) { const FVector2f AbsPoint = FSlateApplication::Get().GetCursorPos(); const FVector2f WindowPoint = AbsPoint - OutDrawElements.GetPaintWindow()->GetPositionInScreen(); if (TSharedPtr PickedWidget = Tree->Pick(WindowPoint)) { FWidgetPath WidgetsToVisualize = InWidgetsToVisualize; FSlateApplication::Get().FindPathToWidget(PickedWidget.ToSharedRef(), WidgetsToVisualize, EVisibility::All); if (!bAttemptingToVisualizeReflector) { SetWidgetsToVisualize(WidgetsToVisualize); return VisualizePickAsRectangles(WidgetsToVisualize, OutDrawElements, LayerId); } } else { SetWidgetsToVisualize(FWidgetPath{}); } } } else if (!bAttemptingToVisualizeReflector) { SetWidgetsToVisualize(InWidgetsToVisualize); return VisualizePickAsRectangles(InWidgetsToVisualize, OutDrawElements, LayerId); } return LayerId; } /* SWidgetReflector implementation *****************************************************************************/ void SWidgetReflector::SelectLiveWidget(TSharedPtr InWidget) { bool bFound = false; if (this->CurrentUIMode == EWidgetReflectorUIMode::Live && InWidget) { TArray> FoundList; FWidgetReflectorNodeUtils::FindLiveWidget(ReflectorTreeRoot, InWidget, FoundList); if (FoundList.Num() > 0) { for (const TSharedRef& FoundItem : FoundList) { ReflectorTree->SetItemExpansion(FoundItem, true); } ReflectorTree->RequestScrollIntoView(FoundList.Last()); ReflectorTree->SetSelection(FoundList.Last()); bFound = true; } } if (!bFound) { ReflectorTree->ClearSelection(); } } namespace WidgetReflectorRecursive { bool FindNodeWithReflectionData(const TArray>& NodeBase, TArray>& Result) { for (const TSharedRef& Node : NodeBase) { if (Node->HasValidWidgetAssetData()) { return true; } } for (const TSharedRef& Node : NodeBase) { if (FindNodeWithReflectionData(Node->GetChildNodes(), Result)) { Result.Add(Node); } } return false; } } void SWidgetReflector::UpdateFilteredTreeRoot() { FilteredTreeRoot.Reset(); if (bFilterReflectorTreeRootWithUMG) { WidgetReflectorRecursive::FindNodeWithReflectionData(ReflectorTreeRoot, FilteredTreeRoot); VisualizeAsTree(PickedWidgetPath); } else { FilteredTreeRoot = ReflectorTreeRoot; VisualizeAsTree(PickedWidgetPath); } } void SWidgetReflector::SetNodesAsReflectorTreeRoot(TArray> RootNodes) { TArray> FilteredNodes = FilterSelectedToSetAsReflectorTreeRoot(RootNodes); if (FilteredNodes.Num() > 0) { FilteredTreeRoot.Reset(); FilteredTreeRoot.Append(FilteredNodes); ReflectorTree->RequestTreeRefresh(); TSharedPtr FirstNodeParent = FilteredNodes[0]->GetParentNode(); if (FirstNodeParent.IsValid()) { CreateCrumbTrailForNode(FirstNodeParent.ToSharedRef()); } else { BreadCrumb->ClearCrumbs(); } } } TArray> SWidgetReflector::FilterSelectedToSetAsReflectorTreeRoot(TArray> RootNodes) { if (RootNodes.Num() > 1) { TArray> ShallowestNodes; ShallowestNodes = RootNodes; for (int32 index = ShallowestNodes.Num() -1 ; index >= 0; index--) { if (ShallowestNodes.Contains(ShallowestNodes[index]->GetParentNode())) { ShallowestNodes.RemoveAt(index); } } TSharedPtr FirstNodeParent = ShallowestNodes[0]->GetParentNode(); for (int32 index = ShallowestNodes.Num() - 1; index >= 0; index--) { if (ShallowestNodes[index]->GetParentNode() != FirstNodeParent) { ShallowestNodes.RemoveAt(index); } } return ShallowestNodes; } return RootNodes; } TSharedPtr SWidgetReflector::GenerateToolTipForReflectorNode( TWeakPtr InReflectorNode ) const { if (TSharedPtr ReflectorNode = InReflectorNode.Pin()) { return SNew(SToolTip) [ SNew(SReflectorToolTipWidget) .WidgetInfoToVisualize(ReflectorNode) ]; } return FSlateApplication::Get().MakeToolTip(LOCTEXT("MissingNode", "The node is invalid.")); } void SWidgetReflector::VisualizeAsTree( const TArray>& WidgetPathToVisualize ) { if (WidgetPathToVisualize.Num() > 0) { const FLinearColor TopmostWidgetColor(1.0f, 0.0f, 0.0f); const FLinearColor LeafmostWidgetColor(0.0f, 1.0f, 0.0f); for (int32 WidgetIndex = 0; WidgetIndex < WidgetPathToVisualize.Num(); ++WidgetIndex) { const auto& CurWidget = WidgetPathToVisualize[WidgetIndex]; // Tint the item based on depth in picked path const float ColorFactor = static_cast(WidgetIndex) / static_cast(WidgetPathToVisualize.Num()); CurWidget->SetTint(FMath::Lerp(TopmostWidgetColor, LeafmostWidgetColor, ColorFactor)); // Make sure the user can see the picked path in the tree. ReflectorTree->SetItemExpansion(CurWidget, true); } ReflectorTree->RequestScrollIntoView(WidgetPathToVisualize.Last()); ReflectorTree->SetSelection(WidgetPathToVisualize.Last()); } else { ReflectorTree->ClearSelection(); } } int32 SWidgetReflector::VisualizePickAsRectangles( const FWidgetPath& InWidgetsToVisualize, FSlateWindowElementList& OutDrawElements, int32 LayerId) { const FLinearColor TopmostWidgetColor(1.0f, 0.0f, 0.0f); const FLinearColor LeafmostWidgetColor(0.0f, 1.0f, 0.0f); for (int32 WidgetIndex = 0; WidgetIndex < InWidgetsToVisualize.Widgets.Num(); ++WidgetIndex) { const FArrangedWidget& WidgetGeometry = InWidgetsToVisualize.Widgets[WidgetIndex]; const float ColorFactor = static_cast(WidgetIndex)/ static_cast(InWidgetsToVisualize.Widgets.Num()); const FLinearColor Tint(1.0f - ColorFactor, ColorFactor, 0.0f, 1.0f); // The FGeometry we get is from a WidgetPath, so it's rooted in desktop space. // We need to APPEND a transform to the Geometry to essentially undo this root transform // and get us back into Window Space. // This is nonstandard so we have to go through some hoops and a specially exposed method // in FPaintGeometry to allow appending layout transforms. FPaintGeometry WindowSpaceGeometry = WidgetGeometry.Geometry.ToPaintGeometry(); WindowSpaceGeometry.AppendTransform(TransformCast(Inverse(InWidgetsToVisualize.TopLevelWindow->GetPositionInScreen()))); FLinearColor Color = FMath::Lerp(TopmostWidgetColor, LeafmostWidgetColor, ColorFactor); DrawWidgetVisualization(WindowSpaceGeometry, Color, OutDrawElements, LayerId); } return LayerId; } int32 SWidgetReflector::VisualizeSelectedNodesAsRectangles( const TArray>& InNodesToDraw, const TSharedRef& VisualizeInWindow, FSlateWindowElementList& OutDrawElements, int32 LayerId ) { for (int32 NodeIndex = 0; NodeIndex < InNodesToDraw.Num(); ++NodeIndex) { const TSharedRef& NodeToDraw = InNodesToDraw[NodeIndex]; const FLinearColor Tint(0.0f, 1.0f, 0.0f); // The FGeometry we get is from a WidgetPath, so it's rooted in desktop space. // We need to APPEND a transform to the Geometry to essentially undo this root transform // and get us back into Window Space. // This is nonstandard so we have to go through some hoops and a specially exposed method // in FPaintGeometry to allow appending layout transforms. FPaintGeometry WindowSpaceGeometry(NodeToDraw->GetAccumulatedLayoutTransform(), NodeToDraw->GetAccumulatedRenderTransform(), NodeToDraw->GetLocalSize(), NodeToDraw->GetGeometry().HasRenderTransform()); WindowSpaceGeometry.AppendTransform(TransformCast(Inverse(VisualizeInWindow->GetPositionInScreen()))); DrawWidgetVisualization(WindowSpaceGeometry, NodeToDraw->GetTint(), OutDrawElements, LayerId); } return LayerId; } void SWidgetReflector::DrawWidgetVisualization(const FPaintGeometry& WidgetGeometry, FLinearColor Color, FSlateWindowElementList& OutDrawElements, int32& LayerId) { WidgetGeometry.CommitTransformsIfUsingLegacyConstructor(); const FVector2D LocalSize = WidgetGeometry.GetLocalSize(); // If the size is 0 in any dimension, we're going to draw a line to represent the widget, since it's going to take up // padding space since it's visible, even though it's zero sized. if (FMath::IsNearlyZero(LocalSize.X) || FMath::IsNearlyZero(LocalSize.Y)) { TArray LinePoints; LinePoints.SetNum(2); LinePoints[0] = FVector2D::ZeroVector; LinePoints[1] = LocalSize; FSlateDrawElement::MakeLines( OutDrawElements, ++LayerId, WidgetGeometry, LinePoints, ESlateDrawEffect::None, Color, true, 2 ); } else { // Draw a normal box border around the geometry FSlateDrawElement::MakeBox( OutDrawElements, ++LayerId, WidgetGeometry, FCoreStyle::Get().GetBrush(TEXT("Debug.Border")), ESlateDrawEffect::None, Color ); } } /* SWidgetReflector callbacks *****************************************************************************/ void SWidgetReflector::HandleDisplayTextureAtlases() { static const FName SlateReflectorModuleName("SlateReflector"); FModuleManager::LoadModuleChecked(SlateReflectorModuleName).DisplayTextureAtlasVisualizer(); } void SWidgetReflector::HandleDisplayFontAtlases() { static const FName SlateReflectorModuleName("SlateReflector"); FModuleManager::LoadModuleChecked(SlateReflectorModuleName).DisplayFontAtlasVisualizer(); } /* Picking button *****************************************************************************/ ECheckBoxState SWidgetReflector::HandleGetPickingButtonChecked() const { return PickingMode != EWidgetPickingMode::None ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } void SWidgetReflector::HandlePickingModeStateChanged() { if (PickingMode == EWidgetPickingMode::None) { SetPickingMode(LastPickingMode); } else { SetPickingMode(EWidgetPickingMode::None); } if (IsVisualizingLayoutUnderCursor()) { SetUIMode(EWidgetReflectorUIMode::Live); } } FSlateIcon SWidgetReflector::HandleGetPickingModeImage() const { switch (LastPickingMode) { case EWidgetPickingMode::Focus: return FSlateIcon(FWidgetReflectorStyle::GetStyleSetName(), WidgetReflectorIcon::FocusPicking); case EWidgetPickingMode::HitTesting: return FSlateIcon(FWidgetReflectorStyle::GetStyleSetName(), WidgetReflectorIcon::HitTestPicking); case EWidgetPickingMode::Drawable: return FSlateIcon(FWidgetReflectorStyle::GetStyleSetName(), WidgetReflectorIcon::VisualPicking); case EWidgetPickingMode::None: default: return FSlateIcon(FWidgetReflectorStyle::GetStyleSetName(), "Icon.Empty"); } } FText SWidgetReflector::HandleGetPickingModeText() const { if (PickingMode == EWidgetPickingMode::None) { switch(LastPickingMode) { case EWidgetPickingMode::Focus: return WidgetReflectorText::Focus; case EWidgetPickingMode::Drawable: return WidgetReflectorText::VisualPicking; case EWidgetPickingMode::HitTesting: return WidgetReflectorText::HitTestPicking; } } else if (PickingMode == EWidgetPickingMode::Focus) { return WidgetReflectorText::Focusing; } return WidgetReflectorText::Picking; } TSharedRef SWidgetReflector::HandlePickingModeContextMenu() { const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, nullptr); const bool bIsFocus = PickingMode == EWidgetPickingMode::Focus; MenuBuilder.AddMenuEntry( WidgetReflectorText::Focus, FText::GetEmpty(), FSlateIcon(FWidgetReflectorStyle::GetStyleSetName(), WidgetReflectorIcon::FocusPicking), FUIAction( FExecuteAction::CreateSP(this, &SWidgetReflector::HandlePickButtonClicked, EWidgetPickingMode::Focus), FCanExecuteAction::CreateLambda([bIsFocus](){ return !bIsFocus; }) )); const bool bIsHitTestPicking = PickingMode == EWidgetPickingMode::HitTesting; MenuBuilder.AddMenuEntry( WidgetReflectorText::HitTestPicking, FText::GetEmpty(), FSlateIcon(FWidgetReflectorStyle::GetStyleSetName(), WidgetReflectorIcon::HitTestPicking), FUIAction( FExecuteAction::CreateSP(this, &SWidgetReflector::HandlePickButtonClicked, EWidgetPickingMode::HitTesting), FCanExecuteAction::CreateLambda([bIsHitTestPicking]() { return !bIsHitTestPicking; }) )); const bool bIsDrawable = PickingMode == EWidgetPickingMode::Drawable; MenuBuilder.AddMenuEntry( WidgetReflectorText::VisualPicking, FText::GetEmpty(), FSlateIcon(FWidgetReflectorStyle::GetStyleSetName(), WidgetReflectorIcon::VisualPicking), FUIAction( FExecuteAction::CreateSP(this, &SWidgetReflector::HandlePickButtonClicked, EWidgetPickingMode::Drawable), FCanExecuteAction::CreateLambda([bIsDrawable]() { return !bIsDrawable; }) )); return MenuBuilder.MakeWidget(); } void SWidgetReflector::HandlePickButtonClicked(EWidgetPickingMode InPickingMode) { bool bHasChanged = LastPickingMode != InPickingMode; LastPickingMode = InPickingMode; SetPickingMode(PickingMode != InPickingMode ? InPickingMode : EWidgetPickingMode::None); if (IsVisualizingLayoutUnderCursor()) { SetUIMode(EWidgetReflectorUIMode::Live); } if (bHasChanged) { SaveSettings(); } } bool SWidgetReflector::IsSnapshotTargetComboEnabled() const { if (bIsPendingDelayedSnapshot) { return false; } #if SLATE_REFLECTOR_HAS_SESSION_SERVICES return !RemoteSnapshotRequestId.IsValid(); #else return false; #endif } bool SWidgetReflector::IsTakeSnapshotButtonEnabled() const { return SelectedSnapshotTargetInstanceId.IsValid() && !RemoteSnapshotRequestId.IsValid(); } void SWidgetReflector::HandleTakeSnapshotButtonClicked() { if (!bIsPendingDelayedSnapshot) { if (SnapshotDelay > 0.0f) { bIsPendingDelayedSnapshot = true; TimeOfScheduledSnapshot = FSlateApplication::Get().GetCurrentTime() + SnapshotDelay; } else { TakeSnapshot(); } } else { bIsPendingDelayedSnapshot = false; TimeOfScheduledSnapshot = -1.0f; } } TSharedRef SWidgetReflector::HandleSnapshotOptionsTreeContextMenu() { TSharedRef DelayWidget = SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(LOCTEXT("DelayLabel", "Delay")) ] + SHorizontalBox::Slot() .HAlign(HAlign_Right) [ SNew(SSpinBox) .MinValue(0.f) .MinDesiredWidth(40.f) .Value_Lambda([this]() { return SnapshotDelay; }) .OnValueCommitted_Lambda([this](const float InValue, ETextCommit::Type) { SnapshotDelay = FMath::Max(0.0f, InValue); }) ]; TSharedRef NavigationEventSimulationWidget = SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(LOCTEXT("NavigationEventSimulationLabel", "Navigation Event Simulation")) .ToolTipText(LOCTEXT("NavigationEventSimulationTooltip", "Build a simulation of all the possible Navigation Events that can occur in the windows.")) ] + SHorizontalBox::Slot() .Padding(FMargin(4.f, 0.f)) .HAlign(HAlign_Right) [ SNew(SCheckBox) .IsChecked_Lambda([this]() { return bRequestNavigationSimulation ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }) .OnCheckStateChanged_Lambda([this](ECheckBoxState NewState) { bRequestNavigationSimulation = NewState == ECheckBoxState::Checked; }) ]; return SNew(SVerticalBox) + SVerticalBox::Slot() .Padding(2.f) [ DelayWidget ] + SVerticalBox::Slot() .Padding(2.f) [ NavigationEventSimulationWidget ] + SVerticalBox::Slot() .Padding(2.f) [ AvailableSnapshotTargetsComboBox.ToSharedRef() ]; } void SWidgetReflector::TakeSnapshot() { // Local snapshot? if (SelectedSnapshotTargetInstanceId == FApp::GetInstanceId()) { SetUIMode(EWidgetReflectorUIMode::Snapshot); #if WITH_SLATE_DEBUGGING if (TSharedPtr WidgetHittestGridPin = WidgetHittestGrid.Pin()) { WidgetHittestGridPin->SetPause(true); } #endif // Take a snapshot of any window(s) that are currently open SnapshotData.TakeSnapshot(bRequestNavigationSimulation); // Rebuild the reflector tree from the snapshot data SelectedNodes.Reset(); PickedWidgetPath.Reset(); ReflectorTreeRoot = FilteredTreeRoot = SnapshotData.GetWindowsRef(); ReflectorTree->RequestTreeRefresh(); WidgetSnapshotVisualizer->SnapshotDataUpdated(); #if WITH_SLATE_DEBUGGING if (TSharedPtr WidgetHittestGridPin = WidgetHittestGrid.Pin()) { WidgetHittestGridPin->SetPause(false); } #endif } else { // Remote snapshot - these can take a while, show a progress message FNotificationInfo Info(LOCTEXT("RemoteWidgetSnapshotPendingNotificationText", "Waiting for Remote Widget Snapshot Data")); // Add the buttons with text, tooltip and callback Info.ButtonDetails.Add(FNotificationButtonInfo( LOCTEXT("CancelPendingSnapshotButtonText", "Cancel"), LOCTEXT("CancelPendingSnapshotButtonToolTipText", "Cancel the pending widget snapshot request."), FSimpleDelegate::CreateSP(this, &SWidgetReflector::OnCancelPendingRemoteSnapshot) )); // We will be keeping track of this ourselves Info.bFireAndForget = false; // Launch notification WidgetSnapshotNotificationPtr = FSlateNotificationManager::Get().AddNotification(Info); if (WidgetSnapshotNotificationPtr.IsValid()) { WidgetSnapshotNotificationPtr.Pin()->SetCompletionState(SNotificationItem::CS_Pending); } RemoteSnapshotRequestId = WidgetSnapshotService->RequestSnapshot(SelectedSnapshotTargetInstanceId, FWidgetSnapshotService::FOnWidgetSnapshotResponse::CreateSP(this, &SWidgetReflector::HandleRemoteSnapshotReceived)); if (!RemoteSnapshotRequestId.IsValid()) { TSharedPtr WidgetSnapshotNotificationPin = WidgetSnapshotNotificationPtr.Pin(); if (WidgetSnapshotNotificationPin.IsValid()) { WidgetSnapshotNotificationPin->SetText(LOCTEXT("RemoteWidgetSnapshotFailedNotificationText", "Remote Widget Snapshot Failed")); WidgetSnapshotNotificationPin->SetCompletionState(SNotificationItem::CS_Fail); WidgetSnapshotNotificationPin->ExpireAndFadeout(); WidgetSnapshotNotificationPtr.Reset(); } } } } void SWidgetReflector::OnCancelPendingRemoteSnapshot() { TSharedPtr WidgetSnapshotNotificationPin = WidgetSnapshotNotificationPtr.Pin(); if (WidgetSnapshotNotificationPin.IsValid()) { WidgetSnapshotNotificationPin->SetText(LOCTEXT("RemoteWidgetSnapshotAbortedNotificationText", "Aborted Remote Widget Snapshot")); WidgetSnapshotNotificationPin->SetCompletionState(SNotificationItem::CS_Fail); WidgetSnapshotNotificationPin->ExpireAndFadeout(); WidgetSnapshotNotificationPtr.Reset(); } WidgetSnapshotService->AbortSnapshotRequest(RemoteSnapshotRequestId); RemoteSnapshotRequestId = FGuid(); } void SWidgetReflector::HandleRemoteSnapshotReceived(const TArray& InSnapshotData) { { TSharedPtr WidgetSnapshotNotificationPin = WidgetSnapshotNotificationPtr.Pin(); if (WidgetSnapshotNotificationPin.IsValid()) { WidgetSnapshotNotificationPin->SetText(LOCTEXT("RemoteWidgetSnapshotReceivedNotificationText", "Remote Widget Snapshot Data Received")); WidgetSnapshotNotificationPin->SetCompletionState(SNotificationItem::CS_Success); WidgetSnapshotNotificationPin->ExpireAndFadeout(); WidgetSnapshotNotificationPtr.Reset(); } } RemoteSnapshotRequestId = FGuid(); SetUIMode(EWidgetReflectorUIMode::Snapshot); // Load up the remote data SnapshotData.LoadSnapshotFromBuffer(InSnapshotData); // Rebuild the reflector tree from the snapshot data SelectedNodes.Reset(); PickedWidgetPath.Reset(); ReflectorTreeRoot = FilteredTreeRoot = SnapshotData.GetWindowsRef(); ReflectorTree->RequestTreeRefresh(); WidgetSnapshotVisualizer->SnapshotDataUpdated(); } #if SLATE_REFLECTOR_HAS_DESKTOP_PLATFORM void SWidgetReflector::HandleLoadSnapshotButtonClicked() { IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); if (DesktopPlatform) { TSharedPtr ParentWindow = FSlateApplication::Get().FindWidgetWindow(SharedThis(this)); TArray OpenFilenames; const bool bOpened = DesktopPlatform->OpenFileDialog( (ParentWindow.IsValid()) ? ParentWindow->GetNativeWindow()->GetOSWindowHandle() : nullptr, LOCTEXT("LoadSnapshotDialogTitle", "Load Widget Snapshot").ToString(), FPaths::GameAgnosticSavedDir(), TEXT(""), TEXT("Slate Widget Snapshot (*.widgetsnapshot)|*.widgetsnapshot"), EFileDialogFlags::None, OpenFilenames ); if (bOpened && SnapshotData.LoadSnapshotFromFile(OpenFilenames[0])) { SetUIMode(EWidgetReflectorUIMode::Snapshot); // Rebuild the reflector tree from the snapshot data ReflectorTreeRoot = SnapshotData.GetWindowsRef(); ReflectorTree->RequestTreeRefresh(); WidgetSnapshotVisualizer->SnapshotDataUpdated(); } } } #endif // SLATE_REFLECTOR_HAS_DESKTOP_PLATFORM void SWidgetReflector::UpdateAvailableSnapshotTargets() { AvailableSnapshotTargets.Reset(); #if SLATE_REFLECTOR_HAS_SESSION_SERVICES { TSharedPtr SessionManager = FModuleManager::LoadModuleChecked("SessionServices").GetSessionManager(); if (SessionManager.IsValid()) { TArray> AvailableSessions; SessionManager->GetSessions(AvailableSessions); for (const auto& AvailableSession : AvailableSessions) { // Only allow sessions belonging to the current user if (AvailableSession->GetSessionOwner() != FApp::GetSessionOwner()) { continue; } TArray> AvailableInstances; AvailableSession->GetInstances(AvailableInstances); for (const auto& AvailableInstance : AvailableInstances) { FWidgetSnapshotTarget SnapshotTarget; SnapshotTarget.DisplayName = FText::Format(LOCTEXT("SnapshotTargetDisplayNameFmt", "{0} ({1})"), FText::FromString(AvailableInstance->GetInstanceName()), FText::FromString(AvailableInstance->GetPlatformName())); SnapshotTarget.InstanceId = AvailableInstance->GetInstanceId(); AvailableSnapshotTargets.Add(MakeShareable(new FWidgetSnapshotTarget(SnapshotTarget))); } } } } #else { // No session services, just add an entry that lets us snapshot ourself FWidgetSnapshotTarget SnapshotTarget; SnapshotTarget.DisplayName = FText::FromString(FApp::GetInstanceName()); SnapshotTarget.InstanceId = FApp::GetInstanceId(); AvailableSnapshotTargets.Add(MakeShareable(new FWidgetSnapshotTarget(SnapshotTarget))); } #endif } void SWidgetReflector::UpdateSelectedSnapshotTarget() { if (AvailableSnapshotTargetsComboBox.IsValid()) { const TSharedPtr* FoundSnapshotTarget = AvailableSnapshotTargets.FindByPredicate([this](const TSharedPtr& InAvailableSnapshotTarget) -> bool { return InAvailableSnapshotTarget->InstanceId == SelectedSnapshotTargetInstanceId; }); if (FoundSnapshotTarget) { AvailableSnapshotTargetsComboBox->SetSelectedItem(*FoundSnapshotTarget); } else if (AvailableSnapshotTargets.Num() > 0) { SelectedSnapshotTargetInstanceId = AvailableSnapshotTargets[0]->InstanceId; AvailableSnapshotTargetsComboBox->SetSelectedItem(AvailableSnapshotTargets[0]); } else { SelectedSnapshotTargetInstanceId = FGuid(); AvailableSnapshotTargetsComboBox->SetSelectedItem(nullptr); } } } void SWidgetReflector::OnAvailableSnapshotTargetsChanged() { UpdateAvailableSnapshotTargets(); UpdateSelectedSnapshotTarget(); } FText SWidgetReflector::GetSelectedSnapshotTargetDisplayName() const { if (AvailableSnapshotTargetsComboBox.IsValid()) { TSharedPtr SelectedSnapshotTarget = AvailableSnapshotTargetsComboBox->GetSelectedItem(); if (SelectedSnapshotTarget.IsValid()) { return SelectedSnapshotTarget->DisplayName; } } return FText::GetEmpty(); } TSharedRef SWidgetReflector::HandleGenerateAvailableSnapshotComboItemWidget(TSharedPtr InItem) const { return SNew(STextBlock) .Text(InItem->DisplayName); } void SWidgetReflector::HandleAvailableSnapshotComboSelectionChanged(TSharedPtr InItem, ESelectInfo::Type InSeletionInfo) { if (InItem.IsValid()) { SelectedSnapshotTargetInstanceId = InItem->InstanceId; } else { SelectedSnapshotTargetInstanceId = FGuid(); } } TSharedRef SWidgetReflector::HandleReflectorTreeGenerateRow( TSharedRef InReflectorNode, const TSharedRef& OwnerTable ) { return SNew(SReflectorTreeWidgetItem, OwnerTable) .WidgetInfoToVisualize(InReflectorNode) .ToolTip(GenerateToolTipForReflectorNode(InReflectorNode)) .SourceCodeAccessor(SourceAccessDelegate) .AssetAccessor(AsseetAccessDelegate); } void SWidgetReflector::HandleReflectorTreeGetChildren(TSharedRef InReflectorNode, TArray>& OutChildren) { OutChildren = InReflectorNode->GetChildNodes(); } void SWidgetReflector::HandleReflectorTreeSelectionChanged( TSharedPtr, ESelectInfo::Type /*SelectInfo*/ ) { SelectedNodes = ReflectorTree->GetSelectedItems(); if (CurrentUIMode == EWidgetReflectorUIMode::Snapshot) { WidgetSnapshotVisualizer->SetSelectedWidgets(SelectedNodes); } #if WITH_EDITOR TArray SelectedWidgetObjects; for (TSharedRef& Node : SelectedNodes) { TSharedPtr Widget = Node->GetLiveWidget(); if (Widget.IsValid()) { TSharedPtr ReflectinMetaData = Widget->GetMetaData(); if (ReflectinMetaData.IsValid()) { if (UObject* SourceObject = ReflectinMetaData->SourceObject.Get()) { SelectedWidgetObjects.Add(SourceObject); } } } } if (GIsEditor && SelectedWidgetObjects.Num() > 0) { if (WidgetReflectorCVars::bEnableFocusOnPick) { TabManager->TryInvokeTab(WidgetReflectorTabID::WidgetDetails); } if (PropertyViewPtr.IsValid()) { PropertyViewPtr->SetObjects(SelectedWidgetObjects); } } //else //{ // CloseTab(WidgetReflectorTabID::WidgetDetails); //} #endif } TSharedRef SWidgetReflector::HandleReflectorTreeContextMenu() { // We spawn a large tooltip, close it immediately to prevent context menu from hiding. FSlateApplication::Get().CloseToolTip(); const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, nullptr); bool bHasFilteredTreeRoot = ReflectorTreeRoot != FilteredTreeRoot; MenuBuilder.AddMenuEntry( LOCTEXT("SetAsRootLabel", "Selected node as root"), LOCTEXT("SetAsRootTooltip", "Set selected node as the root of the graph"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SWidgetReflector::SetNodesAsReflectorTreeRoot, SelectedNodes), FCanExecuteAction::CreateSP(this, &SWidgetReflector::DoesReflectorTreeHasSelectedItem) )); MenuBuilder.AddMenuEntry( LOCTEXT("ShowOnlyUMGLabel", "UMG as root"), LOCTEXT("ShowOnlyUMGTooltip", "Set UMG as the root of the graph"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SWidgetReflector::HandleStartTreeWithUMG), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SWidgetReflector::HandleIsStartTreeWithUMGEnabled) ), NAME_None, EUserInterfaceActionType::ToggleButton); MenuBuilder.AddMenuSeparator(); MenuBuilder.AddMenuEntry( LOCTEXT("ResetRoot", "Reset filter"), FText::GetEmpty(), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SWidgetReflector::HandleResetFilteredTreeRoot), FCanExecuteAction::CreateLambda([bHasFilteredTreeRoot](){ return bHasFilteredTreeRoot; }) )); return MenuBuilder.MakeWidget(); } TSharedPtr SWidgetReflector::HandleReflectorTreeContextMenuPtr() { return HandleReflectorTreeContextMenu(); } void SWidgetReflector::HandleReflectorTreeHiddenColumnsListChanged() { #if WITH_EDITOR if (ReflectorTree && ReflectorTree->GetHeaderRow()) { const TArray HiddenColumnIds = ReflectorTree->GetHeaderRow()->GetHiddenColumnIds(); HiddenReflectorTreeColumns.Reset(HiddenColumnIds.Num()); for (const FName Id : HiddenColumnIds) { HiddenReflectorTreeColumns.Add(Id.ToString()); } SaveSettings(); } #endif } void SWidgetReflector::CreateCrumbTrailForNode(TSharedRef InReflectorNode) { FWidgetPath PathToWidget; TSharedRef SelectedNodeAsWidget = InReflectorNode.Get().GetLiveWidget().ToSharedRef(); TArray> PickedNodePath; FWidgetReflectorNodeUtils::FindLiveWidget(ReflectorTreeRoot, SelectedNodeAsWidget, PickedNodePath); BreadCrumb->ClearCrumbs(); if (PickedNodePath.Num() > 1) { for (int32 i = 0; i < PickedNodePath.Num(); i++) { TSharedPtr Parent = PickedNodePath[i]->GetLiveWidget(); FText WidgetType = FWidgetReflectorNodeUtils::GetWidgetType(Parent); BreadCrumb->PushCrumb(WidgetType, PickedNodePath[i]); } } } void SWidgetReflector::HandleReflectorTreeOnMouseClick(TSharedRef InReflectorNode) { const FModifierKeysState ModKeyState = FSlateApplication::Get().GetModifierKeys(); if (ModKeyState.IsLeftAltDown()) { if (SelectedNodes.Num() > 0) { TArray> RootNodes; RootNodes.Add(InReflectorNode); SetNodesAsReflectorTreeRoot(RootNodes); } } } void SWidgetReflector::HandleBreadcrumbOnClick(const TSharedRef& InReflectorNode) { FilteredTreeRoot.Reset(); FilteredTreeRoot.Add(InReflectorNode); BreadCrumb->PopCrumb(); ReflectorTree->RequestTreeRefresh(); } TSharedRef< SWidget > SWidgetReflector::HandleBreadcrumbDelimiterMenu(const TSharedRef& InReflectorNode) { const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, nullptr); bool bHasFilteredTreeRoot = ReflectorTreeRoot != FilteredTreeRoot; TArray> Children = InReflectorNode->GetChildNodes(); for (int32 ChildIndex = 0; ChildIndex < Children.Num(); ++ChildIndex) { TSharedPtr ChildWidget = Children[ChildIndex]->GetLiveWidget(); FText WidgetType = FWidgetReflectorNodeUtils::GetWidgetType(ChildWidget); TArray> RootNodes; RootNodes.Add(Children[ChildIndex]); MenuBuilder.AddMenuEntry( WidgetType, FText::GetEmpty(), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SWidgetReflector::SetNodesAsReflectorTreeRoot, RootNodes) )); } return MenuBuilder.MakeWidget(); } EVisibility SWidgetReflector::HandleIsBreadcrumbVisible() const { return BreadCrumb->HasCrumbs() ? EVisibility::Visible : EVisibility::Collapsed; } void SWidgetReflector::HandleResetFilteredTreeRoot() { BreadCrumb->ClearCrumbs(); bFilterReflectorTreeRootWithUMG = false; UpdateFilteredTreeRoot(); ReflectorTree->RequestTreeRefresh(); } void SWidgetReflector::HandleStartTreeWithUMG() { bFilterReflectorTreeRootWithUMG = !bFilterReflectorTreeRootWithUMG; UpdateFilteredTreeRoot(); ReflectorTree->RequestTreeRefresh(); } // console command static FDelegateHandle TakeSnapshotEndFrameHandle; void TakeSnapshotCommand_EndFrame(double RequestedTime, bool bRequestNavigation) { if (RequestedTime <= FApp::GetCurrentTime()) { FWidgetSnapshotData SnapshotData; SnapshotData.TakeSnapshot(bRequestNavigation); FString Filename = FPaths::CreateTempFilename(*FPaths::GameAgnosticSavedDir(), TEXT(""), TEXT(".widgetsnapshot")); SnapshotData.SaveSnapshotToFile(Filename); FCoreDelegates::OnEndFrame.Remove(TakeSnapshotEndFrameHandle); TakeSnapshotEndFrameHandle.Reset(); } } void TakeSnapshotCommand(const TArray& Args) { FCoreDelegates::OnEndFrame.Remove(TakeSnapshotEndFrameHandle); float RequestedDelay = 0.001f; bool bRequestNavigation = false; for (const FString& Arg : Args) { if (FParse::Value(*Arg, TEXT("Delay="), RequestedDelay)) { continue; } if (FParse::Bool(*Arg, TEXT("Navigation="), bRequestNavigation)) { continue; } } const double CurrentTime = FApp::GetCurrentTime(); const double RequestedTimeDelay = CurrentTime + (double)RequestedDelay; TakeSnapshotEndFrameHandle = FCoreDelegates::OnEndFrame.AddStatic(&TakeSnapshotCommand_EndFrame, RequestedTimeDelay, bRequestNavigation); } void DumpFontAtlasesCommand(const TArray& Args) { FString SaveDirectoryName = FPaths::ProjectSavedDir() / TEXT("WidgetReflector") / TEXT("FontAtlases"); FString SaveFilePrefix; for (const FString& Arg : Args) { if (FParse::Value(*Arg, TEXT("SaveDirectory"), SaveDirectoryName)) { continue; } if (FParse::Value(*Arg, TEXT("SaveFilePrefix"), SaveFilePrefix)) { continue; } } // We append the timestamp on to the directory names to differentiate between various runs of the command. SaveDirectoryName = FString::Format(TEXT("{0}_{1}"), {SaveDirectoryName, FDateTime::Now().ToString()}); SaveDirectoryName = FPaths::ConvertRelativePathToFull(SaveDirectoryName); UE_LOG(LogSlate, Display, TEXT("Dumping all current font atlases to directory '%s'. All textures will be saved with a user provided file prefix of '%s'"), *SaveDirectoryName, *SaveFilePrefix); IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked(FName("ImageWrapper")); TSharedRef FontCache = FSlateApplication::Get().GetRenderer()->GetFontCache(); int32 NumFontAtlases = FontCache->GetNumAtlasPages(); for (int32 Index = 0; Index < NumFontAtlases; ++Index) { // @TODOFonts: Refactor this out into its own function so the widget reflector could have a button to save a single texture page or save all texture pages ISlateFontTexture* FontTexture = FontCache->GetFontTexture(Index); if (!FontTexture) { UE_LOG(LogSlate, Warning, TEXT("Font texture page %d is not a valid font texture. This page will not be saved."), Index); continue; } TArray FontTextureData; FontTexture->GetAtlasDataCopy(FontTextureData); if (FontTextureData.IsEmpty()) { UE_LOG(LogSlate, Log, TEXT("Texture page %d has no texture data. The texture will not be saved."), Index); continue; } // We need this to get access to the height and width of the texture FSlateShaderResource* ShaderResource = FontTexture->GetSlateTexture(); if (!ShaderResource) { UE_LOG(LogSlate, Log, TEXT("Unable to retrieve the shader resource for texture page %d. Skipping saving this texture."), Index); continue; } ESlateFontAtlasContentType ContentType = FontTexture->GetContentType(); ERawImageFormat::Type ImageFormat = ERawImageFormat::Invalid; EGammaSpace GammaSpace = EGammaSpace::Invalid; switch (ContentType) { case ESlateFontAtlasContentType::Alpha: ImageFormat = ERawImageFormat::G8; GammaSpace = EGammaSpace::Linear; break; case ESlateFontAtlasContentType::Color: ImageFormat = ERawImageFormat::BGRA8; GammaSpace = EGammaSpace::sRGB; break; case ESlateFontAtlasContentType::Msdf: ImageFormat = ERawImageFormat::BGRA8; GammaSpace = EGammaSpace::Linear; break; default: break; } FImageView ImageView((void*) FontTextureData.GetData(), ShaderResource->GetWidth(), ShaderResource->GetHeight(), 1 /* InNumSlices*/, ImageFormat, GammaSpace); TArray64 SaveImageBuffer; bool bSavedImage = ImageWrapperModule.CompressImage(SaveImageBuffer, EImageFormat::PNG, ImageView); if (bSavedImage) { FString SaveFileName = FString::Format(TEXT("{0}FontAtlasPage_{1}.png"), { SaveFilePrefix, FString::FromInt(Index)}); FString AbsoluteSaveFileName = SaveDirectoryName / SaveFileName; UE_LOG(LogSlate, Display, TEXT("Saving font texture '%s'"), *AbsoluteSaveFileName); if (!FFileHelper::SaveArrayToFile(SaveImageBuffer, *AbsoluteSaveFileName)) { UE_LOG(LogSlate, Warning, TEXT("Failed to save font texture '%s'"), *AbsoluteSaveFileName); } } else { UE_LOG(LogSlate, Warning, TEXT("Failed to convert texture page %d to a buffer to be saved."), Index); } } } } // namespace WidgetReflectorImpl TSharedRef SWidgetReflector::New() { return MakeShareable( new WidgetReflectorImpl::SWidgetReflector() ); } #undef LOCTEXT_NAMESPACE