// Copyright Epic Games, Inc. All Rights Reserved. #include "Navigation/SWidgetDesignerNavigation.h" #include "Async/Async.h" #include "Blueprint/UserWidget.h" #include "DesignerExtension.h" #include "Editor.h" #include "EditorFontGlyphs.h" #include "Styling/AppStyle.h" #include "IHasDesignerExtensibility.h" #include "INavigationEventSimulationView.h" #include "ISlateReflectorModule.h" #include "IStructureDetailsView.h" #include "Modules/ModuleManager.h" #include "PropertyEditorModule.h" #include "UObject/StructOnScope.h" #include "WidgetBlueprint.h" #include "WidgetBlueprintEditor.h" #include "WidgetBlueprintEditorUtils.h" #include "Application/SlateWindowHelper.h" #include "Framework/Application/SlateApplication.h" #include "Types/ReflectionMetadata.h" #include "Widgets/Input/SButton.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/SVirtualWindow.h" #define LOCTEXT_NAMESPACE "UMG" namespace WidgetDesignerNavigation { class FNavigationExtensionFactory; class FNavigationExtension; static const FName SlateReflectorModuleName("SlateReflector"); static TWeakPtr NavigationExtensionFactory; /** Extension factory that also knows which editor did consctructed the extension */ class FNavigationExtensionFactory : public IDesignerExtensionFactory, public TSharedFromThis { public: FNavigationExtensionFactory() = default; virtual ~FNavigationExtensionFactory() = default; virtual TSharedRef CreateDesignerExtension() const { return StaticCastSharedRef(MakeShared()); } void PreviewContentChanged(const TSharedRef Extension, TSharedRef NewContent) { int32 FoundIndex = IndexOfItem(Extension); if (Items.IsValidIndex(FoundIndex)) { if (Items[FoundIndex].WidgetDesigner) { Items[FoundIndex].WidgetDesigner->HandleWidgetPreviewUpdated(NewContent); } } } void Paint(const TSharedRef Extension, IUMGDesigner* Designer, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId) { int32 FoundIndex = IndexOfItem(Extension); if (Items.IsValidIndex(FoundIndex)) { if (Items[FoundIndex].WidgetDesigner) { Items[FoundIndex].WidgetDesigner->HandlePaint(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId); } } } void AddExtension(UWidgetBlueprint* WidgetBlueprint, TSharedRef Extension) { int32 FoundIndex = IndexOfItem(WidgetBlueprint); if (!Items.IsValidIndex(FoundIndex)) { FoundIndex = Items.AddDefaulted(); Items[FoundIndex].WidgetBlueprint = WidgetBlueprint; } Items[FoundIndex].Extension = Extension; } void RemoveExtension(TSharedRef Extension) { int32 FoundIndex = IndexOfItem(Extension); if (Items.IsValidIndex(FoundIndex)) { if (Items[FoundIndex].WidgetDesigner) { Items[FoundIndex].Extension.Reset(); } else { Items.RemoveAtSwap(FoundIndex); } } } void AddDesigner(UWidgetBlueprint* WidgetBlueprint, SWidgetDesignerNavigation* WidgetDesigner) { int32 FoundIndex = IndexOfItem(WidgetBlueprint); if (!Items.IsValidIndex(FoundIndex)) { FoundIndex = Items.AddDefaulted(); Items[FoundIndex].WidgetBlueprint = WidgetBlueprint; } Items[FoundIndex].WidgetDesigner = WidgetDesigner; } void RemoveDesigner(SWidgetDesignerNavigation* WidgetDesigner) { int32 FoundIndex = IndexOfItem(WidgetDesigner); if (Items.IsValidIndex(FoundIndex)) { if (Items[FoundIndex].Extension.IsValid()) { Items[FoundIndex].WidgetDesigner = nullptr; } else { Items.RemoveAtSwap(FoundIndex); } } } private: struct FItem { TWeakObjectPtr WidgetBlueprint; TWeakPtr Extension; SWidgetDesignerNavigation* WidgetDesigner = nullptr; }; int32 IndexOfItem(const UWidgetBlueprint* WidgetBlueprint) const { return Items.IndexOfByPredicate([WidgetBlueprint](const FItem& Item) { return Item.WidgetBlueprint == WidgetBlueprint; }); } int32 IndexOfItem(const TSharedRef& Extension) const { return Items.IndexOfByPredicate([Extension](const FItem& Item) { return Item.Extension == Extension; }); } int32 IndexOfItem(const SWidgetDesignerNavigation* Designer) const { return Items.IndexOfByPredicate([Designer](const FItem& Item) { return Item.WidgetDesigner == Designer; }); } TArray Items; }; class FNavigationExtension : public FDesignerExtension { private: using Super = FDesignerExtension; public: FNavigationExtension() { ExtensionId = FName(TEXT("NavigationSimulation")); } virtual void Initialize(IUMGDesigner* InDesigner, UWidgetBlueprint* InBlueprint) override { Super::Initialize(InDesigner, InBlueprint); TSharedPtr FactoryPinned = WidgetDesignerNavigation::NavigationExtensionFactory.Pin(); if (ensure(FactoryPinned)) { FactoryPinned->AddExtension(InBlueprint, SharedThis(this)); } } virtual void Uninitialize() override { TSharedPtr FactoryPinned = WidgetDesignerNavigation::NavigationExtensionFactory.Pin(); if (ensure(FactoryPinned)) { FactoryPinned->RemoveExtension(SharedThis(this)); } Super::Uninitialize(); } virtual void PreviewContentChanged(TSharedRef NewContent) override { TSharedPtr FactoryPinned = WidgetDesignerNavigation::NavigationExtensionFactory.Pin(); if (ensure(FactoryPinned)) { FactoryPinned->PreviewContentChanged(SharedThis(this), NewContent); } } virtual void Paint(const TSet< FWidgetReference >& Selection, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId) const override { TSharedPtr FactoryPinned = WidgetDesignerNavigation::NavigationExtensionFactory.Pin(); if (ensure(FactoryPinned)) { TSharedRef Shared = SharedThis(this); FactoryPinned->Paint(Shared, Designer, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId); } } }; } TSharedRef SWidgetDesignerNavigation::MakeDesignerExtension() { TSharedPtr Pinned = WidgetDesignerNavigation::NavigationExtensionFactory.Pin(); if (Pinned) { ensure(false); // this should only be called only once by the module return StaticCastSharedRef(Pinned.ToSharedRef()); } TSharedRef Temp = MakeShareable(new WidgetDesignerNavigation::FNavigationExtensionFactory); WidgetDesignerNavigation::NavigationExtensionFactory = Temp; return StaticCastSharedRef(Temp); } void SWidgetDesignerNavigation::Construct(const FArguments& InArgs, TSharedPtr InBlueprintEditor) { BlueprintEditor = InBlueprintEditor; SimulationWidgetRequested = 0; bIsInSelection = false; TSharedPtr FactoryPinned = WidgetDesignerNavigation::NavigationExtensionFactory.Pin(); if (ensure(FactoryPinned)) { FactoryPinned->AddDesigner(GetBlueprint(), this); } ISlateReflectorModule& ReflectorModule = FModuleManager::LoadModuleChecked(WidgetDesignerNavigation::SlateReflectorModuleName); FNavigationEventSimulationViewArgs ViewArgs; ViewArgs.OnWidgetSelected = FSimpleWidgetDelegate::CreateSP(this, &SWidgetDesignerNavigation::HandleSelectWidget); ViewArgs.OnNavigateToWidget = FSimpleWidgetDelegate::CreateSP(this, &SWidgetDesignerNavigation::HandleSelectWidget); NavigationEventSimulationView = ReflectorModule.CreateNavigationEventSimulationView(ViewArgs); BlueprintEditor.Pin()->OnSelectedWidgetsChanged.AddRaw(this, &SWidgetDesignerNavigation::HandleEditorSelectionChanged); FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked(TEXT("PropertyEditor")); FDetailsViewArgs DetailsViewArgs; DetailsViewArgs.bShowPropertyMatrixButton = false; DetailsViewArgs.bShowOptions = false; DetailsViewArgs.bShowModifiedPropertiesOption = false; DetailsViewArgs.bAllowFavoriteSystem = false; DetailsViewArgs.bAllowSearch = false; DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea; FStructureDetailsViewArgs StructureDetailsViewArgs; TSharedPtr StructOnScope = MakeShared(FNavigationSimulationArguments::StaticStruct(), (uint8*)&SimulationArguments); TSharedRef DetailView = PropertyEditorModule.CreateStructureDetailView(DetailsViewArgs, StructureDetailsViewArgs, StructOnScope); ChildSlot [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SNew(SVerticalBox) + SVerticalBox::Slot() .Padding(4.0f) .FillHeight(1.f) [ NavigationEventSimulationView.ToSharedRef() ] + SVerticalBox::Slot() .Padding(4.0f) .AutoHeight() [ DetailView->GetWidget().ToSharedRef() ] + SVerticalBox::Slot() .Padding(4.0f) .AutoHeight() [ SNew(SButton) .ContentPadding(3.0f) .OnClicked(this, &SWidgetDesignerNavigation::HandleRefreshClicked) .IsEnabled(this, &SWidgetDesignerNavigation::HandleRefreshEnabled) .Content() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ SNew(STextBlock) .Font(FAppStyle::Get().GetFontStyle("FontAwesome.10")) .Text(FEditorFontGlyphs::Refresh) ] + SHorizontalBox::Slot() .Padding(4.f, 0.f, 0.f, 0.f) .FillWidth(1.f) [ SNew(STextBlock) .Text(LOCTEXT("Refresh", "Refresh")) ] ] ] ] ]; } SWidgetDesignerNavigation::~SWidgetDesignerNavigation() { TSharedPtr FactoryPinned = WidgetDesignerNavigation::NavigationExtensionFactory.Pin(); if (ensure(FactoryPinned)) { FactoryPinned->RemoveDesigner(this); } if (TSharedPtr BlueprintEditorPinned = BlueprintEditor.Pin()) { BlueprintEditorPinned->OnSelectedWidgetsChanged.RemoveAll(this); } } UWidgetBlueprint* SWidgetDesignerNavigation::GetBlueprint() const { TSharedPtr BlueprintEditorPinned = BlueprintEditor.Pin(); return BlueprintEditorPinned ? CastChecked(BlueprintEditorPinned->GetBlueprintObj()) : nullptr; } void SWidgetDesignerNavigation::HandleWidgetPreviewUpdated(TSharedRef NewContent) { ++SimulationWidgetRequested; TWeakPtr AsWeak = SharedThis(this); TWeakPtr WeakRequest = NewContent; AsyncTask(ENamedThreads::GameThread, [AsWeak, WeakRequest]() { if (TSharedPtr AsPinned = AsWeak.Pin()) { --(AsPinned->SimulationWidgetRequested); if (AsPinned->SimulationWidgetRequested == 0) { if (TSharedPtr RequestPin = WeakRequest.Pin()) { AsPinned->RunNewSimulation(RequestPin.ToSharedRef()); } else { AsPinned->ClearSimulationResult(); } } } }); } void SWidgetDesignerNavigation::RunNewSimulation(TSharedRef NewContent) { if (!FSlateApplication::IsInitialized() || !FApp::CanEverRender()) { LastSimulationContent.Reset(); return; } LastSimulationContent = NewContent; // Create a virtual window to create a hit test grid const FVector2D WidgetSize{ 1024, 1024 }; const float Scale = 1.f; const float DeltaTime = 0.1f; TSharedPtr OldParent = NewContent->GetParentWidget(); TSharedRef VirtualWindow = SNew(SVirtualWindow) .Size(WidgetSize); VirtualWindow->AddMetadata(MakeShared(TEXT("Simulation Window"), nullptr, nullptr, nullptr)); VirtualWindow->SetContent(NewContent); FWidgetBlueprintEditorUtils::UpdateHittestGrid(VirtualWindow->GetHittestGrid(), VirtualWindow, Scale, WidgetSize, DeltaTime); // Find the content inside the virtual window FWidgetPath FoundWidgetPath; { TArray> SearchWindow; SearchWindow.Add(VirtualWindow); FSlateWindowHelper::FindPathToWidget(SearchWindow, NewContent, FoundWidgetPath); } // Run the simulation TArray SimulationResult; if (FoundWidgetPath.IsValid()) { FSlateNavigationEventSimulator* Simulator = FModuleManager::GetModulePtr(WidgetDesignerNavigation::SlateReflectorModuleName)->GetNavigationEventSimulator(); check(Simulator); if (SimulationArguments.bOverrideUINavigation) { SimulationResult = Simulator->SimulateForEachWidgets(FoundWidgetPath, SimulationArguments.UserIndex, SimulationArguments.NavigationGenesis, SimulationArguments.UINavigation); } else { SimulationResult = Simulator->SimulateForEachWidgets(FoundWidgetPath, SimulationArguments.UserIndex, SimulationArguments.NavigationGenesis, FSlateNavigationEventSimulator::ENavigationStyle::FourCardinalDirections); } } NavigationEventSimulationView->SetSimulationResult(SimulationResult); // Reset the old parent if (OldParent.IsValid()) { NewContent->AssignParentWidget(OldParent); } } void SWidgetDesignerNavigation::ClearSimulationResult() { TArray SimulationResult; NavigationEventSimulationView->SetSimulationResult(SimulationResult); } void SWidgetDesignerNavigation::HandleEditorSelectionChanged() { if (bIsInSelection) { return; } TGuardValue Tmp(bIsInSelection, true); if (TSharedPtr BlueprintEditorPinned = BlueprintEditor.Pin()) { const TSet& SelectedWidgetReferences = BlueprintEditorPinned->GetSelectedWidgets(); if (SelectedWidgetReferences.Num() > 0) { auto It = SelectedWidgetReferences.CreateConstIterator(); const FWidgetReference& WidgetReference = *It; NavigationEventSimulationView->SelectWidget(WidgetReference.GetPreviewSlate()); } else { NavigationEventSimulationView->SelectWidget(TSharedPtr()); } } } void SWidgetDesignerNavigation::HandleSelectWidget(TWeakPtr WeakWidget) { if (bIsInSelection) { return; } TGuardValue Tmp(bIsInSelection, true); TSharedPtr BlueprintEditorPinned = BlueprintEditor.Pin(); TSharedPtr WidgetPinned = WeakWidget.Pin(); if (BlueprintEditorPinned && WidgetPinned) { if (UUserWidget* PreviewUserWidget = BlueprintEditorPinned->GetPreview()) { if (UWidget* UMGWidget = PreviewUserWidget->GetWidgetHandle(ConstCastSharedRef(WidgetPinned.ToSharedRef()))) { FWidgetReference Reference = BlueprintEditorPinned->GetReferenceFromPreview(UMGWidget); if (Reference.IsValid()) { TSet WidgetReferences; WidgetReferences.Add(Reference); BlueprintEditorPinned->SelectWidgets(WidgetReferences, false); } } } } } void SWidgetDesignerNavigation::HandlePaint(const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId) { if (SimulationArguments.bShowPreview) { if (TSharedPtr BlueprintEditorPinned = BlueprintEditor.Pin()) { if (UUserWidget* PreviewUserWidget = BlueprintEditorPinned->GetPreview()) { NavigationEventSimulationView->PaintSimuationResult(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId); } } } } FReply SWidgetDesignerNavigation::HandleRefreshClicked() { if (SimulationWidgetRequested == 0) { if (TSharedPtr NewConent = LastSimulationContent.Pin()) { HandleWidgetPreviewUpdated(NewConent.ToSharedRef()); } } return FReply::Handled(); } bool SWidgetDesignerNavigation::HandleRefreshEnabled() const { return LastSimulationContent.IsValid(); } #undef LOCTEXT_NAMESPACE