// Copyright Epic Games, Inc. All Rights Reserved. #include "Framework/Application/MenuStack.h" #include "Layout/LayoutUtils.h" #include "Layout/WidgetPath.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SPopup.h" #include "Framework/Application/Menu.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Images/SImage.h" #define LOCTEXT_NAMESPACE "MenuStack" namespace FMenuStackDefs { /** Maximum size of menus as a fraction of the work area height */ const float MaxMenuScreenHeightFraction = 0.8f; const float AnimationDuration = 0.15f; } #if WITH_SLATE_DEBUGGING FAutoConsoleVariable CVarSlateDismissMenuStacksOnFocusLost( TEXT("Slate.DismissMenuStacksOnFocusLost"), true, TEXT("Should open menus disappear when whey lose focus?") ); #endif /** Overlay widget class used to hold menu contents and display them on top of the current window */ class SMenuPanel : public SOverlay { public: SLATE_BEGIN_ARGS(SMenuPanel) { _Visibility = EVisibility::SelfHitTestInvisible; } SLATE_END_ARGS() void Construct(const FArguments& InArgs) { SOverlay::Construct(SOverlay::FArguments()); } void PushMenu(TSharedRef InMenu, const FVector2f& InLocation) { check(InMenu->GetContent().IsValid()); TSharedPtr ParentWindow = InMenu->GetParentWindow(); check(ParentWindow.IsValid()); // Transform InLocation into a position local to this panel (assumes the panel is in an overlay that covers the whole of the panel window) FVector2f PanelInScreen = ParentWindow->GetRectInScreen().GetTopLeft(); FVector2f PanelInWindow = ParentWindow->GetLocalToScreenTransform().Inverse().TransformPoint(PanelInScreen); FVector2f LocationInWindow = ParentWindow->GetLocalToScreenTransform().Inverse().TransformPoint(InLocation); FVector2f LocationInPanel = LocationInWindow - PanelInWindow; // Add the new menu into a slot on this panel and set the padding so that its position is correct AddSlot() .HAlign(HAlign_Left) .VAlign(VAlign_Top) .Padding(LocationInPanel.X, LocationInPanel.Y, 0, 0) [ InMenu->GetContent().ToSharedRef() ]; // Make sure that the menu will remove itself from the panel when dismissed InMenu->GetOnMenuDismissed().AddSP(this, &SMenuPanel::OnMenuClosed); } void OnMenuClosed(TSharedRef InMenu) { RemoveSlot(InMenu->GetContent().ToSharedRef()); } }; namespace MenuStackInternal { /** Widget that wraps any menu created in FMenuStack to provide default key handling, focus tracking and helps us spot menus in widget paths */ DECLARE_DELEGATE_RetVal_OneParam(FReply, FOnKeyDown, FKey) DECLARE_DELEGATE_OneParam(FOnMenuLostFocus, const FWidgetPath&) class SMenuContentWrapper : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SMenuContentWrapper) : _MenuContent() , _OnKeyDown() , _OptionalMinMenuWidth() , _OptionalMinMenuHeight() , _bShowBackground(true) {} SLATE_DEFAULT_SLOT(FArguments, MenuContent) SLATE_EVENT(FOnKeyDown, OnKeyDown) SLATE_EVENT(FOnMenuLostFocus, OnMenuLostFocus) SLATE_ARGUMENT(FOptionalSize, OptionalMinMenuWidth) SLATE_ARGUMENT(FOptionalSize, OptionalMinMenuHeight) SLATE_ARGUMENT(bool, bShowBackground) SLATE_END_ARGS() /** Construct this widget */ void Construct(const FArguments& InArgs) { // The visibility of the content wrapper should match that of the provided content SetVisibility(AccessWidgetVisibilityAttribute(InArgs._MenuContent.Widget)); OnKeyDownDelegate = InArgs._OnKeyDown; OnMenuLostFocus = InArgs._OnMenuLostFocus; TSharedPtr ChildContent; if (InArgs._bShowBackground) { // Always add a background to the menu. This includes a small outline around the background to distinguish open menus from each other ChildContent = SNew(SOverlay) + SOverlay::Slot() [ SNew(SImage) .Image(FCoreStyle::Get().GetBrush("Menu.Background")) ] + SOverlay::Slot() [ SNew(SImage) .Image(FCoreStyle::Get().GetOptionalBrush("Menu.Outline", nullptr)) ] + SOverlay::Slot() [ SNew(SBorder) .Padding(0.f) .BorderImage(FStyleDefaults::GetNoBrush()) .ForegroundColor(FCoreStyle::Get().GetSlateColor("DefaultForeground")) [ InArgs._MenuContent.Widget ] ]; } else { ChildContent = SNew(SOverlay) + SOverlay::Slot() [ SNew(SBorder) .Padding(0.f) .BorderImage(FStyleDefaults::GetNoBrush()) .ForegroundColor(FCoreStyle::Get().GetSlateColor("DefaultForeground")) [ InArgs._MenuContent.Widget ] ]; } ChildSlot [ SNew(SBox) .MinDesiredWidth(InArgs._OptionalMinMenuWidth) .MaxDesiredHeight(InArgs._OptionalMinMenuHeight) [ ChildContent.ToSharedRef() ] ]; } virtual void OnFocusChanging(const FWeakWidgetPath& PreviousFocusPath, const FWidgetPath& NewWidgetPath, const FFocusEvent& InFocusEvent) override { // if focus changed and this menu content had focus (or one of its children did) then inform the stack via the OnMenuLostFocus event if (OnMenuLostFocus.IsBound() && PreviousFocusPath.ContainsWidget(this)) { return OnMenuLostFocus.Execute(NewWidgetPath); } } private: /** This widget must support keyboard focus */ virtual bool SupportsKeyboardFocus() const override { return true; } virtual FReply OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent ) override { if (OnKeyDownDelegate.IsBound()) { return OnKeyDownDelegate.Execute(InKeyEvent.GetKey()); } return FReply::Unhandled(); } /** Delegate to forward keys down events on the menu */ FOnKeyDown OnKeyDownDelegate; /** Delegate to inform the stack that a menu has lost focus and might need to be closed */ FOnMenuLostFocus OnMenuLostFocus; }; /** Global handler used to handle key presses on popup menus */ FReply OnMenuKeyDown(const FKey Key) { if (Key == EKeys::Escape) { FSlateApplication::Get().DismissAllMenus(); return FReply::Handled().ClearUserFocus(); } return FReply::Unhandled(); } FSimpleDelegate MenuStackPushDebuggingInfo; } // anon namespace TSharedRef FMenuStack::Push(const FWidgetPath& InOwnerPath, const TSharedRef& InContent, const UE::Slate::FDeprecateVector2DParameter& SummonLocation, const FPopupTransitionEffect& TransitionEffect, const bool bFocusImmediately, const UE::Slate::FDeprecateVector2DParameter& SummonLocationSize, TOptional InMethod, const bool bIsCollapsedByParent, const bool bEnablePerPixelTransparency, const int32 FocusUserIndex) { // We want to ensure that when the window is restored, we restore the current keyboard focus, // but only if it is valid, otherwise we could end up clearing a previously valid path. TSharedPtr FocusedWidget = (FocusUserIndex == INDEX_NONE) ? FSlateApplication::Get().GetKeyboardFocusedWidget() : FSlateApplication::Get().GetUserFocusedWidget(FocusUserIndex); if (FocusedWidget.IsValid()) { InOwnerPath.GetWindow()->SetWidgetToFocusOnActivate(FocusedWidget); } FSlateRect Anchor(SummonLocation, SummonLocation + SummonLocationSize); TSharedPtr ParentMenu; if (HasMenus()) { // Find a menu in the stack in InOwnerPath to determine the level to insert this ParentMenu = FindMenuInWidgetPath(InOwnerPath); check(HostWindow.IsValid()); } if (!ParentMenu.IsValid()) { // pushing a new root menu (leave ParentMenu invalid) // The active method is determined when a new root menu is pushed ActiveMethod = InMethod.IsSet() ? FPopupMethodReply::UseMethod(InMethod.GetValue()) : QueryPopupMethod(InOwnerPath); // The host window is determined when a new root menu is pushed // This must be set prior to PushInternal below, as it will be referenced if the menu being created is a new root menu. SetHostPath(InOwnerPath); } MenuStackInternal::MenuStackPushDebuggingInfo.Unbind(); #if WITH_EDITOR // If there is an error, this delegate will be called to print out additional details if (InOwnerPath.Widgets.Num() > 0) { // Copy data in case this array is modified TArray DebugSourceFilenames; DebugSourceFilenames.Reserve(InOwnerPath.Widgets.Num()); for (int32 i=0; i < InOwnerPath.Widgets.Num(); ++i) { DebugSourceFilenames.Add(InOwnerPath.Widgets[i].Widget->GetCreatedInLocation()); } MenuStackInternal::MenuStackPushDebuggingInfo.BindLambda([DebugSourceFilenames]() { for (const FName SourceFilename : DebugSourceFilenames) { UE_LOG(LogSlate, Warning, TEXT(" WidgetSourceFile: %s"), *SourceFilename.ToString()); } }); } #endif TGuardValue Guard(bHostWindowGuard, true); return PushInternal(ParentMenu, InContent, Anchor, TransitionEffect, bFocusImmediately, ActiveMethod.GetShouldThrottle(), bIsCollapsedByParent, bEnablePerPixelTransparency, FocusUserIndex); } TSharedRef FMenuStack::Push(const TSharedPtr& InParentMenu, const TSharedRef& InContent, const UE::Slate::FDeprecateVector2DParameter& SummonLocation, const FPopupTransitionEffect& TransitionEffect, const bool bFocusImmediately, const UE::Slate::FDeprecateVector2DParameter& SummonLocationSize, const bool bIsCollapsedByParent, const bool bEnablePerPixelTransparency, const int32 FocusUserIndex) { check(Stack.Contains(InParentMenu)); check(HostWindow.IsValid()); FSlateRect Anchor(SummonLocation, SummonLocation + SummonLocationSize); return PushInternal(InParentMenu, InContent, Anchor, TransitionEffect, bFocusImmediately, EShouldThrottle::Yes, bIsCollapsedByParent, bEnablePerPixelTransparency, FocusUserIndex); } TSharedRef FMenuStack::PushHosted(const FWidgetPath& InOwnerPath, const TSharedRef& InMenuHost, const TSharedRef& InContent, TSharedPtr& OutWrappedContent, const FPopupTransitionEffect& TransitionEffect, EShouldThrottle ShouldThrottle, const bool bIsCollapsedByParent) { TSharedPtr ParentMenu; if (HasMenus()) { // Find a menu in the stack in InOwnerPath to determine the level to insert this ParentMenu = FindMenuInWidgetPath(InOwnerPath); check(HostWindow.IsValid()); } if (!ParentMenu.IsValid()) { // pushing a new root menu (leave ParentMenu invalid) // The active method is determined when a new root menu is pushed and hosted menus are always UseCurrentWindow ActiveMethod = FPopupMethodReply::UseMethod(EPopupMethod::UseCurrentWindow); // The host window is determined when a new root menu is pushed SetHostPath(InOwnerPath); } return PushHosted(ParentMenu, InMenuHost, InContent, OutWrappedContent, TransitionEffect, ShouldThrottle); } TSharedRef FMenuStack::PushHosted(const TSharedPtr& InParentMenu, const TSharedRef& InMenuHost, const TSharedRef& InContent, TSharedPtr& OutWrappedContent, const FPopupTransitionEffect& TransitionEffect, EShouldThrottle ShouldThrottle, const bool bIsCollapsedByParent) { check(HostWindow.IsValid()); // Create a FMenuInHostWidget TSharedRef WrappedContent = WrapContent(InContent, FOptionalSize(), FOptionalSize(), InMenuHost->bShowMenuBackground); TSharedRef OutMenu = MakeShareable(new FMenuInHostWidget(InMenuHost, WrappedContent, bIsCollapsedByParent)); PendingNewMenu = OutMenu; // Set the returned content - this must be drawn by the hosting widget until the menu gets dismissed and calls IMenuHost::OnMenuDismissed on its host OutWrappedContent = WrappedContent; // Register to get a callback when it's dismissed - to fixup stack OutMenu->GetOnMenuDismissed().AddRaw(this, &FMenuStack::OnMenuDestroyed); PostPush(InParentMenu, OutMenu, ShouldThrottle); PendingNewMenu.Reset(); return OutMenu; } TSharedRef FMenuStack::PushInternal(const TSharedPtr& InParentMenu, const TSharedRef& InContent, FSlateRect Anchor, const FPopupTransitionEffect& TransitionEffect, const bool bFocusImmediately, EShouldThrottle ShouldThrottle, const bool bIsCollapsedByParent, const bool bEnablePerPixelTransparency, const int32 FocusUserIndex) { FPrePushArgs PrePushArgs; PrePushArgs.Content = InContent; PrePushArgs.Anchor = Anchor; PrePushArgs.TransitionEffect = TransitionEffect; PrePushArgs.FocusUserIndex = FocusUserIndex; PrePushArgs.bFocusImmediately = bFocusImmediately; PrePushArgs.bIsCollapsedByParent = bIsCollapsedByParent; // Pre-push stage // Determines correct layout // Wraps content // Other common setup steps needed by all (non-hosted) menus const FPrePushResults PrePushResults = PrePush(PrePushArgs); // Menu object creation stage TSharedRef OutMenu = ActiveMethod.GetPopupMethod() == EPopupMethod::CreateNewWindow ? PushNewWindow(InParentMenu, PrePushResults, bEnablePerPixelTransparency, FocusUserIndex) : PushPopup(InParentMenu, PrePushResults, FocusUserIndex); // Post-push stage // Updates the stack and content map member variables const bool bInInsertAfterDismiss = ActiveMethod.GetPopupMethod() == EPopupMethod::CreateNewWindow; PostPush(InParentMenu, OutMenu, ShouldThrottle, bInInsertAfterDismiss); PendingNewMenu.Reset(); return OutMenu; } FMenuStack::FPrePushResults FMenuStack::PrePush(const FPrePushArgs& InArgs) { FPrePushResults OutResults; OutResults.bIsCollapsedByParent = InArgs.bIsCollapsedByParent; OutResults.bFocusImmediately = InArgs.bFocusImmediately; if (InArgs.bFocusImmediately) { OutResults.WidgetToFocus = InArgs.Content; } // Only enable window position/size transitions if we're running at a decent frame rate OutResults.bAllowAnimations = FSlateApplication::Get().AreMenuAnimationsEnabled() && FSlateApplication::Get().IsRunningAtTargetFrameRate(); // Calc the max height available on screen for the menu float MaxHeight; const float ApplicationScale = FSlateApplication::Get().GetApplicationScale() * HostWindow->GetNativeWindow()->GetDPIScaleFactor(); if (ActiveMethod.GetPopupMethod() == EPopupMethod::CreateNewWindow) { FSlateRect WorkArea = FSlateApplication::Get().GetWorkArea(InArgs.Anchor); MaxHeight = FMenuStackDefs::MaxMenuScreenHeightFraction * WorkArea.GetSize().Y / ApplicationScale; } else { MaxHeight = FMenuStackDefs::MaxMenuScreenHeightFraction * HostWindow->GetClientSizeInScreen().Y / ApplicationScale; } bool bAnchorSetsMinWidth = InArgs.TransitionEffect.SlideDirection == FPopupTransitionEffect::ComboButton; // Wrap menu content in a box needed for various sizing and tracking purposes FOptionalSize OptionalMinWidth = bAnchorSetsMinWidth ? InArgs.Anchor.GetSize().X : FOptionalSize(); FOptionalSize OptionalMinHeight = MaxHeight; // Wrap content in an SPopup before the rest of the wrapping process - should make menus appear on top using deferred presentation TSharedRef TempContent = SNew(SPopup)[InArgs.Content.ToSharedRef()]; OutResults.WrappedContent = WrapContent(TempContent, OptionalMinWidth, OptionalMinHeight); OutResults.WrappedContent->SlatePrepass(ApplicationScale); // @todo slate: Doesn't take into account potential window border size OutResults.ExpectedSize = OutResults.WrappedContent->GetDesiredSize() * ApplicationScale; EOrientation Orientation = (InArgs.TransitionEffect.SlideDirection == FPopupTransitionEffect::SubMenu) ? Orient_Horizontal : Orient_Vertical; // Calculate the correct position for the menu based on the popup method if (ActiveMethod.GetPopupMethod() == EPopupMethod::CreateNewWindow) { // already handled const bool bAutoAdjustForDPIScale = false; // Places the menu's window in the work area OutResults.AnimStartLocation = OutResults.AnimFinalLocation = FSlateApplication::Get().CalculatePopupWindowPosition(InArgs.Anchor, OutResults.ExpectedSize, bAutoAdjustForDPIScale, FVector2f::ZeroVector, Orientation); } else { // Places the menu's content in the host window const FVector2f ProposedPlacement( Orientation == Orient_Horizontal ? InArgs.Anchor.Right : InArgs.Anchor.Left, Orientation == Orient_Horizontal ? InArgs.Anchor.Top : InArgs.Anchor.Bottom); OutResults.AnimStartLocation = OutResults.AnimFinalLocation = ComputePopupFitInRect(InArgs.Anchor, FSlateRect(ProposedPlacement, ProposedPlacement + OutResults.ExpectedSize), Orientation, HostWindow->GetClientRectInScreen()); } // Start the pop-up menu at an offset location, then animate it to its target location over time // @todo: Anims aren't supported or attempted - this is legacy code left in in case we reinstate menu anims if (OutResults.bAllowAnimations) { const bool bSummonRight = OutResults.AnimFinalLocation.X >= OutResults.AnimStartLocation.X; const bool bSummonBelow = OutResults.AnimFinalLocation.Y >= OutResults.AnimStartLocation.Y; const int32 SummonDirectionX = bSummonRight ? 1 : -1; const int32 SummonDirectionY = bSummonBelow ? 1 : -1; switch (InArgs.TransitionEffect.SlideDirection) { case FPopupTransitionEffect::None: // No sliding break; case FPopupTransitionEffect::ComboButton: OutResults.AnimStartLocation.Y = FMath::Max(OutResults.AnimStartLocation.Y + 30.0f * SummonDirectionY, 0.0f); break; case FPopupTransitionEffect::TopMenu: OutResults.AnimStartLocation.Y = FMath::Max(OutResults.AnimStartLocation.Y + 60.0f * SummonDirectionY, 0.0f); break; case FPopupTransitionEffect::SubMenu: OutResults.AnimStartLocation.X += 60.0f * SummonDirectionX; break; case FPopupTransitionEffect::TypeInPopup: OutResults.AnimStartLocation.Y = FMath::Max(OutResults.AnimStartLocation.Y + 30.0f * SummonDirectionY, 0.0f); break; case FPopupTransitionEffect::ContextMenu: OutResults.AnimStartLocation.X += 30.0f * SummonDirectionX; OutResults.AnimStartLocation.Y += 50.0f * SummonDirectionY; break; } } // Release the mouse so that context can be properly restored upon closing menus. See CL 1411833 before changing this. if (InArgs.bFocusImmediately) { if (InArgs.FocusUserIndex == INDEX_NONE) { FSlateApplication::Get().ReleaseAllPointerCapture(); } else { FSlateApplication::Get().ReleaseAllPointerCapture(InArgs.FocusUserIndex); } } return OutResults; } TSharedRef FMenuStack::PushNewWindow(TSharedPtr InParentMenu, const FPrePushResults& InPrePushResults, const bool bEnablePerPixelTransparency, const int32 FocusUserIndex) { check(ActiveMethod.GetPopupMethod() == EPopupMethod::CreateNewWindow); // Start pop-up windows out transparent, then fade them in over time #if ALPHA_BLENDED_WINDOWS const EWindowTransparency Transparency(bEnablePerPixelTransparency ? EWindowTransparency::PerPixel : InPrePushResults.bAllowAnimations ? EWindowTransparency::PerWindow : EWindowTransparency::None); #else const EWindowTransparency Transparency(InPrePushResults.bAllowAnimations ? EWindowTransparency::PerWindow : EWindowTransparency::None); #endif const float InitialWindowOpacity = InPrePushResults.bAllowAnimations ? 0.0f : 1.0f; const float TargetWindowOpacity = 1.0f; // Create a new window for the menu TSharedRef NewMenuWindow = SNew(SWindow) .Type(EWindowType::Menu) .IsPopupWindow(true) .SizingRule(ESizingRule::Autosized) .ScreenPosition(InPrePushResults.AnimStartLocation) .AutoCenter(EAutoCenter::None) .ClientSize(InPrePushResults.ExpectedSize) .AdjustInitialSizeAndPositionForDPIScale(false) .InitialOpacity(InitialWindowOpacity) .SupportsTransparency(Transparency) .FocusWhenFirstShown(InPrePushResults.bFocusImmediately) .FocusUserIndex(FocusUserIndex) .ActivationPolicy(InPrePushResults.bFocusImmediately ? EWindowActivationPolicy::Always : EWindowActivationPolicy::Never) [ InPrePushResults.WrappedContent.ToSharedRef() ]; PendingNewWindow = NewMenuWindow; if (InPrePushResults.bFocusImmediately && InPrePushResults.WidgetToFocus.IsValid()) { // Focus the unwrapped content rather than just the window NewMenuWindow->SetWidgetToFocusOnActivate(InPrePushResults.WidgetToFocus); } TSharedRef Menu = MakeShareable(new FMenuInWindow(NewMenuWindow, InPrePushResults.WrappedContent.ToSharedRef(), InPrePushResults.bIsCollapsedByParent)); PendingNewMenu = Menu; TSharedPtr ParentWindow; if (InParentMenu.IsValid()) { ParentWindow = InParentMenu->GetParentWindow(); } else { ParentWindow = HostWindow; } FSlateApplication::Get().AddWindowAsNativeChild(NewMenuWindow, ParentWindow.ToSharedRef(), true); // Kick off the intro animation! // @todo: Anims aren't supported or attempted - this is legacy code left in in case we reinstate menu anims if (InPrePushResults.bAllowAnimations) { FCurveSequence Sequence; Sequence.AddCurve(0, FMenuStackDefs::AnimationDuration, ECurveEaseFunction::CubicOut); NewMenuWindow->MorphToPosition(Sequence, TargetWindowOpacity, InPrePushResults.AnimFinalLocation); } PendingNewWindow.Reset(); return Menu; } TSharedRef FMenuStack::PushPopup(TSharedPtr InParentMenu, const FPrePushResults& InPrePushResults, const int32 FocusUserIndex) { check(ActiveMethod.GetPopupMethod() == EPopupMethod::UseCurrentWindow); // Create a FMenuInPopup check(InPrePushResults.WrappedContent.IsValid()); TSharedRef Menu = MakeShareable(new FMenuInPopup(InPrePushResults.WrappedContent.ToSharedRef(), InPrePushResults.bIsCollapsedByParent)); PendingNewMenu = Menu; // Register to get callback when it's dismissed - to fixup stack Menu->GetOnMenuDismissed().AddRaw(this, &FMenuStack::OnMenuDestroyed); // Add it to a slot on the menus panel widget HostWindowPopupPanel->PushMenu(Menu, InPrePushResults.AnimFinalLocation); if (InPrePushResults.bFocusImmediately && InPrePushResults.WidgetToFocus.IsValid()) { if (FocusUserIndex == INDEX_NONE) { FSlateApplication::Get().SetKeyboardFocus(InPrePushResults.WidgetToFocus, EFocusCause::SetDirectly); } else { FSlateApplication::Get().SetUserFocus(FocusUserIndex, InPrePushResults.WidgetToFocus, EFocusCause::SetDirectly); } } return Menu; } void FMenuStack::PostPush(TSharedPtr InParentMenu, TSharedRef InMenu, EShouldThrottle ShouldThrottle, bool bInInsertAfterDismiss) { // Determine at which index we insert this new menu in the stack int32 InsertIndex = 0; if (InParentMenu.IsValid()) { int32 ParentIndex = Stack.IndexOfByKey(InParentMenu); check(ParentIndex != INDEX_NONE); InsertIndex = ParentIndex + 1; } // Do original behavior of insert before dismiss // Note: This will often crash because DismissFrom() may trigger FMenuStack::OnWindowActivated() then DismissAll() that empties Stack int32 RemovingAtIndex = InsertIndex; if (!bInInsertAfterDismiss) { Stack.Insert(InMenu, InsertIndex); CachedContentMap.Add(InMenu->GetContent(), InMenu); RemovingAtIndex = InsertIndex + 1; } // Dismiss menus after the insert point if (Stack.Num() > RemovingAtIndex) { // Note: DismissFrom() may trigger FMenuStack::OnWindowActivated() then DismissAll() that empties Stack DismissFrom(Stack[RemovingAtIndex]); // tidy the stack data now (it will happen via callbacks from the dismissed menus but that might be delayed) for (int32 StackIndex = Stack.Num() - 1; StackIndex >= RemovingAtIndex; --StackIndex) { CachedContentMap.Remove(Stack[StackIndex]->GetContent()); Stack.RemoveAt(StackIndex); } } // Note: DismissFrom() above may trigger FMenuStack::OnWindowActivated() then DismissAll() that empties Stack // Insert menu after the dismiss when possible to avoid menu being deleted if (bInInsertAfterDismiss) { if (InsertIndex != Stack.Num()) { MenuStackInternal::MenuStackPushDebuggingInfo.ExecuteIfBound(); // A menu was unexpectedly removed or added UE_LOG(LogSlate, Warning, TEXT("A menu was unexpectedly removed or added. InsertIndex:%d, Stack.Num:%d, InParentMenu.IsValid:%d"), InsertIndex, Stack.Num(), InParentMenu.IsValid()); } Stack.Add(InMenu); CachedContentMap.Add(InMenu->GetContent(), InMenu); } // When a new menu is pushed, if we are not already in responsive mode for Slate UI, enter it now // to ensure the menu is responsive in low FPS situations if (!ThrottleHandle.IsValid() && ShouldThrottle == EShouldThrottle::Yes) { ThrottleHandle = FSlateThrottleManager::Get().EnterResponsiveMode(); } } FPopupMethodReply FMenuStack::QueryPopupMethod(const FWidgetPath& PathToQuery) { for (int32 WidgetIndex = PathToQuery.Widgets.Num() - 1; WidgetIndex >= 0; --WidgetIndex) { FPopupMethodReply PopupMethod = PathToQuery.Widgets[WidgetIndex].Widget->OnQueryPopupMethod(); if (PopupMethod.IsEventHandled()) { return PopupMethod; } } return FPopupMethodReply::UseMethod(EPopupMethod::CreateNewWindow); } void FMenuStack::DismissFrom(const TSharedPtr& InFromMenu) { int32 Index = Stack.IndexOfByKey(InFromMenu); if (Index != INDEX_NONE) { DismissInternal(Index); } } void FMenuStack::DismissAll() { const int32 TopLevel = 0; DismissInternal(TopLevel); } void FMenuStack::DismissInternal(int32 FirstStackIndexToRemove) { // Dismiss the stack in reverse order so we destroy children before parents (causes focusing issues if done the other way around) for ( int32 StackIndex = Stack.Num() - 1; StackIndex >= FirstStackIndexToRemove; --StackIndex ) { if ( Stack.IsValidIndex(StackIndex) ) { Stack[StackIndex]->Dismiss(); } } } void FMenuStack::SetHostPath(const FWidgetPath& InOwnerPath) { if (bHostWindowGuard) { return; } if ( HostPopupLayer.IsValid() ) { if ( !InOwnerPath.ContainsWidget(&HostPopupLayer->GetHost().Get()) ) { HostPopupLayer->Remove(); HostPopupLayer.Reset(); HostWindowPopupPanel.Reset(); } } HostWindow = InOwnerPath.IsValid() ? InOwnerPath.GetWindow() : TSharedPtr(); HostWidget = InOwnerPath.IsValid() ? InOwnerPath.GetLastWidget() : TWeakPtr(); if ( HostWindow.IsValid() && !HostWindowPopupPanel.IsValid() ) { TSharedRef NewHostWindowPopupPanel = SNew(SMenuPanel); for ( int i = InOwnerPath.Widgets.Num() - 1; i >= 0; i-- ) { const TSharedRef& CurrentWidget = InOwnerPath.Widgets[i].Widget; HostPopupLayer = CurrentWidget->OnVisualizePopup(NewHostWindowPopupPanel); if ( HostPopupLayer.IsValid() ) { HostWindowPopupPanel = NewHostWindowPopupPanel; break; } } } } void FMenuStack::OnMenuDestroyed(TSharedRef InMenu) { int32 Index = Stack.IndexOfByKey(InMenu); if (Index != INDEX_NONE) { // Dismiss menus below this one for (int32 StackIndex = Stack.Num() - 1; StackIndex > Index; --StackIndex) { Stack[StackIndex]->Dismiss(); // this will cause OnMenuDestroyed() to re-enter } if (MenuDestroyedEvent.IsBound()) { MenuDestroyedEvent.Broadcast(InMenu); } // Clean up the stack and content map arrays for (int32 StackIndex = Stack.Num() - 1; StackIndex >= Index; --StackIndex) { CachedContentMap.Remove(Stack[StackIndex]->GetContent()); Stack.RemoveAt(StackIndex); } // Leave responsive mode once the last menu closes if (Stack.Num() == 0) { if (ThrottleHandle.IsValid()) { FSlateThrottleManager::Get().LeaveResponsiveMode(ThrottleHandle); } SetHostPath(FWidgetPath()); } } } void FMenuStack::OnMenuContentLostFocus(const FWidgetPath& InFocussedPath) { // In UseCurrentWindow mode we must look for focus moving from menus // Window activation messages will make menus collapse when in CreateNewWindow mode // However, we cannot rely on window activation messages because they might not be generated on Mac. // So, always do this focus/collapse code, even in CreateNewWindow mode. if (HasMenus() && !PendingNewMenu.IsValid()) { // If focus is switching determine which of our menus it is in, if any TSharedPtr FocussedMenu = FindMenuInWidgetPath(InFocussedPath); if (FocussedMenu.IsValid()) { // dismiss menus below FocussedMenu int32 FocussedIndex = Stack.IndexOfByKey(FocussedMenu); check(FocussedIndex != INDEX_NONE); for (int32 DismissIndex = FocussedIndex + 1; DismissIndex < Stack.Num(); DismissIndex++) { if (Stack[DismissIndex]->IsCollapsedByParent()) { DismissFrom(Stack[DismissIndex]); break; } } } else { #if WITH_SLATE_DEBUGGING if (!CVarSlateDismissMenuStacksOnFocusLost->GetBool()) { return; } #endif // Focus has moved outside all menus - collapse the stack DismissAll(); } } } TSharedRef FMenuStack::WrapContent(TSharedRef InContent, FOptionalSize OptionalMinWidth, FOptionalSize OptionalMinHeight, bool bShouldShowBackground) { // Wrap menu content in a box that limits its maximum height // and in a SMenuContentWrapper that handles key presses and focus changes. return SNew(MenuStackInternal::SMenuContentWrapper) .OnKeyDown_Static(&MenuStackInternal::OnMenuKeyDown) .OnMenuLostFocus_Raw(this, &FMenuStack::OnMenuContentLostFocus) .OptionalMinMenuWidth(OptionalMinWidth) .OptionalMinMenuHeight(OptionalMinHeight) .bShowBackground(bShouldShowBackground) .MenuContent() [ InContent ]; } TSharedPtr FMenuStack::FindMenuInWidgetPath(const FWidgetPath& PathToQuery) const { for (int32 PathIndex = PathToQuery.Widgets.Num() - 1; PathIndex >= 0; --PathIndex) { TSharedPtr Widget = PathToQuery.Widgets[PathIndex].Widget; const TSharedPtr* FoundMenu = CachedContentMap.Find(Widget); if (FoundMenu != nullptr) { return *FoundMenu; } } return TSharedPtr(); } void FMenuStack::OnWindowDestroyed(TSharedRef InWindow) { if (HostWindow == InWindow) { // If the host window is destroyed, collapse the whole stack and reset all state Stack.Empty(); CachedContentMap.Empty(); SetHostPath(FWidgetPath()); } else { // A window was requested to be destroyed, so make sure it's not in the menu stack to avoid it // becoming a parent to a freshly-created window! TSharedPtr Menu = FindMenuFromWindow(InWindow); if (Menu.IsValid()) { OnMenuDestroyed(Menu.ToSharedRef()); } } } void FMenuStack::OnWindowActivated( TSharedRef ActivatedWindow ) { if (ActivatedWindow != PendingNewWindow && HasMenus() && !FSlateApplication::Get().IsWindowHousingInteractiveTooltip(ActivatedWindow)) { TWeakPtr ActivatedMenu = FindMenuFromWindow(ActivatedWindow); if (ActivatedMenu.IsValid()) { // Dismiss menus below ActivatedMenu int32 ActivatedIndex = Stack.IndexOfByKey(ActivatedMenu); check(ActivatedIndex != INDEX_NONE); for (int32 DismissIndex = ActivatedIndex + 1; DismissIndex < Stack.Num(); DismissIndex++) { if (Stack[DismissIndex]->IsCollapsedByParent()) { DismissFrom(Stack[DismissIndex]); break; } } } else { #if WITH_SLATE_DEBUGGING if (!CVarSlateDismissMenuStacksOnFocusLost->GetBool()) { return; } #endif // Activated a window that isn't a menu - collapse the stack DismissAll(); } } } TSharedPtr FMenuStack::FindMenuFromWindow(TSharedRef InWindow) const { const TSharedPtr* FoundMenu = Stack.FindByPredicate([InWindow](TSharedPtr Menu) { return Menu->GetOwnedWindow() == InWindow; }); if (FoundMenu != nullptr) { return *FoundMenu; } return TSharedPtr(); } bool FMenuStack::GetToolTipForceFieldRect(const TSharedRef& InMenu, const FWidgetPath& InPathContainingMenu, FSlateRect& OutSlateRect) const { bool bWasSolutionFound = false; OutSlateRect = FSlateRect(0, 0, 0, 0); int32 StackLevel = Stack.IndexOfByKey(InMenu); if (StackLevel != INDEX_NONE) { for (int32 StackLevelIndex = StackLevel + 1; StackLevelIndex < Stack.Num(); ++StackLevelIndex) { TSharedPtr MenuContent = Stack[StackLevelIndex]->GetContent(); if (MenuContent.IsValid()) { FWidgetPath WidgetPath = InPathContainingMenu.GetPathDownTo(MenuContent.ToSharedRef()); if (!WidgetPath.IsValid()) { FSlateApplication::Get().GeneratePathToWidgetChecked(MenuContent.ToSharedRef(), WidgetPath); } if (WidgetPath.IsValid()) { const FGeometry& ContentGeometry = WidgetPath.Widgets.Last().Geometry; // No first time: Expand if (bWasSolutionFound) { OutSlateRect = OutSlateRect.Expand(ContentGeometry.GetLayoutBoundingRect()); } // First time: assign it // Otherwise, it would assume that the point [0,0,0,0] is part of the final rectangle, which is not the case in multiple scenarios. // E.g., if the monitor where Slate is running is not the main one or if the Slate window is restored to the right half of the monitor. else { OutSlateRect = ContentGeometry.GetLayoutBoundingRect(); bWasSolutionFound = true; } } } } } return bWasSolutionFound; } bool FMenuStack::HasMenus() const { return Stack.Num() > 0; } bool FMenuStack::HasOpenSubMenus(TSharedPtr InMenu) const { int32 StackIndex = Stack.IndexOfByKey(InMenu); return StackIndex != INDEX_NONE && StackIndex < Stack.Num() - 1; } TSharedPtr FMenuStack::GetHostWindow() const { return HostWindow; } TSharedPtr FMenuStack::GetHostWidget() const { return HostWidget.Pin(); } #undef LOCTEXT_NAMESPACE