// Copyright Epic Games, Inc. All Rights Reserved. #include "Toolkits/SStandaloneAssetEditorToolkitHost.h" #include "Toolkits/AssetEditorToolkitMenuContext.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Widgets/Layout/SBorder.h" #include "Misc/ConfigCacheIni.h" #include "Modules/ModuleManager.h" #include "Framework/Application/SlateApplication.h" #include "EditorStyleSet.h" #include "Toolkits/ToolkitManager.h" #include "Interfaces/IMainFrameModule.h" #include "Widgets/Docking/SDockTab.h" #include "UObject/Package.h" #include "ToolMenus.h" #define LOCTEXT_NAMESPACE "StandaloneAssetEditorToolkit" void SStandaloneAssetEditorToolkitHost::Construct(const SStandaloneAssetEditorToolkitHost::FArguments& InArgs, const TSharedPtr& InTabManager, const FName InitAppName) { EditorCloseRequest = InArgs._OnRequestClose; EditorClosing = InArgs._OnClose; AppName = InitAppName; MyTabManager = InTabManager; } void SStandaloneAssetEditorToolkitHost::SetupInitialContent(const TSharedRef& DefaultLayout, const TSharedPtr& InHostTab, const bool bCreateDefaultStandaloneMenu) { // @todo toolkit major: Expose common asset editing features here! (or require the asset editor's content to do this itself!) // - Add a "toolkit menu" // - Toolkits can access this and add menu items as needed // - In world-centric, main frame menu becomes extendable // - e.g., "Blueprint", "Debug" menus added // - In standalone, toolkits get their own menu // - Also, the core menu is just added as the first pull-down in the standalone menu // - Multiple toolkits can be active and add their own menu items! // - In world-centric, the core toolkit menu is available from the drop down // - No longer need drop down next to toolkit display? Not sure... Probably still want this // - Add a "toolkit toolbar" // - In world-centric, draws next to the level editor tool bar (or on top of) // - Could either extend existing tool bar or add additional tool bars // - May need to change arrangement to allow for wider tool bars (maybe displace grid settings too) // - In standalone, just draws under the toolkit's menu const FName AssetEditorMenuName = GetMenuName(); if (!UToolMenus::Get()->IsMenuRegistered(AssetEditorMenuName)) { UToolMenu* Menu = UToolMenus::Get()->RegisterMenu(AssetEditorMenuName, "MainFrame.MainMenu"); if (bCreateDefaultStandaloneMenu) { CreateDefaultStandaloneMenuBar(Menu); } } DefaultMenuWidget = SNullWidget::NullWidget; HostTabPtr = InHostTab; RestoreFromLayout(DefaultLayout); GenerateMenus(bCreateDefaultStandaloneMenu); } void SStandaloneAssetEditorToolkitHost::CreateDefaultStandaloneMenuBar(UToolMenu* MenuBar) { struct Local { static void ExtendFileMenu(UToolMenu* InMenuBar) { const FName MenuName = *(InMenuBar->GetMenuName().ToString() + TEXT(".") + TEXT("File")); UToolMenu* Menu = UToolMenus::Get()->ExtendMenu(MenuName); FToolMenuSection& Section = Menu->FindOrAddSection("FileLoadAndSave"); Section.AddDynamicEntry(NAME_None, FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) { InSection.FindContext()->Toolkit.Pin()->FillDefaultFileMenuCommands(InSection); })); } static void FillAssetMenu(UToolMenu* InMenu) { FToolMenuSection& Section = InMenu->AddSection("AssetEditorActions", LOCTEXT("ActionsHeading", "Actions")); Section.AddDynamicEntry(NAME_None, FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) { InSection.FindContext()->Toolkit.Pin()->FillDefaultAssetMenuCommands(InSection); })); } static void ExtendHelpMenu(UToolMenu* InMenuBar) { const FName MenuName = *(InMenuBar->GetMenuName().ToString() + TEXT(".") + TEXT("Help")); UToolMenu* Menu = UToolMenus::Get()->ExtendMenu(MenuName); FToolMenuSection& Section = Menu->AddSection("HelpBrowse", NSLOCTEXT("MainHelpMenu", "Browse", "Browse")); Section.InsertPosition = FToolMenuInsert("HelpOnline", EToolMenuInsertType::Before); Section.AddDynamicEntry(NAME_None, FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) { InSection.FindContext()->Toolkit.Pin()->FillDefaultHelpMenuCommands(InSection); })); } }; // Add asset-specific menu items to the top of the "File" menu Local::ExtendFileMenu(MenuBar); // Add the "Asset" menu, if we're editing an asset MenuBar->FindOrAddSection(NAME_None).AddDynamicEntry("DynamicAssetEntry", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) { UAssetEditorToolkitMenuContext* Context = InSection.FindContext(); if (Context->Toolkit.Pin()->IsActuallyAnAsset()) { InSection.AddSubMenu( "Asset", LOCTEXT("AssetMenuLabel", "Asset"), // @todo toolkit major: Either use "Asset", "File", or the asset type name e.g. "Blueprint" (Also update custom pull-down menus) LOCTEXT("AssetMenuLabel_ToolTip", "Opens a menu with commands for managing this asset"), FNewToolMenuDelegate::CreateStatic(&Local::FillAssetMenu)) .InsertPosition = FToolMenuInsert("Edit", EToolMenuInsertType::After); } })); // Add asset-specific menu items to the "Help" menu Local::ExtendHelpMenu(MenuBar); } void SStandaloneAssetEditorToolkitHost::RestoreFromLayout(const TSharedRef& NewLayout) { const TSharedRef HostTab = HostTabPtr.Pin().ToSharedRef(); HostTab->SetCanCloseTab(EditorCloseRequest); HostTab->SetOnTabClosed(SDockTab::FOnTabClosedCallback::CreateSP(this, &SStandaloneAssetEditorToolkitHost::OnTabClosed)); this->ChildSlot[SNullWidget::NullWidget]; MyTabManager->CloseAllAreas(); TSharedPtr ParentWindow = FSlateApplication::Get().FindWidgetWindow(HostTab); TSharedPtr RestoredUI = MyTabManager->RestoreFrom(NewLayout, ParentWindow); checkf(RestoredUI.IsValid(), TEXT("The layout must have a primary dock area")); MenuOverlayWidgetContent.Reset(); MenuWidgetContent.Reset(); this->ChildSlot [SNew(SVerticalBox) // Menu bar area + SVerticalBox::Slot() .AutoHeight() [SNew(SOverlay) // The menu bar + SOverlay::Slot() [SAssignNew(MenuWidgetContent, SBorder) .Padding(0) .BorderImage(FEditorStyle::GetBrush("NoBorder")) [DefaultMenuWidget.ToSharedRef()]] // The menu bar overlay + SOverlay::Slot() .HAlign(HAlign_Right) [SNew(SHorizontalBox) + SHorizontalBox::Slot() [SAssignNew(MenuOverlayWidgetContent, SBorder) .Padding(0) .BorderImage(FEditorStyle::GetBrush("NoBorder"))]]] // Viewport/Document/Docking area + SVerticalBox::Slot() .Padding(1.0f) // Fills all leftover space .FillHeight(1.0f) [RestoredUI.ToSharedRef()]]; } FName SStandaloneAssetEditorToolkitHost::GetMenuName() const { FName MenuAppName; if (HostedAssetEditorToolkit.IsValid()) { MenuAppName = HostedAssetEditorToolkit->GetToolMenuAppName(); } else { MenuAppName = AppName; } return *(FString(TEXT("AssetEditor.")) + MenuAppName.ToString() + TEXT(".MainMenu")); } void SStandaloneAssetEditorToolkitHost::GenerateMenus(bool bForceCreateMenu) { if (bForceCreateMenu || DefaultMenuWidget != SNullWidget::NullWidget) { const FName AssetEditorMenuName = GetMenuName(); UAssetEditorToolkitMenuContext* ContextObject = NewObject(); ContextObject->Toolkit = HostedAssetEditorToolkit; FToolMenuContext ToolMenuContext(HostedAssetEditorToolkit->GetToolkitCommands(), FExtender::Combine(MenuExtenders).ToSharedRef(), ContextObject); HostedAssetEditorToolkit->InitToolMenuContext(ToolMenuContext); IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked("MainFrame"); DefaultMenuWidget = MainFrameModule.MakeMainMenu(MyTabManager, AssetEditorMenuName, ToolMenuContext); MenuWidgetContent->SetContent(DefaultMenuWidget.ToSharedRef()); } } void SStandaloneAssetEditorToolkitHost::SetMenuOverlay(TSharedRef NewOverlay) { MenuOverlayWidgetContent->SetContent(NewOverlay); } SStandaloneAssetEditorToolkitHost::~SStandaloneAssetEditorToolkitHost() { // Let the toolkit manager know that we're going away now FToolkitManager::Get().OnToolkitHostDestroyed(this); HostedToolkits.Reset(); HostedAssetEditorToolkit.Reset(); } TSharedRef SStandaloneAssetEditorToolkitHost::GetParentWidget() { return AsShared(); } void SStandaloneAssetEditorToolkitHost::BringToFront() { // If our host window is not active, force it to front to ensure the tab will be visible // The tab manager won't activate a tab on an inactive window in all cases const TSharedPtr HostTab = HostTabPtr.Pin(); if (HostTab.IsValid()) { TSharedPtr ParentWindow = HostTab->GetParentWindow(); if (ParentWindow.IsValid() && !ParentWindow->IsActive()) { ParentWindow->BringToFront(); } } FGlobalTabmanager::Get()->DrawAttentionToTabManager(this->MyTabManager.ToSharedRef()); } TSharedRef SStandaloneAssetEditorToolkitHost::GetTabSpot(const EToolkitTabSpot::Type TabSpot) { return TSharedPtr().ToSharedRef(); } void SStandaloneAssetEditorToolkitHost::OnToolkitHostingStarted(const TSharedRef& Toolkit) { // Keep track of the toolkit we're hosting HostedToolkits.Add(Toolkit); // The tab manager needs to know how to spawn tabs from this toolkit Toolkit->RegisterTabSpawners(MyTabManager.ToSharedRef()); if (!HostedAssetEditorToolkit.IsValid()) { HostedAssetEditorToolkit = StaticCastSharedRef(Toolkit); } else { HostedAssetEditorToolkit->OnToolkitHostingStarted(Toolkit); } } void SStandaloneAssetEditorToolkitHost::OnToolkitHostingFinished(const TSharedRef& Toolkit) { // The tab manager should forget how to spawn tabs from this toolkit Toolkit->UnregisterTabSpawners(MyTabManager.ToSharedRef()); HostedToolkits.Remove(Toolkit); // Standalone Asset Editors close by shutting down their major tab. if (Toolkit == HostedAssetEditorToolkit) { HostedAssetEditorToolkit.Reset(); const TSharedPtr HostTab = HostTabPtr.Pin(); if (HostTab.IsValid()) { HostTab->RequestCloseTab(); } } else if (HostedAssetEditorToolkit.IsValid()) { HostedAssetEditorToolkit->OnToolkitHostingFinished(Toolkit); } } UWorld* SStandaloneAssetEditorToolkitHost::GetWorld() const { // Currently, standalone asset editors never have a world UE_LOG(LogInit, Warning, TEXT("IToolkitHost::GetWorld() doesn't make sense in SStandaloneAssetEditorToolkitHost currently")); return NULL; } FReply SStandaloneAssetEditorToolkitHost::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) { // Check to see if any of the actions for the level editor can be processed by the current event // If we are in debug mode do not process commands if (FSlateApplication::Get().IsNormalExecution()) { // Figure out if any of our toolkit's tabs is the active tab. This is important because we want // the toolkit to have it's own keybinds (which may overlap the level editor's keybinds or any // other toolkit). When a toolkit tab is active, we give that toolkit a chance to process // commands instead of the level editor. TSharedPtr ActiveToolkit; { const TSharedPtr CurrentActiveTab; // = FSlateApplication::xxxGetGlobalTabManager()->GetActiveTab(); for (auto HostedToolkitIt = HostedToolkits.CreateConstIterator(); HostedToolkitIt && !ActiveToolkit.IsValid(); ++HostedToolkitIt) { const auto& CurToolkit = *HostedToolkitIt; if (CurToolkit.IsValid()) { // Iterate over this toolkits spawned tabs const auto& ToolkitTabsInSpots = CurToolkit->GetToolkitTabsInSpots(); for (auto CurSpotIt(ToolkitTabsInSpots.CreateConstIterator()); CurSpotIt && !ActiveToolkit.IsValid(); ++CurSpotIt) { const auto& TabsForSpot = CurSpotIt.Value(); for (auto CurTabIt(TabsForSpot.CreateConstIterator()); CurTabIt; ++CurTabIt) { const auto& PinnedTab = CurTabIt->Pin(); if (PinnedTab.IsValid()) { if (PinnedTab == CurrentActiveTab) { ActiveToolkit = CurToolkit; } } } } } } } //@TODO: This seems wrong (should prioritize it but not totally block it) if (ActiveToolkit.IsValid()) { // A toolkit tab is active, so direct all command processing to it if (ActiveToolkit->ProcessCommandBindings(InKeyEvent)) { return FReply::Handled(); } } else { // No toolkit tab is active, so let the underlying asset editor have a chance at the keystroke if (HostedAssetEditorToolkit->ProcessCommandBindings(InKeyEvent)) { return FReply::Handled(); } } } return SCompoundWidget::OnKeyDown(MyGeometry, InKeyEvent); } void SStandaloneAssetEditorToolkitHost::OnTabClosed(TSharedRef TabClosed) const { check(TabClosed == HostTabPtr.Pin()); EditorClosing.ExecuteIfBound(); MyTabManager->SetMenuMultiBox(nullptr); if (HostedAssetEditorToolkit.IsValid()) { const TArray* const ObjectsBeingEdited = HostedAssetEditorToolkit->GetObjectsCurrentlyBeingEdited(); if (ObjectsBeingEdited) { const bool IsDockedAssetEditor = TabClosed->HasSiblingTab(FName("DockedToolkit"), false /*TreatIndexNoneAsWildcard*/); const EAssetEditorToolkitTabLocation AssetEditorToolkitTabLocation = (IsDockedAssetEditor) ? EAssetEditorToolkitTabLocation::Docked : EAssetEditorToolkitTabLocation::Standalone; for (const UObject* ObjectBeingEdited: *ObjectsBeingEdited) { // Only record assets that have a valid saved package UPackage* const Package = ObjectBeingEdited->GetOutermost(); if (Package && Package->GetFileSize()) { GConfig->SetInt( TEXT("AssetEditorToolkitTabLocation"), *ObjectBeingEdited->GetPathName(), static_cast(AssetEditorToolkitTabLocation), GEditorPerProjectIni); } } } } } #undef LOCTEXT_NAMESPACE