// Copyright Epic Games, Inc. All Rights Reserved. #include "Sidebar/SSidebar.h" #include "Framework/Application/SlateApplication.h" #include "SidebarButtonMenuContext.h" #include "Sidebar/ISidebarDrawerContent.h" #include "Sidebar/SidebarState.h" #include "Sidebar/SSidebarButton.h" #include "Sidebar/SSidebarContainer.h" #include "Sidebar/SSidebarDrawer.h" #include "Sidebar/SSidebarDrawerContent.h" #include "ToolMenu.h" #include "ToolMenus.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SScrollBox.h" #include "Widgets/SOverlay.h" #define LOCTEXT_NAMESPACE "SSidebar" SSidebar::~SSidebar() { } void SSidebar::Construct(const FArguments& InArgs, const TSharedRef& InContainerWidget) { ContainerWidgetWeak = InContainerWidget; TabLocation = InArgs._TabLocation; InContainerWidget->SidebarSizePercent = InArgs._InitialDrawerSize; OnGetContent = InArgs._OnGetContent; bHideWhenAllDocked = InArgs._HideWhenAllDocked; bAlwaysUseMaxButtonSize = InArgs._AlwaysUseMaxButtonSize; bDisablePin = InArgs._DisablePin; bDisableDock = InArgs._DisableDock; OnStateChanged = InArgs._OnStateChanged; check(OnGetContent.IsBound()); SetVisibility(EVisibility::Visible); ChildSlot .Padding(FMargin( TabLocation == ESidebarTabLocation::Right ? 2.f : 0.f, TabLocation == ESidebarTabLocation::Bottom ? 2.f : 0.f, TabLocation == ESidebarTabLocation::Left ? 2.f : 0.f, TabLocation == ESidebarTabLocation::Top ? 2.f : 0.f)) [ SNew(SBorder) .Padding(0.f) .BorderImage(FAppStyle::Get().GetBrush(TEXT("Docking.Sidebar.Background"))) [ SAssignNew(TabButtonContainer, SScrollBox) .Orientation(IsHorizontal() ? Orient_Horizontal : Orient_Vertical) .ScrollBarAlwaysVisible(false) .ScrollBarVisibility(EVisibility::Collapsed) ] ]; } bool SSidebar::RegisterDrawer(FSidebarDrawerConfig&& InDrawerConfig) { if (ContainsDrawer(InDrawerConfig.UniqueId)) { return false; } const TSharedRef NewDrawer = MakeShared(MoveTemp(InDrawerConfig)); NewDrawer->State = InDrawerConfig.InitialState; NewDrawer->bDisablePin = bDisablePin; NewDrawer->bDisableDock = bDisableDock; NewDrawer->ContentWidget = NewDrawer->Config.OverrideContentWidget.IsValid() ? NewDrawer->Config.OverrideContentWidget : SNew(SSidebarDrawerContent, NewDrawer); // Add tab button TabButtonContainer->AddSlot() [ SAssignNew(NewDrawer->ButtonWidget, SSidebarButton, NewDrawer, GetTabLocation()) .MinButtonSize(bAlwaysUseMaxButtonSize ? MaxTabButtonSize : MinTabButtonSize) .MaxButtonSize(MaxTabButtonSize) .ButtonThickness(TabButtonThickness) .OnPressed(this, &SSidebar::OnTabDrawerButtonPressed) .OnPinToggled(this, &SSidebar::OnDrawerTabPinToggled) .OnDockToggled(this, &SSidebar::OnDrawerTabDockToggled) .OnGetContextMenuContent(this, &SSidebar::OnGetTabDrawerContextMenuWidget, NewDrawer) ]; Drawers.Add(NewDrawer); const FName DrawerId = NewDrawer->GetUniqueId(); if (NewDrawer->State.bIsPinned) { UndockAllDrawers(); SetDrawerPinned(DrawerId, true); } else if (NewDrawer->State.bIsDocked) { SetDrawerDocked(DrawerId, true); } TSharedPtr ContainerWidget = ContainerWidgetWeak.Pin(); if (ensure(ContainerWidget.IsValid())) { ContainerWidget->UpdateDrawerTabAppearance(); } if (bHideWhenAllDocked && !AreAllDrawersDocked()) { SetVisibility(EVisibility::Visible); } return true; } bool SSidebar::UnregisterDrawer(const FName InDrawerId) { TSharedPtr ContainerWidget = ContainerWidgetWeak.Pin(); if (ensure(ContainerWidget.IsValid()) && IsDrawerOpened(InDrawerId)) { ContainerWidget->CloseAllDrawerWidgets(false); } const int32 IndexToRemove = Drawers.IndexOfByPredicate( [InDrawerId](const TSharedRef& InDrawer) { return InDrawerId == InDrawer->GetUniqueId(); }); if (IndexToRemove == INDEX_NONE) { return false; } RemoveDrawer(Drawers[IndexToRemove]); if (ContainerWidget.IsValid()) { ContainerWidget->SummonPinnedTabIfNothingOpened(); } // Clear the pinned flag when the tab is removed from the sidebar. // (Users probably expect that pinning a tab, restoring it/closing it, // then moving it to the sidebar again will leave it unpinned the second time.) SetDrawerPinned(InDrawerId, false); if (Drawers.Num() == 0) { SetVisibility(EVisibility::Collapsed); } else if (ContainerWidget.IsValid()) { ContainerWidget->UpdateDrawerTabAppearance(); } return true; } bool SSidebar::ContainsDrawer(const FName InDrawerId) const { return FindDrawer(InDrawerId).IsValid(); } int32 SSidebar::GetDrawerCount() const { return Drawers.Num(); } bool SSidebar::RegisterDrawerSection(const FName InDrawerId, const TSharedPtr& InSection) { const TSharedPtr Drawer = FindDrawer(InDrawerId); if (!Drawer.IsValid()) { return false; } const FName SectionUniqueId = InSection->GetUniqueId(); if (Drawer->ContentSections.Contains(SectionUniqueId)) { return false; } Drawer->ContentSections.Add(SectionUniqueId, InSection.ToSharedRef()); const TSharedPtr DrawerSection = StaticCastSharedPtr(Drawer->ContentWidget); if (DrawerSection.IsValid()) { DrawerSection->BuildContent(); } return false; } bool SSidebar::UnregisterDrawerSection(const FName InDrawerId, const FName InSectionId) { const TSharedPtr Drawer = FindDrawer(InDrawerId); if (!Drawer.IsValid()) { return false; } if (!Drawer->ContentSections.Contains(InSectionId)) { return false; } Drawer->ContentSections.Remove(InSectionId); return false; } bool SSidebar::TryOpenDrawer(const FName InDrawerId) { if (IsDrawerOpened(InDrawerId) || IsDrawerDocked(InDrawerId)) { return false; } const TSharedPtr Drawer = FindDrawer(InDrawerId); if (!Drawer.IsValid()) { return false; } TSharedPtr ContainerWidget = ContainerWidgetWeak.Pin(); if (ensure(ContainerWidget.IsValid())) { ContainerWidget->OpenDrawerNextFrame(Drawer.ToSharedRef()); } return true; } void SSidebar::CloseAllDrawers(const bool bInAnimate) { TSharedPtr ContainerWidget = ContainerWidgetWeak.Pin(); if (ensure(ContainerWidget.IsValid())) { ContainerWidget->CloseAllDrawerWidgets(bInAnimate); } } void SSidebar::OnTabDrawerButtonPressed(const TSharedRef& InDrawer) { TSharedPtr ContainerWidget = ContainerWidgetWeak.Pin(); if (InDrawer->bIsOpen) { // When clicking on the button of an active (but unpinned) tab, close that tab drawer if (!IsDrawerPinned(InDrawer->GetUniqueId())) { if (ensure(ContainerWidget.IsValid())) { ContainerWidget->CloseDrawer_Internal(InDrawer); } } else if (!InDrawer->DrawerWidget->HasKeyboardFocus()) { FSlateApplication::Get().SetKeyboardFocus(InDrawer->DrawerWidget); } } else if (!InDrawer->State.bIsDocked && ensure(ContainerWidget.IsValid())) { // Otherwise clicking on an inactive tab should open the drawer ContainerWidget->OpenDrawer_Internal(InDrawer); } } void SSidebar::OnDrawerTabPinToggled(const TSharedRef& InDrawer, const bool bIsPinned) { // Set pin state for given tab; clear the pin state for all other tabs for (const TSharedRef& DrawerTab : Drawers) { SetDrawerPinned(DrawerTab->GetUniqueId(), DrawerTab == InDrawer ? bIsPinned : false); } } void SSidebar::OnDrawerTabDockToggled(const TSharedRef& InDrawer, const bool bIsDocked) { SetDrawerDocked(InDrawer->GetUniqueId(), bIsDocked); if (!bIsDocked) { SetWidgetDrawerSize(InDrawer); } } TSharedRef SSidebar::OnGetTabDrawerContextMenuWidget(TSharedRef InDrawer) { UToolMenus* const ToolMenus = UToolMenus::Get(); if (!IsValid(ToolMenus)) { return SNullWidget::NullWidget; } static constexpr const TCHAR* MenuName = TEXT("SidebarTabMenu"); if (!ToolMenus->IsMenuRegistered(MenuName)) { UToolMenu* const NewMenu = ToolMenus->RegisterMenu(MenuName, NAME_None, EMultiBoxType::Menu); check(IsValid(NewMenu)); NewMenu->AddDynamicSection(TEXT("Options"), FNewToolMenuDelegate::CreateSP(this, &SSidebar::BuildOptionsMenu)); } USidebarButtonMenuContext* const ContextObject = NewObject(); ContextObject->Init(SharedThis(this), InDrawer); const FToolMenuContext MenuContext(nullptr, nullptr, ContextObject); return ToolMenus->GenerateWidget(MenuName, MenuContext); } void SSidebar::BuildOptionsMenu(UToolMenu* const InMenu) { if (!IsValid(InMenu)) { return; } USidebarButtonMenuContext* const ContextMenu = InMenu->FindContext(); if (!IsValid(ContextMenu)) { return; } const TSharedPtr Drawer = ContextMenu->GetDrawer(); if (!Drawer.IsValid()) { return; } FToolMenuSection& Section = InMenu->FindOrAddSection(TEXT("Options"), LOCTEXT("Options", "Options")); if (Drawer->State.bIsDocked) { Section.AddMenuEntry(TEXT("Undock"), LOCTEXT("UndockLabel", "Undock"), LOCTEXT("UndockToolTip", "Undocks the drawer"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SSidebar::SetDrawerDocked, Drawer->GetUniqueId(), false))); } else { Section.AddMenuEntry(TEXT("Dock"), LOCTEXT("DockLabel", "Dock"), LOCTEXT("DockToolTip", "Docks the drawer"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SSidebar::SetDrawerDocked, Drawer->GetUniqueId(), true))); } if (Drawer->State.bIsPinned) { Section.AddMenuEntry(TEXT("Unpin"), LOCTEXT("UnpinLabel", "Unpin"), LOCTEXT("UnpinTooltip", "Unpins the drawer from always being displayed"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SSidebar::SetDrawerPinned, Drawer->GetUniqueId(), false))); } else { Section.AddMenuEntry(TEXT("Pin"), LOCTEXT("PinLabel", "Pin"), LOCTEXT("PinTooltip", "Pins the drawer to always be displayed"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SSidebar::SetDrawerPinned, Drawer->GetUniqueId(), true))); } } void SSidebar::RemoveDrawer(const TSharedRef& InDrawer) { const FName DrawerId = InDrawer->GetUniqueId(); const TSharedPtr ContainerWidget = ContainerWidgetWeak.Pin(); if (ensure(ContainerWidget.IsValid()) && InDrawer->DrawerWidget.IsValid()) { ContainerWidget->RemoveDrawerOverlaySlot(InDrawer, false); } if (InDrawer->ButtonWidget.IsValid()) { TabButtonContainer->RemoveSlot(InDrawer->ButtonWidget.ToSharedRef()); } const int32 IndexToRemove = Drawers.IndexOfByPredicate( [DrawerId](const TSharedRef& InDrawer) { return DrawerId == InDrawer->GetUniqueId(); }); if (IndexToRemove != INDEX_NONE) { Drawers.RemoveAt(IndexToRemove); } InDrawer->DrawerClosedDelegate.ExecuteIfBound(DrawerId); if (ContainerWidget.IsValid()) { ContainerWidget->UpdateDrawerTabAppearance(); } } void SSidebar::RemoveAllDrawers() { for (const TSharedRef& Drawer : Drawers) { RemoveDrawer(Drawer); } Drawers.Empty(); } TSharedPtr SSidebar::FindDrawer(const FName InDrawerId) const { const TSharedRef* const FoundDrawer = Drawers.FindByPredicate( [InDrawerId](const TSharedRef& InDrawer) { return InDrawerId == InDrawer->GetUniqueId(); }); return FoundDrawer ? *FoundDrawer : TSharedPtr(); } bool SSidebar::HasDrawerOpened() const { for (const TSharedRef& DrawerTab : Drawers) { if (DrawerTab->bIsOpen) { return true; } } return false; } bool SSidebar::IsDrawerOpened(const FName InDrawerId) const { for (const TSharedRef& DrawerTab : Drawers) { if (DrawerTab->bIsOpen && DrawerTab->GetUniqueId() == InDrawerId) { return true; } } return false; } FName SSidebar::GetOpenedDrawerId() const { TSharedPtr ContainerWidget = ContainerWidgetWeak.Pin(); if (ensure(ContainerWidget.IsValid())) { return ContainerWidget->GetOpenedDrawerId(); } return NAME_None; } bool SSidebar::HasDrawerPinned() const { for (const TSharedRef& DrawerTab : Drawers) { if (DrawerTab->State.bIsPinned) { return true; } } return false; } bool SSidebar::IsDrawerPinned(const FName InDrawerId) const { const TSharedPtr Drawer = FindDrawer(InDrawerId); if (!Drawer.IsValid()) { return false; } return Drawer->State.bIsPinned; } TSet SSidebar::GetPinnedDrawerIds() const { TSet OutDrawerIds; for (const TSharedRef& Drawer : Drawers) { if (Drawer->State.bIsPinned) { OutDrawerIds.Add(Drawer->GetUniqueId()); } } return OutDrawerIds; } void SSidebar::SetDrawerPinned(const FName InDrawerId, const bool bInIsPinned) { const TSharedPtr DrawerToPin = FindDrawer(InDrawerId); if (!DrawerToPin.IsValid()) { return; } if (bInIsPinned) { UnpinAllDrawers(); if (DrawerToPin->State.bIsDocked) { SetDrawerDocked(InDrawerId, false); } if (!DrawerToPin->bIsOpen) { TSharedPtr ContainerWidget = ContainerWidgetWeak.Pin(); if (ensure(ContainerWidget.IsValid())) { ContainerWidget->OpenDrawerNextFrame(DrawerToPin.ToSharedRef(), false); } } // In case two modules attempt to register drawers with initially pinned states for (const TSharedRef& Drawer : Drawers) { Drawer->State.bIsPinned = false; } } DrawerToPin->State.bIsPinned = bInIsPinned; if (DrawerToPin->State.bIsPinned) { DrawerToPin->bIsOpen = true; } OnStateChanged.ExecuteIfBound(GetState()); } bool SSidebar::HasDrawerDocked() const { for (const TSharedRef& Drawer : Drawers) { if (Drawer->State.bIsDocked) { return true; } } return false; } bool SSidebar::IsDrawerDocked(const FName InDrawerId) const { const TSharedPtr Drawer = FindDrawer(InDrawerId); if (!Drawer.IsValid()) { return false; } return Drawer->State.bIsDocked; } TSet SSidebar::GetDockedDrawerIds() const { TSet OutDrawerIds; for (const TSharedRef& Drawer : Drawers) { if (Drawer->State.bIsDocked) { OutDrawerIds.Add(Drawer->GetUniqueId()); } } return OutDrawerIds; } void SSidebar::SetDrawerDocked(const FName InDrawerId, const bool bInIsDocked) { const TSharedPtr DrawerToDock = FindDrawer(InDrawerId); if (!DrawerToDock.IsValid()) { return; } const TSharedPtr ContainerWidget = ContainerWidgetWeak.Pin(); if (!ensure(ContainerWidget.IsValid())) { return; } // Unpin and undock all over tabs for (const TSharedRef& Drawer : Drawers) { if (bInIsDocked) { SetDrawerPinned(Drawer->GetUniqueId(), false); } ContainerWidget->UndockDrawer_Internal(Drawer); } if (bInIsDocked) { if (DrawerToDock->State.bIsPinned) { SetDrawerPinned(InDrawerId, false); } ContainerWidget->DockDrawer_Internal(DrawerToDock.ToSharedRef()); if (DrawerToDock->ContentWidget.IsValid()) { if (bHideWhenAllDocked && AreAllDrawersDocked()) { SetVisibility(EVisibility::Collapsed); } } else { if (bHideWhenAllDocked && !AreAllDrawersDocked()) { SetVisibility(EVisibility::Visible); } } } else { if (bHideWhenAllDocked && !AreAllDrawersDocked()) { SetVisibility(EVisibility::Visible); } } OnStateChanged.ExecuteIfBound(GetState()); } void SSidebar::UndockAllDrawers() { for (const TSharedRef& Drawer : Drawers) { SetDrawerDocked(Drawer->GetUniqueId(), false); } } void SSidebar::UnpinAllDrawers() { for (const TSharedRef& Drawer : Drawers) { SetDrawerPinned(Drawer->GetUniqueId(), false); } } bool SSidebar::ContainsDrawerSection(const FName InDrawerId, const FName InDrawerSectionId) const { const TSharedPtr Drawer = FindDrawer(InDrawerId); if (!Drawer.IsValid()) { return false; } for (const TPair>& DrawerSection : Drawer->ContentSections) { if (DrawerSection.Value->GetSectionId() == InDrawerSectionId) { return true; } } return false; } bool SSidebar::IsHorizontal() const { return TabLocation == ESidebarTabLocation::Top || TabLocation == ESidebarTabLocation::Bottom; } bool SSidebar::IsVertical() const { return TabLocation == ESidebarTabLocation::Left || TabLocation == ESidebarTabLocation::Right; } FSidebarState SSidebar::GetState() const { FSidebarState OutState; OutState.SetHidden(false); TSharedPtr ContainerWidget = ContainerWidgetWeak.Pin(); if (ensure(ContainerWidget.IsValid())) { const float CurrentDrawerSize = ContainerWidget->GetCurrentDrawerSize(); OutState.SetDrawerSizes(CurrentDrawerSize, 1.f - CurrentDrawerSize); } for (const TSharedRef& Drawer : Drawers) { OutState.FindOrAddDrawerState(Drawer->State); } return OutState; } ESidebarTabLocation SSidebar::GetTabLocation() const { return TabLocation; } TSharedRef SSidebar::GetMainContent() const { return OnGetContent.IsBound() ? OnGetContent.Execute() : SNullWidget::NullWidget; } void SSidebar::RebuildDrawer(const FName InDrawerId, const bool bInOnlyIfOpen) { const TSharedPtr Drawer = FindDrawer(InDrawerId); if (!Drawer.IsValid() || Drawer->Config.OverrideContentWidget.IsValid()) { return; } if (bInOnlyIfOpen && !IsDrawerOpened(InDrawerId)) { return; } const TSharedPtr DrawerContent = StaticCastSharedPtr(Drawer->ContentWidget); if (!DrawerContent.IsValid()) { return; } DrawerContent->BuildContent(); } const TArray>& SSidebar::GetAllDrawers() const { return Drawers; } void SSidebar::SetWidgetDrawerSize(const TSharedRef& InDrawer) { if (!InDrawer->DrawerWidget.IsValid()) { return; } TSharedPtr ContainerWidget = ContainerWidgetWeak.Pin(); if (!ensure(ContainerWidget.IsValid())) { return; } const float DrawerOverlayWidth = ContainerWidget->GetOverlaySize().X; const float CurrentDrawerSize = ContainerWidget->GetCurrentDrawerSize(); const float PixelWidth = CurrentDrawerSize * DrawerOverlayWidth; InDrawer->DrawerWidget->SetCurrentSize(PixelWidth); } bool SSidebar::AreAllDrawersDocked() const { for (const TSharedRef& Drawer : Drawers) { if (!Drawer->State.bIsDocked) { return false; } } return true; } #undef LOCTEXT_NAMESPACE