// Copyright Epic Games, Inc. All Rights Reserved. #include "PropertyEditorToolkit.h" #include "Engine/Blueprint.h" #include "Editor.h" #include "Styling/AppStyle.h" #include "PropertyEditorModule.h" #include "Modules/ModuleManager.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SButton.h" #include "IPropertyTableColumn.h" #include "IPropertyTreeRow.h" #include "IPropertyTableRow.h" #include "SSimpleButton.h" #include "Widgets/Docking/SDockTab.h" #include "Subsystems/AssetEditorSubsystem.h" #include "Styling/StyleColors.h" #define LOCTEXT_NAMESPACE "PropertyEditorToolkit" const FName FPropertyEditorToolkit::ToolkitFName( TEXT( "PropertyEditor" ) ); const FName FPropertyEditorToolkit::ApplicationId( TEXT( "PropertyEditorToolkitApp" ) ); const FName FPropertyEditorToolkit::TreeTabId( TEXT( "PropertyEditorToolkit_PropertyTree" ) ); const FName FPropertyEditorToolkit::GridTabId( TEXT( "PropertyEditorToolkit_PropertyTable" ) ); const FName FPropertyEditorToolkit::TreePinAsColumnHeaderId( TEXT( "PropertyEditorToolkit_PinAsColumnHeader" ) ); const FName FPropertyEditorToolkit::DetailsTabId(TEXT("PropertyEditorToolkit_DetailsPanel")); void FPropertyEditorToolkit::RegisterTabSpawners(const TSharedRef& InTabManager) { WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(LOCTEXT("WorkspaceMenu_PropertyEditorToolkit", "Property Editor")); InTabManager->RegisterTabSpawner( GridTabId, FOnSpawnTab::CreateSP(this, &FPropertyEditorToolkit::SpawnTab_PropertyTable) ) .SetDisplayName( LOCTEXT("PropertyTableTab", "Grid") ) .SetGroup( WorkspaceMenuCategory.ToSharedRef() ) .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Viewports")); InTabManager->RegisterTabSpawner( TreeTabId, FOnSpawnTab::CreateSP(this, &FPropertyEditorToolkit::SpawnTab_PropertyTree) ) .SetDisplayName( LOCTEXT("PropertiesTab", "Pinned Columns") ) .SetGroup( WorkspaceMenuCategory.ToSharedRef() ) .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "PropertyEditor.Grid.TabIcon")); InTabManager->RegisterTabSpawner(DetailsTabId, FOnSpawnTab::CreateSP(this, &FPropertyEditorToolkit::SpawnTab_DetailsPanel)) .SetDisplayName(LOCTEXT("DetailsTab", "Details")) .SetGroup(WorkspaceMenuCategory.ToSharedRef()) .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Details")); } void FPropertyEditorToolkit::UnregisterTabSpawners(const TSharedRef& InTabManager) { InTabManager->UnregisterTabSpawner( GridTabId ); InTabManager->UnregisterTabSpawner( TreeTabId ); InTabManager->UnregisterTabSpawner( DetailsTabId ); } FPropertyEditorToolkit::FPropertyEditorToolkit() : PropertyTree() , PropertyTable() , PathToRoot() , DetailsView() { } TSharedPtr FPropertyEditorToolkit::FindExistingEditor( UObject* Object ) { // Find any existing property editor instances for this asset const TArray Editors = GEditor->GetEditorSubsystem()->FindEditorsForAsset( Object ); IAssetEditorInstance* const * ExistingInstance = Editors.FindByPredicate( [&]( IAssetEditorInstance* Editor ){ return Editor->GetEditorName() == ToolkitFName; } ); if( ExistingInstance ) { auto* PropertyEditor = static_cast( *ExistingInstance ); return PropertyEditor->SharedThis( PropertyEditor ); } return nullptr; } TSharedRef FPropertyEditorToolkit::CreateEditor( const EToolkitMode::Type Mode, const TSharedPtr< IToolkitHost >& InitToolkitHost, UObject* ObjectToEdit ) { auto ExistingEditor = FindExistingEditor( ObjectToEdit ); if( ExistingEditor.IsValid() ) { ExistingEditor->FocusWindow(); return ExistingEditor.ToSharedRef(); } TSharedRef< FPropertyEditorToolkit > NewEditor( new FPropertyEditorToolkit() ); TArray ObjectsToEdit; ObjectsToEdit.Add( ObjectToEdit ); NewEditor->Initialize( Mode, InitToolkitHost, ObjectsToEdit ); return NewEditor; } TSharedRef FPropertyEditorToolkit::CreateEditor( const EToolkitMode::Type Mode, const TSharedPtr< IToolkitHost >& InitToolkitHost, const TArray& ObjectsToEdit ) { if( ObjectsToEdit.Num() == 1 ) { auto ExistingEditor = FindExistingEditor( ObjectsToEdit[0] ); if( ExistingEditor.IsValid() ) { ExistingEditor->FocusWindow(); return ExistingEditor.ToSharedRef(); } } TSharedRef< FPropertyEditorToolkit > NewEditor( new FPropertyEditorToolkit() ); NewEditor->Initialize( Mode, InitToolkitHost, ObjectsToEdit ); return NewEditor; } void FPropertyEditorToolkit::Initialize( const EToolkitMode::Type Mode, const TSharedPtr< class IToolkitHost >& InitToolkitHost, const TArray& ObjectsToEdit ) { TArray< UObject* > AdjustedObjectsToEdit; for( auto ObjectIter = ObjectsToEdit.CreateConstIterator(); ObjectIter; ++ObjectIter ) { //@todo Remove this and instead extend the blueprints Edit Defaults editor to use a Property Table as well [12/6/2012 Justin.Sargent] UObject* Object = *ObjectIter; if ( Object->IsA( UBlueprint::StaticClass() ) ) { UBlueprint* Blueprint = Cast( Object ); // Make sure that the generated class is valid, in case the super has been removed, and this class can't be loaded. if( ensureMsgf(Blueprint->GeneratedClass != NULL, TEXT("Blueprint %s has no generated class"), *Blueprint->GetName()) ) { AdjustedObjectsToEdit.Add( Blueprint->GeneratedClass->GetDefaultObject() ); } } else { AdjustedObjectsToEdit.Add( Object ); } } if(AdjustedObjectsToEdit.Num() > 0) { CreatePropertyTree(); CreatePropertyTable(); CreateDetailsPanel(); PropertyTable->SetObjects(AdjustedObjectsToEdit); TableColumnsChanged(); TSharedRef StandaloneDefaultLayout = FTabManager::NewLayout("Standalone_PropertyEditorToolkit_Layout_v1.1") ->AddArea ( FTabManager::NewPrimaryArea()->SetOrientation(Orient_Horizontal) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(0.65f) ->SetHideTabWell(true) ->AddTab(GridTabId, ETabState::OpenedTab) ) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(0.35f) ->AddTab(TreeTabId, ETabState::OpenedTab) ->AddTab(DetailsTabId, ETabState::OpenedTab) ->SetForegroundTab(TreeTabId) ) ); const bool bCreateDefaultStandaloneMenu = true; const bool bCreateDefaultToolbar = true; FAssetEditorToolkit::InitAssetEditor(Mode, InitToolkitHost, ApplicationId, StandaloneDefaultLayout, bCreateDefaultStandaloneMenu, bCreateDefaultToolbar, AdjustedObjectsToEdit); TArray AdjustedObjectsToEditWeak; for (auto ObjectIter = AdjustedObjectsToEdit.CreateConstIterator(); ObjectIter; ++ObjectIter) { AdjustedObjectsToEditWeak.Add(*ObjectIter); } PropertyTree->SetObjectArray(AdjustedObjectsToEditWeak); DetailsView->SetObjects(AdjustedObjectsToEditWeak, true); } } TSharedRef FPropertyEditorToolkit::SpawnTab_PropertyTree( const FSpawnTabArgs& Args ) { check( Args.GetTabId() == TreeTabId ); TSharedRef TreeToolkitTab = SNew(SDockTab) .Label( LOCTEXT("GenericDetailsTitle", "Pinned Columns") ) .TabColorScale( GetTabColorScale() ) .Content() [ SNew(SBorder) .Padding(4.0f) .BorderImage( FAppStyle::GetBrush( "ToolPanel.GroupBorder" ) ) .Content() [ PropertyTree.ToSharedRef() ] ]; return TreeToolkitTab; } TSharedRef FPropertyEditorToolkit::SpawnTab_PropertyTable( const FSpawnTabArgs& Args ) { check( Args.GetTabId() == GridTabId ); FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked( "PropertyEditor" ); TSharedRef GridToolkitTab = SNew(SDockTab) .Label( LOCTEXT("GenericGridTitle", "Grid") ) .TabColorScale( GetTabColorScale() ) .Content() [ PropertyEditorModule.CreatePropertyTableWidget( PropertyTable.ToSharedRef() ) ]; return GridToolkitTab; } TSharedRef FPropertyEditorToolkit::SpawnTab_DetailsPanel(const FSpawnTabArgs& Args) { check(Args.GetTabId() == DetailsTabId); TSharedPtr DetailsTab = SNew(SDockTab) .Label(LOCTEXT("GenericDetailsPanel", "Details")) [ DetailsView.ToSharedRef() ]; return DetailsTab.ToSharedRef(); } void FPropertyEditorToolkit::CreatePropertyTree() { PropertyTree = SNew( SPropertyTreeViewImpl ) .AllowFavorites( false ) .ShowTopLevelNodes(false) .OnPropertyMiddleClicked( this, &FPropertyEditorToolkit::ToggleColumnForProperty ) .ConstructExternalColumnHeaders( this, &FPropertyEditorToolkit::ConstructTreeColumns ) .ConstructExternalColumnCell( this, &FPropertyEditorToolkit::ConstructTreeCell ) .NameColumnWidth( 0.5f ); } void FPropertyEditorToolkit::CreatePropertyTable() { FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked( "PropertyEditor" ); PropertyTable = PropertyEditorModule.CreatePropertyTable(); PropertyTable->OnSelectionChanged()->AddSP( this, &FPropertyEditorToolkit::GridSelectionChanged ); PropertyTable->OnColumnsChanged()->AddSP( this, &FPropertyEditorToolkit::TableColumnsChanged ); PropertyTable->OnRootPathChanged()->AddSP( this, &FPropertyEditorToolkit::GridRootPathChanged ); } void FPropertyEditorToolkit::CreateDetailsPanel() { FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked("PropertyEditor"); FDetailsViewArgs DetailsViewArgs; DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea; DetailsView = PropertyEditorModule.CreateDetailView(DetailsViewArgs); } void FPropertyEditorToolkit::ConstructTreeColumns( const TSharedRef< class SHeaderRow >& HeaderRow ) { SHeaderRow::FColumn::FArguments ColumnArgs; ColumnArgs .ColumnId( TreePinAsColumnHeaderId ) .FixedWidth(24) [ SNew(SBorder) .Padding( 0.0f ) .BorderImage( FAppStyle::GetBrush( "NoBorder" ) ) .HAlign( HAlign_Center ) .VAlign( VAlign_Center ) .ToolTipText( LOCTEXT("AddColumnLabel", "Push Pins to Add Columns") ) [ SNew( SImage ) .Image( FAppStyle::GetBrush(TEXT("Icons.Unpinned")) ) .ColorAndOpacity(FStyleColors::Foreground) ] ]; HeaderRow->InsertColumn( ColumnArgs, 0 ); } TSharedRef< SWidget > FPropertyEditorToolkit::ConstructTreeCell( const FName& ColumnName, const TSharedRef< IPropertyTreeRow >& Row ) { if ( ColumnName == TreePinAsColumnHeaderId ) { const TWeakPtr RowPtr = Row; PinRows.Add( Row ); return SNew(SSimpleButton) .ToolTipText(NSLOCTEXT("PropertyEditor", "ToggleColumnButtonToolTip", "Toggle Column")) .OnClicked( this, &FPropertyEditorToolkit::OnToggleColumnClicked, RowPtr ) .Icon(this, &FPropertyEditorToolkit::GetToggleColumnButtonImageBrush, RowPtr); } return SNullWidget::NullWidget; } void FPropertyEditorToolkit::ToggleColumnForProperty( const TSharedPtr< FPropertyPath >& PropertyPath ) { if ( !PropertyPath.IsValid() ) { return; } TSharedRef< FPropertyPath > NewPath = PropertyPath->TrimRoot( PropertyTable->GetRootPath()->GetNumProperties() ); const TSet< TSharedRef< IPropertyTableRow > > SelectedRows = PropertyTable->GetSelectedRows(); if (auto RowIter = SelectedRows.CreateConstIterator(); RowIter) { NewPath = NewPath->TrimRoot( (*RowIter)->GetPartialPath()->GetNumProperties() ); } if ( NewPath->GetNumProperties() == 0 ) { return; } TSharedPtr< IPropertyTableColumn > ExistingColumn; for( auto ColumnIter = PropertyTable->GetColumns().CreateConstIterator(); ColumnIter; ++ColumnIter ) { TSharedRef< IPropertyTableColumn > Column = *ColumnIter; const TSharedPtr< FPropertyPath > Path = Column->GetDataSource()->AsPropertyPath(); if ( Path.IsValid() && FPropertyPath::AreEqual( Path.ToSharedRef(), NewPath ) ) { ExistingColumn = Column; } } if ( ExistingColumn.IsValid() ) { PropertyTable->RemoveColumn( ExistingColumn.ToSharedRef() ); const TSharedRef< FPropertyPath > ColumnPath = ExistingColumn->GetDataSource()->AsPropertyPath().ToSharedRef(); for (int Index = PropertyPathsAddedAsColumns.Num() - 1; Index >= 0 ; Index--) { if ( FPropertyPath::AreEqual( ColumnPath, PropertyPathsAddedAsColumns[ Index ] ) ) { PropertyPathsAddedAsColumns.RemoveAt( Index ); } } } else { PropertyTable->AddColumn( NewPath ); PropertyPathsAddedAsColumns.Add( NewPath ); } } bool FPropertyEditorToolkit::TableHasCustomColumns() const { return PropertyPathsAddedAsColumns.Num() > 0; } bool FPropertyEditorToolkit::IsExposedAsColumn( const TWeakPtr< IPropertyTreeRow >& Row ) const { bool Result = false; if (Row.IsValid()) { const TSharedPtr< FPropertyPath > RowPathPtr = Row.Pin()->GetPropertyPath(); if ( RowPathPtr.IsValid() ) { TSharedRef< FPropertyPath > TrimmedPath = RowPathPtr->TrimRoot( PropertyTable->GetRootPath()->GetNumProperties() ); const TSet< TSharedRef< IPropertyTableRow > > SelectedRows = PropertyTable->GetSelectedRows(); if (auto RowIter = SelectedRows.CreateConstIterator(); RowIter) { TrimmedPath = TrimmedPath->TrimRoot( (*RowIter)->GetPartialPath()->GetNumProperties() ); } for (int Index = 0; Index < PropertyPathsAddedAsColumns.Num(); Index++) { if ( FPropertyPath::AreEqual( TrimmedPath, PropertyPathsAddedAsColumns[ Index ] ) ) { Result = true; break; } } } } return Result; } void FPropertyEditorToolkit::TableColumnsChanged() { PropertyPathsAddedAsColumns.Empty(); for( auto ColumnIter = PropertyTable->GetColumns().CreateConstIterator(); ColumnIter; ++ColumnIter ) { TSharedRef< IPropertyTableColumn > Column = *ColumnIter; TSharedPtr< FPropertyPath > ColumnPath = Column->GetDataSource()->AsPropertyPath(); if ( ColumnPath.IsValid() && ColumnPath->GetNumProperties() > 0 ) { PropertyPathsAddedAsColumns.Add( ColumnPath.ToSharedRef() ); } } } void FPropertyEditorToolkit::GridSelectionChanged() { TArray> SelectedObjects; PropertyTable->GetSelectedTableObjects( SelectedObjects ); if (SelectedObjects.Num() == 0) { // If none are selected, show all of them to match the initial open behavior SelectedObjects = PropertyTable->GetSelectedObjects(); } TArray SelectedRawObjects; SelectedRawObjects.Reserve(SelectedObjects.Num()); for (const TWeakObjectPtr& Object : SelectedObjects) { SelectedRawObjects.Add(Object.Get()); } PropertyTree->SetObjectArray(SelectedRawObjects); DetailsView->SetObjects(SelectedRawObjects, true); const TSet< TSharedRef< IPropertyTableRow > > SelectedRows = PropertyTable->GetSelectedRows(); if ( SelectedRows.Num() == 1 ) { if (auto RowIter = SelectedRows.CreateConstIterator(); RowIter) { PropertyTree->SetRootPath( PropertyTable->GetRootPath()->ExtendPath( (*RowIter)->GetPartialPath() ) ); } } else if ( !FPropertyPath::AreEqual( PropertyTree->GetRootPath(), PropertyTable->GetRootPath() ) ) { PropertyTree->SetRootPath( PropertyTable->GetRootPath() ); } } void FPropertyEditorToolkit::GridRootPathChanged() { GridSelectionChanged(); PropertyTree->SetRootPath( PropertyTable->GetRootPath() ); } FName FPropertyEditorToolkit::GetToolkitFName() const { return ToolkitFName; } FText FPropertyEditorToolkit::GetBaseToolkitName() const { return LOCTEXT("AppLabel", "Property Editor"); } FText FPropertyEditorToolkit::GetToolkitName() const { const auto& EditingObjs = GetEditingObjects(); int32 NumEditingObjects = EditingObjs.Num(); check( NumEditingObjects > 0 ); if( NumEditingObjects == 1 ) { const UObject* EditingObject = EditingObjs[ 0 ]; FFormatNamedArguments Args; Args.Add( TEXT("ObjectName"), FText::FromString( EditingObject->GetName() ) ); Args.Add( TEXT("ToolkitName"), GetBaseToolkitName() ); return FText::Format( LOCTEXT("ToolkitName_SingleObject", "{ObjectName} - {ToolkitName}"), Args ); } else { UClass* SharedBaseClass = NULL; for( int32 x = 0; x < NumEditingObjects; ++x ) { UObject* Obj = EditingObjs[ x ]; check( Obj ); UClass* ObjClass = Cast(Obj); if (ObjClass == NULL) { ObjClass = Obj->GetClass(); } check( ObjClass ); // Initialize with the class of the first object we encounter. if( SharedBaseClass == NULL ) { 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(); } } FFormatNamedArguments Args; Args.Add( TEXT("NumberOfObjects"), EditingObjs.Num() ); Args.Add( TEXT("ClassName"), FText::FromString( SharedBaseClass->GetName() ) ); return FText::Format( LOCTEXT("ToolkitName_MultiObject", "{NumberOfObjects} {ClassName} Objects - Property Matrix Editor"), Args ); } } FText FPropertyEditorToolkit::GetToolkitToolTipText() const { const auto& EditingObjs = GetEditingObjects(); int32 NumEditingObjects = EditingObjs.Num(); check( NumEditingObjects > 0 ); if( NumEditingObjects == 1 ) { const UObject* EditingObject = EditingObjs[ 0 ]; return FAssetEditorToolkit::GetToolTipTextForObject(EditingObject); } else { UClass* SharedBaseClass = NULL; for( int32 x = 0; x < NumEditingObjects; ++x ) { UObject* Obj = EditingObjs[ x ]; check( Obj ); UClass* ObjClass = Cast(Obj); if (ObjClass == NULL) { ObjClass = Obj->GetClass(); } check( ObjClass ); // Initialize with the class of the first object we encounter. if( SharedBaseClass == NULL ) { 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(); } } FFormatNamedArguments Args; Args.Add( TEXT("NumberOfObjects"), NumEditingObjects ); Args.Add( TEXT("ClassName"), FText::FromString( SharedBaseClass->GetName() ) ); return FText::Format( LOCTEXT("ToolkitName_MultiObjectToolTip", "{NumberOfObjects} {ClassName} Objects - Property Matrix Editor"), Args ); } } FReply FPropertyEditorToolkit::OnToggleColumnClicked( const TWeakPtr< IPropertyTreeRow > Row ) { if (Row.IsValid()) { ToggleColumnForProperty( Row.Pin()->GetPropertyPath() ); } return FReply::Handled(); } const FSlateBrush* FPropertyEditorToolkit::GetToggleColumnButtonImageBrush( const TWeakPtr< IPropertyTreeRow > Row ) const { if ( IsExposedAsColumn( Row ) ) { return FAppStyle::GetBrush("Icons.Pinned"); } return FAppStyle::GetBrush("Icons.Unpinned"); } FString FPropertyEditorToolkit::GetWorldCentricTabPrefix() const { check(0); return TEXT(""); } FLinearColor FPropertyEditorToolkit::GetWorldCentricTabColorScale() const { check(0); return FLinearColor(); } #undef LOCTEXT_NAMESPACE