// 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 "Styling/AppStyle.h" #include "Toolkits/ToolkitManager.h" #include "Interfaces/IMainFrameModule.h" #include "Widgets/Docking/SDockTab.h" #include "UObject/Package.h" #include "StatusBarSubsystem.h" #include "ToolMenus.h" #include "EditorModeManager.h" #include "EditorModes.h" #include "WidgetDrawerConfig.h" #include "Elements/Framework/TypedElementCommonActions.h" #define LOCTEXT_NAMESPACE "StandaloneAssetEditorToolkit" static int32 StatusBarIdGenerator = 0; void SStandaloneAssetEditorToolkitHost::Construct( const SStandaloneAssetEditorToolkitHost::FArguments& InArgs, const TSharedPtr& InTabManager, const FName InitAppName ) { ToolbarSlot = nullptr; EditorCloseRequest = InArgs._OnRequestClose; EditorClosing = InArgs._OnClose; AppName = InitAppName; // Asset editors have non-unique names. For example every material editor is just "MaterialEditor" so the base app name plus a unique number to generate uniqueness. This number is not used across sessions and should never be saved. StatusBarName = FName(AppName, ++StatusBarIdGenerator); MyTabManager = InTabManager; CommonActions = NewObject(); CommonActions->AddToRoot(); } 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; MenuOverlayWidgetContent = SNew(SBox); RestoreFromLayout(DefaultLayout); GenerateMenus(bCreateDefaultStandaloneMenu); if (InHostTab) { InHostTab->SetRightContent( SNew(SHorizontalBox) +SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(8.0f, 0.0f, 8.0f, 0.0f) [ MenuOverlayWidgetContent.ToSharedRef() ] ); } } 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& FileAssetSection = Menu->FindOrAddSection("FileAsset"); FileAssetSection.Label = LOCTEXT("FileAssetSectionHeading", "Open"); FileAssetSection.AddDynamicEntry(NAME_None, FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) { if (UAssetEditorToolkitMenuContext* Context = InSection.FindContext()) { Context->Toolkit.Pin()->FillDefaultFileMenuOpenCommands(InSection); } })); FToolMenuSection& Section = Menu->FindOrAddSection("FileLoadAndSave"); Section.AddDynamicEntry(NAME_None, FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) { if (UAssetEditorToolkitMenuContext* Context = InSection.FindContext()) { Context->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) { if (UAssetEditorToolkitMenuContext* Context = InSection.FindContext()) { Context->Toolkit.Pin()->FillDefaultAssetMenuCommands(InSection); } })); } static void ExtendHelpMenu(UToolMenu* InMenuBar) { const FName MenuName = *(InMenuBar->GetMenuName().ToString() + TEXT(".") + TEXT("Help")); UToolMenu* Menu = UToolMenus::Get()->ExtendMenu(MenuName); Menu->AddDynamicSection(NAME_None, FNewToolMenuDelegate::CreateLambda([](UToolMenu* InMenu) { TSharedPtr Toolkit = InMenu->FindContext()->Toolkit.Pin(); FFormatNamedArguments Args; Args.Add(TEXT("Editor"), Toolkit->GetBaseToolkitName()); FToolMenuSection& Section = InMenu->AddSection("HelpResources", FText::Format(NSLOCTEXT("MainHelpMenu", "AssetEditorHelpResources", "{Editor} Resources"), Args)); Section.InsertPosition = FToolMenuInsert("Learn", EToolMenuInsertType::First); Toolkit->FillDefaultHelpMenuCommands(Section); })); } }; // 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 && Context->Toolkit.IsValid() && 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 ) { BindEditorCloseRequestToHostTab(); const TSharedRef HostTab = HostTabPtr.Pin().ToSharedRef(); HostTab->SetOnTabClosed(SDockTab::FOnTabClosedCallback::CreateSP(this, &SStandaloneAssetEditorToolkitHost::OnTabClosed)); // Let the manager handle some logic on the Areas before destroying MyTabManager->CloseAllAreas(); this->ChildSlot[SNullWidget::NullWidget]; TSharedPtr ParentWindow = FSlateApplication::Get().FindWidgetWindow( HostTab ); TSharedPtr RestoredUI = MyTabManager->RestoreFrom( NewLayout, ParentWindow ); checkf(RestoredUI.IsValid(), TEXT("The layout must have a primary dock area")); UE::Editor::Toolbars::ECreateStatusBarOptions StatusBarCreationOptions = UE::Editor::Toolbars::ECreateStatusBarOptions::Default; if (HostedAssetEditorToolkit.IsValid()) { StatusBarCreationOptions = HostedAssetEditorToolkit->GetStatusBarCreationOptions(); } TSharedRef AssetEditorContent = SNew(SVerticalBox) + SVerticalBox::Slot() .Padding(FMargin(0.0f, 0.0f, 0.0f, 2.0f)) .AutoHeight() .Expose(ToolbarSlot) + SVerticalBox::Slot() .Padding(4.f, 2.f, 4.f, 2.f) .FillHeight(1.0f) [ RestoredUI.ToSharedRef() ]; /** * The ordering is important here, otherwise the status bar won't have a summon tab in PanelDrawer widget. * The layout and panel drawer area must be restored before creating the status bar. */ if (TSharedPtr PanelDrawer = MyTabManager->RestorePanelDrawer(AssetEditorContent, ParentWindow.ToSharedRef())) { AssetEditorContent = PanelDrawer.ToSharedRef(); } if (UStatusBarSubsystem* StatusBarSubsystem = GEditor ? GEditor->GetEditorSubsystem() : nullptr) { StatusBarWidget = StatusBarSubsystem->MakeStatusBarWidget(StatusBarName, HostTab, StatusBarCreationOptions); } this->ChildSlot [ SNew(SVerticalBox) + SVerticalBox::Slot() .FillHeight(1.f) [ AssetEditorContent ] + SVerticalBox::Slot() .Padding(0.0f, 2.0f, 0.0f, 0.0f) .AutoHeight() [ StatusBarWidget.IsValid() ? StatusBarWidget.ToSharedRef() : SNullWidget::NullWidget ] ]; } FName SStandaloneAssetEditorToolkitHost::GetMenuName() const { FName MenuAppName; if (HostedAssetEditorToolkit.IsValid()) { MenuAppName = HostedAssetEditorToolkit->GetToolMenuAppName(); } else { MenuAppName = AppName; } return *(FString(TEXT("AssetEditor.")) + MenuAppName.ToString() + TEXT(".MainMenu")); } TSharedRef SStandaloneAssetEditorToolkitHost::CreateMenuBar(FToolMenuContext& ToolMenuContext) const { TSharedPtr ToolbarMenu; const FName AssetEditorMenuName = GetMenuName(); if (HostedAssetEditorToolkit.IsValid()) { // This might return nullptr if the implementation doesn't provide a custom toolbar menu. ToolbarMenu = HostedAssetEditorToolkit->CreateMenuBar(MyTabManager, AssetEditorMenuName, ToolMenuContext); } if (!ToolbarMenu) { IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked( "MainFrame" ); ToolbarMenu = MainFrameModule.MakeMainMenu(MyTabManager, AssetEditorMenuName, ToolMenuContext); } return ToolbarMenu.ToSharedRef(); } void SStandaloneAssetEditorToolkitHost::GenerateMenus(bool bForceCreateMenu) { if( bForceCreateMenu || DefaultMenuWidget != SNullWidget::NullWidget ) { UAssetEditorToolkitMenuContext* ContextObject = NewObject(); ContextObject->Toolkit = HostedAssetEditorToolkit; FToolMenuContext ToolMenuContext(HostedAssetEditorToolkit->GetToolkitCommands(), FExtender::Combine(MenuExtenders), ContextObject); HostedAssetEditorToolkit->InitToolMenuContext(ToolMenuContext); DefaultMenuWidget = CreateMenuBar(ToolMenuContext); } } void SStandaloneAssetEditorToolkitHost::SetMenuOverlay( TSharedRef NewOverlay ) { MenuOverlayWidgetContent->SetContent(NewOverlay); } void SStandaloneAssetEditorToolkitHost::SetToolbar(TSharedPtr Toolbar) { if (Toolbar) { (*ToolbarSlot) [ Toolbar.ToSharedRef() ]; } else { (*ToolbarSlot) [ SNullWidget::NullWidget ]; } } void SStandaloneAssetEditorToolkitHost::RegisterDrawer(FWidgetDrawerConfig&& Drawer, int32 SlotIndex) { if (StatusBarWidget.IsValid()) { GEditor->GetEditorSubsystem()->RegisterDrawer(StatusBarName, MoveTemp(Drawer), SlotIndex); } } FEditorModeTools& SStandaloneAssetEditorToolkitHost::GetEditorModeManager() const { check(HostedAssetEditorToolkit.IsValid()); return HostedAssetEditorToolkit->GetEditorModeManager(); } void SStandaloneAssetEditorToolkitHost::BindEditorCloseRequestToHostTab() { if (TSharedPtr HostTab = HostTabPtr.Pin()) { HostTab->SetCanCloseTab(EditorCloseRequest); } } void SStandaloneAssetEditorToolkitHost::UnbindEditorCloseRequestFromHostTab() { if (TSharedPtr HostTab = HostTabPtr.Pin()) { HostTab->SetCanCloseTab(SDockTab::FCanCloseTab()); } } SStandaloneAssetEditorToolkitHost::~SStandaloneAssetEditorToolkitHost() { ShutdownToolkitHost(); CommonActions->RemoveFromRoot(); } TSharedRef< SWidget > 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() ); } void SStandaloneAssetEditorToolkitHost::OnToolkitHostingStarted( const TSharedRef< class IToolkit >& 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::ShutdownToolkitHost() { const TSharedPtr HostTab = HostTabPtr.Pin(); if (HostTab.IsValid()) { HostTab->RequestCloseTab(); } // Let the toolkit manager know that we're going away now FToolkitManager::Get().OnToolkitHostDestroyed(this); HostedToolkits.Reset(); HostedAssetEditorToolkit.Reset(); } void SStandaloneAssetEditorToolkitHost::OnToolkitHostingFinished( const TSharedRef< class IToolkit >& 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) { ShutdownToolkitHost(); } 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; } UTypedElementCommonActions* SStandaloneAssetEditorToolkitHost::GetCommonActions() const { return CommonActions.Get(); } void SStandaloneAssetEditorToolkitHost::AddViewportOverlayWidget(TSharedRef InOverlaidWidget, int32 ZOrder, TSharedPtr InViewport) { if (HostedAssetEditorToolkit.IsValid()) { HostedAssetEditorToolkit->AddViewportOverlayWidget(InOverlaidWidget, ZOrder); } } void SStandaloneAssetEditorToolkitHost::RemoveViewportOverlayWidget(TSharedRef InOverlaidWidget, TSharedPtr InViewport) { if (HostedAssetEditorToolkit.IsValid()) { HostedAssetEditorToolkit->RemoveViewportOverlayWidget(InOverlaidWidget); } } FReply SStandaloneAssetEditorToolkitHost::OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent ) { // Check to see if any of the actions for the toolkits can be processed by the current event // If we are in debug mode do not process commands if (FSlateApplication::Get().IsNormalExecution()) { for (TSharedPtr& HostedToolkit : HostedToolkits) { if (HostedToolkit->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, 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