// Copyright Epic Games, Inc. All Rights Reserved. #include "Framework/Docking/SDockingTabStack.h" #include "Framework/Commands/UIAction.h" #include "Framework/Commands/UICommandList.h" #include "Widgets/Text/STextBlock.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Layout/WidgetPath.h" #include "Framework/Application/MenuStack.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Layout/SSpacer.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SButton.h" #include "Framework/Docking/SDockingTabWell.h" #include "Framework/Docking/SDockingCross.h" #include "Framework/Docking/FDockingDragOperation.h" #include "Framework/Docking/TabCommands.h" #include "Brushes/SlateColorBrush.h" #define LOCTEXT_NAMESPACE "DockTabStack" static const FVector2D ContextButtonTargetSize(24,24); static const float TriggerAreaFraction = 0.24f; /** * Like a missing widget, but says it's a document area */ class SDocumentAreaWidget { public: static TSharedRef MakeDocumentAreaWidget() { return SNew(SBorder) .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SNew( STextBlock ) .Text(LOCTEXT("DocumentArea", "Document Area") ) .TextStyle( FCoreStyle::Get(), "EmbossedText" ) ]; } }; void SDockingTabStack::Construct( const FArguments& InArgs, const TSharedRef& PersistentNode ) { BindTabCommands(); Tabs = PersistentNode->Tabs; this->SetSizeCoefficient(PersistentNode->GetSizeCoefficient()); // the value of this is determined every time a tab is added bShowingTitleBarArea = false; bIsDocumentArea = InArgs._IsDocumentArea; InlineContentAreaLeft = nullptr; InlineContentAreaRight = nullptr; TitleBarSlot = nullptr; // Animation that toggles the tabs { ShowHideTabWell = FCurveSequence(0,0.15); if (PersistentNode->bHideTabWell) { ShowHideTabWell.JumpToStart(); } else { ShowHideTabWell.JumpToEnd(); } } // In TabStack mode we glue together a TabWell, InlineContent areas and a ContentOverlay // that shows the content of the currently selected Tab. // ________ TabWell // | // +-------------------------------------v-------------------------------+ // | +--------------------+ | // | InlineContentAreaLeft | Tab0 | Tab1 | Tab2 | InlineContentAreaRight | // +---------------------------------------------------------------------+ // | | // | | <-- Content area overlay // | | // +---------------------------------------------------------------------+ // const FButtonStyle* const UnhideTabWellButtonStyle = &FCoreStyle::Get().GetWidgetStyle< FButtonStyle >( "Docking.UnhideTabwellButton" ); // create inline title bar content TitleBarContent = SNew(SOverlay) + SOverlay::Slot() [ SNew(SHorizontalBox) .Visibility(EVisibility::SelfHitTestInvisible) + SHorizontalBox::Slot() .AutoWidth() .Expose(InlineContentAreaLeft) + SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Bottom) .Padding(0.0f, 0.0f, 0.0f, 0.0f) [ SNew(SVerticalBox) .Visibility(EVisibility::SelfHitTestInvisible) + SVerticalBox::Slot() .AutoHeight() [ SNew(SSpacer) .Visibility(this, &SDockingTabStack::GetMaximizeSpacerVisibility) .Size(FVector2D(0.0f, 10.0f)) ] + SVerticalBox::Slot() .AutoHeight() [ // TabWell SAssignNew(TabWell, SDockingTabWell) .ParentStackNode(SharedThis(this)) ] ] + SHorizontalBox::Slot() .AutoWidth() .Expose(InlineContentAreaRight) .Padding(5.0f, 0.0f, 0.0f, 0.0f) .VAlign(VAlign_Center) ]; ChildSlot [ SNew(SVerticalBox) .Visibility( EVisibility::SelfHitTestInvisible ) + SVerticalBox::Slot() .AutoHeight() [ // tab well area SNew(SBorder) .Visibility(this, &SDockingTabStack::GetTabWellVisibility) .DesiredSizeScale(this, &SDockingTabStack::GetTabWellScale) .BorderImage(this, &SDockingTabStack::GetTabStackBorderImage) .VAlign(VAlign_Bottom) .OnMouseButtonDown(this, &SDockingTabStack::TabWellRightClicked) .Padding(0.0f) [ SNew(SVerticalBox) .Visibility(EVisibility::SelfHitTestInvisible) + SVerticalBox::Slot() .Expose(TitleBarSlot) .AutoHeight() + SVerticalBox::Slot() .AutoHeight() [ SNew(SImage) .Image(this, &SDockingTabStack::GetTabWellBrush) ] ] ] + SVerticalBox::Slot() .FillHeight(1.0f) [ // tab content area SAssignNew(OverlayManagement.ContentAreaOverlay, SOverlay) + SOverlay::Slot() [ // content goes here SAssignNew(ContentSlot, SBorder) .BorderImage(this, &SDockingTabStack::GetContentAreaBrush) .Padding(this, &SDockingTabStack::GetContentPadding) .Clipping(EWidgetClipping::ClipToBounds) .IsEnabled(this, &SDockingTabStack::IsContentEnabled) [ SNew(STextBlock) .Text(LOCTEXT("EmptyTabMessage", "Empty Tab!")) ] ] + SOverlay::Slot() .Padding(0.0f) .HAlign(HAlign_Left) .VAlign(VAlign_Top) [ // unhide tab well button (yellow triangle) SNew(SButton) .ButtonStyle(UnhideTabWellButtonStyle) .OnClicked(this, &SDockingTabStack::UnhideTabWell) .ContentPadding(0.0f) .Visibility(this, &SDockingTabStack::GetUnhideButtonVisibility) .DesiredSizeScale(this, &SDockingTabStack::GetUnhideTabWellButtonScale) .ButtonColorAndOpacity(this, &SDockingTabStack::GetUnhideTabWellButtonOpacity) .ToolTipText(LOCTEXT("UnhideTabWellToolTip", "Show Tabs")) [ // button should be big enough to show its own image SNew(SSpacer) .Size(UnhideTabWellButtonStyle->Normal.ImageSize) ] ] #if DEBUG_TAB_MANAGEMENT + SOverlay::Slot() .HAlign(HAlign_Left) .VAlign(VAlign_Top) [ SNew(SBorder) .BorderImage(FCoreStyle::Get().GetBrush("Docking.Border")) .BorderBackgroundColor(FLinearColor(1.0f, 0.5f, 0.0f, 0.75f)) .Visibility(EVisibility::HitTestInvisible) [ SNew(STextBlock) .Text(this, &SDockingTabStack::ShowPersistentTabs) .ShadowOffset(FVector2D::UnitVector) ] ] #endif ] ]; if (bIsDocumentArea) { this->SetNodeContent(SDocumentAreaWidget::MakeDocumentAreaWidget(), FDockingStackOptionalContent()); } } void SDockingTabStack::OnLastTabRemoved() { if (!bIsDocumentArea) { // Stop holding onto any meaningful window content. // The user should not see any content in this DockNode. this->SetNodeContent(SNullWidget::NullWidget, FDockingStackOptionalContent()); } else { this->SetNodeContent(SDocumentAreaWidget::MakeDocumentAreaWidget(), FDockingStackOptionalContent()); } } void SDockingTabStack::OnTabClosed(const TSharedRef& ClosedTab, SDockingNode::ELayoutModification RemovalMethod) { const FTabId& TabIdBeingClosed = ClosedTab->GetLayoutIdentifier(); // Document-style tabs are positioned per use-case. const bool bIsTabPersistable = TabIdBeingClosed.IsTabPersistable(); if (bIsTabPersistable) { // Sidebar tabs should still exist in the stacks layout so we can restore it if (RemovalMethod != SDockingNode::ELayoutModification::TabRemoval_Sidebar) { ClosePersistentTab(TabIdBeingClosed); } } else { RemovePersistentTab(TabIdBeingClosed); } } void SDockingTabStack::OnTabRemoved( const FTabId& TabId ) { RemovePersistentTab( TabId ); } void SDockingTabStack::OpenTab(const TSharedRef& InTab, int32 InsertLocationAmongActiveTabs, bool bKeepInactive) { const int32 InsertIndex = OpenPersistentTab(InTab->GetLayoutIdentifier(), InsertLocationAmongActiveTabs); // The tab may be a nomad tab, in which case it should inherit whichever tab manager it is being put into! InTab->SetTabManager(GetDockArea()->GetTabManager()); const FTabId TabId = InTab->GetLayoutIdentifier(); // the insert index is not the same as the tab index in the array for new tabs so find the tab again to check the tab state. const FTabManager::FTab& TabInfo = *Tabs.FindByPredicate([TabId](const FTabManager::FTab& TestTab) {return TestTab.TabId == TabId; }); if (TabInfo.TabState == ETabState::SidebarTab) { FSidebarTabLists SidebarLists; if (TabInfo.SidebarLocation == ESidebarLocation::Left) { SidebarLists.LeftSidebarTabs.Add(InTab); } else { ensure(TabInfo.SidebarLocation == ESidebarLocation::Right); SidebarLists.RightSidebarTabs.Add(InTab); } AddSidebarTab(InTab); GetDockArea()->AddSidebarTabsFromRestoredLayout(SidebarLists); } else { AddTabWidget(InTab, InsertIndex, bKeepInactive); OnLiveTabAdded(); TabWell->RefreshParentContent(); } } void SDockingTabStack::AddTabWidget(const TSharedRef& InTab, int32 AtLocation, bool bKeepInactive) { TabWell->AddTab(InTab, AtLocation, bKeepInactive); if ( IsTabWellHidden() && TabWell->GetNumTabs() > 1 ) { SetTabWellHidden(false); } // We just added a tab, so if there was a cross up we no longer need it. HideCross(); TSharedPtr ParentDockArea = GetDockArea(); if (ParentDockArea.IsValid()) { ParentDockArea->HideCross(); } } void SDockingTabStack::AddSidebarTab(const TSharedRef& InTab) { InTab->SetParent(TabWell); } float SDockingTabStack::GetTabSidebarSizeCoefficient(const TSharedRef& InTab) { FTabManager::FTab* Tab = Tabs.FindByPredicate(FTabMatcher(InTab->GetLayoutIdentifier())); if (Tab) { return Tab->SidebarSizeCoefficient; } return 0; } void SDockingTabStack::SetTabSidebarSizeCoefficient(const TSharedRef& InTab, float InSizeCoefficient) { FTabManager::FTab* Tab = Tabs.FindByPredicate(FTabMatcher(InTab->GetLayoutIdentifier())); if (Tab) { Tab->SidebarSizeCoefficient = InSizeCoefficient; } } bool SDockingTabStack::IsTabPinnedInSidebar(const TSharedRef& InTab) { FTabManager::FTab* Tab = Tabs.FindByPredicate(FTabMatcher(InTab->GetLayoutIdentifier())); if (Tab) { return Tab->bPinnedInSidebar; } return false; } void SDockingTabStack::SetTabPinnedInSidebar(const TSharedRef& InTab, bool bPinnedInSidebar) { FTabManager::FTab* Tab = Tabs.FindByPredicate(FTabMatcher(InTab->GetLayoutIdentifier())); if (Tab) { Tab->bPinnedInSidebar = bPinnedInSidebar; } } const TSlotlessChildren& SDockingTabStack::GetTabs() const { return TabWell->GetTabs(); } int32 SDockingTabStack::GetNumTabs() const { return TabWell->GetNumTabs(); } bool SDockingTabStack::HasTab(const struct FTabMatcher& TabMatcher) const { return Tabs.IndexOfByPredicate(TabMatcher) != INDEX_NONE; } FGeometry SDockingTabStack::GetTabStackGeometry() const { return GetTickSpaceGeometry(); } void SDockingTabStack::RemoveClosedTabsWithName( FName InName ) { for (int32 TabIndex=0; TabIndex < Tabs.Num(); ) { const FTabManager::FTab& ThisTab = Tabs[TabIndex]; if ( ThisTab.TabState == ETabState::ClosedTab && ThisTab.TabId == InName ) { Tabs.RemoveAtSwap(TabIndex); } else { ++TabIndex; } } } bool SDockingTabStack::IsShowingLiveTabs() const { return this->TabWell->GetNumTabs() > 0; } void SDockingTabStack::BringToFront( const TSharedRef& TabToBringToFront ) { TabWell->BringTabToFront(TabToBringToFront); } void SDockingTabStack::SetNodeContent(const TSharedRef& InContent, const FDockingStackOptionalContent& OptionalContent) { ContentSlot->SetContent(InContent); (*InlineContentAreaLeft)[OptionalContent.ContentLeft]; (*InlineContentAreaRight)[OptionalContent.ContentRight]; if(TabWell->GetForegroundTab()) { if (TSharedPtr ParentWindow = TabWell->GetForegroundTab()->GetParentWindow()) { ParentWindow->GetTitleBar()->UpdateBackgroundContent(OptionalContent.TitleBarContentRight); } } } FReply SDockingTabStack::OnDragOver( const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent ) { TSharedPtr DragDropOperation = DragDropEvent.GetOperationAs(); if ( DragDropOperation.IsValid() ) { if (DragDropOperation->CanDockInNode(SharedThis(this), FDockingDragOperation::DockingViaTarget)) { FGeometry OverlayGeometry = this->FindChildGeometry( MyGeometry, OverlayManagement.ContentAreaOverlay.ToSharedRef() ); if ( OverlayGeometry.IsUnderLocation( DragDropEvent.GetScreenSpacePosition() ) ) { ShowCross(); } else { HideCross(); } return FReply::Handled(); } } return FReply::Unhandled(); } void SDockingTabStack::OnDragLeave( const FDragDropEvent& DragDropEvent ) { if (DragDropEvent.GetOperationAs().IsValid()) { HideCross(); } } FReply SDockingTabStack::OnDrop( const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent ) { if (DragDropEvent.GetOperationAs().IsValid()) { HideCross(); } return FReply::Unhandled(); } void SDockingTabStack::OnFocusChanging( const FWeakWidgetPath& PreviousFocusPath, const FWidgetPath& NewWidgetPath, const FFocusEvent& InFocusEvent) { const TSharedPtr ForegroundTab = TabWell->GetForegroundTab(); if ( ForegroundTab.IsValid() ) { const bool bIsForegroundTabActive = NewWidgetPath.ContainsWidget( this ); if (bIsForegroundTabActive) { // If a widget inside this tab stack got focused, activate this tab. FGlobalTabmanager::Get()->SetActiveTab( ForegroundTab ); ForegroundTab->ActivateInParent(ETabActivationCause::SetDirectly); } } } FReply SDockingTabStack::OnMouseButtonDown( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent ) { const TSharedPtr ForegroundTab = TabWell->GetForegroundTab(); const bool bIsRelevantButtonForTabFocus = MouseEvent.GetPressedButtons().Contains(EKeys::LeftMouseButton) || MouseEvent.GetPressedButtons().Contains(EKeys::RightMouseButton) || MouseEvent.GetPressedButtons().Contains(EKeys::MiddleMouseButton); if (bIsRelevantButtonForTabFocus && ForegroundTab.IsValid() && !ForegroundTab->IsActive()) { FGlobalTabmanager::Get()->SetActiveTab( ForegroundTab ); #if PLATFORM_LINUX // Don't stop further event handling in case the user wants to move this window. // Returning FReply::Handled() here will prevent SWindow from seeing the event. // FIXME: In some cases the foreground tab is never the active tab and this handler will consume every mouse down event. return FReply::Unhandled(); #else return FReply::Handled(); #endif } else { return FReply::Unhandled(); } } FReply SDockingTabStack::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) { if (ActionList->ProcessCommandBindings(InKeyEvent)) { return FReply::Handled(); } return SDockingNode::OnKeyDown(MyGeometry, InKeyEvent); } FReply SDockingTabStack::OnUserAttemptingDock( SDockingNode::RelativeDirection Direction, const FDragDropEvent& DragDropEvent ) { TSharedPtr DragDropOperation = DragDropEvent.GetOperationAs(); if ( DragDropOperation.IsValid() ) { // We want to replace this placeholder with whatever is being dragged. CreateNewTabStackBySplitting( Direction )->OpenTab( DragDropOperation->GetTabBeingDragged().ToSharedRef() ); HideCross(); return FReply::Handled(); } else { return FReply::Unhandled(); } } TArray< TSharedRef > SDockingTabStack::GetAllChildTabs() const { return GetTabs().AsArrayCopy(); } void SDockingTabStack::CloseForegroundTab() { TSharedPtr ForegroundTab = TabWell->GetForegroundTab(); if (ForegroundTab.IsValid()) { ForegroundTab->RequestCloseTab(); } } void SDockingTabStack::CloseTabsInDirectionFromForegroundTab(ETabsToClose TabsToClose, ECloseTabsDirection Direction) { TSharedPtr ForegroundTab = TabWell->GetForegroundTab(); if (ForegroundTab.IsValid()) { const int32 ForegroundTabIndex = TabWell->GetForegroundTabIndex(); const int32 Increment = GetIncrementFromCloseTabsDirection(Direction); int32 DestroyIndex = ForegroundTabIndex + Increment; while (DestroyIndex >= 0 && DestroyIndex < TabWell->GetNumTabs()) { const TSharedRef& Tab = TabWell->GetTabs()[DestroyIndex]; const ETabRole VisualTabRole = Tab->GetVisualTabRole(); const bool bCanClose = (TabsToClose == ETabsToClose::CloseAllTabs) || (TabsToClose == ETabsToClose::CloseDocumentTabs && VisualTabRole == ETabRole::DocumentTab) || (TabsToClose == ETabsToClose::CloseDocumentAndMajorTabs && (VisualTabRole == ETabRole::DocumentTab || VisualTabRole == ETabRole::MajorTab)); // Need to increment index if we failed to close a tab to the right or if we're closing tabs to the left regardless of if we succeeded if (!bCanClose || !Tab->RequestCloseTab() || Direction == ECloseTabsDirection::Left) { DestroyIndex += Increment; } } } } void SDockingTabStack::CloseAllButForegroundTab(ETabsToClose TabsToClose) { TSharedPtr ForegroundTab = TabWell->GetForegroundTab(); if (ForegroundTab.IsValid()) { int32 DestroyIndex = 0; while ((TabWell->GetNumTabs() > 1) && (DestroyIndex < TabWell->GetNumTabs())) { const TSharedRef& Tab = TabWell->GetTabs()[DestroyIndex]; const ETabRole VisualTabRole = Tab->GetVisualTabRole(); const bool bCanClose = (TabsToClose == ETabsToClose::CloseAllTabs) || (TabsToClose == ETabsToClose::CloseDocumentTabs && VisualTabRole == ETabRole::DocumentTab) || (TabsToClose == ETabsToClose::CloseDocumentAndMajorTabs && (VisualTabRole == ETabRole::DocumentTab || VisualTabRole == ETabRole::MajorTab)); if ((Tab == ForegroundTab) || !bCanClose || !Tab->RequestCloseTab()) { ++DestroyIndex; } } } } FReply SDockingTabStack::TabWellRightClicked( const FGeometry& TabWellGeometry, const FPointerEvent& MouseEvent ) { if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton) { FWidgetPath WidgetPath = MouseEvent.GetEventPath() != nullptr ? *MouseEvent.GetEventPath() : FWidgetPath(); FSlateApplication::Get().PushMenu(AsShared(), WidgetPath, MakeContextMenu(), FSlateApplication::Get().GetCursorPos(), FPopupTransitionEffect::ContextMenu); return FReply::Handled(); } else { return FReply::Unhandled(); } } SDockingNode::ECleanupRetVal SDockingTabStack::CleanUpNodes() { if (TabWell->GetNumTabs() > 0) { return VisibleTabsUnderNode; } else if (Tabs.Num() > 0) { SetVisibility(EVisibility::Collapsed); return HistoryTabsUnderNode; } else { return NoTabsUnderNode; } } TSharedRef SDockingTabStack::MakeContextMenu() { // Show a menu that allows users to toggle whether // a specific tab should hide if it is the sole tab // in its tab well. const bool bCloseAfterSelection = true; const bool bCloseSelfOnly = false; FMenuBuilder MenuBuilder( bCloseAfterSelection, NULL, TSharedPtr(), bCloseSelfOnly, &FCoreStyle::Get() ); { MenuBuilder.BeginSection("DockingTabStackOptions", LOCTEXT("TabOptionsHeading", "Options") ); { MenuBuilder.AddMenuEntry( LOCTEXT("CollapseTabWell", "Hide Tabs"), LOCTEXT("CollapseTabWellTooltip", "Collapses the tabs headers to save room."), FSlateIcon(), FUIAction( FExecuteAction::CreateSP( this, &SDockingTabStack::ToggleTabWellVisibility ), FCanExecuteAction::CreateSP( this, &SDockingTabStack::CanHideTabWell ) ) ); if(IsTabAllowedInSidebar(TabWell->GetForegroundTab())) { MenuBuilder.AddMenuEntry( LOCTEXT("MoveToSidebar", "Dock to Sidebar"), LOCTEXT("MoveToSidebarTooltip", "Moves this tab to a sidebar drawer on the side of the window closest to the tab.\nThe tab can be opened from the drawer and will automatically close again when clicking off it."), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SDockingTabStack::MoveForegroundTabToSidebar), FCanExecuteAction::CreateSP(this, &SDockingTabStack::CanMoveForegroundTabToSidebar) ) ); } } MenuBuilder.EndSection(); MenuBuilder.BeginSection("DockingTabStackCloseTabs"); { MenuBuilder.AddMenuEntry( LOCTEXT("CloseTab", "Close"), LOCTEXT("CloseTabTooltil", "Close this tab."), FSlateIcon(), FUIAction( FExecuteAction::CreateSP( this, &SDockingTabStack::CloseForegroundTab ), FCanExecuteAction::CreateSP(this, &SDockingTabStack::CanCloseForegroundTab) ) ); const ETabsToClose TabsToClose = ETabsToClose::CloseDocumentAndMajorTabs; MenuBuilder.AddMenuEntry( LOCTEXT("CloseTabsToTheLeft", "Close Tabs to the Left"), LOCTEXT("CloseTabsToTheLeftTooltip", "Closes all tabs to the left of the active tab."), FSlateIcon(), FUIAction( FExecuteAction::CreateSP( this, &SDockingTabStack::CloseTabsInDirectionFromForegroundTab, TabsToClose, ECloseTabsDirection::Left), FCanExecuteAction::CreateSP( this, &SDockingTabStack::CanCloseTabsInDirectionOfForegroundTab, ECloseTabsDirection::Left) ) ); MenuBuilder.AddMenuEntry( LOCTEXT("CloseTabsToTheRight", "Close Tabs to the Right"), LOCTEXT("CloseTabsToTheRightTooltip", "Closes all tabs to the right of the active tab."), FSlateIcon(), FUIAction( FExecuteAction::CreateSP( this, &SDockingTabStack::CloseTabsInDirectionFromForegroundTab, TabsToClose, ECloseTabsDirection::Right), FCanExecuteAction::CreateSP( this, &SDockingTabStack::CanCloseTabsInDirectionOfForegroundTab, ECloseTabsDirection::Right) ) ); MenuBuilder.AddMenuEntry( LOCTEXT("CloseOtherTabs", "Close Other Tabs"), LOCTEXT("CloseOtherTabsTooltil", "Closes all tabs except for the active tab."), FSlateIcon(), FUIAction( FExecuteAction::CreateSP( this, &SDockingTabStack::CloseAllButForegroundTab, TabsToClose ), FCanExecuteAction::CreateSP(this, &SDockingTabStack::CanCloseAllButForegroundTab) ) ); } MenuBuilder.EndSection(); TSharedPtr ForegroundTab = TabWell->GetForegroundTab(); if (ForegroundTab.IsValid()) { ForegroundTab->ExtendContextMenu(MenuBuilder); } } return MenuBuilder.MakeWidget(); } void SDockingTabStack::ShowCross() { const float DockTargetSize = 32.0f; if (!OverlayManagement.bShowingCross) { this->GetDockArea()->ShowCross(); OverlayManagement.bShowingCross = true; OverlayManagement.ContentAreaOverlay->AddSlot() . HAlign(HAlign_Fill) . VAlign(VAlign_Fill) [ SNew( SDockingCross, SharedThis(this) ) //SNew(SBorder) //. BorderImage( FStyleDefaults::GetNoBrush() ) //. Padding( FMargin(0,0,0,0)) //. Content() //[ // SNew(SVerticalBox) // + SVerticalBox::Slot() //.AutoHeight() // [ // // TOP ROW // SNew(SHorizontalBox) // + SHorizontalBox::Slot() .FillWidth(1.0f) // + SHorizontalBox::Slot() // [ // SNew(SDockingTarget) // . OwnerNode( SharedThis(this) ) // . DockDirection( SDockingNode::Above ) // ] // + SHorizontalBox::Slot() .FillWidth(1.0f) // ] // + SVerticalBox::Slot() //.AutoHeight() // [ // // MIDDLE ROW // SNew(SHorizontalBox) // + SHorizontalBox::Slot() // .FillWidth(1.0f) // [ // SNew(SDockingTarget) // . OwnerNode( SharedThis(this) ) // . DockDirection( SDockingNode::LeftOf ) // ] // + SHorizontalBox::Slot().AutoWidth() // [ // // The center node is redundant with just moving the tab into place. // // It was also confusing to many. // SNew(SDockingTarget) // . Visibility(EVisibility::Hidden) // . OwnerNode( SharedThis(this) ) // . DockDirection( SDockingNode::Center ) // ] // + SHorizontalBox::Slot() // .FillWidth(1.0f) // [ // SNew(SDockingTarget) // . OwnerNode( SharedThis(this) ) // . DockDirection( SDockingNode::RightOf ) // ] // ] // + SVerticalBox::Slot() //.AutoHeight() // .HAlign(HAlign_Center) // [ // // BOTTOM ROW // SNew(SHorizontalBox) // + SHorizontalBox::Slot() .FillWidth(1.0f) // + SHorizontalBox::Slot().AutoWidth() // [ // SNew(SDockingTarget) // . OwnerNode( SharedThis(this) ) // . DockDirection( SDockingNode::Below ) // ] // + SHorizontalBox::Slot() .FillWidth(1.0f) // ] //] ]; } } void SDockingTabStack::HideCross() { if (OverlayManagement.bShowingCross) { OverlayManagement.ContentAreaOverlay->RemoveSlot(); OverlayManagement.bShowingCross = false; } } TSharedPtr SDockingTabStack::GatherPersistentLayout() const { if( Tabs.Num() > 0 ) { // Each live tab might want to save custom visual state. { const TArray< TSharedRef > MyTabs = this->GetTabs().AsArrayCopy(); for (int32 TabIndex=0; TabIndex < MyTabs.Num(); ++TabIndex) { MyTabs[TabIndex]->PersistVisualState(); } } // Persist layout TSharedRef PersistentStack = FTabManager::NewStack() ->SetSizeCoefficient( this->GetSizeCoefficient() ) ->SetHideTabWell( this->IsTabWellHidden() ); TSharedPtr ForegroundTab = TabWell->GetForegroundTab(); if(ForegroundTab.IsValid()) { PersistentStack->SetForegroundTab(ForegroundTab->GetLayoutIdentifier()); } for (int32 TabIndex=0; TabIndex < Tabs.Num(); ++TabIndex) { // We do not persist document tabs. Document tabs have a valid InstanceId in addition to a TabType. const bool bIsTabPersistable = Tabs[TabIndex].TabId.IsTabPersistable(); if ( bIsTabPersistable ) { PersistentStack->AddTab(Tabs[TabIndex]); } } return PersistentStack; } else { return TSharedPtr(); } } void SDockingTabStack::ClearReservedSpace() { bShowingTitleBarArea = false; TitleBarSlot->SetPadding(0.f); } void SDockingTabStack::ReserveSpaceForWindowChrome(EChromeElement Element, bool bIncludePaddingForMenuBar, bool bOnlyMinorTabs) { FMargin ControlsPadding; FMargin IconPadding; #if PLATFORM_MAC if (bIncludePaddingForMenuBar) { static const float TopPaddingForTrafficLightsAndMenuBar = 30.0f; // Always add padding on top, because on the Mac there is always either a main menu bar or the "traffic light" buttons (close, minimize, and maximize) above controls. // Always add padding to the left, because on the Mac there's no Unreal icon to the left of controls, only the window edge, so we need some space. ControlsPadding = FMargin(8.0f, TopPaddingForTrafficLightsAndMenuBar, 0, 0); } else { // Without a main menu bar in the title bar, we just need to pad on the left to avoid overlapping with the "traffic light" buttons (close, minimize, and maximize). ControlsPadding = FMargin(67.0f, 0, 0, 0); } #else static const float TopPaddingForMenuBar = 25.0f; static const float LeftPaddingForIcon = FSlateApplication::Get().GetAppIcon()->GetImageSize().X; // If we are including top padding for the menu bar we do not need to pad the outer sides since we will be below the left icon and the right controls. if (bIncludePaddingForMenuBar) { ControlsPadding = FMargin(8.f, TopPaddingForMenuBar, 0.f, 0.f); IconPadding = FMargin(LeftPaddingForIcon + 12.f, bOnlyMinorTabs ? 5.f : 0.f, 0.f, 0.f); } else { ControlsPadding = FMargin(8.f, 2.f, 128.f, 0.f); IconPadding = FMargin(25.f, bOnlyMinorTabs ? 5.f : 0.f, 0.f, 0.f); } #endif bShowingTitleBarArea = true; const FMargin CurrentPadding = TitleBarSlot->GetPadding(); switch (Element) { case EChromeElement::Controls: TitleBarSlot->SetPadding(CurrentPadding + ControlsPadding); break; case EChromeElement::Icon: TitleBarSlot->SetPadding(CurrentPadding + IconPadding); break; default: ensure(false); break; } } TSharedRef< SDockingTabStack > SDockingTabStack::CreateNewTabStackBySplitting( const SDockingNode::RelativeDirection Direction ) { TSharedPtr ParentNode = ParentNodePtr.Pin(); check(ParentNode.IsValid()); TSharedRef NewStack = SNew(SDockingTabStack, FTabManager::NewStack()); { NewStack->SetSizeCoefficient( this->GetSizeCoefficient() ); } ParentNode->PlaceNode( NewStack, Direction, SharedThis(this) ); return NewStack; } void SDockingTabStack::SetParentNode( TSharedRef InParent ) { SDockingNode::SetParentNode(InParent); TitleBarSlot->AttachWidget(TitleBarContent.ToSharedRef()); } bool SDockingTabStack::IsContentEnabled() const { TSharedRef TabManager = GetDockArea()->GetTabManager(); if(!TabManager->IsReadOnly()) { return true; } // If we are in read only mode, and the foreground tab desires custom behavior (i.e not hidden or disabled) it is enabled // and the tab owner is responsible for handling the content in read only mode if(TSharedPtr ForegroundTab = TabWell->GetForegroundTab()) { return TabManager->GetTabReadOnlyBehavior(ForegroundTab->GetLayoutIdentifier()) == ETabReadOnlyBehavior::Custom; } return true; } /** What should the content area look like for the current tab? */ const FSlateBrush* SDockingTabStack::GetContentAreaBrush() const { TSharedPtr ForegroundTab = TabWell->GetForegroundTab(); return (ForegroundTab.IsValid()) ? ForegroundTab->GetContentAreaBrush() : FStyleDefaults::GetNoBrush(); } FMargin SDockingTabStack::GetContentPadding() const { TSharedPtr ForegroundTab = TabWell->GetForegroundTab(); return (ForegroundTab.IsValid()) ? ForegroundTab->GetContentPadding() : FMargin(0); } EVisibility SDockingTabStack::GetTabWellVisibility() const { const bool bTabWellVisible = // If we are playing, we're in transition, so tab is visible. ShowHideTabWell.IsPlaying() || // Playing forward expands the tab, so it is always visible then as well. !ShowHideTabWell.IsInReverse(); return (!bTabWellVisible) ? EVisibility::Collapsed : EVisibility::SelfHitTestInvisible; // Visible, but allow clicks to pass through self (but not children) } const FSlateBrush* SDockingTabStack::GetTabWellBrush() const { TSharedPtr ForegroundTab = TabWell->GetForegroundTab(); return ( ForegroundTab.IsValid() ) ? ForegroundTab->GetTabWellBrush() : FStyleDefaults::GetNoBrush(); } EVisibility SDockingTabStack::GetUnhideButtonVisibility() const { const bool bShowUnhideButton = // If we are playing, we're in transition, so tab is visible. ShowHideTabWell.IsPlaying() || // Playing forward expands the tab, so it is always visible then as well. ShowHideTabWell.IsInReverse(); return (bShowUnhideButton) ? EVisibility::Visible : EVisibility::Collapsed; } void SDockingTabStack::ToggleTabWellVisibility() { ShowHideTabWell.Reverse(); } void SDockingTabStack::MoveForegroundTabToSidebar() { if (TSharedPtr ForegroundTabPtr = TabWell->GetForegroundTab()) { MoveTabToSidebar(ForegroundTabPtr.ToSharedRef()); } } void SDockingTabStack::MoveTabToSidebar(TSharedRef Tab) { const int32 TabIndex = Tabs.IndexOfByPredicate(FTabMatcher(Tab->GetLayoutIdentifier(), ETabState::OpenedTab)); if(TabIndex != INDEX_NONE) { ESidebarLocation SidebarLoc = GetDockArea()->AddTabToSidebar(Tab); if (SidebarLoc != ESidebarLocation::None) { Tabs[TabIndex].TabState = ETabState::SidebarTab; Tabs[TabIndex].SidebarLocation = SidebarLoc; TabWell->RemoveAndDestroyTab(Tab, ELayoutModification::TabRemoval_Sidebar); } } } void SDockingTabStack::RestoreTabFromSidebar(TSharedRef Tab) { const int32 TabIndex = Tabs.IndexOfByPredicate(FTabMatcher(Tab->GetLayoutIdentifier(), ETabState::SidebarTab)); if (TabIndex != INDEX_NONE) { FTabManager::FTab& TabInfo = Tabs[TabIndex]; TabInfo.SidebarSizeCoefficient = 0; // Set the sate to closed so its reopened by OpenTab TabInfo.TabState = ETabState::ClosedTab; TabInfo.SidebarLocation = ESidebarLocation::None; OpenTab(Tab); } } FReply SDockingTabStack::UnhideTabWell() { SetTabWellHidden(false); return FReply::Handled(); } bool SDockingTabStack::CanHideTabWell() const { const TSharedPtr ParentNode = ParentNodePtr.Pin(); if (ParentNode && !ParentNode->GetAllChildTabs().IsEmpty()) { // Is target tab located at the upper and left most among tabs in the parent window(as first child). Unreal icon will overlap golden triangle(unhide button) when the tab is the first child of the window. const bool bIsUpperLeftmostTab = (FGlobalTabmanager::Get()->GetActiveTab() == ParentNode->GetAllChildTabs()[0]); // Is target tab in the Floating Window. Unreal icon will overlap when the tab is in the floating window(without menu) const bool bIsInFloatingWindow = ParentNode->GetDockArea()->GetParentWindow().IsValid(); return GetNumTabs() == 1 && FGlobalTabmanager::Get()->CanSetAsActiveTab(GetTabs()[0]) && !(bIsUpperLeftmostTab && bIsInFloatingWindow); } /* in the case where there are no parent splitter or child tabs, it is invalid to hide the tab well. * The likely case for this would be in a sidebar flyout */ return false; } bool SDockingTabStack::CanCloseForegroundTab() const { TSharedPtr ForegroundTabPtr = TabWell->GetForegroundTab(); return ForegroundTabPtr.IsValid() && ForegroundTabPtr->CanCloseTab(); } int32 SDockingTabStack::GetIncrementFromCloseTabsDirection(ECloseTabsDirection Direction) { switch (Direction) { case ECloseTabsDirection::Left: return -1; case ECloseTabsDirection::Right: return 1; default: return 1; } } bool SDockingTabStack::CanCloseTabsInDirectionOfForegroundTab(ECloseTabsDirection Direction) const { TSharedPtr ForegroundTabPtr = TabWell->GetForegroundTab(); if (!ForegroundTabPtr.IsValid()) { return false; } const ETabRole VisualTabRole = ForegroundTabPtr->GetVisualTabRole(); if (VisualTabRole == ETabRole::DocumentTab || VisualTabRole == ETabRole::MajorTab) { const TArray< TSharedRef > MyTabs = this->GetTabs().AsArrayCopy(); const int32 Increment = GetIncrementFromCloseTabsDirection(Direction); for (int32 TabIndex = TabWell->GetForegroundTabIndex() + Increment; TabIndex >= 0 && TabIndex < MyTabs.Num(); TabIndex += Increment) { TSharedRef Tab = MyTabs[TabIndex]; if (Tab->CanCloseTab()) { return true; } } } return false; } bool SDockingTabStack::CanCloseAllButForegroundTab() const { // If the active tab is a document tab or major tab and there is at least 1 other closeable tab, offer to close the others TSharedPtr ForegroundTabPtr = TabWell->GetForegroundTab(); if (!ForegroundTabPtr.IsValid()) { return false; } const ETabRole VisualTabRole = ForegroundTabPtr->GetVisualTabRole(); if ((VisualTabRole == ETabRole::DocumentTab || VisualTabRole == ETabRole::MajorTab) && (TabWell->GetNumTabs() > 1)) { const TArray< TSharedRef > MyTabs = this->GetTabs().AsArrayCopy(); for (int32 TabIndex = 0; TabIndex < MyTabs.Num(); ++TabIndex) { TSharedRef Tab = MyTabs[TabIndex]; if (Tab != ForegroundTabPtr && Tab->CanCloseTab()) { return true; } } } return false; } bool SDockingTabStack::CanMoveForegroundTabToSidebar() const { if(TSharedPtr ForegroundTabPtr = TabWell->GetForegroundTab()) { return CanMoveTabToSideBar(ForegroundTabPtr.ToSharedRef()); } return false; } bool SDockingTabStack::CanMoveTabToSideBar(TSharedRef Tab) const { const FTabId TabIdBeingClosed = Tab->GetLayoutIdentifier(); // Only persistable non-major tabs can be put into a sidebar. There must also be more than one tab or else adding to a sidebar doesnt make si return TabIdBeingClosed.IsTabPersistable() && Tab->GetVisualTabRole() != ETabRole::MajorTab && GetDockArea()->GetNumTabs() > 1; } bool SDockingTabStack::IsTabAllowedInSidebar(TSharedPtr Tab) const { // Major tabs are not allowed to be sidebared if (Tab.IsValid()) { if (TSharedPtr TabManager = Tab->GetTabManagerPtr()) { return Tab->GetVisualTabRole() != ETabRole::MajorTab && TabManager->IsTabAllowedInSidebar(Tab->GetLayoutIdentifier()); } } return false; } SSplitter::ESizeRule SDockingTabStack::GetSizeRule() const { int32 NumTabs = this->GetNumTabs(); if (NumTabs > 0) { for (int32 Index = 0; Index < NumTabs; ++Index) { if (!this->GetTabs()[Index]->ShouldAutosize()) { return SSplitter::FractionOfParent; } } // If all tabs in this stack are sized to content, then the stack's cell should size to Content. return SSplitter::SizeToContent; } else { return SSplitter::FractionOfParent; } } void SDockingTabStack::SetTabWellHidden( bool bShouldHideTabWell ) { // If the tab well is already hidden or visible, don't replay the animations. if ( (bShouldHideTabWell && IsTabWellHidden()) || (!bShouldHideTabWell && !IsTabWellHidden())) { return; } if (bShouldHideTabWell) { ShowHideTabWell.PlayReverse( this->AsShared() ); } else { ShowHideTabWell.Play( this->AsShared() ); } } bool SDockingTabStack::IsTabWellHidden() const { return ShowHideTabWell.IsInReverse(); } FVector2D SDockingTabStack::GetTabWellScale() const { return FVector2D(1,ShowHideTabWell.GetLerp()); } FVector2D SDockingTabStack::GetUnhideTabWellButtonScale() const { return FMath::Lerp(FVector2D::UnitVector, 8*FVector2D::UnitVector, ShowHideTabWell.GetLerp()); } FSlateColor SDockingTabStack::GetUnhideTabWellButtonOpacity() const { return FLinearColor( 1,1,1, 1.0f - ShowHideTabWell.GetLerp() ); } const FSlateBrush* SDockingTabStack::GetTabStackBorderImage() const { static const FSlateBrush* MajorTabBackgroundBrush = FAppStyle::Get().GetBrush("Brushes.Title"); static const FSlateBrush* MinorTabBackgroundBrush = FAppStyle::Get().GetBrush("Brushes.Background"); return bShowingTitleBarArea ? MajorTabBackgroundBrush : MinorTabBackgroundBrush; } int32 SDockingTabStack::OpenPersistentTab( const FTabId& TabId, int32 OpenLocationAmongActiveTabs ) { int32 ExistingClosedTabIndex = Tabs.IndexOfByPredicate(FTabMatcher(TabId, static_cast(ETabState::ClosedTab|ETabState::SidebarTab))); if (ExistingClosedTabIndex == INDEX_NONE) { // Check for a persistent opened tab that isn't actually opened ( in the live tabs ). // This situation can happen in some corner cases, e.g: // - If the process is terminated after a periodic layout save. // - If the layout save on editor shutdown occurs before an editor mode is deactivated. // In any case we want to treat this opened tab as a closed tab instead of creating a new tab. const bool bTreatIndexNoneAsWildcard = false; const int32 ExistingOpenedTabIndex = Tabs.IndexOfByPredicate(FTabMatcher(TabId, static_cast(ETabState::OpenedTab), bTreatIndexNoneAsWildcard)); if (ExistingOpenedTabIndex != INDEX_NONE) { bool bHasLiveTab = false; const TArray< TSharedRef > LiveTabs = this->GetTabs().AsArrayCopy(); for (int32 TabIndex = 0; TabIndex < LiveTabs.Num(); ++TabIndex) { if (TabId == LiveTabs[TabIndex]->GetLayoutIdentifier()) { bHasLiveTab = true; break; } } if (!bHasLiveTab) { ExistingClosedTabIndex = ExistingOpenedTabIndex; } } } if (OpenLocationAmongActiveTabs == INDEX_NONE) { if (ExistingClosedTabIndex != INDEX_NONE) { FTabManager::FTab& Tab = Tabs[ExistingClosedTabIndex]; Tab.TabState = Tab.SidebarLocation == ESidebarLocation::None ? ETabState::OpenedTab : ETabState::SidebarTab; return ExistingClosedTabIndex; } else { // This tab was never opened in the tab stack before; add it. Tabs.Add( FTabManager::FTab( TabId, ETabState::OpenedTab ) ); return Tabs.Num()-1; } } else { // @TODO: This branch maybe needs to become a separate function: More like MoveOrAddTab // We need to open a tab in a specific location. // We have the index of the open tab where to insert. But we need the index in the persistent // array, which is an ordered list of all tabs ( both open and closed ). int32 OpenLocationInGlobalList=INDEX_NONE; for (int32 TabIndex = 0, OpenTabIndex=0; TabIndex < Tabs.Num() && OpenLocationInGlobalList == INDEX_NONE; ++TabIndex) { const bool bThisTabIsOpen = (Tabs[TabIndex].TabState == ETabState::OpenedTab); if ( bThisTabIsOpen ) { if (OpenTabIndex == OpenLocationAmongActiveTabs) { OpenLocationInGlobalList = TabIndex; } ++OpenTabIndex; } } if (OpenLocationInGlobalList == INDEX_NONE) { OpenLocationInGlobalList = Tabs.Num(); } if ( ExistingClosedTabIndex == INDEX_NONE ) { // Create a new tab. Tabs.Insert( FTabManager::FTab( TabId, ETabState::OpenedTab ), OpenLocationInGlobalList ); return OpenLocationAmongActiveTabs; } else { // Move the existing closed tab to the new desired location FTabManager::FTab TabToMove = Tabs[ExistingClosedTabIndex]; Tabs.RemoveAt( ExistingClosedTabIndex ); // If the element we removed was before the insert location, subtract one since the index was shifted during the removal if ( ExistingClosedTabIndex <= OpenLocationInGlobalList ) { OpenLocationInGlobalList--; } // Mark the tab opened TabToMove.TabState = ETabState::OpenedTab; Tabs.Insert( TabToMove, OpenLocationInGlobalList ); return OpenLocationAmongActiveTabs; } } } int32 SDockingTabStack::ClosePersistentTab( const FTabId& TabId ) { const int32 TabIndex = Tabs.IndexOfByPredicate(FTabMatcher(TabId, static_cast(ETabState::OpenedTab|ETabState::SidebarTab))); if (TabIndex != INDEX_NONE) { Tabs[TabIndex].TabState = ETabState::ClosedTab; } return TabIndex; } void SDockingTabStack::RemovePersistentTab( const FTabId& TabId ) { const int32 TabIndex = Tabs.IndexOfByPredicate(FTabMatcher(TabId)); if(TabIndex != INDEX_NONE) { Tabs.RemoveAtSwap(TabIndex); } } EVisibility SDockingTabStack::GetMaximizeSpacerVisibility() const { /* if(GetDockArea().IsValid() && GetDockArea()->GetParentWindow().IsValid()) { if (GetDockArea()->GetParentWindow()->IsWindowMaximized()) { return EVisibility::Collapsed; } else { return EVisibility::SelfHitTestInvisible; } }*/ return EVisibility::Collapsed; } #if DEBUG_TAB_MANAGEMENT FString SDockingTabStack::ShowPersistentTabs() const { FString AllTabs; for (int32 TabIndex=0; TabIndex < Tabs.Num(); ++TabIndex) { AllTabs += (Tabs[TabIndex].TabState == ETabState::OpenedTab ) ? TEXT("[^]") : TEXT("[x]"); AllTabs += Tabs[TabIndex].TabId.ToString(); AllTabs += TEXT(" "); } return AllTabs; } #endif void SDockingTabStack::BindTabCommands() { check(!ActionList.IsValid()); ActionList = MakeShareable(new FUICommandList); const FTabCommands& Commands = FTabCommands::Get(); ActionList->MapAction(Commands.CloseMajorTab, FExecuteAction::CreateSP(this, &SDockingTabStack::ExecuteCloseMajorTabCommand), FCanExecuteAction::CreateSP(this, &SDockingTabStack::CanExecuteCloseMajorTabCommand)); ActionList->MapAction(Commands.CloseMinorTab, FExecuteAction::CreateSP(this, &SDockingTabStack::ExecuteCloseMinorTabCommand), FCanExecuteAction::CreateSP(this, &SDockingTabStack::CanExecuteCloseMinorTabCommand)); ActionList->MapAction(Commands.CloseFocusedTab, FExecuteAction::CreateSP(this, &SDockingTabStack::ExecuteCloseFocusedTabCommand), FCanExecuteAction::CreateSP(this, &SDockingTabStack::CanExecuteCloseFocusedTabCommand)); } void SDockingTabStack::ExecuteCloseMajorTabCommand() { // Close this stack's foreground tab (if it's a major tab) if (CanExecuteCloseMajorTabCommand()) { TabWell->GetForegroundTab()->RequestCloseTab(); } } bool SDockingTabStack::CanExecuteCloseMajorTabCommand() { // Can we close this stack's foreground tab (if it's a major tab)? TSharedPtr ForegroundTab = TabWell->GetForegroundTab(); return ForegroundTab.IsValid() && !FGlobalTabmanager::Get()->CanSetAsActiveTab(ForegroundTab); } void SDockingTabStack::ExecuteCloseMinorTabCommand() { if (CanExecuteCloseMinorTabCommand()) { // Close the global active (minor) tab FGlobalTabmanager::Get()->GetActiveTab()->RequestCloseTab(); } } bool SDockingTabStack::CanExecuteCloseMinorTabCommand() { if (TSharedPtr DockArea = GetDockArea()) { TSharedPtr GlobalTabManager = FGlobalTabmanager::Get(); TSharedPtr ActiveTab = GlobalTabManager->GetActiveTab(); if (ActiveTab.IsValid()) { if (ActiveTab->GetParentWindow() == DockArea->GetParentWindow()) { // Can close the global active (minor) tab because it's in the same window as this tab stack return true; } } } return false; } void SDockingTabStack::ExecuteCloseFocusedTabCommand() { if (CanExecuteCloseMinorTabCommand()) { ExecuteCloseMinorTabCommand(); } else { ExecuteCloseMajorTabCommand(); } } bool SDockingTabStack::CanExecuteCloseFocusedTabCommand() { return CanExecuteCloseMinorTabCommand() || CanExecuteCloseMajorTabCommand(); } void SDockingTabStack::OnResized() { if (TSharedPtr DockArea = GetDockArea()) { DockArea->GetTabManager()->RequestSavePersistentLayout(); } } #undef LOCTEXT_NAMESPACE