// Copyright Epic Games, Inc. All Rights Reserved. #include "ProxyTableEditor.h" #include "AssetViewUtils.h" #include "Framework/Docking/TabManager.h" #include "Modules/ModuleManager.h" #include "Styling/AppStyle.h" #include "Widgets/Docking/SDockTab.h" #include "PropertyEditorModule.h" #include "IDetailsView.h" #include "Editor.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Widgets/Input/SHyperlink.h" #include "Widgets/Input/SEditableTextBox.h" #include "Widgets/Input/SComboButton.h" #include "SAssetDropTarget.h" #include "SClassViewer.h" #include "SourceCodeNavigation.h" #include "ProxyTable.h" #include "ClassViewerFilter.h" #include "DetailLayoutBuilder.h" #include "IPropertyAccessEditor.h" #include "LandscapeRender.h" #include "Widgets/Layout/SWidgetSwitcher.h" #include "DragAndDrop/DecoratedDragDropOp.h" #include "GraphEditorSettings.h" #include "IDetailCustomization.h" #include "ObjectChooserClassFilter.h" #include "ScopedTransaction.h" #include "ObjectChooserWidgetFactories.h" #include "IObjectChooser.h" #include "Widgets/Layout/SSeparator.h" #include "PropertyCustomizationHelpers.h" #include "ProxyTableEditorCommands.h" #include "LookupProxy.h" #include "SPropertyAccessChainWidget.h" #include "Widgets/Layout/SScrollBox.h" #include "ToolMenus.h" #define LOCTEXT_NAMESPACE "ProxyTableEditor" namespace UE::ProxyTableEditor { const FName FProxyTableEditor::ToolkitFName( TEXT( "GenericAssetEditor" ) ); const FName FProxyTableEditor::PropertiesTabId( TEXT( "ProxyEditor_Properties" ) ); const FName FProxyTableEditor::TableTabId( TEXT( "ProxyEditor_Table" ) ); class FProxyRowDetails : public IDetailCustomization { public: FProxyRowDetails() {}; virtual ~FProxyRowDetails() override {}; static TSharedRef MakeInstance() { return MakeShareable( new FProxyRowDetails() ); } // IDetailCustomization interface virtual void CustomizeDetails(class IDetailLayoutBuilder& DetailBuilder) override; }; // Make the details panel show the values for the selected row, showing each column value void FProxyRowDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) { TArray> Objects; DetailBuilder.GetObjectsBeingCustomized(Objects); UProxyRowDetails* Row = Cast(Objects[0]); UProxyTable* ProxyTable = Row->ProxyTable; if (ProxyTable->Entries.IsValidIndex(Row->Row)) { IDetailCategoryBuilder& PropertiesCategory = DetailBuilder.EditCategory("Row Properties"); TSharedPtr ProxyTableProperty = DetailBuilder.GetProperty("ProxyTable", Row->StaticClass()); DetailBuilder.HideProperty(ProxyTableProperty); TSharedPtr EntriesArrayProperty = ProxyTableProperty->GetChildHandle("Entries"); TSharedPtr CurrentEntryProperty = EntriesArrayProperty->AsArray()->GetElement(Row->Row); IDetailPropertyRow& NewEntryProperty = PropertiesCategory.AddProperty(CurrentEntryProperty); NewEntryProperty.DisplayName(LOCTEXT("Entry","Selected Entry")); NewEntryProperty.ShowPropertyButtons(false); // hide array add button NewEntryProperty.ShouldAutoExpand(true); } } void FProxyTableEditor::RegisterTabSpawners(const TSharedRef& InTabManager) { WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(LOCTEXT("WorkspaceMenu_GenericAssetEditor", "Asset Editor")); FAssetEditorToolkit::RegisterTabSpawners(InTabManager); InTabManager->RegisterTabSpawner( PropertiesTabId, FOnSpawnTab::CreateSP(this, &FProxyTableEditor::SpawnPropertiesTab) ) .SetDisplayName( LOCTEXT("PropertiesTab", "Details") ) .SetGroup(WorkspaceMenuCategory.ToSharedRef()) .SetIcon(FSlateIcon("EditorStyle", "LevelEditor.Tabs.Details")); InTabManager->RegisterTabSpawner( TableTabId, FOnSpawnTab::CreateSP(this, &FProxyTableEditor::SpawnTableTab) ) .SetDisplayName( LOCTEXT("TableTab", "Proxy Table") ) .SetGroup(WorkspaceMenuCategory.ToSharedRef()) .SetIcon(FSlateIcon("EditorStyle", "LevelEditor.Tabs.Details")); } void FProxyTableEditor::UnregisterTabSpawners(const TSharedRef& InTabManager) { FAssetEditorToolkit::UnregisterTabSpawners(InTabManager); InTabManager->UnregisterTabSpawner( TableTabId ); InTabManager->UnregisterTabSpawner( PropertiesTabId ); } const FName FProxyTableEditor::ProxyEditorAppIdentifier( TEXT( "ProxyEditorApp" ) ); FProxyTableEditor::~FProxyTableEditor() { ClearSelectedRows(); GEditor->GetEditorSubsystem()->OnAssetPostImport.RemoveAll(this); FCoreUObjectDelegates::OnObjectsReplaced.RemoveAll(this); FCoreUObjectDelegates::OnObjectTransacted.RemoveAll(this); DetailsView.Reset(); } void FProxyTableEditor::RegisterToolbar() { UToolMenus* ToolMenus = UToolMenus::Get(); UToolMenu* ToolBar; FName ParentName; const FName MenuName = GetToolMenuToolbarName(ParentName); if (ToolMenus->IsMenuRegistered(MenuName)) { ToolBar = ToolMenus->ExtendMenu(MenuName); } else { ToolBar = UToolMenus::Get()->RegisterMenu(MenuName, ParentName, EMultiBoxType::ToolBar); } const FProxyTableEditorCommands& Commands = FProxyTableEditorCommands::Get(); FToolMenuInsert InsertAfterAssetSection("Asset", EToolMenuInsertType::After); { FToolMenuSection& Section = ToolBar->AddSection("Proxy Table", TAttribute(), InsertAfterAssetSection); Section.AddEntry(FToolMenuEntry::InitToolBarButton( Commands.EditTableSettings, TAttribute(), TAttribute(), FSlateIcon("EditorStyle", "FullBlueprintEditor.EditGlobalOptions"))); } } void FProxyTableEditor::BindCommands() { const FProxyTableEditorCommands& Commands = FProxyTableEditorCommands::Get(); ToolkitCommands->MapAction( Commands.EditTableSettings, FExecuteAction::CreateSP(this, &FProxyTableEditor::SelectRootProperties)); } void FProxyTableEditor::InitEditor( const EToolkitMode::Type Mode, const TSharedPtr< class IToolkitHost >& InitToolkitHost, const TArray& ObjectsToEdit, FGetDetailsViewObjects GetDetailsViewObjects ) { EditingObjects = ObjectsToEdit; FCoreUObjectDelegates::OnObjectsReplaced.AddSP(this, &FProxyTableEditor::OnObjectsReplaced); FCoreUObjectDelegates::OnObjectTransacted.AddSP(this, &FProxyTableEditor::OnObjectTransacted); FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked( "PropertyEditor" ); FDetailsViewArgs DetailsViewArgs; DetailsViewArgs.NotifyHook = this; DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea; DetailsView = PropertyEditorModule.CreateDetailView( DetailsViewArgs ); const TSharedRef StandaloneDefaultLayout = FTabManager::NewLayout( "Standalone_ProxyTableEditor_Layout_v1" ) ->AddArea ( FTabManager::NewPrimaryArea() ->SetOrientation(Orient_Vertical) ->Split ( FTabManager::NewSplitter() ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(0.7) ->AddTab( TableTabId, ETabState::OpenedTab ) ) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(0.3) ->AddTab( PropertiesTabId, ETabState::OpenedTab ) ) ) ); const bool bCreateDefaultStandaloneMenu = true; const bool bCreateDefaultToolbar = true; FAssetEditorToolkit::InitAssetEditor( Mode, InitToolkitHost, FProxyTableEditor::ProxyEditorAppIdentifier, StandaloneDefaultLayout, bCreateDefaultStandaloneMenu, bCreateDefaultToolbar, ObjectsToEdit ); BindCommands(); RegenerateMenusAndToolbars(); RegisterToolbar(); SelectRootProperties(); } FName FProxyTableEditor::GetToolkitFName() const { return ToolkitFName; } FText FProxyTableEditor::GetBaseToolkitName() const { return LOCTEXT("AppLabel", "Proxy Table Editor"); } void FProxyTableEditor::PostUndo(bool bSuccess) { UpdateTableRows(); } void FProxyTableEditor::PostRedo(bool bSuccess) { UpdateTableRows(); } void FProxyTableEditor::NotifyPreChange(FProperty* PropertyAboutToChange) { } void FProxyTableEditor::NotifyPostChange(const FPropertyChangedEvent& PropertyChangedEvent, FProperty* PropertyThatChanged) { // for all details panel changes, just refresh the table UpdateTableRows(); } FText FProxyTableEditor::GetToolkitName() const { const auto& 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 FProxyTableEditor::GetToolkitToolTipText() const { const auto& 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 FProxyTableEditor::GetWorldCentricTabColorScale() const { return FLinearColor( 0.5f, 0.0f, 0.0f, 0.5f ); } void FProxyTableEditor::SetPropertyVisibilityDelegate(FIsPropertyVisible InVisibilityDelegate) { DetailsView->SetIsPropertyVisibleDelegate(InVisibilityDelegate); DetailsView->ForceRefresh(); } void FProxyTableEditor::SetPropertyEditingEnabledDelegate(FIsPropertyEditingEnabled InPropertyEditingDelegate) { DetailsView->SetIsPropertyEditingEnabledDelegate(InPropertyEditingDelegate); DetailsView->ForceRefresh(); } TSharedRef FProxyTableEditor::SpawnPropertiesTab( const FSpawnTabArgs& Args ) { check( Args.GetTabId() == PropertiesTabId ); return SNew(SDockTab) .Label( LOCTEXT("GenericDetailsTitle", "Details") ) .TabColorScale( GetTabColorScale() ) .OnCanCloseTab_Lambda([]() { return false; }) [ DetailsView.ToSharedRef() ]; } class FProxyRowDragDropOp : public FDecoratedDragDropOp { public: DRAG_DROP_OPERATOR_TYPE(FWidgetTemplateDragDropOp, FDecoratedDragDropOp) TSharedPtr Row; /** Constructs the drag drop operation */ static TSharedRef New(TSharedPtr InRow) { TSharedRef Operation = MakeShareable(new FProxyRowDragDropOp()); Operation->Row = InRow; Operation->DefaultHoverText = LOCTEXT("Proxy Row", "Proxy Row"); Operation->CurrentHoverText = Operation->DefaultHoverText; Operation->Construct(); return Operation; }; }; class SProxyRowHandle : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SProxyRowHandle) {} SLATE_ARGUMENT(FProxyTableEditor*, Editor) SLATE_ARGUMENT(TSharedPtr, Row) SLATE_END_ARGS() void Construct(const FArguments& InArgs) { ProxyEditor = InArgs._Editor; Row = InArgs._Row; ChildSlot [ SNew(SBox) .Padding(0.0f) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .WidthOverride(16.0f) [ SNew(SImage) .Image(FCoreStyle::Get().GetBrush("VerticalBoxDragIndicatorShort")) ] ]; } FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override { ProxyEditor->SelectRow(Row); return FReply::Handled().DetectDrag(SharedThis(this), EKeys::LeftMouseButton); }; FReply OnDragDetected(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override { // clear row selection so that delete key can't cause the selected row to be deleted ProxyEditor->ClearSelectedRows(); TSharedRef DragDropOp = FProxyRowDragDropOp::New(Row); return FReply::Handled().BeginDragDrop(DragDropOp); } private: FProxyTableEditor* ProxyEditor = nullptr; TSharedPtr Row; }; class SProxyTableRow : public SMultiColumnTableRow> { public: SLATE_BEGIN_ARGS(SProxyTableRow) {} /** The list item for this row */ SLATE_ARGUMENT(TSharedPtr, Entry) SLATE_ARGUMENT(FProxyTableEditor*, Editor) SLATE_END_ARGS() static constexpr int SpecialIndex_AddRow = -1; static constexpr int SpecialIndex_InheritedFrom = -2; void Construct(const FArguments& Args, const TSharedRef& OwnerTableView) { Row = Args._Entry; Editor = Args._Editor; SMultiColumnTableRow>::Construct( FSuperRowType::FArguments(), OwnerTableView ); } /** Overridden from SMultiColumnTableRow. Generates a widget for this column of the list view. */ virtual TSharedRef GenerateWidgetForColumn(const FName& ColumnName) override { static FName Handles = "Handles"; static FName Key = "Key"; static FName OldKey = "OldKey"; static FName Value = "Value"; if (Row->RowIndex >=0) { if (ColumnName == Handles) { // row drag handle return SNew(SProxyRowHandle).Row(Row).Editor(Editor); } else if (ColumnName == Value) { UClass* ObjectType = nullptr; if (Row->ProxyTable->Entries[Row->RowIndex].Proxy) { ObjectType = Row->ProxyTable->Entries[Row->RowIndex].Proxy->Type; } bool bReadOnly = Row->ProxyTable != Editor->GetProxyTable(); TSharedPtr ResultWidget = ChooserEditor::FObjectChooserWidgetFactories::CreateWidget( bReadOnly, Row->ProxyTable, FObjectChooserBase::StaticStruct(), Row->ProxyTable->Entries[Row->RowIndex].ValueStruct.GetMutableMemory(), Row->ProxyTable->Entries[Row->RowIndex].ValueStruct.GetScriptStruct(), ObjectType, FOnStructPicked::CreateLambda([this](const UScriptStruct* ChosenStruct) { { const FScopedTransaction Transaction(LOCTEXT("Change Value Type", "Change Value Type")); Row->ProxyTable->Modify(true); Row->ProxyTable->Entries[Row->RowIndex].ValueStruct.InitializeAs(ChosenStruct); } ChooserEditor::FObjectChooserWidgetFactories::CreateWidget(false, Row->ProxyTable, FObjectChooserBase::StaticStruct(), Row->ProxyTable->Entries[Row->RowIndex].ValueStruct.GetMutableMemory(), Row->ProxyTable->Entries[Row->RowIndex].ValueStruct.GetScriptStruct(), Row->ProxyTable->Entries[Row->RowIndex].Proxy ? Row->ProxyTable->Entries[Row->RowIndex].Proxy->Type.Get() : UObject::StaticClass(), FOnStructPicked(), &CacheBorder); }), &CacheBorder ); return SNew(SOverlay) + SOverlay::Slot() [ ResultWidget.ToSharedRef() ] + SOverlay::Slot().VAlign(VAlign_Bottom) [ SNew(SSeparator).SeparatorImage(FCoreStyle::Get().GetBrush("FocusRectangle")) .Visibility_Lambda([this]() { return bDragActive && !bDropAbove ? EVisibility::Visible : EVisibility::Hidden; }) ] + SOverlay::Slot().VAlign(VAlign_Top) [ SNew(SSeparator).SeparatorImage(FCoreStyle::Get().GetBrush("FocusRectangle")) .Visibility_Lambda([this]() { return bDragActive && bDropAbove ? EVisibility::Visible : EVisibility::Hidden; }) ]; } else if (ColumnName == OldKey) { bool bReadOnly = Row->ProxyTable != Editor->GetProxyTable(); return SNew(SEditableTextBox) .IsEnabled(!bReadOnly) .Text_Lambda([this](){ return Row->ProxyTable->Entries.Num() > Row->RowIndex ? FText::FromName(Row->ProxyTable->Entries[Row->RowIndex].Key) : FText();}) .OnTextCommitted_Lambda([this](const FText& Text, ETextCommit::Type CommitType) { if (Row->ProxyTable->Entries.Num() > Row->RowIndex) { Row->ProxyTable->Entries[Row->RowIndex].Key = FName(Text.ToString()); } }); } else if (ColumnName == Key) { bool bReadOnly = Row->ProxyTable != Editor->GetProxyTable(); return SNew(SObjectPropertyEntryBox).IsEnabled(!bReadOnly) .AllowedClass(UProxyAsset::StaticClass()) .ObjectPath_Lambda([this]() { return Row->ProxyTable->Entries.Num() > Row->RowIndex ? Row->ProxyTable->Entries[Row->RowIndex].Proxy.GetPath() : FString(); }) .OnObjectChanged_Lambda([this](const FAssetData& AssetData) { if (Row->ProxyTable->Entries.Num() > Row->RowIndex) { const FScopedTransaction Transaction(LOCTEXT("Edit Proxy Asset", "Edit Proxy Asset")); Row->ProxyTable->Modify(true); Row->ProxyTable->Entries[Row->RowIndex].Proxy = Cast(AssetData.GetAsset()); // ideally just need to rebuild the widget for the "Value" to update the UObject type filtering. // For now just trigger a full refresh Editor->UpdateTableRows(); } }); } } else { // special case row for "Add Row" button if (Row->RowIndex == SpecialIndex_AddRow) { // on the row past the end, show an Add button in the result column if (ColumnName == Value) { return SNew(SOverlay) + SOverlay::Slot() [ Editor->GetCreateRowComboButton().ToSharedRef() ] + SOverlay::Slot().VAlign(VAlign_Top) [ SNew(SSeparator).SeparatorImage(FCoreStyle::Get().GetBrush("FocusRectangle")) .Visibility_Lambda([this]() { return bDragActive ? EVisibility::Visible : EVisibility::Hidden; }) ]; } } } return SNullWidget::NullWidget; } virtual void OnDragEnter(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) override { if (TSharedPtr Operation = DragDropEvent.GetOperationAs()) { if (Row->ProxyTable == Editor->GetProxyTable()) { bDragActive = true; float Center = MyGeometry.Position.Y + MyGeometry.Size.Y; bDropAbove = DragDropEvent.GetScreenSpacePosition().Y < Center; } } } virtual void OnDragLeave(const FDragDropEvent& DragDropEvent) override { bDragActive = false; } virtual FReply OnDragOver(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) override { if (Row->ProxyTable == Editor->GetProxyTable()) { if (TSharedPtr Operation = DragDropEvent.GetOperationAs()) { float Center = MyGeometry.AbsolutePosition.Y + MyGeometry.Size.Y/2; bDropAbove = DragDropEvent.GetScreenSpacePosition().Y < Center; return FReply::Handled(); } } return FReply::Unhandled(); } virtual FReply OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) override { if (TSharedPtr Operation = DragDropEvent.GetOperationAs()) { if (Row->ProxyTable == Editor->GetProxyTable()) // only allow dropping on rows that are part of this actual table (not inherited entries) { int InsertIndex = Row->RowIndex; if (InsertIndex < 0) { InsertIndex = Editor->GetProxyTable()->Entries.Num(); } else { if (!bDropAbove) { InsertIndex++; } } if (Row->ProxyTable == Operation->Row->ProxyTable) { // move row within a proxy table int NewIndex = Editor->MoveRow(Operation->Row->RowIndex, InsertIndex); Editor->SelectRow(NewIndex); return FReply::Handled(); } else { Editor->InsertEntry(Operation->Row->ProxyTable->Entries[Operation->Row->RowIndex], InsertIndex); Editor->SelectRow(InsertIndex); return FReply::Handled(); } } } return FReply::Unhandled(); } private: TSharedPtr Row; FProxyTableEditor* Editor; TSharedPtr CacheBorder; bool bDragActive = false; bool bDropAbove = false; }; TSharedRef FProxyTableEditor::GenerateTableRow(TSharedPtr InItem, const TSharedRef& OwnerTable) { UProxyTable* ProxyTable = Cast(EditingObjects[0]); if (InItem->RowIndex == SProxyTableRow::SpecialIndex_InheritedFrom) { return SNew(STableRow>, OwnerTable) [ SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth() [ SNew(STextBlock) .Text(InItem->Children.Num() > 0 ? LOCTEXT("Inherited from ", "Inherited from ") : LOCTEXT("No rows inherited from ", "No rows inherited from ") ) ] + SHorizontalBox::Slot().AutoWidth() [ SNew(SHyperlink) .Text(FText::FromString(InItem->ProxyTable->GetName())) .OnNavigate_Lambda([InItem]() { AssetViewUtils::OpenEditorForAsset(InItem->ProxyTable); }) ] ]; } else { return SNew(SProxyTableRow, OwnerTable) .Entry(InItem).Editor(this); } } void FProxyTableEditor::SelectRootProperties() { if( DetailsView.IsValid() ) { // Make sure details window is pointing to our object DetailsView->SetObjects( EditingObjects ); } } void FProxyTableEditor::DeleteSelectedRows() { UProxyTable* ProxyTable = Cast(EditingObjects[0]); const FScopedTransaction Transaction(LOCTEXT("Delete Row Transaction", "Delete Row")); ProxyTable->Modify(true); // delete selected rows. TArray RowsToDelete; for(auto& SelectedRow:SelectedRows) { if (ProxyTable->Entries.IsValidIndex(SelectedRow->Row)) { RowsToDelete.Add(SelectedRow->Row); } } // sort indices in reverse RowsToDelete.Sort([](int32 A, int32 B){ return A>B; }); for(uint32 RowIndex : RowsToDelete) { ProxyTable->Entries.RemoveAt(RowIndex); } ClearSelectedRows(); UpdateTableRows(); } void FProxyTableEditor::ClearSelectedRows() { for(UObject* SelectedRow : SelectedRows) { SelectedRow->RemoveFromRoot(); } SelectedRows.SetNum(0); TableView->ClearSelection(); SelectRootProperties(); } void FProxyTableEditor::SelectRow(TSharedPtr Row) { if (!TableView->IsItemSelected(Row)) { TableView->ClearSelection(); TableView->SetItemSelection(Row, true, ESelectInfo::OnMouseClick); } } void FProxyTableEditor::InsertEntry(FProxyEntry& Entry, int RowIndex) { UProxyTable* Table = Cast(EditingObjects[0]); RowIndex = FMath::Min(RowIndex,Table->Entries.Num()); const FScopedTransaction Transaction(LOCTEXT("Move Row", "Move Row")); Table->Modify(true); RowIndex = FMath::Clamp(RowIndex, 0, Table->Entries.Num()); Table->Entries.Insert(Entry, RowIndex); UpdateTableRows(); } int FProxyTableEditor::MoveRow(int SourceRowIndex, int TargetRowIndex) { UProxyTable* Table = Cast(EditingObjects[0]); TargetRowIndex = FMath::Min(TargetRowIndex,Table->Entries.Num()); const FScopedTransaction Transaction(LOCTEXT("Move Row", "Move Row")); Table->Modify(true); FProxyEntry Entry = Table->Entries[SourceRowIndex]; Table->Entries.RemoveAt(SourceRowIndex); if (SourceRowIndex < TargetRowIndex) { TargetRowIndex--; } Table->Entries.Insert(Entry, TargetRowIndex); UpdateTableRows(); return TargetRowIndex; } void FProxyTableEditor::TreeViewExpansionChanged(TSharedPtr InItem, bool bShouldBeExpanded) { if (InItem->RowIndex == SProxyTableRow::SpecialIndex_InheritedFrom) { ImportedTablesExpansionState.Add(InItem->ProxyTable, bShouldBeExpanded); } } void FProxyTableEditor::UpdateTableColumns() { HeaderRow->ClearColumns(); HeaderRow->AddColumn(SHeaderRow::Column("Handles") .DefaultLabel(FText()) .ManualWidth(30)); HeaderRow->AddColumn(SHeaderRow::Column("Key") .DefaultLabel(LOCTEXT("KeyColumnName", "Proxy")) .ManualWidth(500)); // Code for allowing editing of deprecated Key data // if (UProxyTable* Table = GetProxyTable()) // { // if (Table->Entries.Num() > 0) // { // // if the first entry has a non-none key, assume this is an old table and make an extra column with the old FName Key property // if (Table->Entries[0].Key != NAME_None) // { // HeaderRow->AddColumn(SHeaderRow::Column("OldKey") // .DefaultLabel(LOCTEXT("OldKeyColumnName", "Key (Deprecated)")) // .ManualWidth(500)); // } // } // } HeaderRow->AddColumn(SHeaderRow::Column("Value") .DefaultLabel(LOCTEXT("ValueColumnName", "Value")) .ManualWidth(500)); } TSharedRef FProxyTableEditor::SpawnTableTab( const FSpawnTabArgs& Args ) { check( Args.GetTabId() == TableTabId ); UProxyTable* ProxyTable = Cast(EditingObjects[0]); CreateRowComboButton = SNew(SComboButton) .ComboButtonStyle(FAppStyle::Get(), "SimpleComboButton") .ButtonContent() [ SNew(STextBlock).Text(LOCTEXT("AddRow", "+ Add Row")) ] .OnGetMenuContent_Lambda([this]() { FStructViewerInitializationOptions Options; Options.StructFilter = MakeShared(FObjectChooserBase::StaticStruct()); Options.NameTypeToDisplay = EStructViewerNameTypeToDisplay::DisplayName; TSharedRef Widget = FModuleManager::LoadModuleChecked("StructViewer").CreateStructViewer(Options, FOnStructPicked::CreateLambda([this](const UScriptStruct* ChosenStruct) { CreateRowComboButton->SetIsOpen(false); UProxyTable* ProxyTable = Cast(EditingObjects[0]); const FScopedTransaction Transaction(LOCTEXT("Add Row Transaction", "Add Row")); ProxyTable->Modify(true); ProxyTable->Entries.SetNum(ProxyTable->Entries.Num()+1); ProxyTable->Entries.Last().ValueStruct.InitializeAs(ChosenStruct); UpdateTableRows(); })); return Widget; }); HeaderRow = SNew(SHeaderRow); TableView = SNew(STreeView>) .TreeItemsSource(&TableRows) .OnExpansionChanged(this, &FProxyTableEditor::TreeViewExpansionChanged) .OnGetChildren_Lambda( [] (TSharedPtr Row, TArray>& OutChildren) { OutChildren = Row->Children; }) .OnKeyDownHandler_Lambda([this](const FGeometry&, const FKeyEvent& Event) { if ( Event.GetKey() == EKeys::Delete) { UProxyTable* ProxyTable = GetProxyTable(); DeleteSelectedRows(); return FReply::Handled(); } return FReply::Unhandled(); }) .OnSelectionChanged_Lambda([this](TSharedPtr SelectedItem, ESelectInfo::Type SelectInfo) { if (SelectedItem) { for (UObject* SelectedRow : SelectedRows) { SelectedRow->RemoveFromRoot(); } SelectedRows.SetNum(0); UProxyTable* ProxyTable = Cast(EditingObjects[0]); // Get the list of objects to edit the details of TObjectPtr Selection = NewObject(); Selection->ProxyTable = ProxyTable; Selection->Row = SelectedItem->RowIndex; Selection->AddToRoot(); SelectedRows.Add(Selection); TArray DetailsObjects; for(auto& Item : SelectedRows) { DetailsObjects.Add(Item.Get()); } if( DetailsView.IsValid() ) { // Make sure details window is pointing to our object DetailsView->SetObjects( DetailsObjects ); } } }) .OnGenerateRow_Raw(this, &FProxyTableEditor::GenerateTableRow) .HeaderRow(HeaderRow); UpdateTableColumns(); UpdateTableRows(); return SNew(SDockTab) .Label( LOCTEXT("ProxtTableTitle", "Proxy Table") ) .TabColorScale( GetTabColorScale() ) .OnCanCloseTab_Lambda([]() { return false; }) [ SNew(SScrollBox).Orientation(Orient_Horizontal) + SScrollBox::Slot() [ TableView.ToSharedRef() ] ]; } void FProxyTableEditor::AddInheritedRows(UProxyTable* ProxyTable) { if (ProxyTable == nullptr || ReferencedProxyTables.Find(ProxyTable)) { return; } // prevent infinite inheritance loops ReferencedProxyTables.Add(ProxyTable); // add "Inherited from" header above inherited rows TSharedPtr ParentTableRow = MakeShared(SProxyTableRow::SpecialIndex_InheritedFrom, ProxyTable); TableRows.Add(ParentTableRow); bool* CachedExpansionState = ImportedTablesExpansionState.Find(ProxyTable); bool Expansion = CachedExpansionState ? *CachedExpansionState : true; TableView->SetItemExpansion(ParentTableRow, Expansion); int NumRows = TableRows.Num(); for(int i =0; iEntries.Num(); i++) { // check if there's already an entry in TableRows for the same ProxyAsset if (!ReferencedProxyEntries.Find(ProxyTable->Entries[i])) { ParentTableRow->Children.Add(MakeShared(i, ProxyTable)); ReferencedProxyEntries.Add(ProxyTable->Entries[i]); } } // recursively add inherited entries for(UProxyTable* InheritedTable : ProxyTable->InheritEntriesFrom) { AddInheritedRows(InheritedTable); } } void FProxyTableEditor::UpdateTableRows() { UProxyTable* ProxyTable = Cast(EditingObjects[0]); TableRows.SetNum(0); ReferencedProxyEntries.Empty(ReferencedProxyEntries.Num()); ReferencedProxyTables.Empty(ReferencedProxyTables.Num()); // add rows from this table for(int i =0; iEntries.Num(); i++) { TableRows.Add(MakeShared(i, ProxyTable)); ReferencedProxyEntries.Add(ProxyTable->Entries[i]); } // Add 1 at the end, for the "Add Row" control TableRows.Add(MakeShared(SProxyTableRow::SpecialIndex_AddRow, ProxyTable)); if (ProxyTable->InheritEntriesFrom.Num()>0) { // add imported rows in to the table for(UProxyTable* InheritedTable : ProxyTable->InheritEntriesFrom) { AddInheritedRows(InheritedTable); } } if (TableView.IsValid()) { TableView->RebuildList(); } } void FProxyTableEditor::OnObjectTransacted(UObject* InObject, const FTransactionObjectEvent& InTransactionObjectEvent) { if (UProxyTable* ModifiedProxyTable = Cast(InObject)) { if (ReferencedProxyTables.Contains(ModifiedProxyTable)) { UpdateTableRows(); } } } void FProxyTableEditor::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 FProxyTableEditor::GetWorldCentricTabPrefix() const { return LOCTEXT("WorldCentricTabPrefix", "Generic Asset ").ToString(); } TSharedRef FProxyTableEditor::CreateEditor( const EToolkitMode::Type Mode, const TSharedPtr< IToolkitHost >& InitToolkitHost, UObject* ObjectToEdit, FGetDetailsViewObjects GetDetailsViewObjects ) { TSharedRef< FProxyTableEditor > NewEditor( new FProxyTableEditor() ); TArray ObjectsToEdit; ObjectsToEdit.Add( ObjectToEdit ); NewEditor->InitEditor( Mode, InitToolkitHost, ObjectsToEdit, GetDetailsViewObjects ); return NewEditor; } TSharedRef FProxyTableEditor::CreateEditor( const EToolkitMode::Type Mode, const TSharedPtr< IToolkitHost >& InitToolkitHost, const TArray& ObjectsToEdit, FGetDetailsViewObjects GetDetailsViewObjects ) { TSharedRef< FProxyTableEditor > NewEditor( new FProxyTableEditor() ); NewEditor->InitEditor( Mode, InitToolkitHost, ObjectsToEdit, GetDetailsViewObjects ); return NewEditor; } /// Result widgets /// TSharedRef CreateProxyTablePropertyWidget(bool bReadOnly, UObject* TransactionObject, void* Value, UClass* ResultBaseClass, UE::ChooserEditor::FChooserWidgetValueChanged ValueChanged) { IHasContextClass* HasContextClass = Cast(TransactionObject); FProxyTableContextProperty* ContextProperty = reinterpret_cast(Value); return SNew(UE::ChooserEditor::SPropertyAccessChainWidget).ContextClassOwner(HasContextClass).AllowFunctions(false).BindingColor("ClassPinTypeColor").TypeFilter("UProxyTable*") .PropertyBindingValue(&ContextProperty->Binding) .OnValueChanged(ValueChanged); } TSharedRef CreateLookupProxyWidget(bool bReadOnly, UObject* TransactionObject, void* Value, UClass* ResultBaseClass, ChooserEditor::FChooserWidgetValueChanged ValueChanged) { FLookupProxy* LookupProxy = static_cast(Value); TSharedPtr ProxyTableWidget = UE::ChooserEditor::FObjectChooserWidgetFactories::CreateWidget(false, TransactionObject, LookupProxy->ProxyTable.GetMutableMemory(),LookupProxy->ProxyTable.GetScriptStruct(), ResultBaseClass); TSharedRef ProxyAssetWidget =SNew(SObjectPropertyEntryBox) .IsEnabled(!bReadOnly) .AllowedClass(UProxyAsset::StaticClass()) .DisplayBrowse(false) .DisplayUseSelected(false) .ObjectPath_Lambda([LookupProxy](){ return LookupProxy->Proxy.GetPath();}) .OnShouldFilterAsset_Lambda([ResultBaseClass](const FAssetData& AssetData) { if (ResultBaseClass == nullptr) { return false; } if (AssetData.IsInstanceOf(UProxyAsset::StaticClass())) { if (UProxyAsset* Proxy = Cast(AssetData.GetAsset())) { return !(Proxy->Type && Proxy->Type->IsChildOf(ResultBaseClass)); } } return true; }) .OnObjectChanged_Lambda([TransactionObject, LookupProxy, ValueChanged](const FAssetData& AssetData) { const FScopedTransaction Transaction(LOCTEXT("Edit Chooser", "Edit Chooser")); TransactionObject->Modify(true); LookupProxy->Proxy = Cast(AssetData.GetAsset()); ValueChanged.ExecuteIfBound(); }); return SNew(SHorizontalBox) +SHorizontalBox::Slot() [ ProxyTableWidget ? ProxyTableWidget.ToSharedRef() : SNullWidget::NullWidget ] +SHorizontalBox::Slot() [ ProxyAssetWidget ]; } void FProxyTableEditor::RegisterWidgets() { UE::ChooserEditor::FObjectChooserWidgetFactories::RegisterWidgetCreator(FLookupProxy::StaticStruct(), CreateLookupProxyWidget); UE::ChooserEditor::FObjectChooserWidgetFactories::RegisterWidgetCreator(FProxyTableContextProperty::StaticStruct(), CreateProxyTablePropertyWidget); FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); PropertyModule.RegisterCustomClassLayout("ProxyRowDetails", FOnGetDetailCustomizationInstance::CreateStatic(&FProxyRowDetails::MakeInstance)); } } #undef LOCTEXT_NAMESPACE