// Copyright Epic Games, Inc. All Rights Reserved. #include "LevelViewportLayout.h" #include "Layout/Margin.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SCompoundWidget.h" #include "Widgets/SOverlay.h" #include "Framework/Docking/LayoutService.h" #include "Misc/ConfigCacheIni.h" #include "Modules/ModuleManager.h" #include "Layout/WidgetPath.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SSpacer.h" #include "Widgets/SCanvas.h" #include "Styling/AppStyle.h" #include "Editor/UnrealEdEngine.h" #include "UnrealEdGlobals.h" #include "LevelEditor.h" #include "Widgets/Docking/SDockTab.h" #include "EditorViewportTabContent.h" #include "LevelViewportLayoutEntity.h" #include "SAssetEditorViewport.h" static const FName LevelEditorModName("LevelEditor"); namespace ViewportLayoutDefs { /** How many seconds to interpolate from restored to maximized state */ static const float MaximizeTransitionTime = 0.15f; /** How many seconds to interpolate from maximized to restored state */ static const float RestoreTransitionTime = 0.2f; /** Default maximized state for new layouts - will only be applied when no config data is restoring state */ static const bool bDefaultShouldBeMaximized = true; /** Default immersive state for new layouts - will only be applied when no config data is restoring state */ static const bool bDefaultShouldBeImmersive = false; } // FLevelViewportLayout ///////////////////////////// FLevelViewportLayout::FLevelViewportLayout() : bIsTransitioning( false ), bIsReplacement( false ), bIsQueryingLayoutMetrics( false ), bIsMaximizeSupported( true ), bIsMaximized( false ), bIsImmersive( false ), bWasMaximized( false ), bWasImmersive( false ), MaximizedViewportStartPosition( FVector2D::ZeroVector ), MaximizedViewportStartSize( FVector2D::ZeroVector ) { ViewportReplacementWidget = SNew( SSpacer ); } FLevelViewportLayout::~FLevelViewportLayout() { FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked(LevelEditorModName); LevelEditor.OnTakeHighResScreenShots().RemoveAll(this); // Make sure that we're not locking the immersive window after we go away if( bIsImmersive || ( bWasImmersive && bIsTransitioning ) ) { TSharedPtr< SWindow > OwnerWindow( CachedOwnerWindow.Pin() ); if( OwnerWindow.IsValid() ) { OwnerWindow->SetFullWindowOverlayContent( NULL ); } } } TSharedRef FLevelViewportLayout::BuildViewportLayout(TSharedPtr InParentDockTab, TSharedPtr InParentTab, const FString& LayoutString) { TSharedRef ViewportLayoutWidget = FAssetEditorViewportLayout::BuildViewportLayout(InParentDockTab, InParentTab, LayoutString); // Important: We use raw bindings here because we are releasing our binding in our destructor (where a weak pointer would be invalid) // It's imperative that our delegate is removed in the destructor for the level editor module to play nicely with reloading. FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked(LevelEditorModName); LevelEditor.OnTakeHighResScreenShots().AddRaw(this, &FLevelViewportLayout::TakeHighResScreenShot); // Prevent maximize if we only have a single viewport bIsMaximizeSupported = (Viewports.Num() > 1); return ViewportLayoutWidget; } TSharedRef FLevelViewportLayout::FactoryViewport(FName InTypeName, const FAssetEditorViewportConstructionArgs& ConstructionArgs) { TSharedPtr ViewportLayoutEntity; TSharedPtr PinnedTabContent = ParentTabContent.Pin(); if (PinnedTabContent.IsValid()) { // Manually use the factory function here based on type, because legacy viewport types don't register with our factory functions // The level editor module will return an appropriate default if the legacy lookup fails too. if (const AssetEditorViewportFactoryFunction* FactoryFunc = PinnedTabContent->FindViewportCreationFactory(InTypeName)) { TSharedPtr EditorViewport = (*FactoryFunc)(ConstructionArgs); ViewportLayoutEntity = MakeShareable(new FLevelViewportLayoutEntity(EditorViewport)); } } PRAGMA_DISABLE_DEPRECATION_WARNINGS if (!ViewportLayoutEntity.IsValid()) { FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked(LevelEditorModName); ViewportLayoutEntity = LevelEditor.FactoryViewport(InTypeName, ConstructionArgs); } PRAGMA_ENABLE_DEPRECATION_WARNINGS // Both the legacy level editor factory viewport, and the viewport creation functions should fall back to a valid default check(ViewportLayoutEntity.IsValid()); Viewports.Add(ConstructionArgs.ConfigKey, ViewportLayoutEntity); return ViewportLayoutEntity->AsWidget(); } void FLevelViewportLayout::BeginThrottleForAnimatedResize() { // Only enter this mode if there is not already a request if( !ViewportResizeThrottleRequest.IsValid() ) { if( !FSlateApplication::Get().IsRunningAtTargetFrameRate() ) { ViewportResizeThrottleRequest = FSlateThrottleManager::Get().EnterResponsiveMode(); } } } void FLevelViewportLayout::EndThrottleForAnimatedResize() { // Only leave this mode if there is a request if( ViewportResizeThrottleRequest.IsValid() ) { FSlateThrottleManager::Get().LeaveResponsiveMode( ViewportResizeThrottleRequest ); } } void FLevelViewportLayout::InitCommonLayoutFromString( const FString& SpecificLayoutString, const FName PerspectiveViewportKey ) { FName DefaultMaximizedViewport = PerspectiveViewportKey; MaximizedViewport = NAME_None; bool bShouldBeMaximized = bIsMaximizeSupported && ViewportLayoutDefs::bDefaultShouldBeMaximized; bool bShouldBeImmersive = ViewportLayoutDefs::bDefaultShouldBeImmersive; if (!SpecificLayoutString.IsEmpty()) { const FString& IniSection = FLayoutSaveRestore::GetAdditionalLayoutConfigIni(); // NOTE: We don't support starting back up in immersive mode, even if the user shut down with a window that way. See the // comment below in SaveCommonLayoutString() for more info. GConfig->GetBool(*IniSection, *(SpecificLayoutString + TEXT(".bIsMaximized")), bShouldBeMaximized, GEditorPerProjectIni); FString MaximizedViewportString; if (GConfig->GetString(*IniSection, *(SpecificLayoutString + TEXT(".MaximizedViewport")), MaximizedViewportString, GEditorPerProjectIni)) { DefaultMaximizedViewport = *MaximizedViewportString; } } // Replacement layouts (those selected by the user via a command) don't start maximized so the layout can be seen clearly. if (!bIsReplacement && bIsMaximizeSupported && Viewports.Contains(DefaultMaximizedViewport) && (bShouldBeMaximized || bShouldBeImmersive)) { // we are not toggling maximize or immersive state but setting it directly const bool bToggle=false; // Do not allow animation at startup as it hitches const bool bAllowAnimation=false; MaximizeViewport(DefaultMaximizedViewport, bShouldBeMaximized, bShouldBeImmersive, bAllowAnimation); } } void FLevelViewportLayout::SaveCommonLayoutString( const FString& SpecificLayoutString ) const { const FString& IniSection = FLayoutSaveRestore::GetAdditionalLayoutConfigIni(); // Save all our data using the additional layout config for (auto& Pair : Viewports) { // The Viewports map is keyed on the full config name, so no need to prepend the SpecificLayoutString FString ConfigName = Pair.Key.ToString(); Pair.Value->SaveConfig(ConfigName); GConfig->SetString( *IniSection, *( ConfigName + TEXT(".TypeWithinLayout") ), *Pair.Value->GetType().ToString(), GEditorPerProjectIni ); } // We don't bother saving that we were in immersive mode, because we never want to start back up directly in immersive mode // unless the user asks for that on the command-line. The reason is it can be disorientating to not see any editor UI when // to restart the editor. In this case, we'll store the mode they were previously in before they switched to immersive mode. if( bIsImmersive ) { GConfig->SetBool( *IniSection, *( SpecificLayoutString + TEXT( ".bIsMaximized" ) ), bIsMaximizeSupported && bWasMaximized, GEditorPerProjectIni ); } else { GConfig->SetBool(*IniSection, *(SpecificLayoutString + TEXT(".bIsMaximized")), bIsMaximizeSupported && bIsMaximized, GEditorPerProjectIni); } GConfig->SetString( *IniSection, *( SpecificLayoutString + TEXT( ".MaximizedViewport" ) ), *MaximizedViewport.ToString(), GEditorPerProjectIni ); } void FLevelViewportLayout::RequestMaximizeViewport( FName ViewportToMaximize, const bool bWantMaximize, const bool bWantImmersive, const bool bAllowAnimation ) { if( bAllowAnimation ) { // Ensure the UI is responsive when animating the transition to/from maximize BeginThrottleForAnimatedResize(); // We flush commands here because there could be a pending slow viewport draw already enqueued in the render thread // We take the hitch here so that our transition to/from maximize animation is responsive next tick FlushRenderingCommands(); DeferredMaximizeCommands.Add( FMaximizeViewportCommand(ViewportToMaximize, bWantMaximize, bWantImmersive) ); } else { // Not animating so just maximise now MaximizeViewport( ViewportToMaximize, bWantMaximize, bWantImmersive, bAllowAnimation ); } } void FLevelViewportLayout::MaximizeViewport( FName ViewportToMaximize, const bool bWantMaximize, const bool bWantImmersive, const bool bAllowAnimation ) { TSharedPtr Entity = StaticCastSharedPtr(Viewports.FindRef(ViewportToMaximize)); // Should never get into a situation where the viewport is being maximized and there is already a maximized viewport. // I.E Maximized viewport is NULL which means this is a new maximize or MaximizeViewport is equal to the passed in one which means this is a restore of the current maximized viewport check( Entity.IsValid() ); check( MaximizedViewport.IsNone() || MaximizedViewport == ViewportToMaximize ); check(LayoutConfiguration.IsValid()); // If we're already in immersive mode, toggling maximize just needs to update some state (no visual change) if( bIsImmersive ) { bIsMaximized = bWantMaximize; } // Any changes? if( bWantMaximize != bIsMaximized || bWantImmersive != bIsImmersive ) { // Are we already animating a transition? if( bIsTransitioning ) { // Instantly finish up the current transition FinishMaximizeTransition(); check( !bIsTransitioning ); } TSharedPtr OwnerWindow; bIsQueryingLayoutMetrics = true; FWidgetPath ViewportWidgetPath; if( bIsMaximized || bIsImmersive ) { // Use the replacement widget for metrics, as our viewport widget has been reparented to the overlay FSlateApplication::Get().GeneratePathToWidgetUnchecked( ViewportReplacementWidget.ToSharedRef(), ViewportWidgetPath ); OwnerWindow = ViewportWidgetPath.TopLevelWindow; } else { // Viewport is still within the splitter, so use it for metrics directly FSlateApplication::Get().GeneratePathToWidgetUnchecked( Entity->AsWidget(), ViewportWidgetPath ); OwnerWindow = ViewportWidgetPath.TopLevelWindow; } bIsQueryingLayoutMetrics = false; // If the widget can't be found in the layout pass, attempt to use the cached owner window if(!OwnerWindow.IsValid() && CachedOwnerWindow.IsValid()) { OwnerWindow = CachedOwnerWindow.Pin(); } else { // Keep track of the window we're contained in // @todo immersive: Caching this after the transition is risky -- the widget could be moved to a new window! // We really need a safe way to query a widget's window that doesn't require a full layout pass. Then, // instead of caching the window we can look it up whenever it's needed CachedOwnerWindow = OwnerWindow; } if( !bIsImmersive && bWantImmersive ) { // If we can't find our owner window, that means we're likely hosted in a background tab, thus // can't continue with an immersive transition. We never want immersive mode to take over the // window when the user couldn't even see the viewports before! if( !OwnerWindow.IsValid() ) { return; } // Make sure that our viewport layout has a lock on the window's immersive state. Only one // layout can have a single immersive viewport at a time, so if something else is already immersive, // we need to fail the layout change. if( OwnerWindow->HasFullWindowOverlayContent() ) { // We can't continue with the layout change, a different window is already immersive return; } } // Update state bWasMaximized = bIsMaximized; bWasImmersive = bIsImmersive; bIsMaximized = bWantMaximize; bIsImmersive = bWantImmersive; // Start transition bIsTransitioning = true; if( bAllowAnimation ) { // Ensure responsiveness while transitioning BeginThrottleForAnimatedResize(); } if( ( bWasMaximized && !bIsMaximized ) || ( bWasImmersive && !bIsImmersive ) ) { // Play the transition backwards. Note that when transitioning from immersive mode, depending on // the current state of bIsMaximized, we'll transition to either a maximized state or a "restored" state MaximizeAnimation = FCurveSequence(); MaximizeAnimation.AddCurve( 0.0f, ViewportLayoutDefs::RestoreTransitionTime, ECurveEaseFunction::CubicIn ); MaximizeAnimation.PlayReverse( ViewportsOverlayWidget->AsShared() ); if( bWasImmersive && !bIsImmersive ) { OwnerWindow->BeginFullWindowOverlayTransition(); OwnerWindow->SetNativeWindowButtonsVisibility(true); } } else { if( bIsImmersive && ( bWasMaximized && bIsMaximized ) ) { // Unhook our viewport overlay, as we'll let the window overlay drive this for immersive mode ViewportsOverlayPtr.Pin()->RemoveSlot(); } else { // Store the maximized viewport MaximizedViewport = ViewportToMaximize; TSharedPtr MaximizedEntity = StaticCastSharedPtr(Viewports.FindRef(MaximizedViewport)); if (MaximizedEntity.IsValid()) { // Replace our viewport with a dummy widget in it's place during the maximize transition. We can't // have a single viewport widget in two places at once! LayoutConfiguration->ReplaceWidget( MaximizedEntity->AsWidget(), ViewportReplacementWidget.ToSharedRef() ); // The attributes need the AllocatedSize of the parent. // The size is updated in the Paint function and the attributes in the Prepass (too soon). // Update the value manually Tick function (after the parent's Paint). class SCanvasInternal : public SCanvas { public: SCanvasInternal() { SetCanTick(true); } void Construct(const FArguments& Args, TSharedRef ViewportLayout, TSharedRef MaximizedEntity) { SCanvas::Construct(Args); OwnerViewportLayout = ViewportLayout; AddSlot() .Expose(ViewportsOverlayWidgetSlot) [ MaximizedEntity ]; } virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override { SCanvas::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); if (TSharedPtr Owner = OwnerViewportLayout.Pin()) { ViewportsOverlayWidgetSlot->SetPosition(Owner->GetMaximizedViewportPositionOnCanvas()); ViewportsOverlayWidgetSlot->SetSize(Owner->GetMaximizedViewportSizeOnCanvas()); } } private: using SCanvas::AddSlot; using SCanvas::RemoveSlot; private: SCanvas::FSlot* ViewportsOverlayWidgetSlot = nullptr; TWeakPtr OwnerViewportLayout; }; ViewportsOverlayWidget = SNew(SCanvasInternal, SharedThis(this), MaximizedEntity->AsWidget()); } } // Add the maximized viewport as a top level overlay if( bIsImmersive ) { OwnerWindow->SetFullWindowOverlayContent( ViewportsOverlayWidget ); OwnerWindow->BeginFullWindowOverlayTransition(); } else { // Create a slot in our overlay to hold the content ViewportsOverlayPtr.Pin()->AddSlot() [ ViewportsOverlayWidget.ToSharedRef() ]; } // Play the "maximize" transition MaximizeAnimation = FCurveSequence(); MaximizeAnimation.AddCurve( 0.0f, ViewportLayoutDefs::MaximizeTransitionTime, ECurveEaseFunction::CubicOut ); MaximizeAnimation.Play( ViewportsOverlayWidget->AsShared() ); } // We'll only be able to get metrics if we could find an owner window. Usually that's OK, because the only // chance for this code to trigger without an owner window would be at startup, where we might ask to maximize // a viewport based on saved layout, while that viewport is hosted in a background tab. For this case, we'll // never animate (checked here), so we don't need to store "before" metrics. check( OwnerWindow.IsValid() || !bAllowAnimation ); if( OwnerWindow.IsValid() && ViewportWidgetPath.IsValid() ) { // Setup transition metrics if( bIsImmersive || bWasImmersive ) { const FVector2D WindowScreenPos = OwnerWindow->GetPositionInScreen(); if( bIsMaximized || bWasMaximized ) { FWidgetPath ViewportsOverlayWidgetPath = ViewportWidgetPath.GetPathDownTo( ViewportsOverlayPtr.Pin().ToSharedRef() ); const FArrangedWidget& ViewportsOverlayGeometry = ViewportsOverlayWidgetPath.Widgets.Last(); MaximizedViewportStartPosition = FVector2D(ViewportsOverlayGeometry.Geometry.AbsolutePosition) - WindowScreenPos; MaximizedViewportStartSize = ViewportsOverlayPtr.Pin()->GetCachedSize(); } else { const FArrangedWidget& ViewportGeometry = ViewportWidgetPath.Widgets.Last(); MaximizedViewportStartPosition = FVector2D(ViewportGeometry.Geometry.AbsolutePosition) - WindowScreenPos; MaximizedViewportStartSize = ViewportGeometry.Geometry.Size; } } else { const FArrangedWidget& ViewportGeometry = ViewportWidgetPath.Widgets.Last(); MaximizedViewportStartPosition = FVector2D(ViewportGeometry.Geometry.Position); MaximizedViewportStartSize = ViewportGeometry.Geometry.Size; } } if( !bAllowAnimation ) { // Instantly finish up the current transition FinishMaximizeTransition(); check( !bIsTransitioning ); } // Redraw all other viewports, in case there were changes made while in immersive mode that may affect // the view in other viewports. GUnrealEd->RedrawLevelEditingViewports(); } } FVector2D FLevelViewportLayout::GetMaximizedViewportPositionOnCanvas() const { FVector2D EndPos = FVector2D::ZeroVector; if( bIsImmersive ) { TSharedPtr< SWindow > OwnerWindow( CachedOwnerWindow.Pin() ); if( OwnerWindow.IsValid() && OwnerWindow->IsWindowMaximized() ) { // When maximized we offset by the window border size or else the immersive viewport will be clipped FMargin WindowContentMargin = OwnerWindow->GetWindowBorderSize(); EndPos.Set( WindowContentMargin.Right, WindowContentMargin.Bottom ); } } return FMath::Lerp( MaximizedViewportStartPosition, EndPos, MaximizeAnimation.GetLerp() ); } FVector2D FLevelViewportLayout::GetMaximizedViewportSizeOnCanvas() const { // NOTE: Should ALWAYS be valid, however because MaximizedViewport is changed in Tick, it's possible // for widgets we're adding/removing to already have been reported by ArrangeChildren, thus // we need to be able to handle cases where widgets that are not bound can still have delegates fire if( !MaximizedViewport.IsNone() || bWasImmersive ) { FVector2D TargetSize = FVector2D::ZeroVector; if( bIsImmersive || ( bIsTransitioning && bWasImmersive ) ) { TSharedPtr< SWindow > OwnerWindow( CachedOwnerWindow.Pin() ); if( OwnerWindow.IsValid() ) { FVector2D ClippedArea = FVector2D::ZeroVector; const float AppScale = FSlateApplication::Get().GetApplicationScale(); if( OwnerWindow->IsWindowMaximized() ) { // When the window is maximized and we are in immersive we size the canvas to the size of the visible area which does not include the window border const FMargin& WindowContentMargin = OwnerWindow->GetWindowBorderSize() * AppScale; ClippedArea.Set( WindowContentMargin.GetTotalSpaceAlong(), WindowContentMargin.GetTotalSpaceAlong() ); } const float ScaleFactor = OwnerWindow->GetNativeWindow()->GetDPIScaleFactor() * AppScale; TargetSize = (OwnerWindow->GetSizeInScreen() - ClippedArea)/ScaleFactor; } } else { TargetSize = ViewportsOverlayPtr.Pin()->GetCachedSize(); } return FMath::Lerp( MaximizedViewportStartSize, TargetSize, MaximizeAnimation.GetLerp() ); } // No valid viewport to check size for return FVector2D::ZeroVector; } /** Method for taking high res screen shots of viewports */ void FLevelViewportLayout::TakeHighResScreenShot() { if (bIsImmersive || bIsMaximized) { TSharedPtr MaximizedViewportEntity = StaticCastSharedPtr(Viewports.FindRef(MaximizedViewport)); check(MaximizedViewportEntity.IsValid()); MaximizedViewportEntity->TakeHighResScreenShot(); } else { for (auto& Elem : Viewports) { TSharedPtr ViewportEntity = StaticCastSharedPtr(Elem.Value); if (ViewportEntity.IsValid()) { ViewportEntity->TakeHighResScreenShot(); } } } } /** * @return true if this layout is visible. It is not visible if its parent tab is not active */ bool FLevelViewportLayout::IsVisible() const { return !ParentTab.IsValid() || ParentTab.Pin()->IsForeground(); } /** * Checks to see the specified level viewport is visible in this layout * A viewport is visible in a layout if the layout is visible and the viewport is the maximized viewport or there is no maximized viewport * * @param InViewport The viewport within this layout that should be checked * @return true if the viewport is visible. */ bool FLevelViewportLayout::IsLevelViewportVisible( FName InViewport ) const { // The passed in viewport is visible if the current layout is visible and their is no maximized viewport or the viewport that is maximized was passed in. return IsVisible() && ( MaximizedViewport.IsNone() || MaximizedViewport == InViewport ); } bool FLevelViewportLayout::IsViewportMaximized( FName InViewport ) const { return bIsMaximized && MaximizedViewport == InViewport; } bool FLevelViewportLayout::IsViewportImmersive( FName InViewport ) const { return bIsImmersive && MaximizedViewport == InViewport; } EVisibility FLevelViewportLayout::OnGetNonMaximizedVisibility() const { // The non-maximized viewports are not visible if there is a maximized viewport on top of them return ( !bIsQueryingLayoutMetrics && !MaximizedViewport.IsNone() && !bIsTransitioning && DeferredMaximizeCommands.Num() == 0 ) ? EVisibility::Collapsed : EVisibility::Visible; } void FLevelViewportLayout::FinishMaximizeTransition() { if( bIsTransitioning ) { TSharedPtr MaximizedViewportEntity = StaticCastSharedPtr(Viewports.FindRef(MaximizedViewport)); check(MaximizedViewportEntity.IsValid()); check(LayoutConfiguration.IsValid()); // The transition animation is complete, allow the engine to tick normally EndThrottleForAnimatedResize(); // Jump to the end if we're not already there MaximizeAnimation.JumpToEnd(); if( bIsImmersive ) { TSharedPtr< SWindow > OwnerWindow( CachedOwnerWindow.Pin() ); if( OwnerWindow.IsValid() ) { OwnerWindow->SetNativeWindowButtonsVisibility(false); OwnerWindow->EndFullWindowOverlayTransition(); } // Finished transition from restored/maximized to immersive, if this is a PIE window we need to re-register it to capture the mouse. MaximizedViewportEntity->RegisterGameViewportIfPIE(); } else if( bIsMaximized && !bWasImmersive ) { // Finished transition from restored to immersive, if this is a PIE window we need to re-register it to capture the mouse. MaximizedViewportEntity->RegisterGameViewportIfPIE(); } else if( bWasImmersive ) // Finished transition from immersive to restored/maximized { TSharedPtr< SWindow > OwnerWindow( CachedOwnerWindow.Pin() ); if( OwnerWindow.IsValid() ) { OwnerWindow->SetFullWindowOverlayContent( NULL ); OwnerWindow->EndFullWindowOverlayTransition(); } // Release overlay mouse capture to prevent situations where user is unable to get the mouse cursor back if they were holding one of the buttons down and exited immersive mode. FSlateApplication::Get().ReleaseAllPointerCapture(); if( bIsMaximized ) { // If we're transitioning from immersive to maximized, then we need to add our // viewport back to the viewport overlay ViewportsOverlayPtr.Pin()->AddSlot() [ ViewportsOverlayWidget.ToSharedRef() ]; // Now that the viewport is nested within the overlay again, reset our animation so that // our metrics callbacks return the correct value (not the reserved value) MaximizeAnimation.Reverse(); MaximizeAnimation.JumpToEnd(); } else { // @todo immersive: Viewport flashes yellow for one frame in this transition point (immersive -> restored only!) } } else { // Finished transition from maximized to restored // Kill off our viewport overlay now that the animation has finished ViewportsOverlayPtr.Pin()->RemoveSlot(); } // Stop transitioning if( !bIsImmersive && !bIsMaximized ) { // We're finished with this temporary overlay widget now ViewportsOverlayWidget.Reset(); // Restore the viewport widget into the viewport layout splitter LayoutConfiguration->ReplaceWidget( ViewportReplacementWidget.ToSharedRef(), MaximizedViewportEntity->AsWidget() ); MaximizedViewport = NAME_None; } bIsTransitioning = false; // Update keyboard focus. Focus is usually lost when we re-parent the viewport widget. { // We first need to clear keyboard focus so that Slate doesn't assume that focus won't need to change // simply because the viewport widget object is the same -- it has a new widget path! FSlateApplication::Get().ClearKeyboardFocus( EFocusCause::SetDirectly ); // Set keyboard focus directly MaximizedViewportEntity->SetKeyboardFocus(); } // If this is a PIE window we need to re-register since the maximized window will have registered itself // as the game viewport. MaximizedViewportEntity->RegisterGameViewportIfPIE(); } } void FLevelViewportLayout::Tick( float DeltaTime ) { // If we have an animation that has finished playing, then complete the transition if( bIsTransitioning && !MaximizeAnimation.IsPlaying() ) { FinishMaximizeTransition(); } /** Resolve any maximizes or immersive commands for the viewports */ if (DeferredMaximizeCommands.Num() > 0) { // Allow the engine to tick normally. EndThrottleForAnimatedResize(); for (int32 i = 0; i < DeferredMaximizeCommands.Num(); ++i) { FMaximizeViewportCommand& Command = DeferredMaximizeCommands[i]; // Only bother with deferred maximize if we don't already have a maximized or immersive viewport unless we are toggling if( MaximizedViewport.IsNone() || Command.bToggle ) { MaximizeViewport(Command.Viewport, Command.bMaximize, Command.bImmersive, Command.bAllowAnimation ); } } DeferredMaximizeCommands.Empty(); } } bool FLevelViewportLayout::IsTickable() const { return DeferredMaximizeCommands.Num() > 0 || ( bIsTransitioning && !MaximizeAnimation.IsPlaying() ); } void FLevelViewportLayout::LoadConfig(const FString& LayoutString) { FAssetEditorViewportLayout::LoadConfig(LayoutString); if (!LayoutConfiguration.IsValid()) { return; } LayoutConfiguration->LoadConfig(LayoutString, [this](const FString& SpecificLayoutString, const FName PerspectiveViewportName) { this->InitCommonLayoutFromString(SpecificLayoutString, PerspectiveViewportName); }); } void FLevelViewportLayout::SaveConfig(const FString& LayoutString) const { FAssetEditorViewportLayout::SaveConfig(LayoutString); if (IsTransitioning() || !LayoutConfiguration.IsValid()) { return; } LayoutConfiguration->SaveConfig(LayoutString, [this](const FString& SpecificLayoutString) { this->SaveCommonLayoutString(SpecificLayoutString); }); }