// Copyright Epic Games, Inc. All Rights Reserved. #include "Toolkits/SimpleAssetEditor.h" #include "Modules/ModuleManager.h" #include "EditorStyleSet.h" #include "Widgets/Docking/SDockTab.h" #include "PropertyEditorModule.h" #include "IDetailsView.h" #include "Editor.h" #include "Widgets/Input/SHyperlink.h" #include "SourceCodeNavigation.h" #define LOCTEXT_NAMESPACE "GenericEditor" const FName FSimpleAssetEditor::ToolkitFName(TEXT("GenericAssetEditor")); const FName FSimpleAssetEditor::PropertiesTabId(TEXT("GenericEditor_Properties")); void FSimpleAssetEditor::RegisterTabSpawners(const TSharedRef& InTabManager) { WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(LOCTEXT("WorkspaceMenu_GenericAssetEditor", "Asset Editor")); FAssetEditorToolkit::RegisterTabSpawners(InTabManager); InTabManager->RegisterTabSpawner(PropertiesTabId, FOnSpawnTab::CreateSP(this, &FSimpleAssetEditor::SpawnPropertiesTab)) .SetDisplayName(LOCTEXT("PropertiesTab", "Details")) .SetGroup(WorkspaceMenuCategory.ToSharedRef()) .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.Tabs.Details")); } void FSimpleAssetEditor::UnregisterTabSpawners(const TSharedRef& InTabManager) { FAssetEditorToolkit::UnregisterTabSpawners(InTabManager); InTabManager->UnregisterTabSpawner(PropertiesTabId); } const FName FSimpleAssetEditor::SimpleEditorAppIdentifier(TEXT("GenericEditorApp")); FSimpleAssetEditor::~FSimpleAssetEditor() { GEditor->GetEditorSubsystem()->OnAssetPostImport.RemoveAll(this); GEditor->OnObjectsReplaced().RemoveAll(this); DetailsView.Reset(); PropertiesTab.Reset(); } void FSimpleAssetEditor::InitEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, const TArray& ObjectsToEdit, FGetDetailsViewObjects GetDetailsViewObjects) { const bool bIsUpdatable = false; const bool bAllowFavorites = true; const bool bIsLockable = false; EditingObjects = ObjectsToEdit; GEditor->GetEditorSubsystem()->OnAssetPostImport.AddSP(this, &FSimpleAssetEditor::HandleAssetPostImport); GEditor->OnObjectsReplaced().AddSP(this, &FSimpleAssetEditor::OnObjectsReplaced); FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked("PropertyEditor"); const FDetailsViewArgs DetailsViewArgs(bIsUpdatable, bIsLockable, true, FDetailsViewArgs::ObjectsUseNameArea, false); DetailsView = PropertyEditorModule.CreateDetailView(DetailsViewArgs); const TSharedRef StandaloneDefaultLayout = FTabManager::NewLayout("Standalone_SimpleAssetEditor_Layout_v3") ->AddArea( FTabManager::NewPrimaryArea()->SetOrientation(Orient_Vertical)->Split(FTabManager::NewStack()->SetSizeCoefficient(0.1f)->SetHideTabWell(true)->AddTab(GetToolbarTabId(), ETabState::OpenedTab))->Split(FTabManager::NewSplitter()->Split(FTabManager::NewStack()->AddTab(PropertiesTabId, ETabState::OpenedTab)))); const bool bCreateDefaultStandaloneMenu = true; const bool bCreateDefaultToolbar = true; FAssetEditorToolkit::InitAssetEditor(Mode, InitToolkitHost, FSimpleAssetEditor::SimpleEditorAppIdentifier, StandaloneDefaultLayout, bCreateDefaultStandaloneMenu, bCreateDefaultToolbar, ObjectsToEdit); RegenerateMenusAndToolbars(); // @todo toolkit world centric editing // Setup our tool's layout /*if( IsWorldCentricAssetEditor() && !PropertiesTab.IsValid() ) { const FString TabInitializationPayload(TEXT("")); // NOTE: Payload not currently used for asset properties SpawnToolkitTab(GetToolbarTabId(), FString(), EToolkitTabSpot::ToolBar); PropertiesTab = SpawnToolkitTab( PropertiesTabId, TabInitializationPayload, EToolkitTabSpot::Details ); }*/ // Get the list of objects to edit the details of const TArray ObjectsToEditInDetailsView = (GetDetailsViewObjects.IsBound()) ? GetDetailsViewObjects.Execute(ObjectsToEdit) : ObjectsToEdit; // Ensure all objects are transactable for undo/redo in the details panel for (UObject* ObjectToEditInDetailsView: ObjectsToEditInDetailsView) { ObjectToEditInDetailsView->SetFlags(RF_Transactional); } if (DetailsView.IsValid()) { // Make sure details window is pointing to our object DetailsView->SetObjects(ObjectsToEditInDetailsView); } } FName FSimpleAssetEditor::GetToolkitFName() const { return ToolkitFName; } FText FSimpleAssetEditor::GetBaseToolkitName() const { return LOCTEXT("AppLabel", "Generic Asset Editor"); } FText FSimpleAssetEditor::GetToolkitName() const { const TArray& EditingObjs = GetEditingObjects(); check(EditingObjs.Num() > 0); FFormatNamedArguments Args; Args.Add(TEXT("ToolkitName"), GetBaseToolkitName()); if (EditingObjs.Num() == 1) { const UObject* EditingObject = EditingObjs[0]; return FText::FromString(EditingObject->GetName()); } else { UClass* SharedBaseClass = nullptr; for (int32 x = 0; x < EditingObjs.Num(); ++x) { UObject* Obj = EditingObjs[x]; check(Obj); UClass* ObjClass = Cast(Obj); if (ObjClass == nullptr) { ObjClass = Obj->GetClass(); } check(ObjClass); // Initialize with the class of the first object we encounter. if (SharedBaseClass == nullptr) { SharedBaseClass = ObjClass; } // If we've encountered an object that's not a subclass of the current best baseclass, // climb up a step in the class hierarchy. while (!ObjClass->IsChildOf(SharedBaseClass)) { SharedBaseClass = SharedBaseClass->GetSuperClass(); } } check(SharedBaseClass); Args.Add(TEXT("NumberOfObjects"), EditingObjs.Num()); Args.Add(TEXT("ClassName"), FText::FromString(SharedBaseClass->GetName())); return FText::Format(LOCTEXT("ToolkitTitle_EditingMultiple", "{NumberOfObjects} {ClassName} - {ToolkitName}"), Args); } } FText FSimpleAssetEditor::GetToolkitToolTipText() const { const TArray& EditingObjs = GetEditingObjects(); check(EditingObjs.Num() > 0); FFormatNamedArguments Args; Args.Add(TEXT("ToolkitName"), GetBaseToolkitName()); if (EditingObjs.Num() == 1) { const UObject* EditingObject = EditingObjs[0]; return FAssetEditorToolkit::GetToolTipTextForObject(EditingObject); } else { UClass* SharedBaseClass = NULL; for (int32 x = 0; x < EditingObjs.Num(); ++x) { UObject* Obj = EditingObjs[x]; check(Obj); UClass* ObjClass = Cast(Obj); if (ObjClass == nullptr) { ObjClass = Obj->GetClass(); } check(ObjClass); // Initialize with the class of the first object we encounter. if (SharedBaseClass == nullptr) { SharedBaseClass = ObjClass; } // If we've encountered an object that's not a subclass of the current best baseclass, // climb up a step in the class hierarchy. while (!ObjClass->IsChildOf(SharedBaseClass)) { SharedBaseClass = SharedBaseClass->GetSuperClass(); } } check(SharedBaseClass); Args.Add(TEXT("NumberOfObjects"), EditingObjs.Num()); Args.Add(TEXT("ClassName"), FText::FromString(SharedBaseClass->GetName())); return FText::Format(LOCTEXT("ToolkitTitle_EditingMultipleToolTip", "{NumberOfObjects} {ClassName} - {ToolkitName}"), Args); } } FLinearColor FSimpleAssetEditor::GetWorldCentricTabColorScale() const { return FLinearColor(0.5f, 0.0f, 0.0f, 0.5f); } void FSimpleAssetEditor::SetPropertyVisibilityDelegate(FIsPropertyVisible InVisibilityDelegate) { DetailsView->SetIsPropertyVisibleDelegate(InVisibilityDelegate); DetailsView->ForceRefresh(); } void FSimpleAssetEditor::SetPropertyEditingEnabledDelegate(FIsPropertyEditingEnabled InPropertyEditingDelegate) { DetailsView->SetIsPropertyEditingEnabledDelegate(InPropertyEditingDelegate); DetailsView->ForceRefresh(); } TSharedRef FSimpleAssetEditor::SpawnPropertiesTab(const FSpawnTabArgs& Args) { check(Args.GetTabId() == PropertiesTabId); return SNew(SDockTab) .Icon(FEditorStyle::GetBrush("GenericEditor.Tabs.Properties")) .Label(LOCTEXT("GenericDetailsTitle", "Details")) .TabColorScale(GetTabColorScale()) [DetailsView.ToSharedRef()]; } void FSimpleAssetEditor::HandleAssetPostImport(UFactory* InFactory, UObject* InObject) { if (EditingObjects.Contains(InObject)) { // The details panel likely needs to be refreshed if an asset was imported again DetailsView->SetObjects(EditingObjects); } } void FSimpleAssetEditor::OnObjectsReplaced(const TMap& ReplacementMap) { bool bChangedAny = false; // Refresh our details view if one of the objects replaced was in the map. This gets called before the reinstance GC fixup, so we might as well fixup EditingObjects now too for (int32 i = 0; i < EditingObjects.Num(); i++) { UObject* SourceObject = EditingObjects[i]; UObject* ReplacedObject = ReplacementMap.FindRef(SourceObject); if (ReplacedObject && ReplacedObject != SourceObject) { EditingObjects[i] = ReplacedObject; bChangedAny = true; } } if (bChangedAny) { DetailsView->SetObjects(EditingObjects); } } FString FSimpleAssetEditor::GetWorldCentricTabPrefix() const { return LOCTEXT("WorldCentricTabPrefix", "Generic Asset ").ToString(); } TSharedRef FSimpleAssetEditor::CreateEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, UObject* ObjectToEdit, FGetDetailsViewObjects GetDetailsViewObjects) { TSharedRef NewEditor(new FSimpleAssetEditor()); TArray ObjectsToEdit; ObjectsToEdit.Add(ObjectToEdit); NewEditor->InitEditor(Mode, InitToolkitHost, ObjectsToEdit, GetDetailsViewObjects); return NewEditor; } TSharedRef FSimpleAssetEditor::CreateEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, const TArray& ObjectsToEdit, FGetDetailsViewObjects GetDetailsViewObjects) { TSharedRef NewEditor(new FSimpleAssetEditor()); NewEditor->InitEditor(Mode, InitToolkitHost, ObjectsToEdit, GetDetailsViewObjects); return NewEditor; } void FSimpleAssetEditor::PostRegenerateMenusAndToolbars() { // Find the common denominator class of the assets we're editing TArray ClassList; for (UObject* Obj: EditingObjects) { check(Obj); ClassList.Add(Obj->GetClass()); } UClass* CommonDenominatorClass = UClass::FindCommonBase(ClassList); const bool bNotAllSame = (EditingObjects.Num() > 0) && (EditingObjects[0]->GetClass() != CommonDenominatorClass); // Provide a hyperlink to view that native class if (CommonDenominatorClass) { TWeakObjectPtr WeakClassPtr(CommonDenominatorClass); auto OnNavigateToClassCode = [WeakClassPtr]() { if (UClass* StrongClassPtr = WeakClassPtr.Get()) { if (FSourceCodeNavigation::CanNavigateToClass(StrongClassPtr)) { FSourceCodeNavigation::NavigateToClass(StrongClassPtr); } } }; // build and attach the menu overlay TSharedRef MenuOverlayBox = SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)[SNew(STextBlock).ColorAndOpacity(FSlateColor::UseSubduedForeground()).ShadowOffset(FVector2D::UnitVector).Text(bNotAllSame ? LOCTEXT("SimpleAssetEditor_AssetType_Varied", "Common Asset Type: ") : LOCTEXT("SimpleAssetEditor_AssetType", "Asset Type: "))] + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 8.0f, 0.0f)[SNew(SHyperlink).Style(FEditorStyle::Get(), "Common.GotoNativeCodeHyperlink").OnNavigate_Lambda(OnNavigateToClassCode).Text(FText::FromName(CommonDenominatorClass->GetFName())).ToolTipText(FText::Format(LOCTEXT("GoToCode_ToolTip", "Click to open this source file in {0}"), FSourceCodeNavigation::GetSelectedSourceCodeIDE()))]; SetMenuOverlay(MenuOverlayBox); } } #undef LOCTEXT_NAMESPACE