// Copyright Epic Games, Inc. All Rights Reserved. #include "Toolkits/SGlobalTabSwitchingDialog.h" #include "Modules/ModuleManager.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Images/SImage.h" #include "Widgets/Text/STextBlock.h" #include "Framework/MultiBox/MultiBox.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Widgets/Input/SButton.h" #include "Widgets/Layout/SScrollBorder.h" #include "Widgets/Views/SListView.h" #include "Framework/Docking/TabManager.h" #include "EditorStyleSet.h" #include "Widgets/Docking/SDockTab.h" #include "EngineGlobals.h" #include "Editor.h" #include "LevelEditor.h" #include "Editor/WorkspaceMenuStructure/Public/WorkspaceMenuStructure.h" #include "Editor/WorkspaceMenuStructure/Public/WorkspaceMenuStructureModule.h" #if PLATFORM_MAC #include "Mac/MacApplication.h" #endif #include "Subsystems/AssetEditorSubsystem.h" #define LOCTEXT_NAMESPACE "SGlobalTabSwitchingDialog" ////////////////////////////////////////////////////////////////////////// // FTabSwitchingListItemBase class FTabSwitchingListItemBase { public: FTabSwitchingListItemBase() : LastAccessTime(0.0) { } virtual ~FTabSwitchingListItemBase() {} virtual TSharedRef CreateWidget(TSharedPtr AssetThumbnailPool) { return SNullWidget::NullWidget; } virtual FText GetTypeString() const { return FText::GetEmpty(); } virtual FText GetPathString() const { return FText::GetEmpty(); } virtual void ActivateTab() {} virtual void ShowInContentBrowser() { } virtual TSharedPtr GetAssociatedTabManager() { return TSharedPtr(); } public: double LastAccessTime; }; ////////////////////////////////////////////////////////////////////////// // FTabSwitchingListItem_Asset class FTabSwitchingListItem_Asset: public FTabSwitchingListItemBase { public: FTabSwitchingListItem_Asset(UObject* InAsset) : MyAsset(InAsset) { if (IAssetEditorInstance* EditorInstance = GEditor->GetEditorSubsystem()->FindEditorForAsset(MyAsset, /*bFocusIfOpen=*/false)) { LastAccessTime = EditorInstance->GetLastActivationTime(); } } virtual TSharedRef CreateWidget(TSharedPtr AssetThumbnailPool) override { // Create a label for the asset name const bool bDirtyState = MyAsset->GetOutermost()->IsDirty(); FFormatNamedArguments Args; Args.Add(TEXT("AssetName"), FText::AsCultureInvariant(MyAsset->GetName())); Args.Add(TEXT("DirtyState"), bDirtyState ? LOCTEXT("AssetModified", " [Modified]") : FText::GetEmpty()); FText AssetText = FText::Format(LOCTEXT("AssetEntryLabel", "{AssetName}{DirtyState}"), Args); // Create a thumbnail to represent the asset type const int32 ThumbnailSize = 48; FAssetData AssetData(MyAsset); Thumbnail = MakeShareable(new FAssetThumbnail(MyAsset, ThumbnailSize, ThumbnailSize, AssetThumbnailPool)); FAssetThumbnailConfig ThumbnailConfig; return SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 4.0f)[SNew(SBox).WidthOverride(ThumbnailSize).HeightOverride(ThumbnailSize)[Thumbnail->MakeThumbnailWidget(ThumbnailConfig)]] + SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center).Padding(8.0f, 0.0f, 8.0f, 0.0f)[SNew(STextBlock).TextStyle(FEditorStyle::Get(), "ControlTabMenu.AssetNameStyle").Text(AssetText)]; } virtual void ShowInContentBrowser() override { TArray ObjectsToSync; ObjectsToSync.Add(MyAsset); GEditor->SyncBrowserToObjects(ObjectsToSync); } virtual FText GetTypeString() const override { return MyAsset->GetClass()->GetDisplayNameText(); } virtual FText GetPathString() const override { return FText::AsCultureInvariant(MyAsset->GetOutermost()->GetName()); } virtual void ActivateTab() override { GEditor->GetEditorSubsystem()->FindEditorForAsset(MyAsset, /*bFocusIfOpen=*/true); } virtual TSharedPtr GetAssociatedTabManager() override { IAssetEditorInstance* Instance = GEditor->GetEditorSubsystem()->FindEditorForAsset(MyAsset, /*bFocusIfOpen=*/false); if (Instance) { return Instance->GetAssociatedTabManager(); } return nullptr; } protected: UObject* MyAsset; TSharedPtr Thumbnail; }; ////////////////////////////////////////////////////////////////////////// // FTabSwitchingListItem_World class FTabSwitchingListItem_World: public FTabSwitchingListItem_Asset { public: static TSharedPtr MakeWorldItem() { UWorld* MyWorld = nullptr; for (const FWorldContext& Context: GEngine->GetWorldContexts()) { if (Context.WorldType == EWorldType::PIE) { MyWorld = Context.World(); break; } else if (Context.WorldType == EWorldType::Editor) { MyWorld = Context.World(); } } check(MyWorld != nullptr); return MakeShareable(new FTabSwitchingListItem_World(MyWorld)); } virtual void ActivateTab() override { const TSharedPtr LevelEditorTabManager = GetAssociatedTabManager(); FGlobalTabmanager::Get()->DrawAttention(LevelEditorTabManager->GetOwnerTab().ToSharedRef()); } virtual TSharedPtr GetAssociatedTabManager() override { return FModuleManager::Get().GetModuleChecked("LevelEditor").GetLevelEditorTabManager(); } protected: FTabSwitchingListItem_World(UWorld* InWorld) : FTabSwitchingListItem_Asset(InWorld) { const TSharedPtr LevelEditorTabPtr = FModuleManager::Get().GetModuleChecked("LevelEditor").GetLevelEditorTab(); LastAccessTime = LevelEditorTabPtr->GetLastActivationTime(); } }; ////////////////////////////////////////////////////////////////////////// // FTabSwitchingListItem_Asset bool SGlobalTabSwitchingDialog::bIsAlreadyOpen = false; SGlobalTabSwitchingDialog::~SGlobalTabSwitchingDialog() { bIsAlreadyOpen = false; #if PLATFORM_MAC MacApplication->SetIsRightClickEmulationEnabled(true); #endif } SGlobalTabSwitchingDialog::FTabListItemPtr SGlobalTabSwitchingDialog::GetMainTabListSelectedItem() const { TArray SelectedItems = MainTabsListWidget->GetSelectedItems(); if (SelectedItems.Num() > 0) { return SelectedItems[0]; } else { return FTabListItemPtr(); } } FReply SGlobalTabSwitchingDialog::OnBrowseToSelectedAsset() { FTabListItemPtr SelectedItem = GetMainTabListSelectedItem(); if (SelectedItem.IsValid()) { SelectedItem->ShowInContentBrowser(); FSlateApplication::Get().DismissAllMenus(); } return FReply::Handled(); } void SGlobalTabSwitchingDialog::OnMainTabListSelectionChanged(FTabListItemPtr InItem, ESelectInfo::Type SelectInfo) { TSharedRef NewTopContents = SNullWidget::NullWidget; TSharedRef NewBottomContents = SNullWidget::NullWidget; TSharedRef NewToolTabsContent = SNullWidget::NullWidget; TArray SelectedItems = MainTabsListWidget->GetSelectedItems(); if (SelectedItems.Num() > 0) { FTabListItemPtr SelectedItem = SelectedItems[0]; NewTopContents = SelectedItem->CreateWidget(AssetThumbnailPool); NewBottomContents = SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth()[SNew(SButton).ButtonStyle(FEditorStyle::Get(), "HoverOnlyHyperlinkButton").VAlign(VAlign_Center).HAlign(HAlign_Center).OnClicked(this, &SGlobalTabSwitchingDialog::OnBrowseToSelectedAsset).ToolTipText(LOCTEXT("BrowseButtonToolTipText", "Browse to Asset in Content Browser"))[SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)[SNew(SImage).Image(FEditorStyle::GetBrush("PropertyWindow.Button_Browse"))] + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(FMargin(4.0f, 0.0f, 0.0f, 0.0f))[SNew(STextBlock).TextStyle(FEditorStyle::Get(), "ControlTabMenu.AssetPathStyle").Text(SelectedItem->GetPathString())]]] + SHorizontalBox::Slot().FillWidth(1.0f)[SNullWidget::NullWidget] + SHorizontalBox::Slot().AutoWidth().HAlign(HAlign_Right)[SNew(STextBlock).TextStyle(FEditorStyle::Get(), "ControlTabMenu.AssetTypeStyle").Text(SelectedItem->GetTypeString())]; // Create the list of the tool tabs in the current active tab TSharedPtr SelectedAssetTabManager = SelectedItem->GetAssociatedTabManager(); if (SelectedAssetTabManager.IsValid()) { FMenuBuilder ToolTabMenuBuilder(/*bShouldCloseAfterSelection=*/true, /*CommandList=*/nullptr); ToolTabMenuBuilder.GetMultiBox()->SetStyle(&FEditorStyle::Get(), "ToolBar"); // Local editor tabs SelectedAssetTabManager->PopulateLocalTabSpawnerMenu(ToolTabMenuBuilder); // General tabs const IWorkspaceMenuStructure& MenuStructure = WorkspaceMenu::GetMenuStructure(); SelectedAssetTabManager->PopulateTabSpawnerMenu(ToolTabMenuBuilder, MenuStructure.GetStructureRoot()); // Turn the builder into a widget NewToolTabsContent = ToolTabMenuBuilder.MakeWidget(); } } NewTabItemToActivateDisplayBox->SetContent(NewTopContents); NewTabItemToActivatePathBox->SetContent(NewBottomContents); ToolTabsListBox->SetContent(NewToolTabsContent); } void SGlobalTabSwitchingDialog::OnMainTabListItemClicked(FTabListItemPtr InItem) { DismissDialog(); } void SGlobalTabSwitchingDialog::CycleSelection(bool bForwards) { // This is done here each time in case someone clicks off of the selected item (and to prime the pump at startup), // otherwise the code below wouldn't cycle back into an item if nothing was selected if ((MainTabsListWidget->GetNumItemsSelected() == 0) && (MainTabsListDataSource.Num() > 0)) { MainTabsListWidget->SetSelection(MainTabsListDataSource[0]); } // Move to the next/previous item FTabListItemPtr OldSelectedItem = GetMainTabListSelectedItem(); if (OldSelectedItem.IsValid()) { int32 OldSelectionIndex; if (MainTabsListDataSource.Find(OldSelectedItem, /*out*/ OldSelectionIndex)) { const int32 NewSelectionIndex = (OldSelectionIndex + MainTabsListDataSource.Num() + (bForwards ? 1 : -1)) % MainTabsListDataSource.Num(); if (NewSelectionIndex != OldSelectionIndex) { FTabListItemPtr NewSelectedItem = MainTabsListDataSource[NewSelectionIndex]; MainTabsListWidget->SetSelection(NewSelectedItem); MainTabsListWidget->RequestScrollIntoView(NewSelectedItem); } } } } void SGlobalTabSwitchingDialog::DismissDialog() { FTabListItemPtr SelectedItem = GetMainTabListSelectedItem(); if (SelectedItem.IsValid()) { SelectedItem->ActivateTab(); } FSlateApplication::Get().DismissAllMenus(); } void SGlobalTabSwitchingDialog::Construct(const FArguments& InArgs, FVector2D InSize, FInputChord InTriggerChord) { check(!bIsAlreadyOpen); bIsAlreadyOpen = true; #if PLATFORM_MAC // On Mac we emulate right click with ctrl+left click. This needs to be disabled for tab navigator, so that users can click on its widgets while they keep the ctrl key pressed MacApplication->SetIsRightClickEmulationEnabled(false); #endif TriggerChord = InTriggerChord; AssetThumbnailPool = MakeShareable(new FAssetThumbnailPool(128)); // Populate the list with open asset editors TArray OpenAssetList = GEditor->GetEditorSubsystem()->GetAllEditedAssets(); for (UObject* OpenAsset: OpenAssetList) { if (OpenAsset->GetOuter() != GetTransientPackage()) { MainTabsListDataSource.Add(MakeShareable(new FTabSwitchingListItem_Asset(OpenAsset))); } } MainTabsListDataSource.Add(FTabSwitchingListItem_World::MakeWorldItem()); // Sort the list by access time MainTabsListDataSource.Sort([](const FTabListItemPtr& A, const FTabListItemPtr& B) { return (A->LastAccessTime > B->LastAccessTime); }); // Create the widgets NewTabItemToActivateDisplayBox = SNew(SBox) .Padding(FMargin(0.0f, 0.0f, 10.0f, 0.0f)) .HeightOverride(70.0f) .VAlign(VAlign_Top); NewTabItemToActivatePathBox = SNew(SBox) .Padding(FMargin(0.0f, 10.0f, 10.0f, 10.0f)) .HeightOverride(40.0f) .VAlign(VAlign_Center); ToolTabsListBox = SNew(SBox) .Padding(FMargin(0.0f, 0.0f, 15.0f, 0.0f)); MainTabsListWidget = SNew(STabListWidget) .ItemHeight(64) .ListItemsSource(&MainTabsListDataSource) .OnGenerateRow(this, &SGlobalTabSwitchingDialog::OnGenerateTabSwitchListItemWidget) .OnSelectionChanged(this, &SGlobalTabSwitchingDialog::OnMainTabListSelectionChanged) .OnMouseButtonClick(this, &SGlobalTabSwitchingDialog::OnMainTabListItemClicked) .SelectionMode(ESelectionMode::Single); TSharedRef ToolTabList = SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight()[SNew(STextBlock).TextStyle(FEditorStyle::Get(), "ControlTabMenu.HeadingStyle").Text(LOCTEXT("ChangeToolsHeading", "Tool Windows"))] + SVerticalBox::Slot().FillHeight(1.0f)[ToolTabsListBox.ToSharedRef()]; TSharedRef DocumentTabList = SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight()[SNew(STextBlock).TextStyle(FEditorStyle::Get(), "ControlTabMenu.HeadingStyle").Text(LOCTEXT("OpenAssetsHeading", "Active Files"))] + SVerticalBox::Slot().FillHeight(1.0f)[SNew(SScrollBorder, MainTabsListWidget.ToSharedRef())[MainTabsListWidget.ToSharedRef()]]; ChildSlot [SNew(SBorder) .BorderImage(FEditorStyle::Get().GetBrush("ControlTabMenu.Background")) .ForegroundColor(FCoreStyle::Get().GetSlateColor("DefaultForeground")) [SNew(SBox) .WidthOverride(InSize.X) .HeightOverride(InSize.Y) .Padding(FMargin(12.0f, 12.0f, 12.0f, 0.0f)) [SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight()[NewTabItemToActivateDisplayBox.ToSharedRef()] + SVerticalBox::Slot().FillHeight(1.0f)[SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(0.4f).Padding(FMargin(0.0f, 0.0f, 0.0f, 0.0f))[ToolTabList] + SHorizontalBox::Slot().FillWidth(1.0f)[DocumentTabList] ] + SVerticalBox::Slot().AutoHeight()[NewTabItemToActivatePathBox.ToSharedRef()]]]]; // Pick the second most recent or least recent file based on whether Shift was held down when we were summoned if (MainTabsListDataSource.Num() > 0) { CycleSelection(/*bForwards=*/!FSlateApplication::Get().GetModifierKeys().IsShiftDown()); } } FReply SGlobalTabSwitchingDialog::OnKeyUp(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) { // Check to see if the trigger modifier key was released, which should close the dialog const bool bCloseViaControl = TriggerChord.NeedsControl() && ((InKeyEvent.GetKey() == EKeys::LeftControl) || (InKeyEvent.GetKey() == EKeys::RightControl)); const bool bCloseViaCommand = TriggerChord.NeedsCommand() && ((InKeyEvent.GetKey() == EKeys::LeftCommand) || (InKeyEvent.GetKey() == EKeys::RightCommand)); const bool bCloseViaAlt = TriggerChord.NeedsAlt() && ((InKeyEvent.GetKey() == EKeys::LeftAlt) || (InKeyEvent.GetKey() == EKeys::RightAlt)); if (bCloseViaControl || bCloseViaCommand || bCloseViaAlt) { DismissDialog(); return FReply::Handled(); } return FReply::Unhandled(); } FReply SGlobalTabSwitchingDialog::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) { if (TriggerChord.Key == InKeyEvent.GetKey()) { const bool bCycleForward = !InKeyEvent.IsShiftDown(); CycleSelection(bCycleForward); } return FReply::Unhandled(); } FReply SGlobalTabSwitchingDialog::OnPreviewKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) { if (InKeyEvent.GetKey() == EKeys::Escape) { FSlateApplication::Get().DismissAllMenus(); return FReply::Handled(); } return FReply::Unhandled(); } TSharedRef SGlobalTabSwitchingDialog::OnGenerateTabSwitchListItemWidget(FTabListItemPtr InItem, const TSharedRef& OwnerTable) { return SNew(STableRow, OwnerTable)[InItem->CreateWidget(AssetThumbnailPool)]; } ////////////////////////////////////////////////////////////////////////// #undef LOCTEXT_NAMESPACE