Files
UnrealEngine/Engine/Source/Developer/ToolWidgets/Private/Sidebar/SSidebar.cpp
2025-05-18 13:04:45 +08:00

767 lines
18 KiB
C++

// 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<SSidebarContainer>& 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<FSidebarDrawer> NewDrawer = MakeShared<FSidebarDrawer>(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<SSidebarContainer> ContainerWidget = ContainerWidgetWeak.Pin();
if (ensure(ContainerWidget.IsValid()))
{
ContainerWidget->UpdateDrawerTabAppearance();
}
if (bHideWhenAllDocked && !AreAllDrawersDocked())
{
SetVisibility(EVisibility::Visible);
}
return true;
}
bool SSidebar::UnregisterDrawer(const FName InDrawerId)
{
TSharedPtr<SSidebarContainer> ContainerWidget = ContainerWidgetWeak.Pin();
if (ensure(ContainerWidget.IsValid()) && IsDrawerOpened(InDrawerId))
{
ContainerWidget->CloseAllDrawerWidgets(false);
}
const int32 IndexToRemove = Drawers.IndexOfByPredicate(
[InDrawerId](const TSharedRef<FSidebarDrawer>& 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<ISidebarDrawerContent>& InSection)
{
const TSharedPtr<FSidebarDrawer> 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<SSidebarDrawerContent> DrawerSection = StaticCastSharedPtr<SSidebarDrawerContent>(Drawer->ContentWidget);
if (DrawerSection.IsValid())
{
DrawerSection->BuildContent();
}
return false;
}
bool SSidebar::UnregisterDrawerSection(const FName InDrawerId, const FName InSectionId)
{
const TSharedPtr<FSidebarDrawer> 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<FSidebarDrawer> Drawer = FindDrawer(InDrawerId);
if (!Drawer.IsValid())
{
return false;
}
TSharedPtr<SSidebarContainer> ContainerWidget = ContainerWidgetWeak.Pin();
if (ensure(ContainerWidget.IsValid()))
{
ContainerWidget->OpenDrawerNextFrame(Drawer.ToSharedRef());
}
return true;
}
void SSidebar::CloseAllDrawers(const bool bInAnimate)
{
TSharedPtr<SSidebarContainer> ContainerWidget = ContainerWidgetWeak.Pin();
if (ensure(ContainerWidget.IsValid()))
{
ContainerWidget->CloseAllDrawerWidgets(bInAnimate);
}
}
void SSidebar::OnTabDrawerButtonPressed(const TSharedRef<FSidebarDrawer>& InDrawer)
{
TSharedPtr<SSidebarContainer> 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<FSidebarDrawer>& InDrawer, const bool bIsPinned)
{
// Set pin state for given tab; clear the pin state for all other tabs
for (const TSharedRef<FSidebarDrawer>& DrawerTab : Drawers)
{
SetDrawerPinned(DrawerTab->GetUniqueId(), DrawerTab == InDrawer ? bIsPinned : false);
}
}
void SSidebar::OnDrawerTabDockToggled(const TSharedRef<FSidebarDrawer>& InDrawer, const bool bIsDocked)
{
SetDrawerDocked(InDrawer->GetUniqueId(), bIsDocked);
if (!bIsDocked)
{
SetWidgetDrawerSize(InDrawer);
}
}
TSharedRef<SWidget> SSidebar::OnGetTabDrawerContextMenuWidget(TSharedRef<FSidebarDrawer> 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<USidebarButtonMenuContext>();
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<USidebarButtonMenuContext>();
if (!IsValid(ContextMenu))
{
return;
}
const TSharedPtr<FSidebarDrawer> 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<FSidebarDrawer>& InDrawer)
{
const FName DrawerId = InDrawer->GetUniqueId();
const TSharedPtr<SSidebarContainer> 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<FSidebarDrawer>& 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<FSidebarDrawer>& Drawer : Drawers)
{
RemoveDrawer(Drawer);
}
Drawers.Empty();
}
TSharedPtr<FSidebarDrawer> SSidebar::FindDrawer(const FName InDrawerId) const
{
const TSharedRef<FSidebarDrawer>* const FoundDrawer = Drawers.FindByPredicate(
[InDrawerId](const TSharedRef<FSidebarDrawer>& InDrawer)
{
return InDrawerId == InDrawer->GetUniqueId();
});
return FoundDrawer ? *FoundDrawer : TSharedPtr<FSidebarDrawer>();
}
bool SSidebar::HasDrawerOpened() const
{
for (const TSharedRef<FSidebarDrawer>& DrawerTab : Drawers)
{
if (DrawerTab->bIsOpen)
{
return true;
}
}
return false;
}
bool SSidebar::IsDrawerOpened(const FName InDrawerId) const
{
for (const TSharedRef<FSidebarDrawer>& DrawerTab : Drawers)
{
if (DrawerTab->bIsOpen && DrawerTab->GetUniqueId() == InDrawerId)
{
return true;
}
}
return false;
}
FName SSidebar::GetOpenedDrawerId() const
{
TSharedPtr<SSidebarContainer> ContainerWidget = ContainerWidgetWeak.Pin();
if (ensure(ContainerWidget.IsValid()))
{
return ContainerWidget->GetOpenedDrawerId();
}
return NAME_None;
}
bool SSidebar::HasDrawerPinned() const
{
for (const TSharedRef<FSidebarDrawer>& DrawerTab : Drawers)
{
if (DrawerTab->State.bIsPinned)
{
return true;
}
}
return false;
}
bool SSidebar::IsDrawerPinned(const FName InDrawerId) const
{
const TSharedPtr<FSidebarDrawer> Drawer = FindDrawer(InDrawerId);
if (!Drawer.IsValid())
{
return false;
}
return Drawer->State.bIsPinned;
}
TSet<FName> SSidebar::GetPinnedDrawerIds() const
{
TSet<FName> OutDrawerIds;
for (const TSharedRef<FSidebarDrawer>& Drawer : Drawers)
{
if (Drawer->State.bIsPinned)
{
OutDrawerIds.Add(Drawer->GetUniqueId());
}
}
return OutDrawerIds;
}
void SSidebar::SetDrawerPinned(const FName InDrawerId, const bool bInIsPinned)
{
const TSharedPtr<FSidebarDrawer> DrawerToPin = FindDrawer(InDrawerId);
if (!DrawerToPin.IsValid())
{
return;
}
if (bInIsPinned)
{
UnpinAllDrawers();
if (DrawerToPin->State.bIsDocked)
{
SetDrawerDocked(InDrawerId, false);
}
if (!DrawerToPin->bIsOpen)
{
TSharedPtr<SSidebarContainer> 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<FSidebarDrawer>& 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<FSidebarDrawer>& Drawer : Drawers)
{
if (Drawer->State.bIsDocked)
{
return true;
}
}
return false;
}
bool SSidebar::IsDrawerDocked(const FName InDrawerId) const
{
const TSharedPtr<FSidebarDrawer> Drawer = FindDrawer(InDrawerId);
if (!Drawer.IsValid())
{
return false;
}
return Drawer->State.bIsDocked;
}
TSet<FName> SSidebar::GetDockedDrawerIds() const
{
TSet<FName> OutDrawerIds;
for (const TSharedRef<FSidebarDrawer>& Drawer : Drawers)
{
if (Drawer->State.bIsDocked)
{
OutDrawerIds.Add(Drawer->GetUniqueId());
}
}
return OutDrawerIds;
}
void SSidebar::SetDrawerDocked(const FName InDrawerId, const bool bInIsDocked)
{
const TSharedPtr<FSidebarDrawer> DrawerToDock = FindDrawer(InDrawerId);
if (!DrawerToDock.IsValid())
{
return;
}
const TSharedPtr<SSidebarContainer> ContainerWidget = ContainerWidgetWeak.Pin();
if (!ensure(ContainerWidget.IsValid()))
{
return;
}
// Unpin and undock all over tabs
for (const TSharedRef<FSidebarDrawer>& 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<FSidebarDrawer>& Drawer : Drawers)
{
SetDrawerDocked(Drawer->GetUniqueId(), false);
}
}
void SSidebar::UnpinAllDrawers()
{
for (const TSharedRef<FSidebarDrawer>& Drawer : Drawers)
{
SetDrawerPinned(Drawer->GetUniqueId(), false);
}
}
bool SSidebar::ContainsDrawerSection(const FName InDrawerId, const FName InDrawerSectionId) const
{
const TSharedPtr<FSidebarDrawer> Drawer = FindDrawer(InDrawerId);
if (!Drawer.IsValid())
{
return false;
}
for (const TPair<FName, TSharedRef<ISidebarDrawerContent>>& 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<SSidebarContainer> ContainerWidget = ContainerWidgetWeak.Pin();
if (ensure(ContainerWidget.IsValid()))
{
const float CurrentDrawerSize = ContainerWidget->GetCurrentDrawerSize();
OutState.SetDrawerSizes(CurrentDrawerSize, 1.f - CurrentDrawerSize);
}
for (const TSharedRef<FSidebarDrawer>& Drawer : Drawers)
{
OutState.FindOrAddDrawerState(Drawer->State);
}
return OutState;
}
ESidebarTabLocation SSidebar::GetTabLocation() const
{
return TabLocation;
}
TSharedRef<SWidget> SSidebar::GetMainContent() const
{
return OnGetContent.IsBound() ? OnGetContent.Execute() : SNullWidget::NullWidget;
}
void SSidebar::RebuildDrawer(const FName InDrawerId, const bool bInOnlyIfOpen)
{
const TSharedPtr<FSidebarDrawer> Drawer = FindDrawer(InDrawerId);
if (!Drawer.IsValid() || Drawer->Config.OverrideContentWidget.IsValid())
{
return;
}
if (bInOnlyIfOpen && !IsDrawerOpened(InDrawerId))
{
return;
}
const TSharedPtr<SSidebarDrawerContent> DrawerContent = StaticCastSharedPtr<SSidebarDrawerContent>(Drawer->ContentWidget);
if (!DrawerContent.IsValid())
{
return;
}
DrawerContent->BuildContent();
}
const TArray<TSharedRef<FSidebarDrawer>>& SSidebar::GetAllDrawers() const
{
return Drawers;
}
void SSidebar::SetWidgetDrawerSize(const TSharedRef<FSidebarDrawer>& InDrawer)
{
if (!InDrawer->DrawerWidget.IsValid())
{
return;
}
TSharedPtr<SSidebarContainer> 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<FSidebarDrawer>& Drawer : Drawers)
{
if (!Drawer->State.bIsDocked)
{
return false;
}
}
return true;
}
#undef LOCTEXT_NAMESPACE